ขอ pool สไปรท์ + หัว + อาหาร ครั้งเดียวก่อนเริ่มเกม:
BODY_POOL_SIZE = 26 # ปล้องลำตัว (<= 32 - head - food - score)
body_pool = game.pool("snake_body", BODY_POOL_SIZE) # ครั้งเดียว — ห้ามใน on_each_frame()!
head_sprite = game.Sprite("snake_head_r", 0, 0)
food_sprite = game.Sprite("snake_food", 0, 0)
เพดาน widget ของคอนโซล Python:

สังเกตว่า
game.pool(...)อยู่ นอกon_each_frame()เราเตรียมปล้องไว้ล่วงหน้าครั้งเดียว
show() + วางตำแหน่งhide() เก็บคืนลัง
ข้อห้ามสำคัญ: อย่าสร้างสไปรท์ใหม่ใน on_each_frame() เพราะถ้าสร้างทุกเฟรม จำนวน widget จะทะลุเพดาน 32 แล้วเกมจะแครช
ดูตามภาพนี้สักครู่นะ บนคืองูที่ผู้เล่นเห็น ล่างคือ "ลัง" (pool) ที่เราเตรียมไว้ครั้งเดียว เวลางูยาวขึ้นเราแค่หยิบปล้องในลังมา show() พองูสั้นลงก็ hide() คืนเข้าลัง

จุดที่อยากให้จับให้ได้: ตัวเลข จำนวน widget ไม่เคยเปลี่ยน ไม่ว่างูจะยาวแค่ไหน เพราะเราใช้ของชิ้นเดิมวนไปมา ไม่ได้สร้างเพิ่ม นี่แหละหัวใจของ reuse-don't-create ที่ทำให้ไม่ทะลุเพดาน 32
เทียบกับโค้ด:
show()/hide()ในredraw()คือการ "หยิบ/คืน" ที่เห็นในคลิปนี้ตรง ๆ — ไม่มีgame.Sprite(...)สร้างใหม่เลย
NES วาด sprite ได้แค่ 64 ตัว และ 8 ตัวต่อ scanline เป็น hardware limit จริง — เกินไม่ได้

เชื่อมกับวันนี้: bento ก็มีเพดาน 32 sprite (UI_MAX_WIDGETS) → ต้อง reuse ไม่ใช่สร้างใหม่ทุกเฟรม นี่คือกับดัก embedded แท้ ๆ
ที่มา: NES OAM · ภาพ Evan-Amos (public domain) · nesdev.org/wiki/PPU_OAM
redraw) HEAD_SPRITE_FOR = {(1, 0): "snake_head_r", (-1, 0): "snake_head_l",
(0, -1): "snake_head_u", (0, 1): "snake_head_d"}
def redraw():
head_sprite.frame(HEAD_SPRITE_FOR[(step_col, step_row)]) # หัวหันทิศ
head_sprite.move_to(snake_body[0][0]*CELL_PX, snake_body[0][1]*CELL_PX)
for i in range(BODY_POOL_SIZE):
if i + 1 < len(snake_body):
body_pool[i].show()
body_pool[i].move_to(snake_body[i+1][0]*CELL_PX, snake_body[i+1][1]*CELL_PX)
else:
body_pool[i].hide()
grid → pixel:

สังเกตว่าในฟังก์ชันนี้ ไม่มี
game.Sprite(...)เลย มีแต่show/hide/move_to/frame(สไลด์ย่อชื่อตัวแปรจาก
pool_indexเป็นiให้พอดีจอ โค้ดจริงใช้pool_index)
key ต้องเป็น (step_col, step_row) ระวังอย่าสลับลำดับนะ ไม่อย่างนั้นจะเจอ KeyError หรือหัวงูหันผิดด้าน
นี่คือ sprite sheet จริงจากเกม 2D ตัวละครเดินหนึ่งก้าวถูกวาดแยกเป็นหลาย ๆ ภาพย่อย (เฟรม) เก็บรวมในไฟล์เดียว เวลาเล่นเกมจะวาด ทีละเฟรม สลับกันเร็ว ๆ ตาเราจึงเห็นเป็นการเคลื่อนไหว
![]()
ที่มา: "Barbarian Sprite Sheet Right Walk" — Kurt Kaiser, CC0, Wikimedia Commons
หัวงูของเราใช้หลักเดียวกัน เรามี 4 เฟรม (หันขวา/ซ้าย/ขึ้น/ลง) แล้วเลือกเฟรมที่ถูกทิศด้วย head_sprite.frame("snake_head_d") — ลำตัวงูก็คือเฟรม snake_body ซ้ำ ๆ กัน
What is a sprite sheet? — Andreas Löw
HEAD_SPRITE_FOR คือ dict ที่จับคู่ "ทิศที่งูเดิน" เข้ากับ "ชื่อเฟรมหัว" เราไม่ต้องคำนวณมุมหรือหมุนภาพเลย แค่เปิดตารางหยิบเฟรมที่เตรียมไว้มาวาด
![]()
ตรรกะการเดินของงูเหมือนเดิมเป๊ะ สิ่งเดียวที่เปลี่ยนคือ "เฟรมไหนถูกเลือกมาวาด" — นี่คือเหตุผลที่เราพูดเสมอว่า ผิวเกมเปลี่ยน แต่ตรรกะไม่เปลี่ยน
เราคิดตำแหน่งงูเป็น "ช่อง" (col, row) แต่จอวาดเป็น "พิกเซล" สองหน่วยนี้ต่างกัน จึงต้องแปลงด้วยการคูณก่อนสั่ง move_to(...) ทุกครั้ง
![]()
ความสัมพันธ์นี้เป็น เส้นตรง: ช่องเพิ่มทีละ 1 พิกเซลขยับทีละ CELL_PX (=16) พอดี ช่องที่ 4 จึงอยู่ที่ 64 พิกเซล ช่องที่ 8 อยู่ที่ 128 พิกเซล
ถ้าลืมคูณ งูจะกองอยู่มุมซ้ายบน เพราะ col/row เป็นเลขช่องค่าน้อย ๆ (หลักสิบ) ซึ่งถ้าเอาไปเป็นพิกเซลตรง ๆ จะอยู่ติดขอบจอทั้งหมด
DIFFICULTY_MODES = [("RELAXED", 6), ("CLASSIC", 9), ("TURBO", 14)] # (ชื่อ, fps)
def choose_mode():
selected = 1 # CLASSIC ค่าตั้งต้น
menu_text = game.Text("", 250, 170, game.GB_LIGHTEST)
while True:
menu_text.set("MODE: < %s > (UP/DOWN, A=start)" % DIFFICULTY_MODES[selected][0])
keys = game.keys()
if keys.up: selected = (selected - 1) % len(DIFFICULTY_MODES)
if keys.down: selected = (selected + 1) % len(DIFFICULTY_MODES)
if keys.a or keys.start:
menu_text.set(""); return DIFFICULTY_MODES[selected][1] # คืนค่า fps
time.sleep_ms(125) # กันลั่นปุ่ม + ไม่ busy-hang (สำคัญมาก!)
fps ที่เลือกจะถูกส่งเข้า
game.run(on_each_frame, fps=fps)ทำให้งูเดินเร็วช้าต่างกันจริง
ถ้าลืม time.sleep_ms(125) while True จะกลายเป็น busy-loop ที่กิน CPU 100% ทำให้ REPL ค้างและบอร์ดร้อน
game.run()จัดการ timing ให้เราเอง แต่ menu loop เราเขียนเอง เลยต้อง throttle เองด้วย
next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row]
# 30% ของน้อง ๆ: ตรรกะเดิมจาก step4 — ผิวเกมเปลี่ยนเป็นสไปรท์เท่านั้น
if (next_head[0] < 0 or next_head[0] >= GRID_COLS or
next_head[1] < 0 or next_head[1] >= GRID_ROWS or next_head in snake_body):
game.sfx("die")
if score > best_score: best_score = score # ← ใหม่: เก็บ Best
game.Text("GAME OVER", 330, 180, game.RED); return False
snake_body.insert(0, next_head)
if next_head == food_cell:
score += 1
if score > best_score: best_score = score # ← ใหม่: Best สด ๆ
game.sfx("eat"); score_text.set("Score: %d Best: %d" % (score, best_score))
place_food_at_random_empty_cell()
if len(snake_body) > MAX_SNAKE_LENGTH: snake_body.pop()
else:
snake_body.pop()
redraw()

ลองเทียบกับ step4 ดู เหมือนกันทุกบรรทัด ต่างแค่ 2 บรรทัด
best_scoreกับเรียกredraw()แทนdraw()
snake_step5.py ในภาพเดียว — เมนู → เล่นจนถึงตรงนี้เราเห็นชิ้นส่วนครบแล้ว ทีนี้ต่อจิ๊กซอว์ให้เห็น "ทั้งโปรแกรม": แถวบน = เฟสเตรียม (รัน ครั้งเดียว ก่อนเข้าเกม) · แถวล่าง = หนึ่งเฟรม ใน on_each_frame() ที่ game.run() วนให้เอง (snake_step5.py:131)
สัญกรณ์เดียวกันทุกผังในคอร์ส: ▭ฟ้า = ประมวลผล · ▭เขียว = อ่าน input / วาดจอ · ◇ม่วง = ตัดสินใจ (ใช่/ไม่) · ▭เทามน = จุดเริ่ม/จบ · เส้นประ = วนกลับเฟรมถัดไป
ทฤษฎี → โค้ด: grid→pixel
col × CELL_PX→:66,70-71· dict หัวหันทิศ →:51,65· เดินแบบคิวinsert(0,..)/pop()→:116,125,127·best = max(best,score)→:112-113,119-120· เมนูวน% len→:85-86· เพดาน pool32-1-1-1=29→:34
ตาย เมื่อ: ชนกำแพง หรือ หัวซ้ำตำแหน่งในลำตัว
อย่าลืมประกาศ
Bestไว้ที่ระดับบนสุด แล้วใส่global best_scoreในลูป ไม่อย่างนั้นมันจะรีเซ็ตเป็น 0 ทุกรอบ
on_each_frame())
วิธีรันบนบอร์ด: เปิดไฟล์ใน BENTO IDE แล้วกด Program to Device เกมจะขึ้นบนจอบอร์ดเลย
exec(open(...)) ในการรันเกม ให้ใช้ปุ่ม Program to Device เสมอfull_games/snake_sprite_full.pyก้าวเล็กของผิวเกม sprite เริ่มจาก
snake_sprite_step1.pyและsnake_sprite_step2.pyก่อนมารวมที่ step5
เส้นเขียว = ใช้ของในลังซ้ำ (จำนวน widget คงที่) · เส้นแดง = game.Sprite()/pool() ใน on_each_frame() ทำให้ widget เพิ่มทุกเฟรมจนทะลุ 32 แล้วแครช
| อาการ | สาเหตุ | แก้ |
|---|---|---|
| เกมค้าง/แครช สไปรท์หาย | เรียก Sprite/pool ใน on_each_frame() ทะลุเพดาน 32 |
ย้ายออกนอก loop สร้างครั้งเดียว |
| เมนูค้าง / บอร์ดร้อน | while True แต่ลืม time.sleep_ms(125) |
ใส่ sleep ทุกรอบ menu loop |
| หัวหันผิด / KeyError | สลับ (step_row, step_col) หรือทิศ (0,0) |
key = (step_col, step_row); เริ่ม 1,0 |
| Best รีเซ็ต 0 ทุกรอบ | ลืม global best_score |
ประกาศบนสุด + global ใน loop |
| บนบอร์ดเห็นกล่องเขียว | (ไม่ใช่บั๊ก) รันบน sim เก่า → fallback | บนบอร์ดจริงเป็น sprite |
ผ่านคาบ 6 เมื่อ:
eatGAME OVER มีเสียง die และ Best ค้างข้ามรอบภาพจริง BENTO Playground: งู pixel-art เขียว + แอปเปิลแดง +
Scoreมุมซ้ายบน
ถึงตรงนี้คือเรามีเกมเต็มเกมแรกของคอร์สแล้ว
เสียงในคาบนี้ — Snake เล่น
game.sfx("eat")ตอนงูกินอาหาร และgame.sfx("die")ตอนชนกำแพง/ชนตัวเอง (กดเล่นฟังได้จริง):
การเปลี่ยนกล่องเป็นสไปรท์ดูเหมือนแค่ "แต่งหน้าเกม" แต่จริง ๆ มันแตะรากฐานวิศวกรรมหลายด้านที่ใช้ในงานจริง:

Graphics — sprite sheet, การเลือก เฟรม ตามทิศ และการแปลง coordinate จากช่อง (col,row) เป็นพิกเซล คือพื้นฐาน 2D rendering และระบบพิกัดที่เกมและ GUI ทุกตัวใช้
Algorithms — x_px = col × CELL_PX คือ linear mapping; การเดินงูด้วย insert(0,..) + pop() คือคิว/ลิสต์ที่เลื่อนทีละก้าว; ตรวจชนกำแพง/ตัวเองคือการเช็คช่วงและ membership
Embedded / MCU — เพดาน 32 widget (UI_MAX_WIDGETS) คือข้อจำกัดทรัพยากรจริงของฮาร์ดแวร์ เหมือน NES ที่วาดได้ 64 sprite จึงต้อง reuse ของในหน่วยความจำที่จองไว้ ไม่ขอเพิ่มเรื่อย ๆ
Python — dict ทำ lookup ทิศ→เฟรม; list ของ pool ที่เข้าถึงด้วย index; global เพื่อให้ best_score อยู่ข้ามรอบ; time.sleep_ms คุม loop ไม่ให้กิน CPU เต็ม
เกมง่าย ๆ เกมเดียว ฝึกครบทั้งกราฟิก คณิต ฮาร์ดแวร์ และโครงสร้างข้อมูล — นี่คือเหตุผลที่เราเรียนผ่านการเล่น
game.Sprite หัวหันทิศด้วย dictgame.pool สร้างครั้งเดียว (reuse-don't-create)on_each_frame() ใน practise_codes/snake_step5.py↗game.pool(...) แทนสร้างสไปรท์ในลูป?"
คาบหน้า: เราจะเริ่มเกมใหม่ Flappy Bird กับฟิสิกส์แรงโน้มถ่วง
v += g
snake_step5.py — อ่านให้เข้าใจ แล้วปิดไฟล์พิมพ์เองเฉลยนี้มีไว้ให้ เทียบ ไม่ได้มีไว้ให้ลอกวางส่ง คะแนนของคาบอยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง ไม่ใช่ที่โค้ดตรงกับเฉลย วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือ ตอนพิมพ์เองนั่นแหละที่สมองจำ pattern ได้ เดี๋ยวเราจะไล่เฉลยทีละก้อน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ก้อนแรก — จองของครั้งเดียว ก่อนเกมเริ่ม:
snake_body = [[6, 8], [5, 8], [4, 8]] # หัวอยู่ตัวแรก
body_pool = game.pool("snake_body", BODY_POOL_SIZE) # สร้างครั้งเดียว — ห้ามอยู่ใน on_each_frame()!
head_sprite = game.Sprite("snake_head_r", 0, 0)
food_sprite = game.Sprite("snake_food", 0, 0)
score_text = game.Text("Score: 0 Best: 0", 10, 8, game.WHITE)
step_col, step_row, score, best_score = 1, 0, 0, 0
food_cell = [0, 0]
# หัวหันทิศ: ทิศเดิน (step_col,step_row) -> ชื่อสไปรท์หัว (snake_sprite_full.py:32-33)
HEAD_SPRITE_FOR = {(1, 0): "snake_head_r", (-1, 0): "snake_head_l",
(0, -1): "snake_head_u", (0, 1): "snake_head_d"}
:43-46 — body_pool, head_sprite, food_sprite, score_text โยงตรงกับสไลด์ "ขั้น A — Box → Sprite + pool" ที่เราคุยว่าเพดาน widget มีแค่ 32 ถ้าเผลอไปสร้างในลูป จำนวนจะไต่ทะลุแล้วแครช (สไลด์ "กับดักที่เจอบ่อย")HEAD_SPRITE_FOR :51-52 เป็น dict ที่จับคู่ทิศ → ชื่อเฟรมหัว นี่คือของเดิมจากสไลด์ "หัวหันทิศ = dict lookup" เราเลือกทำเป็นตารางแทนการคำนวณมุม เพราะ lookup อ่านง่ายและไม่มีทางคำนวณพลาดstep_col, step_row = 1, 0 :48 แปลว่างูเริ่มเดินไปทางขวา ตรงกับหัวเริ่มต้น snake_head_r ที่ :44 ถ้าตั้งไม่ตรงกัน เฟรมแรกหัวจะหันผิดด้านhead_sprite.frame("snake_head_r") ทุกที่ หัวงูจะไม่มีวันหันตามทางเดิน — dict คือสิ่งที่ทำให้ผิวเกมฉลาดขึ้นโดยไม่แตะตรรกะจำหลักนี้ไว้: ของที่ต้องใช้ซ้ำทั้งเกม จองครั้งเดียวไว้ข้างบน แล้วค่อยหยิบมาใช้ในลูป — นี่คือนิสัยเดียวกับที่งาน embedded จริงทำกับหน่วยความจำ
redraw() เลือกเฟรมหัว แล้วหยิบปล้องจากลังdef redraw():
head_sprite.frame(HEAD_SPRITE_FOR[(step_col, step_row)]) # หัวหันทิศ เหมือนเกม C
head_sprite.move_to(snake_body[0][0] * CELL_PX, snake_body[0][1] * CELL_PX)
for pool_index in range(BODY_POOL_SIZE): # snake_body[1:] -> pool ลำตัว
if pool_index + 1 < len(snake_body):
body_pool[pool_index].show()
body_pool[pool_index].move_to(snake_body[pool_index + 1][0] * CELL_PX,
snake_body[pool_index + 1][1] * CELL_PX)
else:
body_pool[pool_index].hide()
head_sprite.frame(HEAD_SPRITE_FOR[(step_col, step_row)]) :65 — เอาทิศปัจจุบันไปเปิดตาราง dict ได้ชื่อเฟรม แล้วสั่งเปลี่ยนเฟรม ไม่มีการหมุนภาพ ไม่มีตรีโกณ เป็นการ lookup ล้วน ๆ ตามสไลด์ "จากชื่อทิศ ไปเป็นเฟรมที่วาด"move_to(... * CELL_PX ...) :66,70-71 คือ grid → pixel ที่เราพิสูจน์เป็นเส้นตรงในสไลด์ "grid → pixel — ทำไมต้องคูณ CELL_PX" ลืมคูณเมื่อไร งูจะกองมุมซ้ายบนทันทีfor pool_index :67 วน "ลัง" ทั้ง 26 ช่อง: ปล้องไหนมีจริง (pool_index + 1 < len(snake_body)) ก็ show() + ขยับ ปล้องที่เหลือ hide() เก็บคืนลัง :68-73 นี่คือ reuse-don't-create จากสไลด์ "ภาพในใจของ pool"body_pool[pool_index] คู่กับ snake_body[pool_index + 1] เพราะ snake_body[0] คือหัว (วาดด้วย head_sprite ต่างหาก) ลำตัวจึงเริ่มที่ [1]ในฟังก์ชันทั้งก้อนนี้ ไม่มี
game.Sprite(...)สักตัว มีแต่frame/move_to/show/hide— จำนวน widget เลยคงที่ตลอดเกม ไม่ว่างูจะยาวแค่ไหน
selected = 1 # CLASSIC เป็นค่าตั้งต้น
menu_text = game.Text("", 250, 170, game.GB_LIGHTEST)
while True:
menu_text.set("MODE: < %s > (UP/DOWN, A=start)" % DIFFICULTY_MODES[selected][0])
keys = game.keys()
if keys.up: selected = (selected - 1) % len(DIFFICULTY_MODES)
if keys.down: selected = (selected + 1) % len(DIFFICULTY_MODES)
if keys.a or keys.start:
menu_text.set("")
return DIFFICULTY_MODES[selected][1] # คืนค่า fps ของโหมดที่เลือก
time.sleep_ms(125) # 1 รอบ ~8/วิ (กันลั่นปุ่ม + ไม่ busy-hang)
selected = 1 :80 เริ่มที่ CLASSIC เพราะ DIFFICULTY_MODES[1] คือ ("CLASSIC", 9) :38 — เลือกค่ากลางเป็นค่าตั้งต้นคือมารยาทของ UX ไม่ให้ผู้เล่นเริ่มจากยากสุดหรือง่ายสุด(selected - 1) % len(DIFFICULTY_MODES) :85-86 ใช้ % ให้เมนู วนรอบ: อยู่บนสุดกด UP แล้ววาร์ปไปล่างสุด นี่คือ modulo ตัวเดียวกับที่เจอซ้ำในเมนูทุกเกม เขียนแบบนี้แล้วไม่ต้องเช็กขอบ list ด้วยมือreturn DIFFICULTY_MODES[selected][1] :89 คืน แค่ตัวเลข fps ไม่ใช่ทั้ง tuple เพราะปลายทางคือ game.run(on_each_frame, fps=fps) :93,131 ที่ต้องการตัวเลขล้วน — fps ต่างกันคือสิ่งที่ทำให้โหมดเร็ว-ช้าต่างกันจริงtime.sleep_ms(125) :90 คือหัวใจของก้อนนี้ เมนูนี้เป็น while True ของเราเอง (ไม่ใช่ game.run) ถ้าลืม sleep มันจะกลายเป็น busy-loop กิน CPU 100% ตามสไลด์ "menu loop เป็น loop ของเราเอง จึงต้อง sleep เอง"สังเกตเส้นแบ่งความรับผิดชอบ: อะไรที่ engine วนให้ (
game.run) เราไม่ต้องคุมจังหวะ อะไรที่เราวนเอง (while True) เราต้องsleepเองเสมอ
ทั้งสามไฟล์ในบล็อก sprite ต่อกันเป็นบันได แต่ละก้าวเพิ่ม แนวคิดใหม่หนึ่งอย่าง บนโครงเดิม ไม่เคยเขียนใหม่รวดเดียว:
| ไฟล์ | สิ่งที่เพิ่มเข้ามา | บรรทัด | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
snake_sprite_step1.py |
Box → Sprite + pool (ภาพนิ่ง วาดครั้งเดียว) |
:30,33,53 |
list body, draw() |
snake_sprite_step2.py |
ใส่ "สมอง" ครบลูป: เดิน/โต/ตาย insert+pop |
:71,76,83,85 |
pool + draw() ของ step1 |
snake_step5.py |
หัวหันทิศ (dict) + เมนูโหมด + Best ข้ามรอบ | :51,65,76-90,112 |
ตรรกะทั้งลูปของ step2 |
game.pool สร้างครั้งเดียว :27 แล้ว show/hide ยังไม่มีตรรกะเดินเลยdraw() เดิมมาห่อด้วยกติกา Snake ครบ: body.insert(0, next_head) แล้วเลือกว่าจะ pop() หาง (เดิน) หรือไม่ pop (โต) :76,83,85 ตายเมื่อชนกำแพง/ตัวเอง :71:51,65 แล้วเพิ่มสองฟีเจอร์ที่ห่อ รอบนอก ลูป: เมนูก่อนเริ่ม :76-90 และ best_score = max(...) ตอนกิน/ตอนตาย :112,119อยากให้จับจังหวะนี้ให้ได้ เกมเต็มไม่ได้เกิดจากการเขียนทั้งเกมทีเดียว แต่เกิดจากการต่อทีละก้อนบนของเดิมที่รันได้แล้ว — นี่คือวิธีที่เราจะสร้างทุกเกมที่เหลือของคอร์ส
ที่มา ย้อนกลับไปสไลด์ "เกร็ด: ทำไม embedded สำคัญ — NES sprite limit" เราเห็นว่า NES วาดได้แค่ 64 sprite เป็น hardware limit จริง วิศวกรสมัยนั้นจึงต้องหมุนเวียนของที่มีแทนการขอเพิ่ม พอมาถึงสไลด์ "ภาพในใจของ pool — กฎ reuse-don't-create" เราก็เจอเพดานเดียวกันบนบอร์ดของเรา คือ 32 widget (UI_MAX_WIDGETS) game.pool ในเฉลยวันนี้จึงไม่ใช่ลูกเล่น แต่คือคำตอบตรง ๆ ของข้อจำกัดฮาร์ดแวร์ที่มีมาตั้งแต่ยุค NES: จองล่วงหน้า แล้วหยิบมาใช้ซ้ำ
ที่ไป สไลด์สุดท้ายเพิ่งบอกว่าคาบหน้าเป็น Flappy Bird ลองคิดล่วงหน้า นกที่ขยับปีกคือ head_sprite.frame(...) ที่เราเพิ่งเขียน แค่เปลี่ยนจาก "เลือกเฟรมตามทิศ" เป็น "เลือกเฟรมตามจังหวะปีก" และเกมยิงที่มีศัตรู/กระสุนเป็นสิบ ก็คือ game.pool ก้อนเดิม เปลี่ยนจากปล้องงูเป็นกระสุน ทักษะเล็ก ๆ วันนี้คืออิฐก้อนหนึ่งที่จะวางซ้ำไปทั้งคอร์ส
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
body_pool[pool_index].show()/hide() ใน redraw() ได้ไหม ถ้าเปลี่ยนปล้องงูเป็นกระสุนบนจอ โค้ดจะต่างจากเดิมมากไหม (ใบ้: เปลี่ยนแค่ชื่อรูปกับความหมาย ตรรกะ show/hide เท่าเดิม)ถ้าตอบได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อยากให้เกิด — pool กับ frame ไม่ใช่ของเฉพาะ Snake แต่เป็นอิฐก้อนเดียวกับที่ทุกเกมข้างหน้าจะใช้
เทคนิคสามอย่างของเฉลยวันนี้ไม่ใช่ของเล่นในห้องเรียน มันคือสิ่งที่ซอฟต์แวร์จริงใช้กันทุกวัน:
game.pool + show()/hide() เป๊ะ เพราะขอหน่วยความจำใหม่กลางเกมทำให้เฟรมกระตุกHEAD_SPRITE_FOR แทนหมุนหรือคำนวณภาพสด นี่คือ animation state machine เวอร์ชันเล็กที่สุดcol × CELL_PX — พิกัดตรรกะกับพิกัดจอเป็นคนละระบบเสมอห้ามมองว่า Snake เป็นแค่แบบฝึกหัด สามเทคนิคนี้คือของที่วิศวกรเกมและ embedded ใช้ทำมาหากินจริง เราแค่เจอมันในตัวที่เล็กพอจะเข้าใจทั้งหมดได้
เอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
DIFFICULTY_MODES เป็น list เพิ่ม tuple เข้าไป เมนู % len จะรับเองไหม ต้องแตะ choose_mode() อีกไหม)best_score หายทุกครั้งที่รีบูต จะเขียนค่าลงไฟล์บนบอร์ดแล้วอ่านกลับตอนเปิดเกมยังไง (นี่คือ persistence ที่ high-score table ของทุกเกมจริงมี)HEAD_SPRITE_FOR เป็นตารางเฟรมลำตัวที่ดูทั้งทิศเข้า/ออกของแต่ละปล้องได้ไหม (นี่คือสิ่งที่ snake_sprite_full.py ทำ)v += g นกตกเองทุกเฟรมแล้วเด้งขึ้นเมื่อกดปุ่ม ถ้าหัวงูวันนี้ขยับด้วย step คงที่ แต่นกพรุ่งนี้ต้องขยับด้วย ความเร็วที่เปลี่ยนทุกเฟรม โครง on_each_frame() เดิมจะรองรับได้ไหม ต้องเพิ่มตัวแปรอะไรเข้าไปบ้างเลือกมาสักข้อ เขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากเฉลยที่พิมพ์เองวันนี้ — ตรงนั้นคือจุดที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนพิมพ์ตาม
fit-css