ดูทางขวาเป็นจอเกม Catch ย่อ ๆ ทุก ๆ เฟรมวงวนเดิมจะทำ 3 ขั้นไล่กัน: อ่านจอย → คิดกฎเกมใน update() → วาดลงจอ พอครบก็ขึ้นเฟรมใหม่ (ดูเลข frame ที่เพิ่มขึ้น) ของตกมาเรื่อย ๆ พอตะกร้ารับโดน คะแนนถึงค่อยขยับ

สังเกตว่า
update()ของเราคือขั้นเดียวในสามขั้น ที่เหลือ Core ทำให้ และ state อย่างscoreอยู่ข้ามเฟรมได้เพราะเราเก็บไว้เป็นตัวแปร ไม่ได้สร้างใหม่ทุกรอบ
game.run(update, fps=30) คือ game loop ที่ Core เตรียมไว้ให้ มันจะ:
update() ของเรา 1 ครั้งต่อเฟรม (เราใส่กฎเกมตรงนี้)update() จะคืนค่า False แล้วลูปจึงหยุด
fps=30 Core จะคุมให้ทุกเฟรมห่างกันเท่ากัน เกมจึงไม่เร็ว/ช้าตามบอร์ด เราคิดเป็น "ต่อเฟรม" ได้สบาย ๆ
สมการ แปลเป็นกราฟได้แบบนี้ เส้นโค้งบอกว่า fps ยิ่งสูง เวลาต่อเฟรมยิ่งน้อย:

fps=30 ที่เราใช้ในคาบนี้ มีเวลาราว 33 ms ต่อเฟรม ให้ update() ทำงานให้เสร็จupdate() ให้หนักจนใช้เกิน 33 ms เฟรมจะหล่น เกมจะ "กระตุก" — เป็นเหตุผลที่เราเลี่ยงสร้าง object ใหม่ทุกเฟรมขึ้น
fps=60ก็ลื่นขึ้น แต่เวลาเหลือต่อเฟรมเหลือแค่ ~17 ms งานต่อเฟรมต้องเบาลงครึ่งหนึ่ง เป็นการแลกกันเสมอ
start · Box · Textkeys · hit · run · sfx
score, framesdef update()
ตัวเลข 7:3 ไม่ใช่แค่สัดส่วนงาน แต่ผูกกับ เวลาต่อเฟรม ~33 ms ที่เพิ่งคิดในสไลด์ก่อน — Core กินงานหนัก (read/draw) ไป 7 ส่วน เหลือ 3 ส่วนให้ update() ของเราทำให้ทันใน budget เกมจึงไม่กระตุก
จำไว้ให้ดี: verb ชุดนี้คือ Core 70% ชุดเดียวกันในทุกเกม เป็นแกนกลางของทั้งคอร์ส เราเรียนรอบเดียวแต่ใช้ได้ครบทั้งคอร์ส (Catch วันนี้ · Snake · Flappy · Pong · Shooter คาบถัด ๆ ไป)
NES มีชิป PPU แยกต่างหากไว้วาดภาพ ปล่อยให้ CPU (Ricoh 2A03) คิด logic เกม — เป็นต้นแบบของ "CPU + coprocessor วาดภาพ"

เชื่อมกับวันนี้: โมเดล 70/30 ของเราคือเรื่องเดียวกัน — C core (เหมือน PPU) วาด, Python (เหมือน CPU) คิด
game.run() ส่งคำสั่งวาดข้ามไปให้ M55 ผ่าน IPC (กล่องจดหมายในชิป) เหมือน CPU 2A03 ส่ง frame ให้ PPU เป๊ะ
ที่มา: "NES-Console-Set" — Evan-Amos, Public domain, Wikimedia Commons
| verb | หน้าที่ | ใช้ใน Catch |
|---|---|---|
game.start() |
เคลียร์จอ + ปลุกจอย | เรียกครั้งเดียวบนสุด |
game.Box(x,y,w,h,c) |
สี่เหลี่ยมบนจอ | ตะกร้า + ของ |
game.Text(s,x,y,c) |
ข้อความ + .set() |
ป้ายคะแนน |
game.keys() |
อ่านจอย | k.left/.right |
game.hit(a,b) |
ชนกันไหม (AABB) | ตะกร้าโดนของ |
game.run(fn,fps) |
เรียก update() ทุกเฟรม |
ลูปเกม |
verb ทั้งหมดอยู่ใน full_games/bentogame.py เปิดอ่านได้ แต่ไม่ต้องแก้ — เราแค่เรียกใช้
game.hit(a, b) คืนค่า True เมื่อสองกล่อง ทับกันทุกแกน:
game.hit(basket,item) เป็นจริงscore += 1 แล้วย้ายของชิ้นใหม่ทันทีโครงนี้ตรงกับเกมเต็มทุกตัวใน full_games/ (Snake, Flappy, Pong, Shooter) เราเรียนกับ Catch รอบเดียว ที่เหลือใช้โครงเดิม
verb 70% (start/Box/Text/keys/hit/run) อยู่รอบนอกอยู่แล้ว เราเขียนแค่ตรรกะใน
update()เท่านั้น
import bentogame as game
import random
game.title("CATCH") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
basket = game.Box(360, 360, 90, 20, game.GB_LIGHT) # ตะกร้า ล่างจอ
item = game.Box(380, 0, 24, 24, game.GB_LIGHTEST) # ของ เริ่มบนสุด
score_text = game.Text("Score: 0", 10, 8, game.WHITE) # ป้ายคะแนน
score = 0
frames = 0
TOTAL = 30 * 30 # 30 วิ × 30 fps = 900 เฟรม
update() วาดได้แค่ภาพ "ตั้งฉาก": ของ (item) อยู่บนสุด Box(380,0,...), ตะกร้า (basket) ล่างจอ Box(360,360,...), ป้าย Score: 0 มุมซ้ายบน — ทุกอย่างนิ่ง รอ Step 2 ค่อยขยับ
Box กับ Text ต้องสร้าง นอก update() เพียงครั้งเดียว ถ้าไปสร้างในลูป จะได้ของใหม่ทุกเฟรม จนชน sprite ceiling (~32) แล้วจอเต็มหรือค้าง
def update():
global score, frames
k = game.keys()
if k.left: basket.move(-12, 0) # ความเร็วคงที่ (เวอร์ชันสอนแบบย่อ)
if k.right: basket.move(12, 0) # move() clamp ขอบจออัตโนมัติ
# Back = ออกกลับ Playground · Start = เริ่มใหม่ — game.run() จัดการให้เอง
item.move_to(item.x, item.y + 8) # ของตกลง 8 px ต่อเฟรม
if item.y > game.HEIGHT: # ตกพ้นล่างจอ = พลาด
item.move_to(random.randint(0, game.WIDTH - 24), 0)
game.run(update, fps=30)
update() ใน Step 2 ทำทุกเฟรม: ตะกร้าเลื่อนตามจอย (basket.move(±12,0)) และ ของตกลง 8 px/เฟรม (item.move_to(x, y+8)) พอ item.y > HEIGHT ก็ โผล่ที่สุ่มใหม่บนสุด — ภาพเคลื่อนไหวซ้ายช่วยให้เห็น "การเคลื่อน" ก่อนอ่านโค้ด
±12 เพื่อให้อ่านง่ายก่อน ส่วนฉบับเฉลยที่จะเปิดในห้องต่อยอดเป็น ยิ่งกดค้างนานยิ่งเร็ว (BASE_SPEED=6 ไต่ขึ้นไปถึง MAX_SPEED=30 ผ่านตัวนับ hold_frames) น้อง ๆ เทียบทีละบรรทัดได้ใน Step 5
ฉบับเฉลย (solution_codes/s04_catch.py:34-41) แทนความเร็วคงที่ ±12 ด้วยตัวนับ hold_frames — โครงเดียวกับคาบ 3 เป๊ะ แต่ใช้แค่ซ้าย/ขวา:
pressed = game.keys() # :34
if pressed.left or pressed.right:
hold_frames += 1 # :36 กดค้าง → สะสมขึ้นเรื่อย ๆ
else:
hold_frames = 0 # :38 ปล่อย → รีเซ็ต
speed = min(BASE_SPEED + hold_frames, MAX_SPEED) # :39 เพดาน MAX_SPEED
if pressed.left: basket.move(-speed, 0) # :40
if pressed.right: basket.move(speed, 0) # :41
BASE_SPEED=6 → MAX_SPEED=30 (:25-26): แตะเบา ๆ ขยับละเอียด กดค้างนานพอก็พุ่งเต็มสปีดmove() ยัง clamp ขอบจอให้ฟรี ตะกร้าจึงไม่หลุดจอเทียบ
±12คงที่ กับ ramp นี้ จะเห็นว่า "กดค้างยิ่งเร็ว" ทำให้คุมตะกร้าได้ทั้งละเอียด (แตะเบา) และเร็ว (กดค้าง) ในปุ่มเดียว
item.move_to(item.x, item.y + 8) # เก็บ x เดิม เพิ่ม y ทีละ 8
if item.y > game.HEIGHT: # พ้นจอล่าง
item.move_to(random.randint(0, game.WIDTH - 24), 0) # โผล่ที่ใหม่ บนสุด
การตกคือการเลื่อนตำแหน่งด้วยความเร็วคงที่ทุกเฟรม:

move_to ไม่ใช้ move: move() จะ clamp ไม่ให้หลุดขอบจอ ทำให้ item.y ไม่มีวันเกิน HEIGHT เงื่อนไข respawn เลยไม่ทำงาน เราจึงใช้ move_to ที่ปล่อยให้ y เลยจอล่างได้จริง
if game.hit(basket, item): # ตะกร้าโดนของ?
score += 1
score_text.set("Score: %d" % score) # อัปเดตป้าย (ตัวเดิม ไม่สร้างใหม่)
game.sfx("eat") # เสียง "ได้แต้ม"
item.move_to(random.randint(0, game.WIDTH - 24), 0) # ของชิ้นใหม่
Score: 5 ขณะตะกร้าทับของ — พื้นที่แดงคือ AABB ที่ game.hit เป็นจริง) นี่คือเกมจริงเกมแรกที่ทีมของน้อง ๆ ทำได้เอง
ย้าย
item.move_to(...)ทันทีหลังชน ไม่งั้นของจะยังทับตะกร้าอยู่อีกหลายเฟรม แล้วจะนับซ้ำรัว ๆ
เกม Catch เรียกเสียงอยู่ 2 จังหวะ: ตอนตะกร้ารับของได้ game.sfx("eat") เป็นเสียง "ติ๊ง" สั้น ๆ ให้รู้ว่าได้แต้ม และตอนหมดเวลา 30 วิ game.sfx("gameover") ปิดท้ายรอบ
เสียงในคาบนี้ — Catch ใช้ "ติ๊ง" ตอนรับของได้ (
sfx("eat")) และเสียงจบรอบตอนหมดเวลา (sfx("gameover")) (กดเล่นฟังได้จริง):

gameover (จบรอบ) — ยาว ค่อย ๆ จาง
รูปคลื่นต่างกันชัด: "eat" คลื่น square สั้น เด้งเร็ว · "gameover" ยาวกว่าแล้วค่อยจางเสียง
sfx()ทำงานบนบอร์ดจริงเท่านั้น คลิปด้านบนคือไฟล์เดียวกับที่เฟิร์มแวร์เล่น ไว้ฟังเทียบก่อนลง Program to Device
ป้าย Score เมื่อกี้เป็นภาษาอังกฤษ แต่เฟิร์มแวร์ v1.2.0 ทำให้เราใส่ ภาษาไทย บนจอได้แล้ว — ไม่ต้องตั้งค่าอะไรเพิ่ม แค่ส่งสตริงไทยเข้า game.Text ตรง ๆ
game.title("เกมเก็บของ") # ชื่อเกมเป็นไทยได้เลย
score_text = game.Text("คะแนน: 0", 10, 8, game.WHITE) # ป้ายคะแนนภาษาไทย
game.Text("กดจอยซ้าย-ขวา รับของให้ทัน", 230, 360, game.GB_LIGHTEST) # คำแนะนำ
อัปเดตป้ายไทยก็เหมือนเดิม:
score_text.set("คะแนน: %d" % score)— ป้ายตัวเดิม ไม่ต้องสร้างใหม่

ทดสอบจริงด้วยตัวเอง: เปลี่ยน
"Score: 0"ในเกม Catch ของเราเป็น"คะแนน: 0"แล้ว Program to Device ดู ป้ายจะกลายเป็นไทยทันที
game.Text ใช้ขนาดมาตรฐานขนาดเดียว (ไม่มีพารามิเตอร์ขนาด) ถ้าอยากได้ตัวเล็ก/ใหญ่ต่างกัน ให้ใช้ ui.Label ซึ่งมี value= ไว้กำหนด ขนาดตัวอักษร:
import ui
ui.Label("เล็ก 14", x=120, y=140, color=game.BLUE, value=14)
ui.Label("กลาง 20", x=120, y=180, color=game.BLUE, value=20)
ui.Label("ใหญ่ 28", x=120, y=230, color=game.YELLOW, value=28)
ui.Label("ปั๊มน้ำ", x=460, y=150, color=game.PINK, value=24) # คำยากก็ใหญ่ได้
value=14 ตัวเล็กสุด ไล่ขึ้นไปจน value=28 ตัวใหญ่สุด — เห็นความต่างชัด ขนาดที่บอร์ดรองรับ: value = 14 / 16 / 20 / 24 / 28 (เลขยิ่งมาก ตัวยิ่งใหญ่) — ทั้ง game.Text และ ui.Label วาดภาษาไทยได้เหมือนกัน ต่างกันแค่ ui.Label เลือกขนาดได้
ใช้ตัวใหญ่กับ "ชื่อเกม" หรือ "GAME OVER" ส่วนป้ายคะแนน/คำแนะนำใช้ตัวเล็กลง จะอ่านสบายตากว่า
เปิด practise_codes/s04_thai.py↗ ใน BENTO IDE — มีตัวอย่างป้ายไทยหลายขนาดให้ดูแล้ว เหลือ 2 ช่อง ที่เขียนว่า # เติม: ให้เราเติมเอง:
# เติม 1: เพิ่มป้ายไทยเป็นคำยากของเราเอง (เช่น "ผู้รู้" / "เปรี้ยว")
# ui.Label("...", x=460, y=250, color=game.ORANGE, value=24)
# เติม 2: กด A แล้วอัปเดตป้ายคะแนนให้เป็นเลขล่าสุด
# score.set("คะแนน: %d" % points) (ป้ายตัวเดิม ไม่สร้างใหม่)
เก็บทริกนี้ไว้ใช้ในเกม Catch (และทุกเกมต่อจากนี้) — เปลี่ยนป้ายอังกฤษเป็นไทยได้ทุกที่
frames += 1
if frames >= TOTAL: # TOTAL = 30 * 30 = 900 เฟรม
game.Text("GAME OVER", 320, 170, game.RED)
game.Text("Score: %d" % score, 330, 210, game.YELLOW)
game.sfx("gameover")
return False # หยุด game.run()
เราแปลงเวลาเป็นจำนวนเฟรมแบบนี้:
ระหว่าง PLAYING เราวนรับของและนับแต้มทุกเฟรม พอครบ 900 เฟรมก็เข้าสู่ GAME OVER แล้วจบลูป
ประกอบทุกก้อนเข้าด้วยกัน: หนึ่งเฟรมของ update() ทำ 4 งานเรียงกัน (คุมตะกร้า → ของตก → เช็กชน → นับเวลา) แล้ววนใหม่ จนตัวจับเวลาครบ 900 เฟรมจึงหลุดเข้าสถานะ GAME OVER:
จุดตัดสินใจ 3 จุด: ของพ้นจอ? (โผล่ใหม่), ชน? (บวกแต้ม), ครบเวลา? (จบรอบ) — ที่เหลือคือรับ input กับนับเฟรม สองสถานะของ FSM คือ PLAYING (วนอยู่) กับ GAME OVER (
return Falseหลุดลูป)
ไม่ต้อง build ไม่ต้อง flash อะไรเลย ทำตาม 3 ขั้น:
practise_codes/s04_catch.py↗ ใน BENTO IDE# TODO ให้ครบ (4 บล็อกใน update())
ลองปรับดู (ทั้งหมดนี้ยังอยู่ใน 30% ของเรา):
| ปรับอะไร | เปลี่ยนค่า |
|---|---|
| ของตกเร็วขึ้น | item.y + 8 → + 12 |
| ตะกร้ากว้าง/แคบ | 90 ใน game.Box(360,360,90,20,...) |
| เวลาเล่นสั้น/ยาว | 30 * 30 → 20 * 30 |
จดลง notes.md ว่าใน Catch อะไรคือ 70% ที่เราเรียกใช้ และอะไรคือ 30% ที่เราเขียนเอง ไล่ดูทีละ verb
NameError: score มักเพราะลืม global score, framesmove_to หลัง hit จึงนับซ้ำText ใหม่ทุกเฟรม ให้ใช้ .set แทนupdate()sfx() เงียบได้แต่ไม่ทำให้เกมพังเกมรับของที่ดูเล่น ๆ จริง ๆ แล้วซ้อนรากฐานที่เราจะใช้ต่อทั้งคอร์ส:
ฝั่ง Embedded / ระบบเรียลไทม์
ฝั่ง Python
update ให้ game.run() เรียกแทน — รูปแบบเดียวกับ event handlerฝั่ง Algorithms / คณิต-ฟิสิกส์
ฝั่ง Graphics
เห็นไหมว่าเกมเดียวร้อยรากฐานหลายด้านเข้าด้วยกัน นี่คือเหตุผลที่เรา "เรียนด้วยการเล่น" แล้วค่อยถอดออกมาเป็นวิศวกรรมจริง
Game Loops Explained in 5 Minutes (With Code) — Dylan Falconer · สรุป game loop พร้อมโค้ดใน 5 นาที แนวคิด read → update → draw เหมือนที่เราทำกับ game.run() เป๊ะ
สิ่งที่อยากให้ทำได้ก่อนเลิก (เช็คปากเปล่าได้ทั้งทีม)
game.run(update) วน read → update → draw จนกว่า update() คืน Falseupdate()งาน 30% ของน้อง ๆ (commit ก่อนเลิก)
# TODO ใน practise_codes/s04_catch.py↗ ให้เล่นจบเป็นรอบได้RED รับแล้ว +5 หรือ "ของห้ามรับ" ที่ score -= 2git add practise_codes/s04_catch.py notes.md
git commit -m "feat(s04): Catch game — game loop + hit + score + timer"
git push
คาบหน้า (คาบ 5): เราจะใช้โครง game loop เดิมทำเกมจริงตัวต่อไป คือ Pong #1 (ลูกบอลเด้ง — ฟิสิกส์การเคลื่อนที่)
ความเร็วเป็น เวกเตอร์ จะปูทาง: และการเด้ง = พลิกเครื่องหมายของแกนที่ชน
s04_catch.py — อ่านให้เข้าใจแล้วพิมพ์เองเฉลยเต็มฉบับนี้ไม่ได้มีไว้ให้ copy-paste ส่ง คะแนนของคาบนี้อยู่ที่ใบงานและการอธิบายด้วยคำพูดของน้องเอง โค้ดตรงกับเฉลยเป๊ะไม่ได้ช่วยอะไรเลย วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง — ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะทีละก้อน ไม่ใช่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ก้อนแรก — ตั้งของให้ครบก่อน แล้วค่อยเข้าลูป:
import bentogame as game
import random
game.title("CATCH") # หน้าเริ่ม: Start=เล่น Back=ออก (title ทำ start ให้ในตัว)
# --- สร้าง Box / Text ครั้งเดียว (ห้ามสร้างใหม่ทุกเฟรม) ---
basket = game.Box(360, 360, 90, 20, game.GB_LIGHT) # ตะกร้า อยู่ล่างจอ
item = game.Box(380, 0, 24, 24, game.GB_LIGHTEST) # ของ เริ่มบนสุด
score_text = game.Text("Score: 0", 10, 8, game.WHITE) # ป้ายคะแนน มุมซ้ายบน
score = 0
frames = 0
TOTAL = 30 * 30 # 30 วินาที ที่ 30 fps = 900 เฟรม
BASE_SPEED = 6 # ความเร็วตะกร้าตอนเพิ่งแตะจอย
MAX_SPEED = 30 # เร็วสูงสุดเมื่อกดค้างนานพอ
hold_frames = 0 # กดค้างมากี่เฟรมแล้ว (ยิ่งมาก ยิ่งเร็ว)
game.title("CATCH") (:14) เรียกก่อนเพื่อนเลย เพราะ verb ตัวนี้ทำหน้าเริ่ม + start ให้ในตัว ตรงกับสไลด์ "6 Verbs ที่ให้มา" — เราไม่ต้องเคลียร์จอหรือปลุกจอยเองbasket, item, score_text สร้าง นอก update() เพียงครั้งเดียว (:17-19) นี่คือหัวใจของสไลด์ "Step 1 — สร้างครั้งเดียว" ถ้าเผลอไปสร้างในลูป จะได้ของใหม่ทุกเฟรมจนชน sprite ceiling ~32 แล้วจอค้างTOTAL = 30 * 30 (:23) เขียนเป็นสูตรค้างไว้ ไม่ใช่พิมพ์ 900 ตรง ๆ เพราะมันคือ จากสไลด์ frame budget — อ่านโค้ดแล้วรู้ทันทีว่ามาจาก 30 วิ ที่ 30 fps เปลี่ยน fps เมื่อไรตัวเลขก็ตามไปเองBASE_SPEED / MAX_SPEED / hold_frames (:25-27) ตั้งเป็นค่าคงที่มีชื่อบนสุด ไม่ฝังเลขไว้กลางโค้ด นี่คือนิสัย "แยกค่าออกจากโครง" ที่จะทำให้ก้อนคุมจอยข้างล่างอ่านออกว่ากำลังทำอะไรก้อนนี้ยังไม่ขยับอะไรเลย แต่มันวาง "เวที" ให้ครบ ของที่ต้องคงอยู่ข้ามเฟรม (object + state) เกิดที่นี่ทีเดียว แล้ว
update()ค่อยมาเล่นกับมันซ้ำ ๆ
def update():
global score, frames, hold_frames
# --- คุมตะกร้าด้วยจอย: กดค้างยิ่งนานยิ่งเร็ว (move() clamp ขอบจออัตโนมัติ) ---
pressed = game.keys()
if pressed.left or pressed.right:
hold_frames += 1
else:
hold_frames = 0
speed = min(BASE_SPEED + hold_frames, MAX_SPEED)
if pressed.left: basket.move(-speed, 0)
if pressed.right: basket.move(speed, 0)
global score, frames, hold_frames (:31) บอก Python ว่าสามตัวนี้คือ state ของด้านนอก ไม่ใช่ตัวแปรใหม่ในฟังก์ชัน ถ้าลืมบรรทัดนี้จะเจอ NameError ตามสไลด์ "กับดักที่เจอบ่อย" — เพราะ update() ถูก game.run() เรียกใหม่ทุกเฟรม ค่าที่อยากให้จำต้องอยู่ข้างนอกhold_frames (:35-38) คือ pattern เดียวกับ "กดค้างยิ่งนานยิ่งเร็ว" ของคาบ 3 เป๊ะ กดค้างก็สะสมขึ้น ปล่อยก็รีเซ็ตเป็น 0 เราเอาของที่เคยเรียนมาใช้ซ้ำ ไม่ได้คิดใหม่speed = min(BASE_SPEED + hold_frames, MAX_SPEED) (:39) ใส่ min เพื่อกันไม่ให้เร็วเกิน MAX_SPEED — สังเกตว่าในเฟรมเดียวมี clamp สองชั้นคนละหน้าที่: min คุม ความเร็ว, ส่วน move() เอง clamp ขอบจอ ให้ตะกร้าไม่หลุดpressed.left / pressed.right (:40-41) คือขยับแกน x อย่างเดียว ต่างจากคาบ 3 ที่กล่องเดินครบ 4 ทิศ — เกม Catch ต้องการแค่ซ้าย-ขวา เราจึงตัดแกน y ทิ้งอย่างจงใจนี่คือ "30%" ก้อนแรกที่เป็นของเราจริง ๆ ที่เหลือ (อ่านจอย/วาดจอ) Core ทำให้แล้ว เราแค่บอกว่า "กดแล้วจะให้ตะกร้าเลื่อนยังไง"
# --- ของตกลงมา; ตกพ้นล่างจอ = พลาด แล้วไปโผล่ที่ใหม่ด้านบน ---
# ใช้ move_to (ไม่ clamp ขอบ) เพื่อให้ item.y เลยจอล่างได้จริง แล้วเงื่อนไข respawn จึงทำงาน
item.move_to(item.x, item.y + 8)
if item.y > game.HEIGHT:
item.move_to(random.randint(0, game.WIDTH - 24), 0)
# --- ชนแล้วได้แต้ม (หัวใจของคาบ) ---
if game.hit(basket, item):
score += 1
score_text.set("Score: %d" % score)
game.sfx("eat") # เสียง "ได้แต้ม" (เงียบถ้า firmware ไม่รองรับ)
item.move_to(random.randint(0, game.WIDTH - 24), 0)
item.move_to(item.x, item.y + 8) (:46) — เก็บ x เดิม เพิ่ม y ทีละ 8 นี่คือ integration เวลาแบบ discrete ที่เราเห็นในสไลด์ "ของที่ตกลงมา = ความเร็วคงที่ต่อเฟรม"move_to ไม่ใช้ move (:45-48) มีเหตุผลเดียว: move() จะ clamp ไม่ให้ y เกินขอบจอ เงื่อนไข item.y > game.HEIGHT เลยจะไม่มีวันเป็นจริง respawn ก็ไม่ทำงาน — เลือกผิดตัวเดียวเกมค้างเงียบ ๆgame.hit(basket, item) (:51) คือ AABB collision จากสไลด์ "หัวใจการชน" คืน True เมื่อสองกล่องทับกันทุกแกน เราไม่ต้องเขียนสูตรเทียบขอบเอง Core ทำให้score_text.set(...) (:53) อัปเดตป้าย ตัวเดิม ไม่สร้าง Text ใหม่ — ตรงกับกับดัก "ป้ายซ้อนกันพรืด" ที่เตือนไว้:55) ถ้าไม่ย้ายของออกไป มันจะยังทับตะกร้าอยู่อีกหลายเฟรม แล้ว score วิ่งรัว ๆ — สไลด์ AABB เตือนเรื่องนี้ไว้แล้วว่า "อย่าลืมย้าย"ก้อนนี้แหละที่ทำให้ "เกมเป็นเกม" — มีเป้าหมาย (รับให้โดน) มีรางวัล (แต้ม + เสียงติ๊ง) และมีการเริ่มรอบใหม่ของ item ทุกอย่างวางอยู่บน
game.hitตัวเดียว
# --- จับเวลา 30 วินาที แล้วจบรอบ ---
frames += 1
if frames >= TOTAL:
game.Text("GAME OVER", 320, 170, game.RED)
game.Text("Score: %d" % score, 330, 210, game.YELLOW)
game.sfx("gameover")
return False # หยุด game.run()
game.run(update, fps=30)
frames += 1 (:58) เดินหน้าตัวนับทุกเฟรม พอ frames >= TOTAL (:59) ก็วาด GAME OVER แล้ว return False (:63) เพื่อหลุดลูป — นี่คือสองสถานะของ FSM ในสไลด์ "State Machine ของรอบเกม" สุดท้าย game.run(update, fps=30) (:66) คือบรรทัดที่มอบ update ให้ Core เรียกแทนเราทุกเฟรม (callback):
| ก้อน | บรรทัด | แนวคิดใหม่ที่เพิ่มเข้ามา | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| 1 ตั้งเวที | :14-27 |
สร้าง object + state ครั้งเดียว, ค่าคงที่มีชื่อ | 6 verbs Core (start/Box/Text) |
| 2 คุมจอย | :33-41 |
hold_frames → speed + min clamp |
จอย + move() ของคาบ 3 |
| 3 ของตก | :44-48 |
move_to + integration |
Box + random |
| 4 ชน+แต้ม | :50-55 |
game.hit (AABB) + .set + respawn ทันที |
เงื่อนไข if + ป้ายตัวเดิม |
| 5 จับเวลา | :57-66 |
นับเฟรม → FSM → return False (callback) |
ตัวนับ frames |
update() เริ่มที่ก้อน 2 คือ "งานของเรา"จับจังหวะการต่อก้อนนี้ไว้ เกมทุกตัวที่เหลือในคอร์สก็ประกอบแบบเดียวกัน เริ่มจาก setup เล็ก ๆ ที่รันได้ แล้วเติมทีละก้อน อย่ากระโดดเขียนทั้งเกมรวดเดียว
update() หนึ่งเฟรมนี้มาจากไหน แล้วจะพาเราไปถึงไหนหยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่อง "มองให้ทะลุ" ว่า update() ก้อนเล็ก ๆ ที่เราเพิ่งเขียน จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — update() วันนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "game loop คืออะไร" เราเห็นวงวน read → update → draw ที่ Core หมุนให้ แล้วสไลด์ "แก่นของคาบ: 70/30" บอกชัดว่าในสามขั้นนั้น เราเขียนแค่ ขั้นกลางขั้นเดียว คือ update() ส่วนตัวคุมจอยแบบ hold_frames ก็ไม่ใช่ของใหม่ มันคือ pattern "กดค้างยิ่งเร็ว" ที่เรายกมาจากคาบ 3 ตรง ๆ พูดอีกอย่าง วันนี้เราไม่ได้เรียนของใหม่ทั้งหมด เราเอาลูป + การจำ state + การอ่านจอย ที่เคยรู้มาประกอบกันเป็น "หนึ่งเฟรมของเกม"
ที่ไป — update() ก้อนนี้จะโตเป็นอะไร ดูสไลด์ "โครงเกมมาตรฐาน (ทุกเกมในคอร์ส)" ให้ดี input → move → hit → score → return False ที่เราเพิ่งเขียนใน Catch คือ แม่พิมพ์เดียวกับ Snake, Flappy, Pong, Shooter คาบหน้าเราแค่เปลี่ยน "ของตกตรง ๆ" เป็น "ลูกบอลที่มีความเร็วสองแกน" — โครง update() ไม่เปลี่ยนเลย เปลี่ยนแค่กฎข้างใน
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
game.run(update, fps=30) ในสไลด์ "game.run() ทำงานยังไง" ได้ไหม ถ้าวันนี้เราเขียน update() ให้ Core เรียกได้ พรุ่งนี้เราจะเขียน update() ให้เกมอื่นแบบเดียวกันได้ไหม? (ใบ้: โครง input → move → hit → score เหมือนเดิม)item.move_to(item.x, item.y + 8) แกนเดียว เป็นขยับสองแกน x += vx และ y += vy แล้วเด้งขอบด้วยการพลิกเครื่องหมาย โค้ดจะหน้าตาเปลี่ยนไปมากไหม? (ใบ้: นั่นคือลูกบอล Pong คาบหน้า)game.hit ตัวเดียวกัน? เปลี่ยนแค่ว่ากล่องสองใบคืออะไรถ้าตอบได้ว่า "อ๋อ มันคือ
update()เดียวกันที่ใส่กฎคนละแบบ" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด เกมทั้งเครื่องไม่ได้ยากขึ้นเป็นสิบเท่า มันคือก้อนวันนี้ที่เราเติมกฎเข้าไปทีละนิด
เทคนิควันนี้ไม่ใช่แค่แบบฝึกหัด มันคือสิ่งที่เฟิร์มแวร์และระบบเรียลไทม์จริงใช้กันทุกวัน:
game.run(fps=30) ที่คุมทุกเฟรมห่างกัน ~33 ms คือหลักเดียวกับ periodic task/timer ISR บน MCU จริง: มอเตอร์รัน PID ทุก 1 ms, ABS อ่านล้อทุกคาบคงที่ งานต่อรอบต้องเสร็จใน budget ไม่งั้นระบบ "กระตุก" เหมือนเฟรมหล่นgame.hit(a, b) ที่เราใช้รับของ คือหลักเดียวกับ hit-testing บนทัชสกรีน (แตะโดนปุ่มไหน) และกล่องครอบวัตถุใน object detection / ระบบกันชนของหุ่นยนต์และโดรนitem.move_to(random..., 0) ที่ย้ายของกลับขึ้นบนเมื่อพ้นจอ คือแนวคิด object pooling ในเกมเอนจินจริง (ใช้ object เดิมซ้ำ) และการรีเซ็ต buffer เซนเซอร์เมื่อวนครบรอบframes และ score ที่ประกาศ global แล้วสะสมไปเรื่อย ๆ คือพื้นฐานของ event counter จริง: เครื่องนับก้าว, มิเตอร์ไฟสะสมพลังงาน, หรือ heartbeat counter ที่ watchdog เฝ้าดูห้าก้อนที่เราต่อกันวันนี้ไม่มีก้อนไหนเป็นของสมมติเลย ทุกก้อนมีเครื่องจริงในโลกที่ทำงานด้วยหลักการเดียวกันอยู่ — เราแค่เจอมันในร่างของเกมก่อน
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อยังอยู่ใน 30% ที่เป็นของเรา และโยงกลับไปหาโปรเจกต์จริงได้:
score -= 2 จะให้ item รู้ "ชนิดของตัวเอง" ยังไงโดยไม่เพิ่ม Box เกินจำเป็น (ใบ้: เก็บชนิดไว้ในตัวแปรคู่กับ item แล้วเช็กตอน game.hit เป็นจริง)frames ยังไง โดยยังคุมไม่ให้เร็วเกินจนรับไม่ทัน (ใบ้: min แบบเดียวกับ MAX_SPEED ของตะกร้า)update() ยังไง โดยไม่พลาดสร้าง Box ใหม่ทุกเฟรมจนชน sprite ceiling ~32item.y + 8) ถ้าเปลี่ยนเป็นลูกบอลที่มีความเร็วสองแกน แล้วชนขอบจอก็ "พลิกเครื่องหมาย" ของแกนนั้น จะแก้ move_to เป็นอะไร — คำถามนี้แหละคือ Pong คาบหน้า ที่ใช้ และการเด้ง = พลิกเครื่องหมายของแกนที่ชนเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากก้อนที่พิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css
CAPTURE: catch_score.png — ถ่ายตอนป้าย Score แสดงเลข >0 ขณะตะกร้าทับของ (มี mock อ้างอิงพิกัดจริงไว้แล้ว)