เกมหนัก ๆ บน SNES เสียบชิป Super FX มาในตลับเกมเพื่อช่วยคำนวณ polygon (Star Fox) — เครื่องคงเดิมแต่แรงขึ้นด้วย coprocessor

ที่มา: "SNES-Mod1-Console-Set" — Evan-Amos, Public domain, Wikimedia Commons
เชื่อมกับวันนี้: งานวาดหนัก ๆ ของน้องก็ฝากให้ C core (coprocessor ของเรา) ทำ ส่วน Python คุม logic
ตลับเกมมี Super FX ช่วยวาด
บอร์ดเรามี C core (CM55) ช่วยวาด — coprocessor ตัวเดียวกันคนละยุค
เกมใหม่ทุกตัวคิดตาม 4 ขั้นเดิมนี้ ไม่มีอะไรเกินนี้ และในไฟล์เราเขียนจริงแค่ 3 ช่อง (Draw เอนจินวาดให้เอง)
game.hit()

ที่มา: ภาพจับจากจอบอร์ด PSoC Edge Eva Kit ผ่าน BENTO IDE — สื่อประกอบคอร์ส (BDH & TESAIoT)

ทั้งสองแนวใช้ engine ชุดเดียวกัน ต่างกันแค่ "30% คิดเป็นช่อง" หรือ "30% คิดเป็นความเร็ว" — เลือกตามไอเดียของกลุ่มได้เลย
ถ้าเลือกแนว grid (เดินทีละช่องแบบ Snake) แปลงช่อง / จุดบนจอด้วยสูตรเดียว:
เก็บกระดานเป็น list เดียว (ดัชนีช่องแบบ 1 มิติ):
board[row * COLS + col] — กริด 3×3 = list ขนาด 9 (Snake ก็เก็บลำตัวงูเป็น list ของช่องแบบนี้)
ถ้าเลือกแนว physics (เคลื่อนลื่นแบบ Flappy) ทุกเฟรมคิดเป็น 2 บรรทัดเดิม:
Flappy ใช้สูตรนี้กับแกน y อย่างเดียว เกมของน้อง ๆ จะใช้ 1 แกนหรือ 2 แกนก็ได้
ลองกาง สองบรรทัดนั้นออกมาทีละเฟรม จะเห็นว่าเลขมันโตเป็นแบบไหน:
a คงที่ ทำให้ v โตเป็นเส้นตรง (เฟรมหลัง ๆ ตกเร็วขึ้น) — พอเอา v ที่โตขึ้นเรื่อย ๆ ไปบวกสะสมเข้า y ผลรวมจึงโค้งเป็น พาราโบลา นี่คือ "ตกแบบมีน้ำหนัก" ที่ทำให้ Flappy รู้สึกเป็นธรรมชาติ (จริง ๆ คือ Euler integration ของ )

กดปุ่ม = เซ็ต
vเป็นค่าลบก้อนเดียว → เส้นพาราโบลาเด้งหัวขึ้นชั่วครู่ แล้วโน้มถ่วงก็ดึงลงต่อ
mygame.py ให้เห็นว่าโครงพร้อม (20 นาที)ก่อนจะออกแบบ เราอยากให้น้อง ๆ เห็นก่อนว่าโครงนี้รันได้จริงตั้งแต่ยังไม่แก้อะไรเลย:
เปิด practise_codes/mygame.py↗ ใน BENTO IDE แล้วกด Program to Device
จะได้เกมตัวอย่าง "วิ่งกล่อง cyan ไปชนเป้า yellow ให้ครบ 5 แต้ม" โครงมี 3 ช่องว่างเขียนป้าย # 1) / # 2) / # 3) ไว้ชัดเจน (Draw เอนจินวาดให้เอง)

# กฎทอง: แก้แค่โค้ดใน 3 ช่อง "# N) ..." เท่านั้น
# ห้ามแตะ bentogame.py — ทุกอย่างที่ต้องใช้คือ game. call:
# game.start() -> ปลุกจอ + joystick
# game.Box(x,y,w,h,col) -> กล่องที่ขยับได้
# k = game.keys() -> k.left/.right/.up/.down/.a/.b/.start
# game.hit(a, b) -> True ถ้ากล่อง 2 ใบทับกัน
# game.run(update, fps) -> เรียก update() ของเราวนตลอด
# (BACK = ออกเกม, START = เริ่มใหม่ -> run() จัดการให้อัตโนมัติ
# update() จึง return False เฉพาะตอน "ชนะ/แพ้จริง" เท่านั้น)
พอโครงพร้อมแบบนี้ แปลว่า engine (70%) ครบแล้ว เหลือแค่เราเติม 3 ช่อง (30%):
ผลงานขั้นนี้: ทุกคนเห็นเกมตัวอย่างวิ่งบนจอ และชี้ "3 ช่องว่าง" ในไฟล์ได้
update()) # 1) STATE — สร้าง Box/Text ของเรา (ครั้งเดียว ก่อนลูป)
player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 32, 32, game.CYAN)
def update():
# 2) TICK — กติกาทุกเฟรม: ขยับของ อ่านปุ่ม
...
# 3) WIN / LOSE — เช็คการชน แล้วตัดสินแพ้/ชนะ
...
โค้ดเต็มอ่านจบได้ใน 5 นาที — ลองเขียนเองให้สุดก่อนนะ เดี๋ยวเฉลยพร้อมกันในห้อง
mygame.py ทั้งไฟล์ — เติม 30% ตรงกล่องเส้นประโครงทั้งไฟล์เป็นผังเดียวแบบนี้: เส้น ทึบ = 70% + ช่องที่โครงให้มาแล้ว (รันได้เลย) · กล่อง/รูปสี่เหลี่ยมข้าวหลามตัด เส้นประ = 30% ที่กลุ่มต้องเติมเอง (Issue #2 — ยังไม่เฉลย)
สัญกรณ์: ▭ฟ้า = ประมวลผล · ▭เขียว = อ่าน input · ◇ม่วง = ตัดสินใจ · ▭เทามน = เริ่ม/จบ · เส้นประ = 30% ที่น้องเติมเอง (ยังไม่เฉลย)
โครงที่ให้มาแล้ว (
practise_codes/mygame.py↗): State:23-31· keys:39· move:40-47· loop:72— 3 ช่องที่ต้องเติมคือ# 1) # 2) # 3)ที่ map ลงกล่องเส้นประนี้พอดี
| ไอเดียเกม | แนว | 30% ที่ต้องเขียนเอง | เหมาะกับ |
|---|---|---|---|
| เก็บของให้ครบ | grid | วาง item ในช่อง, เดินเก็บ, นับครบ | กลุ่มเริ่มต้น |
| เขาวงกตเดินถึงเส้นชัย | grid | กำแพงเป็นช่อง, เดินชนกำแพงไม่ได้ | กลุ่มกลาง |
| หลบสิ่งกีดขวางตก | physics | ของตกลงมา, เลื่อนซ้ายขวาหลบ | กลุ่มกลาง |
| กระโดดข้ามหลุม | physics | โน้มถ่วง + กระโดด + พื้นเลื่อน | กลุ่มเก่ง |

กฎ: ทุกเกมไม่ต้องเขียน engine ใหม่ ต่างกันแค่ "30% เป็น grid หรือ physics" และ "ยากแค่ไหน" เลือกตามกำลังของกลุ่มได้เลย
ผลงานขั้นนี้: กลุ่มตกลงไอเดียเกม 1 อัน + ระบุว่าเป็นแนว grid หรือ physics พร้อมเหตุผล
นี่คือผลงานหลักของคาบ ลองดูตัวอย่างที่กรอกด้วยเกมแนว grid:
=== ใบออกแบบเกมของกลุ่ม ===
ชื่อเกม: Coin Collector · แนว: grid (แบบ Snake)
[70% ที่ฉัน REUSE — ห้ามเขียนเอง] (ต้องเป็น game. call เท่านั้น)
- game.Box -> วาดผู้เล่น + เหรียญ - game.keys() -> เดินทีละช่อง
- game.Text -> คะแนน - game.run() -> ลูปเกม
[30% ที่ฉันต้องเขียนเอง — 4 ขั้นคิด -> ลง 3 ช่องใน mygame.py (Draw เอนจินทำให้)]
1) State: ตำแหน่งผู้เล่น (row,col), เหรียญ, score
2) Tick: อ่านปุ่ม -> เดิน 1 ช่องตามทิศ
3) Win/Lose: เดินทับเหรียญ -> +1 + ย้ายเหรียญ -> ครบ 5 ชนะ
4) Draw: วาดผู้เล่น + เหรียญตามพิกัดช่อง
[MVP] เดินเก็บเหรียญ 5 อันให้ครบ แล้วขึ้น YOU WIN
ภาพรวมของการกรอกใบนี้ คือการ "ตัด" ไอเดียดิบให้เหลือ MVP แล้วจับ MVP ลงให้พอดี 3 ช่อง:

กฎที่ขอย้ำ: 70% = game. call เท่านั้น · 30% ต้อง map ลง 3 ช่องให้ครบ · MVP ต้องเล็กพอจนทำเสร็จได้
ขั้นนี้เราต่อยอด DevOps ที่ฝึกมาในคอร์ส ไปที่ Issues → New issue:
หัวข้อ: [กลุ่ม X] เกมของเรา: Coin Collector (grid) -- MVP คาบ 12-13
## 30% ที่ต้องเขียน (4 ขั้นคิด -> 3 ช่องใน mygame.py) -- checklist
- [ ] 1) State: ตำแหน่งผู้เล่น (row,col), เหรียญ, score
- [ ] 2) Tick: อ่านปุ่ม -> เดิน 1 ช่อง
- [ ] 3) Win/Lose: ทับเหรียญ -> +1 -> ครบ 5 ชนะ
- [ ] 4) Draw: วาดผู้เล่น + เหรียญตามช่อง
## MVP: เดินเก็บเหรียญ 5 อันให้ครบ แล้วขึ้น YOU WIN

ใช้ - [ ] ทุกข้อ — Issue เดียวนี้จะอยู่กับเราไปจนถึงคาบ 13 (สร้าง MVP จริง)
ภาพนี้คือสิ่งที่เกิดขึ้นจริงทุกครั้งที่ทำโปรเจกต์ — ทาง MVP รีบทำแก่นเดียวให้ ส่งได้ก่อน แล้วค่อยต่อยอด ส่วนทาง scope creep มัวแต่กองฟีเจอร์จน ไม่ได้ส่งสักที:

นักพัฒนามืออาชีพไม่ได้เก่งกว่าตรงที่เขียนเร็วกว่า แต่เก่งตรงที่ "ตัด" เป็น — รู้ว่าอะไรคือแก่นที่ต้องมี และอะไรรอได้
Making Your First Game: Minimum Viable Product - Scope Small, Start Right — Extra Credits
| อาการ | วิธีแก้ |
|---|---|
| เลือกเกมใหญ่เกิน (มีหลายด่าน / มี boss) | ตัดเหลือ MVP: 1 หน้าจอ เล่นจบได้ |
| เขียน 70% ว่า "เขียน game loop" | loop = game.run() ให้มาแล้ว · 70% = "เรียก" ไม่ใช่ "เขียน" |
| 30% map ลง 3 ช่องไม่ได้ | เกิน 3 ช่อง = scope ใหญ่ไป ตัดจนเหลือ 3 |
| เลือกแนวไม่ชัด | บอกให้ได้ว่าตำแหน่งเป็น "ช่อง" (grid) หรือ "พิกเซล+ความเร็ว" (physics) |
เริ่มแก้ bentogame.py |
ห้ามแตะ 70% core — แก้ได้แค่ 3 ช่องใน mygame.py |
| Issue ไม่มี checklist | ใช้ - [ ] ทุกข้อ 30% |

game. call · 30% map 3 ช่องครบmygame.py รันบนบอร์ดได้ (Program to Device)DESIGN.md)mygame.py รัน
เกมที่น้องกำลังออกแบบ ไม่ใช่ของเล่นแยกจากวิชาวิศวกรรม — มันคือ 4 รากฐาน ที่เรียนมา มาประกอบกันพอดี:
run() เรียกแบบ callback · กระดาน grid = list/indexing
v+=a; y+=v = Euler integration · grid = พีชคณิตพิกัด · ชน = เงื่อนไข/ค้นหา
ออกแบบเกมเป็น = ใช้ทั้ง 4 รากฐานเป็น ทักษะนี้ตรง ๆ คือทักษะสร้าง "ผลิตภัณฑ์ฝังตัว" จริง
เป้าหมายสำคัญที่สุดของทั้งวิชา: การที่น้อง ๆ ออกแบบเกมของตัวเองได้ ไม่ใช่แค่ลอกตาม
น้อง ๆ ไม่ได้เริ่มจากศูนย์เลย เขาแค่เติม 30% ที่ฝึกมาแล้ว 2 รอบ (Snake = grid, Flappy = physics) ลงใน Game Engine เดิมที่คุ้นมือ
ทำเองต่อ (30% ของคาบ): จาก practise_codes/mygame.py↗ ของกลุ่ม — เติมช่อง 1 (State) + ช่อง 2 (Tick) ตามใบออกแบบ แล้วกด Program to Device ให้ผู้เล่นขยับได้ตามกติกาเกมเรา ที่เหลือ (ช่อง 3 Win/Lose) เก็บไว้ทำต่อในคาบ 13
ลองเขียนเองให้สุดก่อนนะ — เดี๋ยวเฉลยพร้อมกันในห้อง นี่คือจังหวะที่เราจะรู้สึกว่า "เกมนี้เป็นของเราจริง ๆ"
mygame.py — เกมเต็มที่เติม 3 ช่องครบแล้วก่อนเปิดเฉลย ขอย้ำกฎเดิมจาก README: เฉลยนี้มีไว้ เทียบ ไม่ได้มีไว้ลอก คะแนนของคาบนี้อยู่ที่ใบออกแบบกับ Issue ของกลุ่ม ไม่ใช่ที่โค้ดตรงกับไฟล์นี้ วิธีใช้ให้ได้ผลจริงคืออ่านให้เข้าใจว่าแต่ละก้อน "ทำไมต้องเป็นแบบนี้" แล้ว ปิดไฟล์ กลับไปพิมพ์เองในไฟล์ของกลุ่ม ตอนพิมพ์เองนั่นแหละที่มือกับสมองจำ pattern 70/30 ได้จริง เราจะแกะทีละก้อน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ก้อนแรก — ตั้งค่าคงที่ของกติกา แล้วเปิดหน้าเริ่ม:
import bentogame as game
PLAYER_SPEED = 8 # ขยับทีละกี่พิกเซลต่อเฟรม
FPS = 30 # ความถี่ของลูป
WIN_SCORE = 5 # ชนเป้าครบเท่านี้ = ชนะ
game.title("MY GAME") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
PLAYER_SPEED, FPS, WIN_SCORE เป็นค่าคงที่ไว้บนสุด แทนที่จะพิมพ์เลข 8, 30, 5 กระจายทั่วโค้ด? เพราะนี่คือ "30% กติกา" ที่เราอยากปรับได้ง่าย ๆ อยากให้ผู้เล่นเร็วขึ้นแก้บรรทัดเดียว โครงลูปไม่ต้องแตะเลย — ตรงกับหลัก "แยกค่าออกจากโครง" :13-15FPS = 30 คือความถี่ที่ update() ถูกเรียกซ้ำ ตรงกับ "30 Hz" ในผัง mygame.py ที่เราดูไป (return True → เฟรมถัดไป) ทั้ง Snake และ Flappy ก็เต้นด้วยลูปความถี่คงที่แบบนี้ :14WIN_SCORE = 5 คือการ บังคับ MVP ให้เล็ก ถ้าตั้ง 50 เกมจะยืดจนเทสต์ยาก เลข 5 ทำให้เล่นจบเห็นผลได้ในไม่กี่วินาที ตรงกับสไลด์ "ทำไม MVP เล็กถึงสำคัญ" :15game.title("MY GAME") คือของ 70% core ล้วน ๆ เอนจินทำหน้า Start/Back ให้เองครบ เราไม่ต้องเขียน state machine ของเมนูเลย นี่คือสิ่งที่หมายถึง "reuse ก่อนเขียน" :17ค่าคงที่สามตัวบนหัวไฟล์ไม่ใช่แค่ความสวยงาม มันคือ "ปุ่มปรับกติกา" ที่แยกออกจากเครื่องยนต์ เปลี่ยนความรู้สึกของเกมได้โดยไม่ต้องแตะลูปสักบรรทัด
นี่คือ "ขั้นคิดที่ 1) State" จากสไลด์ 4 ขั้นคิด สร้างครั้งเดียวก่อนลูป เพราะเป็นของที่ต้องคงอยู่ข้ามเฟรม:
# 1) STATE — เกมต้องจำอะไรบ้าง (สร้างครั้งเดียว ก่อนลูป)
score = 0
player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 32, 32, game.CYAN)
target = game.Box(120, 120, 28, 28, game.YELLOW)
label = game.Text("Score: 0", 12, 10, game.WHITE)
score = 0 เป็นตัวแปรธรรมดา แต่คือหัวใจของ State ทั้งเกม — ตรงกับรากฐาน "Python: State = ตัวแปร" ที่เราสรุปไว้ตอนท้ายเดค :20player วางที่ game.WIDTH // 2, game.HEIGHT // 2 คือกลางจอพอดี เก็บ object ลงตัวแปรเพราะทุกเฟรมต้อง player.move(...) ซ้ำ ถ้าไม่เก็บก็อ้างถึงมันทีหลังไม่ได้ พิกัดเป็น พิกเซลต่อเนื่อง — นี่คือเกม แนว physics ไม่ใช่ grid :21game.CYAN / game.YELLOW / game.WHITE มาจาก palette ใน 70% core เราไม่ตั้งเลขสีเอง ใช้ชื่อที่เอนจินเตรียมไว้ :21-23game.Box กับ game.Text คือรากฐาน Graphics: 2D coordinate system — บอกแค่ (x, y, w, h) แล้ว C core บน CM55 วาดให้เองผ่าน IPC เราจึงไม่ต้องเขียน "ช่อง 4 Draw" เลย :21-23ตัวช่วยเล็ก ๆ — ย้ายเป้าเมื่อโดนชน:
def _move_target():
# สุ่มตำแหน่งเป้าใหม่แบบเบา ๆ (ใช้ตัวแปร score เป็นเมล็ดสุ่มแบบง่าย)
nx = (target.x * 7 + 113) % (game.WIDTH - target.w)
ny = (target.y * 5 + 71) % (game.HEIGHT - target.h)
target.move_to(nx, ny)
import random? เพราะเราอยากให้ MVP พึ่งของน้อยที่สุด สูตร (x*7+113) % ... ให้ตำแหน่งกระโดดไปมาแบบเดายากพอสำหรับเกม โดยไม่ต้องดึงไลบรารีเพิ่ม :28-29% (game.WIDTH - target.w) คือ พีชคณิตพิกัด (รากฐาน Algorithms) ที่การันตีว่าเป้าไม่หลุดขอบจอ โมดูโลคือตัวหนีบให้อยู่ในกรอบ คล้ายสูตรหนีบช่องของ grid ที่เราเรียน :28-30State คือ "ความจำ" ของเกม ของที่ต้องอยู่ข้ามเฟรมเก็บลงตัวแปรก่อนลูป ของที่ใช้ครั้งเดียวไม่ต้องเก็บ ตัดสินใจแบบนี้เหมือนกันทุกเกม
update() ถูก game.run() เรียกทุกเฟรมแบบ callback ในนั้นมี 2 ช่วง: อ่านปุ่มขยับ (ช่อง 2) แล้วเช็คชนตัดสินผล (ช่อง 3) ตรงกับผัง อ่านปุ่ม keys() → player.move → ◇ ชน?
def update():
"""ถูกเรียกทุกเฟรม. return False = จบเกม."""
global score
# 2) TICK — กติกาทุกเฟรม: อ่านปุ่มแล้วขยับผู้เล่น
k = game.keys()
if k.left:
player.move(-PLAYER_SPEED, 0)
if k.right:
player.move(PLAYER_SPEED, 0)
if k.up:
player.move(0, -PLAYER_SPEED)
if k.down:
player.move(0, PLAYER_SPEED)
# BACK = ออกเกม, START = เริ่มใหม่ — game.run() จัดการให้อัตโนมัติ
# update() จึง return False เฉพาะตอน "ชนะ/แพ้จริง" เท่านั้น
global score ต้องมี เพราะ update() จะไป เขียน ค่า score ที่ประกาศไว้นอกฟังก์ชัน ถ้าไม่ประกาศ Python จะมองว่าเป็นตัวแปรใหม่ในเครื่อง แล้วคะแนนจะไม่สะสม :35if สี่ตัวแยกกัน (ไม่ใช่ elif) จงใจ กด left+up พร้อมกันแล้วเดินทแยงได้ ขยับทีละ PLAYER_SPEED พิกเซล = แนว physics เต็มตัว ต่างจาก Flappy ที่คุมแกน y แกนเดียว เกมนี้คุมทั้ง x และ y :39-46return ในก้อนนี้ เพราะ TICK แค่ "ขยับ" การตัดสินแพ้/ชนะเป็นหน้าที่ของก้อนถัดไป แยกความรับผิดชอบให้ชัด :47-48 # 3) WIN / LOSE — ชนเป้า = +1 แต้ม, ครบ WIN_SCORE = ชนะ
if game.hit(player, target):
score += 1
label.set("Score: %d" % score)
game.sfx("point")
_move_target()
if score >= WIN_SCORE:
game.Text("YOU WIN!", game.WIDTH // 2 - 70, game.HEIGHT // 2, game.GREEN)
game.sfx("win")
return False
return True # เล่นต่อ
game.run(update, fps=FPS)
game.hit(player, target) คือ collision ของ 70% core — ตรงกับกล่องเส้นประ TODO: ชน? ในผัง mygame.py ที่เดิมเป็น 30% ยังไม่เฉลย ตอนนี้เราเติมมันแล้ว :51score += 1 (อัปเดต state) → label.set(...) (บอกผลบนจอ) → game.sfx("point") (เสียงจาก 70%) → _move_target() (ย้ายเป้าใหม่) นี่คือ "ชนแล้วเกิดอะไร" ของขั้นคิดที่ 3 :52-55if score >= WIN_SCORE ซ้อนอยู่ใน if hit เพราะเงื่อนไขชนะเช็คเฉพาะตอนเพิ่งได้แต้ม ไม่ต้องเช็คทุกเฟรม แล้ว return False คือสัญญาณเดียวที่บอก game.run() ให้ "จบเกม" ตรงกับ return False ในผัง :56-59game.run(update, fps=FPS) ส่ง update เข้าไปเป็น callback — เอนจินวนเรียกให้เอง 30 ครั้ง/วินาที เราไม่ได้เขียน while True เอง นี่คือรากฐาน "Tick = ฟังก์ชันที่ run() เรียกแบบ callback" :64TICK เปลี่ยน state จาก input, WIN/LOSE ตัดสินผลแล้วส่งสัญญาณจบด้วย
return Falseแยกสองหน้าที่นี้ให้ขาด แล้วลูปจะอ่านง่ายตลอด ไม่ว่าเกมจะโตแค่ไหน
ทั้งไฟล์คือ 4 ขั้นคิดเดิม ที่ ไต่ระดับ จาก "จำ" → "ขยับ" → "ตัดสิน" โดยแต่ละขั้นหยิบของ 70% มาต่อ:
| ช่อง | ขั้นคิด | บรรทัด | game. call ที่ reuse (70%) | สิ่งที่เราเขียนเอง (30%) |
|---|---|---|---|---|
| — | (ตั้งค่า) | :13-17 | game.title |
ค่าคงที่ PLAYER_SPEED / FPS / WIN_SCORE |
| 1 | State | :20-23 | game.Box · game.Text · palette |
จำอะไร: score · player · target · label |
| 2 | Tick | :38-46 | game.keys() · .move() |
อ่านปุ่ม 4 ทิศ → ขยับ PLAYER_SPEED |
| 3 | Win/Lose | :51-61 | game.hit() · game.sfx() |
ชน → score += 1 · ครบ 5 → return False |
| 4 | Draw | :21-23, :57 | เอนจินวาดให้ผ่าน IPC | — ไม่ต้องเขียน |
| loop | ขับทั้งหมด | :64 | game.run(update, fps=FPS) |
ส่ง update เข้าไปเป็น callback |
สังเกตคอลัมน์ขวา: ทุกอย่างที่เรา "เขียนเอง" รวมแล้วไม่ถึง 15 บรรทัดจริง ที่เหลือคือการ เรียก 70% ที่เอนจินเตรียมไว้ นี่แหละความหมายของ "reuse 70% เขียนเอง 30%" ที่เป็นแก่นของทั้งคาบ และคอลัมน์ "ขั้นคิด" ก็ไล่จาก State → Tick → Win/Lose พอดีตามสไลด์ 4 ขั้นคิด
ไล่ตารางนี้จากบนลงล่างแล้วน้องจะเห็นว่าเกมไม่ได้เขียนรวดเดียว มันประกอบจากขั้นเล็ก ๆ ที่ต่อกัน เกมของกลุ่มก็สร้างด้วยโครงตารางนี้เป๊ะ เปลี่ยนแค่ช่อง 30%
mygame.py คือสะพานจาก "แนวคิด" ไป "เกมของกลุ่ม"ที่มา — โค้ดก้อนนี้โตมาจากไหน มันไม่ใช่ของใหม่เลยสักบรรทัด ทุกอย่างที่เพิ่งแกะคือสไลด์ "30% = 4 ขั้นคิดเท่านั้น" กับ "กฎทอง 70% engine + 30% กติกา" ที่ถูกเติมจนกลายเป็นเกมเล่นได้จริง ช่อง 1-2-3 ที่เคยเป็น กล่องเส้นประ ในสไลด์ "ผัง mygame.py ทั้งไฟล์" ตอนนี้เต็มหมดแล้ว และการที่ผู้เล่นเดินได้ทั้ง x และ y แต่ Flappy คุมแค่แกน y คือความต่างเดียวของ grid vs physics ที่เราคุยกันไป — engine ชุดเดียวกันทั้งคู่
ที่ไป — โค้ดก้อนนี้จะกลายเป็นอะไร ดูไทม์ไลน์ท้ายเดค: คาบ 1-11 → คาบ 12 (วันนี้) → คาบ 13. คาบหน้ากลุ่มจะเอา ใบออกแบบ + GitHub Issue ของตัวเอง มาเติม mygame.py ของกลุ่ม (ไม่ใช่ของอาจารย์) เปลี่ยน target เป็นเหรียญ/ศัตรู/ท่อ เปลี่ยน physics เป็น grid ตามไอเดีย โครงเดิมไม่พังเพราะมันคือ 4 ขั้นคิดเดียวกัน ทักษะ "เติม 3 ช่องบน engine ที่มีอยู่" คือทักษะเดียวกับที่วิศวกรจริงต่อยอดบน framework/BSP ในงานจริง
ลองตอบสามคำถามนี้ในใจ นี่คือการเชื่อมจุดด้วยตัวเอง:
TODO: game.hit → score ในผัง mygame.py ได้ไหม ลองวางเฉลย :51-61 ทับกล่องนั้นดู มันคือชิ้นเดียวกันที่เราเพิ่งเติมใช่ไหมtarget ให้ขยับได้แล้วเกมยังทำงาน พรุ่งนี้เราเปลี่ยนมันเป็น "ศัตรูที่ไล่ผู้เล่น" ในเกมกลุ่มได้ไหม ต้องแก้กี่ช่อง?player.move แบบ physics กับ "เดินทีละช่อง" แบบ grid คือเรื่องเดียวกัน ต่างแค่หน่วยของตำแหน่ง (พิกเซล vs ช่อง)? (ใบ้: ย้อนไปสไลด์ grid vs physics)ถ้าตอบได้ว่า "อ๋อ เฉลยนี้คือ 4 ขั้นคิดที่เติมเต็มแล้ว" — นั่นคือการหยั่งรู้ที่อยากให้เกิด เกมของกลุ่มไม่ได้เริ่มจากศูนย์ มันคือไฟล์นี้ที่เปลี่ยนแค่ 30%
เทคนิคในเฉลยนี้ไม่ใช่ของเล่นในคาบ มันคือสิ่งที่ซอฟต์แวร์จริงใช้ทุกวัน:
game.run(update, fps=FPS) เรียก update() 30 ครั้ง/วินาที คือหัวใจเดียวกับ game engine จริง (Unity Update(), Godot _process) ตรงกับ ช่อง loop :64game.hit(player, target) คือ AABB overlap ตัวเดียวกับที่เฟรมเวิร์ก UI ใช้ตัดสินว่า "นิ้วแตะโดนปุ่มไหน" บนจอสัมผัส ตรงกับ ช่อง 3 Win/Lose :51mygame.py สอน ตรงกับ กฎทองเขียนเกมเล็ก ๆ จบหนึ่งไฟล์วันนี้ น้องได้ฝึกโครงเดียวกับที่ทีมพัฒนาเกมและทีมเฟิร์มแวร์มืออาชีพใช้ ต่างกันแค่ขนาด ไม่ใช่หลักการ
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาเกมของกลุ่มได้:
game.Box อีกใบใน State แล้วเพิ่ม if game.hit(player, enemy): return False ในช่อง 3 — ตรงข้ามกับ target)_move_target ใช้สูตรดัชนี row*COLS+col แทน % ไหมPLAYER_SPEED / FPS / WIN_SCORE ตัวไหน โดยไม่แตะลูปเลย?mygame.py แล้วถามตัวเอง: ช่อง 1/2/3 ของเกมกลุ่มต่างจากเฉลยนี้ตรงไหน? เขียน 3 บรรทัดแรกของช่อง 1 (State) ของกลุ่มลงใบงาน คาบหน้าเราจะเอามันขึ้นจอจริงเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากเฉลยที่พิมพ์เองวันนี้ ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนเติมช่องตามอาจารย์
fit-css
CAPTURE: mygame_demo.png — เปิด practise_codes/mygame.py ใน BENTO IDE แล้วกด Program to Device บนบอร์ด ถ่ายจอตอนกล่อง cyan ชนเป้า yellow + คะแนน