pattern นี้มีชื่อจริงในวงการเกมว่า object pool — เตรียมของไว้จำนวนพอดี แล้วหยิบมาใช้ซ้ำ แทนที่จะสร้าง-ทิ้ง-สร้าง-ทิ้งตลอดเวลา

ถ้าเราสร้างท่อใหม่ทุกเฟรม (50 ครั้ง/วินาที) MicroPython ต้องจองหน่วยความจำใหม่เรื่อยๆ แล้วเก็บกวาด (garbage collect) ทีหลัง บอร์ดจะสะดุดเป็นจังหวะ การถือท่อ 3 ก้อนเดิมไว้แล้วแค่ขยับ (move_to) กับปรับขนาด (resize) เบากว่ามาก นี่คือเหตุผลที่เกมบนเครื่องเล็กๆ ทำแบบนี้กันหมด
ลองคิดเป็นตัวเลข — ท่อ 3 อัน × Box 2 ก้อน = 6 กล่อง ถ้าสร้างใหม่ทุกเฟรมที่ 50 fps:
300 ครั้ง/วินาที คือภาระที่ GC ต้องตามเก็บ — ส่วน pool จองครั้งเดียวจบ เหลือศูนย์ บอร์ดจึงลื่นไม่สะดุด
จากคาบที่แล้ว (ไม่เปลี่ยน) — แรงโน้มถ่วงสะสมเข้าความเร็ว, ความเร็วสะสมเข้าตำแหน่ง:
ดูกราฟ: เส้นแดง (ความเร็ว) เพิ่มเป็นเส้นตรง เพราะบวก g คงที่ทุกเฟรม ส่วนเส้นฟ้า (ตำแหน่ง y) โค้งลงเร็วขึ้น เพราะสะสมความเร็วที่โตขึ้นเรื่อยๆ — จุดที่เส้นแดงดิ่งลงคือตอนกดกระพือ v เด้งเป็น -7.4:

ค่าคงที่ ใหม่ของท่อ คาบนี้:
| ค่า | ความหมาย |
|---|---|
SPEED = 3.4 |
ท่อเลื่อนซ้ายต่อเฟรม (px) |
GAP = 152 |
ความสูงช่องที่นกลอด |
PIPE_WIDTH = 64 |
ความกว้างท่อ |
SPACING = 270 |
ระยะห่างระหว่างท่อ |
GRAVITY/FLAP เป็นค่าเดิมจาก
full_games/flappy_full.pyไม่ต้องแก้ — สมการสะสมbird_velocity += GRAVITY; bird_y += bird_velocityอยู่ที่flappy_step4.py:52-53(Euler integration)
gap_top คือ ขอบบนของช่องว่าง ที่นกจะลอด เราสุ่มมัน แต่ห้ามชิดขอบเกินไป ไม่งั้นช่องจะโผล่ครึ่งจอ:
ขอบล่าง 60 กันไม่ให้ช่องติดบนสุด ขอบบน 186 เผื่อพื้นที่ GAP (152) + ขอบล่างอีก 60 ให้ช่องอยู่ในจอเสมอ แถบเขียวคือค่าที่ random.randint หยิบได้ — ในโค้ดจริงคือ random.randint(60, game.HEIGHT - 60 - GAP) ที่ flappy_step4.py:37 (ตอนสร้างท่อ) และ :64 (ตอน recycle สุ่มช่องใหม่):

อยู่ นอก on_each_frame (สร้างครั้งเดียวพอ):
pipes = []
def add_pipe(start_x):
gap_top = random.randint(60, game.HEIGHT - 60 - GAP) # ขอบบนของช่อง
pipes.append({
"x": float(start_x), "gap_top": gap_top, "scored": False, # ธง scored: กันนับแต้มซ้ำท่อเดิม (flappy_step4.py:39)
"top": game.Box(start_x, 0, PIPE_WIDTH, gap_top, game.GB_DARK),
"bot": game.Box(start_x, gap_top + GAP, PIPE_WIDTH,
game.HEIGHT - gap_top - GAP, game.GB_DARK),
})
for index in range(3):
add_pipe(game.WIDTH + index * SPACING) # วาง 3 ท่อ นอกจอด้านขวา
70% core:
game.Box()สร้างท่อ 6 กล่องให้แล้ว
ท่อ 1 อันเก็บเป็น dict 1 ก้อน แต่บนจอคือ Box สองกล่อง — กล่องบนกินจาก y=0 ถึง gap_top กล่องล่างต่อจาก gap_top + GAP ลงไปจนสุดจอ ดูพิกัดให้ตรงกับ key ในโค้ด:
จับตาท่อสีส้ม — มันคือท่อที่เพิ่งหลุดขอบซ้าย แล้วถูกส่งกลับไปต่อท้ายขวาสุดพร้อมช่องใหม่ ตา 3 ท่อนี้สลับกันเป็นตัวส้มไปเรื่อยๆ ไม่มีท่อตัวที่ 4 เกิดขึ้นเลย

โค้ดหน้าถัดไปทำสองอย่างนี้: (1) เลื่อนทุกท่อซ้าย (2) ท่อไหนพ้นขอบซ้าย ย้ายไปขวาสุด + สุ่มช่องใหม่
อยู่ใน on_each_frame:
for pipe in pipes:
pipe["x"] -= SPEED # เลื่อนซ้าย
if pipe["x"] < -PIPE_WIDTH: # เลยขอบซ้าย → ไปขวาสุด
pipe["x"] = max(other["x"] for other in pipes) + SPACING
pipe["gap_top"] = random.randint(60, game.HEIGHT - 60 - GAP)
pipe["scored"] = False # รีเซ็ตธง: ท่อที่วนกลับมาให้แต้มใหม่ได้ (flappy_step4.py:65)
pipe["top"].resize(PIPE_WIDTH, pipe["gap_top"])
pipe["bot"].resize(PIPE_WIDTH, game.HEIGHT - pipe["gap_top"] - GAP)
pipe["top"].move_to(pipe["x"], 0)
pipe["bot"].move_to(pipe["x"], pipe["gap_top"] + GAP)
ผลที่ต้องเห็น: ท่อสีเขียวเข้ม 3 ท่อเลื่อนขวาไปซ้าย วนไม่รู้จบ ตอนนี้ยังชนไม่ตาย และยังไม่นับแต้ม
ช่องว่างให้น้องเติมเองอยู่ที่
practise_codes/flappy_step3.py:60-69↗
game.hit(a, b) = กล่อง 2 อันซ้อนทับกันไหม (ทุกแกนต้องเหลื่อม):
verb
game.hit()ตัวเดียวกับที่เราใช้ตรวจหัวงูชนตัวเองในเกม Snake
4 อสมการด้านบนถูกยุบเหลือ
game.hit(bird, pipe["top"]) or game.hit(bird, pipe["bot"])บรรทัดเดียวที่flappy_step4.py:72— โค้ด AABB ตัวจริง (4 clause) อยู่ในเอนจินbentogame.py:355
# --- ตาน้อง ๆ เขียน 30% ---
if game.hit(bird, pipe["top"]) or game.hit(bird, pipe["bot"]):
game.sfx("fall")
game.Text("GAME OVER", 320, 180, game.RED); return False
if not pipe["scored"] and pipe["x"] + PIPE_WIDTH < bird.x:
pipe["scored"] = True
score += 1
game.sfx("point")
score_label.set(str(score))
scored ตั้งตอนสร้างท่อ + รีเซ็ต False ตอนท่อ recycle (ให้แต้มใหม่ได้) — ในไฟล์: ตั้งครั้งแรก flappy_step4.py:39, รีเซ็ตตอน recycle :65, เช็คก่อนบวก :75global score บรรทัดแรกของ on_each_frame70% core:
game.hit(),game.sfx("point"/"fall")
on_each_frame ทำงานยังไงจนจบหนึ่งเฟรมก่อนนี้เราเห็นโค้ดเป็นชิ้น ๆ (ฟิสิกส์ · เลื่อนท่อ · ชน · แต้ม) — ผังนี้คือ ทั้งเฟรมประกอบร่างกัน ตาม flappy_step4.py:47-79 เดินตามลูกศรได้เลย
ครึ่งแรก — ตัวนก (:49-58): อ่านปุ่ม → ฟิสิกส์ → เช็คขอบจอ → วาดนก
ครึ่งหลัง — วนทุกท่อ (:60-79): เลื่อน → ◇recycle? → ◇ชนท่อ? → ◇ผ่าน∧¬scored? +แต้ม แล้ววนท่อถัดไป
อ่านผังแล้วจะเห็นว่า หนึ่งเฟรมมีทางออกสองแบบ:
return False(ตกพื้น:55หรือชนท่อ:72→ จบรอบ) หรือวนจนครบทุกท่อแล้วให้game.run(:81) เรียกเฟรมใหม่ — นี่คือหัวใจของ game loop
เสียงในคาบนี้ — Flappy เรียกเสียงสองจังหวะ:
pointตอนลอดท่อได้แต้ม และfallตอนชนท่อจบรอบ (ส่วนflapคือเสียงกระพือจากคาบที่แล้ว) — กดเล่นฟังก่อนลงโค้ดได้เลย:
flappy_die (fall)
flappy_flap (flap)
ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer
ลองฟังเทียบกัน เสียง point สั้นและสว่างเป็นรางวัล (ดูรูปคลื่น: พลังกระจุกสั้น ๆ ด้านบน) ส่วน fall หนักและตกลงเป็นสัญญาณจบรอบ (รูปคลื่นยาวกว่า ลากลงด้านล่าง) คนเล่นแยกออกทันทีโดยไม่ต้องมองจอ นี่คือเหตุผลที่เกมผูกเสียงกับเหตุการณ์สำคัญ
วันนี้เราเขียน Flappy ได้ก็จริง แต่สองแนวคิดที่ใช้ คือเครื่องมือจริงของวิศวกรฝังตัว ไม่ใช่ลูกเล่นของเล่น
game.run(on_each_frame) เรียก callback ของเราทุกเฟรม โครงสร้างข้อมูล + การส่งฟังก์ชันไปให้คนอื่นเรียก คือพื้นฐานที่ใช้ทุกที่x < -PIPE_WIDTH ว่าหลุดจอแล้วค่อยจัดการ คือแนวคิด off-screen culling ที่เครื่องจริงใช้ประหยัดงานวาดLevel up your code with game programming design patterns: Object pool — Unity
เราจะไม่ใช้ exec(open(...)) หรือพิมพ์คำสั่งรันใน REPL — น้อง ๆ ทำแค่ 3 ขั้นนี้:
flappy_step3.py (หรือ flappy_step4.py) ใน BENTO IDE
ขวา: หน้าจอจริงของ BENTO IDE — ปุ่ม Program to Device สีเขียวอยู่มุมซ้ายบน (จับภาพจากคอร์ส © 2026 Wiroon Sriborrirux, BDH)
ถ้าจอไม่ขึ้นภาพ ลองกด Program ซ้ำ 1 ครั้ง หรือ power-cycle บอร์ด แล้วกด Program ใหม่
point และ เลขแต้มมุมบน +1fall จอขึ้น GAME OVER เกมหยุดfull_games/flappy_full.py

ภาพจำลองผลบนจอบอร์ด (ภาพ render ของคอร์ส) — เทียบหน้าจอจริงของน้องได้เลย
global score · ธง scored กันนับซ้ำกับดักอันดับ 1 เห็นชัดที่สุดเมื่อเทียบภาพ — สร้างท่อ ใน on_each_frame จอจะแน่นไปด้วยท่อ (ซ้าย) เทียบกับสร้าง 3 ท่อ ครั้งเดียวก่อนลูป (ขวา):
| อาการ | วิธีแก้ |
|---|---|
| จอเต็มไปด้วยท่อ + ช้า/ค้าง | สร้างท่อ ครั้งเดียวก่อนลูป — ใน frame ใช้ move_to/resize |
UnboundLocalError ตอน score += 1 | ใส่ global score บรรทัดแรกใน function |
| แต้มวิ่งรัวเป็นสิบทุกเฟรม | เช็ค if not pipe["scored"] ก่อนบวก |
| ท่อ recycle แล้วไม่ให้แต้มอีก | ตั้ง pipe["scored"]=False ตอน recycle |
| ชนแล้วไม่ตาย/ตายตลอด | ส่ง object box ทั้งก้อน ไม่ใช่พิกัด |
| ไม่ได้ยินเสียง | power-cycle บอร์ด 1 ครั้งแล้ว Program ใหม่ |
Acceptance checklist:
fall + GAME OVER + หยุดทำเอง 30% (ส่งงาน): เติมช่องที่เว้นไว้ใน practise_codes/flappy_step3.py↗ ให้ครบ แล้วต่อด้วยกติกาชน/นับแต้มตาม flappy_step4.py · โชว์อาจารย์เล่น 1 รอบ (แต้ม ≥ 1 แล้วชนจบ)
Stretch: ลด
GAPให้ช่องแคบลง หรือเพิ่มSPEEDให้ท่อมาเร็วขึ้น แล้วสังเกตว่าความรู้สึกตอนเล่นต่างกันยังไง · ใครพร้อมไปต่อ ลองทำ delta-time + เมนูเลือกโหมดดู เดี๋ยวเฉลยพร้อมกันในห้อง
เฉลยชุดนี้มีไว้ให้ เทียบ ไม่ได้มีไว้ให้ลอกวางส่ง คะแนนคาบนี้อยู่ที่ใบงานกับตอนที่น้องอธิบายเกมด้วยคำพูดตัวเอง ไม่ใช่ที่โค้ดตรงกับเฉลย วิธีใช้ให้ได้ของจริงคือ ลองเติมช่องว่างเองก่อนอย่างน้อย 15 นาที ติดตรงไหนค่อยเปิดเฉลย อ่านให้เข้าใจแล้วปิดไฟล์ กลับไปพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้
คาบนี้เราแกะสองไฟล์ที่ต่อกัน: flappy_step2.py (ฟิสิกส์ของนก) แล้วต่อด้วย flappy_step3.py (ท่อเลื่อน + recycle) ก่อนดูโค้ด ดูค่าคงที่ทั้งชุดของ flappy_step3.py ก่อน มันคือหน้าตัวเลขของทั้งเกม:
GRAVITY = 0.45 # แรงโน้มถ่วง
FLAP = -7.4 # แรงกระพือ
SPEED = 3.4 # ความเร็วท่อเลื่อนซ้าย (flappy_full.py:8)
GAP = 152 # ช่องว่างระหว่างท่อบน-ล่าง (ที่นกลอด)
PIPE_WIDTH = 64 # ความกว้างของท่อ
SPACING = 270 # ระยะห่างระหว่างท่อแต่ละท่อ
GRAVITY/FLAP เป็นค่าเดิมจากคาบก่อน (:20-21) ส่วน SPEED/GAP/PIPE_WIDTH/SPACING เป็นของท่อคาบนี้ (:22-25) — พอเก็บเป็นชื่อไว้ที่เดียว น้องปรับ ความรู้สึกของเกม ได้โดยไม่ต้องไล่แก้กลางลูปPIPE_WIDTH ใช้ทั้งตอนสร้าง ตอน resize และตอนเช็คหลุดจอ) ถ้า hard-code เลข 64 กระจายไว้ แก้ทีต้องไล่ทุกจุด นี่คือนิสัย "ตัวเลขมหัศจรรย์ต้องมีชื่อ" ที่เราจะถือไปทั้งคอร์สGAP = 152 กับ PIPE_WIDTH = 64 คือค่าที่ทำให้สมการสุ่มช่องในสไลด์ "อ่านสมการสุ่มช่อง" ออกมาเป็นช่วง [60, 186] พอดี ถ้าน้องขยับสองค่านี้ ช่วงสุ่มก็ขยับตามค่าคงที่ที่ตั้งชื่อดี ๆ ครึ่งหนึ่งของความเข้าใจโค้ดอยู่ตรงนี้แล้ว ยังไม่ทันดูตรรกะเลยด้วยซ้ำ
flappy_step2.py — ฟิสิกส์ของนกในไม่กี่บรรทัดหัวใจของ Flappy ทั้งเกมอยู่ในบล็อกนี้ อ่านทีละบรรทัดแล้วจะเห็นว่ามันไม่ได้ลึกลับเลย:
def on_each_frame():
global bird_y, bird_velocity
# ----- เติมส่วนนี้เอง: ฟิสิกส์ของ flappy -----
keys = game.keys()
if keys.a or keys.up:
bird_velocity = FLAP # กระพือ = ดีดขึ้น
game.sfx("flap")
bird_velocity += GRAVITY # โน้มถ่วงดึงลงทุกเฟรม
bird_y += bird_velocity # เลื่อนนกตามความเร็ว
if bird_y < 0: # ชนเพดานบน → หยุดความเร็ว
bird_y, bird_velocity = 0, 0
if bird_y > game.HEIGHT - bird.h: # ตกพื้น → จบเกม
game.Text("GAME OVER", 320, 180, game.RED)
return False
bird.move_to(bird.x, bird_y)
:35,37,38): ถ้ากด ปุ่มเซ็ต bird_velocity = FLAP ก่อน แล้วค่อย += GRAVITY แล้วค่อยเอา v ไปบวกเข้า bird_y นี่คือ Euler integration ที่สไลด์ "ค่าคงที่ฟิสิกส์" เขียนเป็น v += g; y += v เป๊ะ สลับลำดับเมื่อไหร่นกจะขยับเพี้ยนไปหนึ่งเฟรมglobal bird_y, bird_velocity (:31) ต้องมี เพราะ callback นี้ไป แก้ค่าตัวแปรระดับโมดูล ถ้าลืมจะโดน UnboundLocalError ทันที ตรงกับกับดักที่สไลด์ "novice traps" เตือนเรื่อง global score:39-40) แค่ตรึง v,y = 0 ให้เล่นต่อ แต่ตกพื้น (:41-43) คือกติกาแพ้ return False จบรอบ — กำแพงกันหลุดจอ ต่างจากเส้นตายgame.sfx("flap") (:36) อยู่ ในกิ่ง if ไม่ใช่ทุกเฟรม เสียงจึงดังเฉพาะตอนกดจริง เตรียมทางให้เสียง point/fall ในสไลด์ "เสียงสองจังหวะ" ที่ผูกเสียงกับเหตุการณ์เหมือนกันนกที่ "รู้สึกมีน้ำหนัก" ไม่ได้มาจากสูตรฟิสิกส์หรูหรา มาจากบวกเลขสองบรรทัดในลำดับที่ถูก — ของจริงมักเรียบง่ายกว่าที่กลัว
flappy_step3.py — สร้างท่อ "ครั้งเดียว" เก็บใน listบล็อกนี้อยู่ นอก on_each_frame ตั้งใจให้รันตอนเริ่มโปรแกรมครั้งเดียว:
# ----- เติมส่วนนี้เอง: สร้างท่อครั้งเดียว แล้วหมุนเวียนใช้ -----
pipes = []
def add_pipe(start_x):
gap_top = random.randint(60, game.HEIGHT - 60 - GAP) # ขอบบนของช่องว่าง
pipes.append({
"x": float(start_x), "gap_top": gap_top,
"top": game.Box(start_x, 0, PIPE_WIDTH, gap_top, game.GB_DARK), # ท่อบน
"bot": game.Box(start_x, gap_top + GAP, PIPE_WIDTH,
game.HEIGHT - gap_top - GAP, game.GB_DARK), # ท่อล่าง
})
for index in range(3):
add_pipe(game.WIDTH + index * SPACING) # วางท่อ 3 ท่อ เริ่มนอกจอด้านขวา
game.Box() ถูกเรียกที่นี่ที่เดียว (:41-43) รวม 6 กล่องตลอดทั้งเกม ตรงกับสไลด์ "ทำไมต้องใช้ซ้ำ ไม่สร้างใหม่" — object pool จองครั้งเดียว เหลือ 0 การจอง/วินาที แทนที่จะ 300 การจอง/วินาที ที่ GC ต้องตามเก็บ"x", "gap_top", "top", "bot" แล้วเอาหลายก้อนใส่ pipes (list) — คือ list of dict ในสไลด์ "5 แนวคิดของคาบนี้" เป๊ะ เก็บ Box object ไว้ใน dict เพราะเดี๋ยวต้องเรียก move_to/resize มันซ้ำrandom.randint(60, game.HEIGHT - 60 - GAP) (:38) คือสมการที่สไลด์ "อ่านสมการสุ่มช่อง" คำนวณไว้แล้วว่าออกมาเป็น [60, 186] — ขอบล่าง 60 กันช่องติดบนสุด ขอบบนเผื่อ GAP + 60 ให้ช่องอยู่ในจอเสมอfor index in range(3) กับ game.WIDTH + index * SPACING (:45-46) วางท่อสามอันเริ่มนอกจอด้านขวา เว้น SPACING เท่ากัน คนเล่นเลยเห็นท่อทยอยไหลเข้ามาเป็นจังหวะสังเกตว่าโค้ดสร้างของแยกออกจากโค้ดขยับของชัดเจน — "สร้างครั้งเดียว" อยู่นอกลูป "ขยับทุกเฟรม" อยู่ในลูป เส้นแบ่งนี้คือหัวใจที่ทำให้บอร์ดเล็ก ๆ วิ่งลื่น
flappy_step3.py — เลื่อน + recycle แล้วมองภาพรวมบล็อกนี้อยู่ ใน on_each_frame ทำงานทุกเฟรม:
# ----- เติมส่วนนี้เอง: เลื่อนท่อ + หมุนเวียนใช้ซ้ำ -----
for pipe in pipes:
pipe["x"] -= SPEED # เลื่อนซ้าย
if pipe["x"] < -PIPE_WIDTH: # เลยขอบซ้าย ก็ย้ายไปขวาสุด
pipe["x"] = max(other["x"] for other in pipes) + SPACING
pipe["gap_top"] = random.randint(60, game.HEIGHT - 60 - GAP)
pipe["top"].resize(PIPE_WIDTH, pipe["gap_top"])
pipe["bot"].resize(PIPE_WIDTH, game.HEIGHT - pipe["gap_top"] - GAP)
pipe["top"].move_to(pipe["x"], 0)
pipe["bot"].move_to(pipe["x"], pipe["gap_top"] + GAP)
pipe["x"] -= SPEED (:62) เลื่อนซ้ายทุกเฟรม เกณฑ์ -PIPE_WIDTH (:63) รอให้ท่อพ้นจอ ทั้งก้อน ก่อนค่อย recycle นี่คือ off-screen culling ที่สไลด์ "เชื่อมโยงรากฐาน" พูดถึง — ไม่จัดการของที่ยังโผล่ค้างขอบจอmax(other["x"] for other in pipes) + SPACING (:64) ต่อท้ายท่อที่ ขวาสุดในขณะนั้น ไม่ใช่ WIDTH ตายตัว ทำให้ระยะห่างสม่ำเสมอไม่ว่าท่อไหนหลุดก่อน ตรงกับ mental model สไลด์ "ท่อ 3 อันเดิม วิ่งวนซ้ำ"resize (:66-67) ไม่ใช่สร้าง Box ใหม่ — ก้อนเดิม เปลี่ยนขนาดตามช่องที่เพิ่งสุ่ม นี่คือใจความของ object pool ทั้งอันmove_to ล่าง (:68-69) อยู่ นอก if ทุกท่อจึงขยับทุกเฟรมเสมอ ไม่ใช่เฉพาะตอน recycleโครงคาบนี้ต่อยอดกันเป็นชั้น ๆ อย่างนี้:
| ชิ้น | ไฟล์:บรรทัด | แนวคิดใหม่ที่เพิ่ม | ของเดิมที่เอามาต่อ |
|---|---|---|---|
| กระพือ | step2:34-36 |
อ่าน input → เปลี่ยน v |
game.keys() คาบก่อน |
| ฟิสิกส์นก | step2:37-38 |
Euler v+=g; y+=v |
ลูป on_each_frame |
| สร้างท่อ | step3:37-46 |
list of dict + create once | game.Box() คาบก่อน |
| recycle | step3:62-67 |
object pool + สุ่มช่องใหม่ | ท่อเดิม move_to/resize |
จับจังหวะการไต่ระดับนี้ไว้: เริ่มจากนกที่ตกได้ เติมกระพือ เติมท่อ เติม recycle — ไม่มีขั้นไหนกระโดด เกมทั้งเกมโตขึ้นจากของเล็กที่รันได้เสมอ
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่องว่าท่อ 3 อันที่วนซ้ำ จริง ๆ แล้วมันคือชิ้นส่วนของความคิดใหญ่แค่ไหน
ที่มา — ความคิด "ถือของจำนวนคงที่แล้วใช้ซ้ำ" ไม่ได้เกิดเพราะเท่ แต่เกิดเพราะข้อจำกัด ย้อนไปสไลด์ "ทำไมต้องใช้ซ้ำ ไม่สร้างใหม่" เราคำนวณกันว่า ถ้าสร้างท่อใหม่ทุกเฟรมจะเป็น 300 การจองหน่วยความจำต่อวินาที ที่ GC ต้องตามเก็บจนบอร์ดสะดุด object pool เกิดจากตรงนั้น และฟิสิกส์นกในสไลด์ "ค่าคงที่ฟิสิกส์" ก็มาจากเหตุผลเดียวกัน คือทำให้ถูกด้วยการบวกเลขเบา ๆ ทุกเฟรมแทนการคำนวณหนัก
ที่ไป — สไลด์ "เชื่อมโยงรากฐาน" เพิ่งบอกใบ้ไว้ว่า ท่อ 3 อันวนซ้ำคือหน้าตาเดียวกับ ring buffer ที่รับสตรีมจาก UART/ADC บน RAM จำกัด พอน้องเข้าใจ recycle วันนี้ น้องเข้าใจ ring buffer ไปครึ่งทางแล้ว และ pool อันเดียวกันนี้จะกลายเป็น "บ่อกระสุน" หรือ "บ่อศัตรู" ในเกมที่ใหญ่ขึ้น ส่วนฟิสิกส์ v+=g; y+=v จะถูกยกไปทำ delta-time ในคาบหน้า
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
add_pipe/recycle จะเปลี่ยนไปมากไหม (ใบ้: เปลี่ยนแค่ชื่อกับสิ่งที่เก็บใน dict)pipe["x"] -= SPEED ทุกเฟรม กับ bird_velocity += GRAVITY ทุกเฟรม มันคือเรื่องเดียวกัน — "ขยับของทีละนิดทุกเฟรม" ที่คาบหน้าจะถามต่อว่า ถ้าเฟรมช้าลงล่ะถ้าตอบได้ว่า "อ๋อ recycle กับ ring buffer มันคืออันเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด ของที่ดูเป็นแค่ท่อในเกม จริง ๆ คือวิธีที่วิศวกรจัดการหน่วยความจำจำกัดมาหลายสิบปี
เทคนิคสามอย่างของคาบนี้ ไม่ใช่ลูกเล่นของเล่น มันคือของที่เครื่องจริงใช้กันทุกวัน:
v += a·dt; x += v·dt คือหัวใจของ dead-reckoning ตรงกับ bird_velocity += GRAVITY; bird_y += bird_velocity ของนกเราrandom.randint(60, ...) ที่สุ่ม gap_top ใหม่ทุกครั้งที่ท่อวนกลับทั้งสี่ตัวอย่างไม่มีอันไหนเป็นของสมมติ ท่อในเกมของน้องวันนี้ ใช้หลักการเดียวกับบัฟเฟอร์ในเราเตอร์ กับตัวประมาณตำแหน่งในโดรน
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
add_pipe + recycle มาทำ "บ่อศัตรู 10 ตัว" ในเกมยิง ต้องเปลี่ยนอะไรใน dict บ้าง แล้วเงื่อนไข "หลุดจอ" ควรเป็นด้านไหนgap_top สุ่มอิสระทุกท่อ ท่อติดกันอาจกระโดดจากบนสุดไปล่างสุดจนเล่นไม่ทัน จะจำกัดช่วงสุ่มให้อิงท่อก่อนหน้ายังไง ให้ยากแต่ยุติธรรมgame.run(..., fps=50) ถ้าบอร์ดช้าลงเหลือ 30 fps นกจะตกช้าลงเพราะบวก GRAVITY น้อยครั้งลง คาบหน้าเราจะทำ delta-time ให้ความเร็วคงที่ไม่ว่าเฟรมเร็วช้า ลองคิดล่วงหน้า: SPEED กับ GRAVITY ควรคูณด้วยอะไรในแต่ละเฟรมเลือกมาสักข้อ เขียนไอเดียของน้องลงใบงานด้วยคำพูดตัวเอง ยังไม่ต้องเขียนโค้ดครบก็ได้ ขอแค่ออกแบบให้เห็นภาพ — ตรงนั้นแหละคือคะแนน และคือของที่จะติดตัวไปคาบหน้า
fit-css
Marp deck — Fundamental of Embedded Systems Developer II (MicroPython-only, 13 คาบ) คาบ 9 · Flappy #2: ท่อเลื่อน + ชน + แต้ม Render: npx @marp-team/marp-cli session-09.md -o session-09.html GUI-first. ภาพจริงบนบอร์ดฝังผ่าน img/.
CAPTURE: flappy_score3.png — (อัปเดตด้วยภาพจริง) รัน flappy_step4.py บนบอร์ด, บินลอดท่อ 3 รอบให้แต้มมุมบนขึ้น "3" แล้วถ่ายมาแทนภาพ render
CAPTURE: flappy_gameover.png — บินชนท่อให้ขึ้น GAME OVER สีแดงบนจอบอร์ด แล้วถ่าย