หน่วยความจำของบอร์ดไม่ใช่ของไม่มีที่สิ้นสุด มันคือชิป RAM จริง ๆ ตัวหนึ่งบัดกรีอยู่บนแผ่นวงจร ความจุตายตัว ดังรูป

ที่มา: "Hynix HY62V8100B SRAM-7313" — Raimond Spekking, CC BY-SA 4.0, Wikimedia Commons
ชิปนี้คือ SRAM — หน่วยความจำชนิดเดียวกับที่อยู่ในชิป PSoC Edge ของเรา
ถ้าเรา game.Box(...) ใหม่ทุกครั้งที่ยิง วัตถุจะกินที่ในชิปนี้เพิ่มขึ้นเรื่อย ๆ ไม่มีคืน สุดท้าย RAM เต็ม โปรแกรมก็ค้างหรือ MemoryError
กฎทองของ pool ที่อยากให้จำคือ จองล่วงหน้า เปิด-ปิด รีไซเคิล อย่าสร้างใหม่ใน loop จำนวน widget จะคงที่ เท่านั้น
ลองมองเป็นกราฟ "หน่วยความจำที่ใช้" เทียบกับ "จำนวนนัดที่ยิงไปแล้ว" จะเห็นภาพชัดทันที:

ให้ = จำนวนนัดที่ยิงไปแล้ว, = ขนาดหน่วยความจำต่อ object หนึ่งตัว:
สังเกตว่า pool ไม่ได้แค่ "เร็วกว่า" แต่มันทำให้ความต้องการหน่วยความจำ ไม่ขึ้นกับเวลาเล่น เลย — เกมเล่นได้ยาวเท่าไรก็ได้โดยใช้ RAM เท่าเดิม นี่คือเหตุผลที่ embedded ชอบ pattern นี้มาก
object pool คือ list ของ object หลายตัว เราต้องคล่องเรื่องนี้ก่อน:
# สร้าง list ของ object ด้วย list comprehension
bullets = [game.Box(0, -50, 6, 14, game.CYAN) for _ in range(6)]
# วนทุกตัวใน list (iterate)
for bullet in bullets:
bullet.hide()
# วนพร้อมเลขลำดับ (index) ด้วย enumerate
for index, enemy in enumerate(enemies):
enemy.move_to(enemy.x, enemy.y + 3)

เคล็ดสำคัญของ pool: เราจะ ไม่ลบ สมาชิกออกจาก list (
remove) แต่ใช้วิธี "ซ่อน" แทน คือเลื่อนออกนอกจอแล้วhide()ของก็ยังอยู่ในบ่อ พร้อมหยิบกลับมาใช้ใหม่ได้เสมอ ในรูปจึงมีกล่องครบ 6 ช่องตลอด เปลี่ยนแค่สถานะ ว่าง/บิน
game.Box · box.show()/.hide() ·box.move_to() · box.set_color() ·game.hit(a,b) · game.sfx() · text.set()
[ engine 70% ] ─── คำสั่งพร้อมใช้ ───► [ กติกาในหัวเรา 30% ] = เกมของน้อง ๆ
30% คือ "กติกาในหัว" เป็นส่วนที่น้อง ๆ เป็นเจ้าของ เอง engine ให้คำสั่งมาครบแล้ว เหลือแค่เราเรียงร้อยกติกา
MAX_BULLETS = 6 # ขนาดบ่อกระสุน
# บ่อกระสุน — สร้างครั้งเดียว นอก on_frame แล้วจอดซ่อนไว้
bullets = [game.Box(0, -50, 6, 14, game.CYAN) for _ in range(MAX_BULLETS)]
for bullet in bullets:
bullet.hide() # y ติดลบ = "ว่าง / จอดอยู่"
# หากระสุนว่างมาใช้ซ้ำ (จอดอยู่ = y < -20)
def find_free_bullet():
for bullet in bullets:
if bullet.y < -20: # จอดอยู่นอกจอ = ว่าง พร้อมใช้
return bullet
return None # บ่อเต็ม (ยิงครบ 6 นัด) — ยังไม่ให้ยิง
ทฤษฎี → โค้ด:
find_free_bullet()(shooter_step3.py:30-34) วน list หานัดแรกที่ว่าง = linear search ต่อการยิง 1 ครั้ง โดยMAX_BULLETS= 6 บ่อเล็กมากจึงคุ้มกว่าการgame.Box(...)ใหม่ (alloc) ทุกนัด ถ้าคืนNoneแปลว่าบ่อเต็ม (ยิงครบ 6 นัดค้างจอ) — เฟรมนั้นยังไม่ให้ยิง
# ยิง (มี cooldown กันยิงรัว)
fire_cooldown = max(0, fire_cooldown - 1)
if (keys.a or keys.up) and fire_cooldown == 0:
bullet = find_free_bullet()
if bullet:
bullet.show()
bullet.move_to(ship_x + ship.w // 2 - 3, 340) # ออกจากหัวยาน
game.sfx("fire") # เสียงยิงจาก engine
fire_cooldown = 8 # หน่วง 8 เฟรม
# เลื่อนกระสุนขึ้น, พ้นจอ -> จอดกลับบ่อ
for bullet in bullets:
if bullet.y >= -20: # เฉพาะนัดที่กำลังบิน
bullet.move_to(bullet.x, bullet.y - 9) # ขึ้น 9 px/เฟรม
if bullet.y < -20:
bullet.hide() # พ้นจอ → คืนบ่อ

ผลขั้นนี้: กด A/UP แล้วเลเซอร์ฟ้าพุ่งขึ้นพร้อมเสียงยิง ยิงต่อเนื่องได้สูงสุด 6 นัด และกดรัวก็ไม่ทะลักเพราะมี cooldown คุมอยู่ — สังเกตแถบ cooldown ในภาพ ต้องวิ่งกลับถึงศูนย์ก่อนถึงจะยิงนัดถัดไปได้
เลื่อนกระสุน (ขึ้น = y ลด):
Cooldown — นับถอยหลังทุกเฟรม, ยิงได้เมื่อถึง 0:
อัตรายิงสูงสุด ที่ 30 FPS:

กราฟนี้คือสมการ cooldown ที่วาดออกมา: ค่าเริ่มที่ 8 ลดทีละ 1 ทุกเฟรม พอแตะ 0 ถึงยิงได้ (จุดแดง) แล้วเด้งกลับเป็น 8 ใหม่ — ฟันเลื่อยซ้ำ ๆ แบบนี้คือสิ่งที่กันไม่ให้กดรัวแล้วกระสุนทะลักออกมาทีเดียวหมดบ่อ ถ้าลอง cooldown จาก 8 เหลือ 4 เส้นจะถี่ขึ้นเท่าตัว ยิงเร็วขึ้นแต่บ่อ 6 นัดก็หมดเร็วขึ้น ลองปรับเล่นดูได้
import random
MAX_ENEMIES = 6
ENEMY_COLORS = [game.RED, game.ORANGE, game.PINK]
# บ่อศัตรู — สร้างครั้งเดียว เริ่มที่ความสูงสุ่ม (ติดลบ = เหนือจอ)
enemies = [game.Box(random.randint(0, game.WIDTH - 30),
-random.randint(40, 400), 30, 24,
random.choice(ENEMY_COLORS)) for _ in range(MAX_ENEMIES)]
enemy_speed = [random.uniform(2.5, 4.5) for _ in range(MAX_ENEMIES)]
def respawn_enemy(index): # ยืม-คืน เหมือนกระสุน
enemies[index].move_to(random.randint(0, game.WIDTH - 30),
-random.randint(20, 200))
enemies[index].set_color(random.choice(ENEMY_COLORS)) # สุ่มสีใหม่
ศัตรูแต่ละตัวเริ่มเหนือจอ (y ติดลบสุ่ม) + ความเร็วสุ่มในช่วง:
ที่เราให้แต่ละตัวเร็วต่างกันด้วย
random.uniformแล้วสุ่มสีใหม่ตอน respawn ก็เพื่อให้ศัตรูดูมีชีวิต ไม่ตกลงมาเป็นแถวเดียวกันน่าเบื่อ — ในรูปจะเห็นว่าศัตรูแต่ละตัวเกิดที่ความสูง (y ติดลบ) ต่างกันในแถบสุ่ม แล้วลูกศรความเร็วยาวไม่เท่ากัน ตัวที่ v มากตกถึงจอก่อน
for index, enemy in enumerate(enemies):
enemy.move_to(enemy.x, enemy.y + enemy_speed[index]) # ตกลง
if enemy.y > game.HEIGHT: # หลุดพื้น = เสียชีวิต
lives -= 1
respawn_enemy(index)
hud.set("Score: %d Lives: %d" % (score, lives))
if lives <= 0:
game.sfx("gameover")
game.Text("GAME OVER", 320, 180, game.RED)
return False # หยุดเกม
continue
for bullet in bullets: # นัดไหนโดนศัตรูตัวนี้?
if bullet.y >= -20 and game.hit(bullet, enemy):
score += 1
game.sfx("hit")
bullet.move_to(0, -50); bullet.hide() # คืนบ่อ
respawn_enemy(index)
break # อย่าลืม break!
จุดที่น้อง ๆ พลาดบ่อยคือลืมประกาศ
global ship_x, ship_speed, score, lives, fire_cooldownไว้บรรทัดแรกของon_frameอย่าลืมนะ — โครงในรูปคือ วนศัตรู → หลุดพื้นไหม → ถ้าไม่ ก็วนเช็คทุกนัดว่าโดนไหม → โดนแล้วbreakทันที (อย่ายิงนัดเดียวได้หลายคะแนน) เป็น control flow ที่ซ้อนกันหนาที่สุดของคาบนี้
โค้ดคาบนี้เรียก game.sfx() อยู่ 3 จังหวะ — ตอนกด A ยิงเลเซอร์ (fire), ตอนกระสุนโดนศัตรู (hit), และตอนชีวิตหมดขึ้น GAME OVER (gameover) เสียงทั้งสามนี้คือ feedback ที่ทำให้ผู้เล่นรู้ทันทีว่ายิงออก โดนเป้า หรือแพ้ โดยไม่ต้องอ่านตัวเลขบนจอ
เสียงในคาบนี้ — Shooter ใช้เสียงตอนยิง / ตอนกระสุนโดนศัตรู / ตอนชีวิตหมด (กดเล่นฟังได้จริง):
sfx_shoot_hit
sfx_game_over
ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer
game.hit(bullet, enemy) คืน True เมื่อกล่อง ซ้อนทับ ทั้งแกน x และ y:
จำนวนการเช็คต่อเฟรม (ทุกนัด × ทุกศัตรู):

ภาพช่วยให้เห็นว่าทำไมต้องเช็คทั้งสองแกน: ฝั่งซ้ายกล่องซ้อนกันทั้ง x และ y → ชน, ฝั่งขวาซ้อนแค่แกน x แต่ y มีช่องว่าง → ยังไม่ชน นี่คือคำสั่งเดียวกับที่เราเคยใช้ตรวจชนในเกม Catch / Snake / Flappy / Pong มาแล้ว เกมแต่ละเกมเขียนต่างกัน แต่คณิตเบื้องหลังเป็นเรื่องเดิมที่น้อง ๆ รู้อยู่แล้ว
ทฤษฎี → โค้ด (bridge):
game.hit()คือ engine ห่ออสมการ 4 ตัวข้างบนไว้ ต้นฉบับที่เขียนเป็น Python จริง ๆ อยู่ในเกมเต็มที่full_games/shooter_full.py:48-50— ฟังก์ชันoverlap():def overlap(ax, ay, aw, ah, ox, oy, ow, oh): return not (ax + aw < ox or ax > ox + ow or ay + ah < oy or ay > oy + oh) # :50สังเกตว่ามันเขียนเป็น "NOT แยกกัน" (De Morgan): แทนที่จะพิสูจน์ว่า ซ้อนกัน มันพิสูจน์ว่า ไม่ซ้อน (อยู่คนละฝั่งซ้าย/ขวา/บน/ล่าง) แล้ว
notกลับผล — ได้ผลลัพธ์เดียวกับ เป๊ะ (คาบ 12 จะเรียกoverlap()นี้ตรง ๆ ที่:237กระสุน×ศัตรู และ:250ยาน×ศัตรู)
return Falseจากon_frame= บอกgame.run()ให้หยุดลูป นี่คือวิธีจบเกมแบบสะอาด ๆ
ผังก่อนหน้าโฟกัสแค่ท่อนศัตรู — ผังนี้คือ on_frame ทั้งก้อน ตามลำดับจริง: ขับยาน → ยิง+cooldown → เลื่อนกระสุน → วนศัตรู แล้ว game.run() วนซ้ำให้เอง 30 ครั้ง/วินาที
เส้น No = เฟรมที่ไม่ได้ยิง (กดอยู่ระหว่าง cooldown หรือไม่กด A) ก็ข้ามท่อนยิงไปเลื่อนกระสุนต่อ — cooldown ที่
:50นับถอยหลังทุกเฟรมไม่ว่าจะยิงหรือไม่
practise_codes/shooter_step3.py↗ (บ่อกระสุน) แล้วต่อด้วย shooter_step4.py (ศัตรู + การชน + คะแนน + ชีวิต + GAME OVER) ใน BENTO IDE
ยานเขียวด้านล่าง · เลเซอร์ฟ้าจากบ่อ · กล่องสีตก = ศัตรู · มุมบน = Score/Lives — ภาพนี้ถ่ายจากบอร์ดจริง ตรงกับสิ่งที่กลุ่มของเราจะเห็นเมื่อรันสำเร็จ
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| จอเต็ม object/ค้าง | สร้างกระสุนใน on_frame |
สร้างครั้งเดียว นอก ลูป |
UnboundLocalError |
ลืม global |
global score, lives, fire_cooldown, ... |
| กดทีออก 6 นัด | ไม่มี cooldown | เช็ค == 0 ก่อนยิง แล้วตั้ง = 8 |
| ยิงไม่ออกเลย | บ่อเต็ม ไม่คืนนัด | พ้นจอ → bullet.hide() |
| ได้คะแนนรัวเป็นสิบ | ลืม break |
break หลัง game.hit เป็นจริง |
| นัดในบ่อไปชนศัตรู | ไม่กรองนัดซ่อน | เช็ค if bullet.y >= -20 ก่อน |
กฎทองของ pool ที่อยากให้จำคือ ของว่างให้จอดนอกจอไว้ reuse อย่า alloc เพราะแบบนี้จำนวน widget จะคงที่ ตลอดเกม
บ่อกระสุนเล็ก ๆ ในเกมนี้ จริง ๆ แล้วคือ pattern เดียวกับที่ใช้ในระบบจริงระดับโรงงาน:
ฝั่ง Embedded / ฮาร์ดแวร์
ฝั่ง Python
for ... in วนเปิด/ปิดสมาชิกy < -20 แทนการลบทิ้งฝั่ง Algorithm
ฝั่ง Graphics
Level up your code with game programming design patterns: Object pool — Unity — คนละ engine แต่แนวคิด "จองล่วงหน้า แล้วรีไซเคิล" เหมือนบ่อกระสุนของเราเป๊ะ
สิ่งที่ผ่านแล้วถือว่าใช้ได้
ทำเอง 30%
practise_codes/shooter_step3.py↗ (บ่อกระสุน) แล้วต่อ shooter_step4.py (ศัตรู+ชน+คะแนน+ชีวิต+GAME OVER) เติมช่องให้ครบfull_games/ลองต่อ (stretch)
MAX_ENEMIES / ความเร็วจบคาบนี้เราก็ทำครบ 4 เกม (Snake / Flappy / Pong / Shooter) แล้ว สิ่งที่อยากให้ติดตัวไปคือคำว่า object pool — จองล่วงหน้า เปิด-ปิด รีไซเคิล อย่าสร้างใหม่ใน loop มันคือวินัยพื้นฐานของการเขียนโปรแกรมบนเครื่องหน่วยความจำจำกัดที่เราจะใช้ไปอีกนาน
shooter_step3.py — อ่านให้เข้าใจ แล้วพิมพ์เองเฉลยชุดนี้มีไว้ให้ เทียบ ไม่ได้มีไว้ให้ลอกวางส่ง คะแนนของคาบอยู่ที่ใบงานกับการอธิบายบ่อกระสุนด้วยคำพูดของกลุ่มเอง วิธีที่ได้ผลจริงคืออ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือ ตอนพิมพ์เองนั่นแหละที่สมองจำ pattern "ยืม-คืน" ได้ ต่อจากนี้เราจะแกะทีละส่วน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ส่วนตั้งต้น — ค่าคงที่ + ยาน + HUD (ทำก่อนเข้า loop):
import bentogame as game
ACCEL, MAX_SPEED, FRICTION = 1.4, 13.0, 0.80
MAX_BULLETS = 6 # ขนาดบ่อกระสุน (เท่าเกมจริง :37)
game.title("SHOOTER") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
ship = game.Box(365, 352, 62, 24, game.GREEN)
ship_x, ship_speed, fire_cooldown = 365.0, 0.0, 0
score, lives = 0, 3
hud = game.Text("Score: 0 Lives: 3", 10, 8, game.WHITE)
MAX_BULLETS = 6 (:15) ตั้งขนาดบ่อไว้เป็น ค่าคงที่ที่เดียว ไม่กระจายเลข 6 ไปทั่วโค้ด วันหลังอยากเพิ่มเป็น 10 ก็แก้จุดเดียว นี่คือนิสัย "แยกค่าออกจากโครง" ที่พูดถึงในสไลด์ 70/30ship_x, ship_speed, fire_cooldown = 365.0, 0.0, 0 (:20) เก็บ 3 state ไว้ระดับ module เพราะ on_frame ต้องแก้ค่าข้ามเฟรม ship_speed เป็น float (ฟิสิกส์เร่ง/ลื่นที่ยกมาจาก step2) ส่วน fire_cooldown เป็น int ไว้นับถอยหลังเฟรมACCEL, MAX_SPEED, FRICTION (:14) ยกมาจาก step2 ทั้งชุด ยานเดิมที่บังคับได้แล้วเราไม่แตะ คาบนี้แค่ต่อกระสุนเพิ่มบนของเดิมship = game.Box(...) (:19) สร้างครั้งเดียวก่อน loop เหมือนที่บ่อกระสุนกำลังจะทำ — pattern เดียวกับสไลด์ list ของ entity คือของที่ใช้ทั้งเกม สร้างทีเดียวเริ่มจากอ่านหัวไฟล์ให้ขาดก่อน ว่าอะไรถูกจองไว้ก่อนเข้า loop บ้าง แล้วค่อยเข้าไปดูว่า loop หยิบของพวกนี้ไปใช้ยังไง
# ----- เติมส่วนนี้เอง (1): บ่อกระสุน สร้างครั้งเดียว แล้วจอดซ่อนไว้ -----
bullets = [game.Box(0, -50, 6, 14, game.CYAN) for _ in range(MAX_BULLETS)]
for bullet in bullets:
bullet.hide()
# ----- เติมส่วนนี้เอง (2): หากระสุนว่างมาใช้ซ้ำ (จอดอยู่ = y < -20) -----
def find_free_bullet():
for bullet in bullets:
if bullet.y < -20:
return bullet
return None
[game.Box(...) for _ in range(MAX_BULLETS)] (:25) สร้างทั้งบ่อในบรรทัดเดียว นอก on_frame — คือ list comprehension ตัวเดียวกับที่เราซ้อมในสไลด์ list ของ entity ตำแหน่งเริ่มที่ y = -50 (นอกจอ) จงใจให้จอดซ่อนfor bullet in bullets: bullet.hide() (:26-27) จอดทุกนัดไว้ตั้งแต่ต้น ไม่มีนัดไหนโผล่บนจอจนกว่าจะถูกยืม สถานะ "ว่าง" ของเราคือ ตำแหน่งนอกจอ + hide() ไม่ใช่การลบทิ้งfind_free_bullet() (:30-34) วน list หานัดแรกที่ y < -20 = linear search โดย บ่อเล็กมากจึงคุ้มกว่าการ game.Box(...) ใหม่ (alloc) ทุกนัด นี่คือ REUSE — DON'T CREATE จากสไลด์ ทำไม embedded ต้องใช้ poolreturn None (:34) เมื่อบ่อเต็ม (6 นัดค้างจออยู่) เราคืน None แทนการสร้างนัดที่ 7 — จำนวน widget จึงคงที่เสมอ ตรงกับกราฟหน่วยความจำเส้นเขียวที่ แบนราบถ้าจับได้ว่า "ว่าง = จอดนอกจอ" ไม่ใช่ "ว่าง = ลบทิ้ง" น้องก็เข้าใจแก่นของ pool แล้ว ที่เหลือคือแค่ยืมกับคืนให้ถูกจังหวะ
# ----- เติมส่วนนี้เอง (3): ยิง (มี cooldown กันยิงรัว) -----
fire_cooldown = max(0, fire_cooldown - 1)
if (keys.a or keys.up) and fire_cooldown == 0:
bullet = find_free_bullet()
if bullet:
bullet.show()
bullet.move_to(ship_x + ship.w // 2 - 3, 340) # ออกจากหัวยาน
game.sfx("fire") # เสียงยิงจาก C engine จริง
fire_cooldown = 8 # หน่วง 8 เฟรมก่อนยิงนัดถัดไป
# ----- เติมส่วนนี้เอง (4): เลื่อนกระสุนขึ้น, พ้นจอ -> จอดกลับ -----
for bullet in bullets:
if bullet.y >= -20:
bullet.move_to(bullet.x, bullet.y - 9)
if bullet.y < -20:
bullet.hide()
fire_cooldown = max(0, fire_cooldown - 1) (:50) นับถอยหลังทุกเฟรม ไม่ว่าจะยิงหรือไม่ max(0, ...) กันไม่ให้ค่าติดลบ — คือสมการ จากสไลด์ ฟิสิกส์กระสุน + cooldownif (keys.a or keys.up) and fire_cooldown == 0 (:51) ยิงได้ก็ต่อเมื่อ cooldown ถึง 0 นี่คือ gate ที่กันกดรัวแล้วกระสุนทะลักหมดบ่อทีเดียว รับทั้งปุ่ม A และ UP ให้ยิงถนัดมือbullet.move_to(ship_x + ship.w // 2 - 3, 340) (:55) ยิงออกจากกลางหัวยาน // 2 คือหารปัดลงเป็นจำนวนเต็มเพราะพิกัด pixel ไม่มีทศนิยม ตามด้วย game.sfx("fire") แล้วตั้ง fire_cooldown = 8 (:57) → อัตรายิง นัด/วินาทีif bullet.y >= -20 (:61) กรองเฉพาะนัดที่กำลังบิน (นัดในบ่อไม่ต้องเลื่อน) — กับดักข้อ "นัดในบ่อไปชนศัตรู" ในสไลด์ กับดักที่เจอบ่อย ป้องกันตรงนี้เอง bullet.y - 9 (:62) ขึ้น 9 px/เฟรม พ้นจอเมื่อไรก็ hide() (:64) คืนบ่อสังเกตว่า "ยิง" กับ "เลื่อน" เป็นคนละท่อนกัน ท่อนยิงแค่ยืมนัดออกมา ส่วนการเดินหน้าของกระสุนเป็นงานของ loop เลื่อนที่ทำให้ ทุกนัด ทุกเฟรม แยกหน้าที่ให้ชัดแล้วโค้ดจะอ่านง่ายขึ้นมาก
บ่อกระสุนไม่ได้โผล่มาลอย ๆ มันต่อยอดจากยานที่บังคับได้ใน step2 ทีละชั้น เหมือนที่เราจะต่อ step4 (ศัตรู) บนบ่อนี้อีกที:
| ส่วน | บรรทัด | แนวคิดใหม่ที่เพิ่มเข้ามา | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| ยาน (จาก step2) | :42-47 |
accel / friction / clamp ให้ยานมีน้ำหนัก | game.keys() · game.Box |
| ตั้งบ่อ | :25-27 |
list comprehension + hide() = สถานะ "ว่าง" |
list ของ entity + for ลูป |
| หานัดว่าง | :30-34 |
linear search + return None เมื่อเต็ม |
for ... in + if |
| ยิง + cooldown | :50-57 |
นับถอยหลัง gate การยิง + ยืม show() |
find_free_bullet() · game.sfx |
| เลื่อน + คืนบ่อ | :60-64 |
เลื่อน y - 9 แล้ว hide() คืนบ่อ |
move_to (ยานก็เลื่อนแบบเดียวกัน) |
move_to เหมือนกัน ต่างแค่ยานเลื่อนตาม ship_speed ส่วนกระสุนเลื่อนคงที่ทีละ 9 — โครงเดียวกัน เปลี่ยนแค่ค่าความเร็วอยากให้เห็นจังหวะการไต่ระดับนี้ให้ชัด เกมทั้งเกมสร้างแบบนี้แหละ เริ่มจากของเล็กที่รันได้ แล้วเติมทีละแนวคิด ไม่กระโดดเขียนรวดเดียว
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่อง "มองให้ทะลุ" ว่าบ่อกระสุน 6 นัดที่เพิ่งพิมพ์ จริง ๆ แล้วเป็นชิ้นส่วนของอะไร
ที่มา — บ่อกระสุนไม่ใช่แนวคิดใหม่เลย ย้อนไปสไลด์ หัวใจคาบนี้ — Object Pool อาจารย์บอกว่ามันคือ pattern เดียวกับ ท่อใน Flappy ที่เราเลื่อนท่อเดิมกลับมาใช้ซ้ำแทนการสร้างท่อใหม่ทุกครั้ง และก่อนหน้านั้นในสไลด์ list ของ entity เราซ้อม list comprehension กับการวน for มาแล้ว บ่อกระสุนก็คือสองอย่างนี้ประกอบกันพอดี บวกกฎจากสไลด์ ทำไม embedded ต้องใช้ pool ว่าอย่า alloc ใน loop
ที่ไป — pool เล็ก ๆ นี้เป็น building block ที่ไปได้ไกล คาบหน้า (คาบ 12) เราจะเอา overlap() มาเช็ค ยาน × ศัตรู (:250) เพิ่มจากที่วันนี้เช็คแค่ กระสุน × ศัตรู (:237) โดยบ่อทั้งสองยังคงที่ 12 ตัวเท่าเดิม และในโปรเจกต์จริงบ่อแบบนี้จะกลายเป็น buffer pool ของ driver ที่จองหน่วยความจำล่วงหน้าแล้วยืม-คืน
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
hide() กับ find_free_bullet() เข้ามาเลือกว่านัดไหนว่าง)respawn_enemy() ก็คือ move_to + สุ่มค่าใหม่ ไม่ต่างจากการคืนกระสุน)find_free_bullet() คือเรื่องเดียวกับการหา "ช่องว่าง" ในตารางที่นั่ง — มันคือ allocation ที่เราเขียนเอง ไม่ได้พึ่ง game.Box ใหม่ถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ กระสุน ท่อ ศัตรู มันเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด — pool ไม่ใช่เทคนิคของเกมยิง แต่เป็นวิธีคิดเรื่องหน่วยความจำที่ใช้ได้กับของทุกอย่างที่ "เกิดแล้วดับ" ซ้ำ ๆ
บ่อกระสุนกับ cooldown ที่เราเพิ่งเขียน ไม่ใช่ของเล่นในห้องเรียน มันคือ pattern ที่ระบบจริงใช้กันทุกวัน:
pbuf เป็นบ่อ, DMA descriptor ring จองจำนวนคงที่ล่วงหน้า เพื่อกัน heap fragmentation บน SRAM ที่จำกัดfire_cooldown == 0 ที่กันการทำงานถี่เกินfind_free_bullet() ที่คืน None เมื่อไม่มีนัดว่างเห็นไหมว่าบ่อ 6 นัดในเกมยิง กับบ่อ connection ในเซิร์ฟเวอร์ธนาคาร ใช้วิธีคิดเดียวกันหมด ของที่ดูเหมือนแค่ทำเกมสนุก จริง ๆ คือทักษะวิศวกรรมที่กินได้ยาว
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
MAX_BULLETS ระหว่างเกมยังไงไม่ให้ RAM พุ่งจนค้าง (ใบ้: จองเพิ่มเป็นก้อนล่วงหน้า ไม่ใช่ทีละนัดใน loop)score สูงยิ่งยิงเร็ว จะผูก fire_cooldown กับ score ยังไงให้ยังกันกดรัวได้ ไม่ปล่อยให้ cooldown เหลือ 0 จนกระสุนทะลักoverlap() ตัวเดียวกับที่วันนี้เช็คกระสุน×ศัตรู ถ้าต้องเช็คทั้ง กระสุน × ศัตรู และ ยาน × ศัตรู ในเฟรมเดียว จะจัดลูปยังไงไม่ให้วนศัตรูซ้ำสองรอบ ลองร่างผังไว้ก่อนเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากบ่อกระสุนที่พิมพ์เองวันนี้ ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นคนออกแบบระบบ ไม่ใช่แค่คนเติมช่องตามเฉลย
fit-css