ลองคิดเป็นตัวเลข ถ้าทั้งเกมคือแรง 1 หน่วย เราเลือก reuse 70% ที่หนักที่สุด (screen · loop · input · sound) เหลือเขียนเองแค่ 30%:
ลดงานลง ~70% — นั่นคือเหตุผลที่เกมหนึ่งเกมทำเสร็จได้ในคาบเดียว ไม่ใช่เพราะเกมมันเล็ก แต่เพราะเราลงแรงเฉพาะส่วนที่เป็นของเราจริง ๆ

มือใหม่มักรู้สึกว่า "เขียนเกม = เขียนทุกอย่างตั้งแต่ศูนย์" จุดที่ทำให้จบงานได้คือ เลือกลงแรงให้ถูกที่ ไม่ใช่ขยันขึ้น
เกมใหม่ทุกตัวคิดตาม 4 ขั้นเดิมนี้ และในไฟล์เราเขียนจริงแค่ 3 ช่อง (Draw เอนจินวาดให้เอง)
mygame.py ให้เห็นว่าโครงพร้อม (20 นาที)ก่อนจะออกแบบ เราอยากให้น้อง ๆ เห็นก่อนว่าโครงนี้รันได้จริงตั้งแต่ยังไม่แก้อะไรเลย:
เปิด practise_codes/mygame.py↗ ใน BENTO IDE แล้วกดปุ่ม Program to Device

ปุ่ม Program to Device ใน BENTO IDE — นี่คือปุ่มที่กดทุกครั้งหลังเติมโค้ดเสร็จ เพื่อ flash mygame.py ลงบอร์ด
จะได้เกมตัวอย่าง "วิ่งกล่อง cyan ไปชนเป้า yellow ให้ครบ 5 แต้ม" โครงมี 3 ช่องว่างเขียนป้าย # 1) / # 2) / # 3) ไว้ชัดเจน (Draw เอนจินวาดให้เอง)

หน้าตาเกมตัวอย่างบนจอเป็นแบบนี้ — กล่อง cyan คือผู้เล่น เลื่อนด้วยจอยไปแตะเป้าเหลือง ครบ 5 แต้มขึ้น YOU WIN! นี่คือเป้าหมาย MVP ที่กลุ่มจะดัดแปลงเป็นเกมของตัวเอง
# กฎทอง: แตะแค่โค้ดในช่อง "# N) ..." สามช่องเท่านั้น ห้ามแก้ bentogame.py
# ทุกอย่างที่ต้องใช้คือ game. call:
# game.start() -> ปลุกจอ + จอยสติก
# game.Box(x,y,w,h,col) -> กล่องที่ขยับได้
# k = game.keys() -> k.left/.right/.up/.down/.a/.b/.start
# game.hit(a, b) -> True ถ้ากล่องสองอันทับกัน
# game.run(update, fps) -> วนเรียก update() ของเราไปเรื่อย ๆ
พอโครงพร้อมแบบนี้ แปลว่า engine (70%) ครบแล้ว เหลือแค่เราเติม 3 ช่อง (30%):
ผลงานขั้นนี้: ทุกคนเห็นเกมตัวอย่างวิ่งบนจอ และชี้ "3 ช่องว่าง" ในไฟล์ได้
update())import bentogame as game
game.title("MY GAME") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
# 1) STATE — เกมของเราต้องจำอะไรบ้าง? (สร้างครั้งเดียว ก่อนลูป)
player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 30, 30, game.CYAN)
target = ____ # เป้าสีเหลือง ขนาด 24x24 วางสุ่ม
def update():
# 2) TICK — ทุกเฟรมอัปเดตยังไง? ขยับผู้เล่นตามปุ่มจอย
...
# 3) WIN / LOSE — ชนเป้าได้คะแนน ครบเป้าแล้วชนะ
...
return True
game.run(update, fps=30)
แต่ละช่องรัน "เมื่อไหร่" ต่างกัน — # 1) STATE รันครั้งเดียวตอนเริ่ม ส่วน # 2) TICK กับ # 3) WIN/LOSE อยู่ใน update() ที่ engine เรียกซ้ำทุกเฟรม:
ไฟล์เว้นช่อง: practise_codes/mygame.py↗
| เกมใหม่ | 30% ที่ต้องเขียนเอง | ระดับ | เหมาะกับ |
|---|---|---|---|
| X-O | กริด 3×3, วางหมาก, ตรวจชนะ 8 แนว | ง่าย | กลุ่มที่ยังไม่มั่นใจ เริ่มที่นี่ |
| Catch | ของหล่นลงมา, รับให้ทัน, +คะแนน | ง่าย | กลุ่มอยากได้ MVP เร็ว |
| Bomberman | วางระเบิด, ระเบิดกากบาทหน่วงเวลา | กลาง | กลุ่มกลาง |
| Tank Battle | รถถังหมุน+ยิง, กระสุนสะท้อน, 2P | กลาง | กลุ่มชอบ Shooter |
| Tetris | บล็อก 7 แบบ, หมุน, ลบแถว | ยาก | กลุ่มเก่ง |
| Pacman | เขาวงกต, จุดกิน, ผี AI ไล่ | ยาก | กลุ่มลอง pathfinding |
กฎ: ทุกเกมไม่ต้องเขียน engine ใหม่ ต่างกันแค่ "30% ยากแค่ไหน" เลือกตามกำลังของกลุ่มได้เลย
หน้าตาคร่าว ๆ ของบางเกมในตาราง — ดูแล้วเลือกที่กลุ่มอยากทำได้เลย:
![]()

ผลงานขั้นนี้: กลุ่มตกลงชื่อเกม 1 เกม พร้อมเหตุผลว่าทำไมถึงเลือกเกมนั้น
เกมกริด (X-O / Bomberman / Pacman / Tetris) แปลงช่อง / จุดบนจอด้วยสูตรเดียว:
ดัชนีช่องแบบ 1 มิติ (เก็บกระดานเป็น list เดียว):
X-O: board[row*3 + col] — กริด 3×3 = list ขนาด 9
หมายเหตุ (design-only): สูตร
row*COLS+colเป็นทางเลือกของการออกแบบสำหรับเกมกริดเท่านั้น (X-O / Tetris / Pacman / Bomberman) —mygame.pyตัวอย่าง (วิ่งชนเป้า) ไม่ได้ใช้ เพราะเก็บตำแหน่งเป็นพิกัด(x, y)ตรง ๆ ผ่านgame.Boxไม่ใช่ดัชนีช่องกริด เลือกใช้ก็ต่อเมื่อกระดานเป็นตารางช่องจริง ๆ
นี่คือผังคิดของกลุ่ม ก่อนลงมือเขียนจริง ลองดูตัวอย่างที่กรอกด้วย X-O:
=== ใบออกแบบเกมของกลุ่ม ===
ชื่อเกม: Tic-Tac-Toe (X-O)
[70% ที่ฉัน REUSE -- ห้ามเขียนเอง] (ต้องเป็น game. call เท่านั้น)
- game.Box -> วาด 9 ช่อง - game.keys() -> เลื่อน cursor + A วางหมาก
- game.Text -> ตา/ผู้ชนะ - game.run() -> ลูปเกม
[30% ที่ฉันต้องเขียนเอง -- 4 ขั้นคิด -> ลง 3 ช่องใน mygame.py (Draw เอนจินทำให้)]
1) State: board[9], cursor, current_player
2) Tick: เลื่อน cursor, กด A -> วางหมาก -> สลับตา
3) Win/Lose: ตรวจ 8 แนวชนะ (3 แถว+3 หลัก+2 ทแยง)
4) Draw: ระบายช่องตาม board[], เน้นกรอบ cursor
[MVP] กระดาน 3x3 + วางหมากสลับ + ตรวจแพ้ชนะ
กฎที่ขอย้ำ: 70% = game. call เท่านั้น · 30% ต้อง map ลง 3 ช่องให้ครบ · MVP ต้องเล็กพอจนทำเสร็จได้ในคาบเดียว
แปลงใบออกแบบเป็น checklist บน GitHub ที่ Issues → New issue:
หัวข้อ: [กลุ่ม X] เกมของเรา: X-O -- MVP คาบ 14
## 30% ที่ต้องเขียน (4 ขั้นคิด -> 3 ช่องใน mygame.py) -- checklist
- [ ] 1) State: board[9], cursor, current_player
- [ ] 2) Tick: เลื่อน cursor + กด A วางหมาก + สลับตา
- [ ] 3) Win/Lose: ตรวจ 8 แนวชนะ
- [ ] 4) Draw: ระบายช่องตาม board[]
## MVP: กระดาน 3x3 + วางหมากสลับ + ตรวจแพ้ชนะ

ใช้ - [ ] ทุกข้อ เราจะ tick ทีละข้อระหว่างเขียน MVP — ปิดครบเมื่อไหร่ = MVP เสร็จ
mygame.py (50 นาที)ลงมือจริง: copy practise_codes/mygame.py↗ เป็นชื่อกลุ่มเรา แล้วเติมทีละช่องตาม Issue
# 1) STATE — สิ่งที่เกมต้องจำ
player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 30, 30, game.CYAN)
tx, ty = random_spot()
target = game.Box(tx, ty, 24, 24, game.YELLOW)
score = 0
label = game.Text("Score: 0", 10, 10, game.WHITE)
def update():
global score
k = game.keys()
# (Back=ออก / Start=เริ่มใหม่ — game.run() จัดการให้)
# 2) TICK — ขยับผู้เล่นตามปุ่มจอย (Box.move clamp ขอบให้เอง)
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)
return True
ช่อง TICK นี้ทำงานแบบนี้ — กดจอยทิศไหน กล่อง cyan ก็เลื่อนทิศนั้น และ Box.move() จะ clamp ให้เองเมื่อชนขอบ (ไม่หลุดจอ) โค้ด clamp อยู่ใน bentogame.py (engine 70%) ไม่ใช่ใน mygame.py — ฝั่งเราแค่เรียก player.move() (mygame.py:40–47) แล้วเอนจินบีบพิกัดให้อยู่ในจอเอง:

เปิดไฟล์ใน BENTO IDE แล้วกด Program to Device ทุกครั้งที่เติมเสร็จ 1 ช่อง — เห็นผลทันที
# 3) WIN / LOSE — ชนเป้าได้คะแนน ครบ GOAL แล้วชนะ
if game.hit(player, target):
score += 1
label.set("Score: %d" % score)
game.sfx("point") # เสียงตอนได้แต้ม
nx, ny = random_spot()
target.move_to(nx, ny) # ย้ายเป้าไปที่ใหม่
if score >= GOAL:
label.set("YOU WIN!")
game.sfx("win")
return False # ชนะ -> จบเกม
โค้ดช่องนี้ map ตรงกับสิ่งที่เห็นบนจอ: แตะเป้า → score += 1 ป้ายอัปเดต เป้าโผล่ที่ใหม่ พอครบ GOAL ก็ขึ้น YOU WIN! แล้วจบรอบ:
สะพานทฤษฎี → โค้ด:
game.hit(player, target)(mygame.py:50) ข้างในคือ AABB overlap ที่เราพิสูจน์สมการมาแล้วในคาบ 11–12 — 4 อสมการ "ขอบซ้าย/ขวา/บน/ล่างเหลื่อมกัน" (shooter_full.py:48–50ฟังก์ชันoverlap()) engine ทำให้หมด เราแค่เรียกใช้ ไม่ต้องเขียนเงื่อนไขชนเอง

เขียนเองก่อน ติดตรงไหนยกมือถามได้เลย เดี๋ยวเฉลยในห้อง — อย่าลอกทั้งก้อน
ผลงานขั้นนี้: เกมขึ้นจอ เล่นได้ครบลูป (เริ่ม → ได้แต้ม → ชนะ/จบ) และ Issue ปิด checklist ครบ
mygame.py — ไล่ทั้งลูป3 ช่องที่เราเติมประกอบกันเป็นผังนี้: STATE สร้างครั้งเดียว จากนั้น game.run() วน update() ทุกเฟรม — อ่านปุ่ม → ขยับ → เช็กชน → เช็กชนะ แล้ว return True วนต่อ (หรือ return False = จบ)
ทั้ง
hit? Noและ≥GOAL? Noให้ผลเดียวกันคือreturn Trueแล้ววนกลับไปอ่านปุ่มเฟรมถัดไป — มีทางเดียวที่เกมจบคือreturn FalseตอนคะแนนครบGOAL
game.sfx() ในเกมตัวอย่างคาบนี้game.sfx("point") / game.sfx("win") คือชื่อ event ที่เราส่งให้ engine — เกม catch ตัวอย่าง (วิ่งกล่อง cyan ไปแตะเป้าเหลือง) ใช้เสียงสั้น ๆ ตอนแตะเป้าได้แต้ม เสียงตอนครบ GOAL แล้วชนะ และเสียงปิดท้ายตอนเกมจบ นี่คือ "เสียง feedback" แบบเดียวกับที่ checklist เช็คผ่านขอให้กลุ่มออกแบบเอง
เสียงในคาบนี้ — เกม catch ตัวอย่าง: แตะเป้า → ได้แต้ม, ครบ GOAL → ชนะ, แล้วปิดเกม (กดเล่นฟังได้จริง):
sfx_ui_start (ครบ GOAL/ชนะ)
sfx_game_over (ปิดเกม)
ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer
เสียงไม่ได้มีแค่ "ได้ยิน" — มันมี รูปร่าง ด้วย นี่คือ waveform ของ sfx_ui_select (เสียงตอนแตะเป้า/ได้แต้ม) เสียงสั้นแหลมจะขึ้นต้นแรงแล้วจางเร็ว:

รูปคลื่น (waveform) ของ sfx_ui_select — แกนนอนคือเวลา แกนตั้งคือความดัง
แต่ละกลุ่มจับคู่ event ของเกมตัวเอง (ได้แต้ม / ชนะ / แพ้) กับเสียงที่เหมาะ แล้วลอง map ชื่อใน game.sfx("...") ให้ตรงกับโทนเกม
จุดประสงค์: ทำให้กลุ่มอื่น โหลดเกมเราไปเล่นได้ ด้วย 3 ก้าวง่าย ๆ
ที่ repo: Releases → Draft a new release ตั้ง tag v0.1 ใส่ชื่อเกม + วิธีเล่น แล้ว แนบไฟล์ mygame.py

หน้า Draft a new release บน GitHub — ช่อง Tag ใส่ v0.1 · กล่อง Attach binaries ลากไฟล์ mygame.py มาวาง · แล้วกด Publish release
เพื่อนกลุ่มอื่นโหลด mygame.py จากหน้า Release → เปิดใน BENTO IDE → กด Program to Device → เล่นได้เลย
DevOps/CI ตัวเต็ม (GitHub Actions, auto-build) เราจะลงลึกในคอร์ส Developer II
ทุกกลุ่มได้เวที 3 นาที โชว์เกมบนบอร์ดจริง
mygame.py จาก Release ของเพื่อน
ที่มา: "WikiGameJam NYC 2025 participants" — Kevin Payravi, CC BY-SA 4.0, Wikimedia Commons
บรรยากาศ showcase จริง — กลุ่มเล็ก ๆ ล้อมโต๊ะ โชว์เกมที่เพิ่งทำเสร็จให้กันดูและลองเล่น นี่คือบทพิสูจน์ว่า "ของเรารันบนเครื่องคนอื่นได้" — หัวใจของการ ship งานจริง
ทั่วโลกมีงานแบบนี้จริง ๆ เช่น Global Game Jam ที่นักพัฒนาหลายพันคนทั่วโลกรวมตัวกันทำเกมให้เสร็จในเวลาจำกัด ภาพล่างคือบรรยากาศจริงงานหนึ่งที่ฝรั่งเศส — คนกลุ่มเล็ก ๆ ตั้งโจทย์ ลงมือ แล้วปล่อยผลงานในไม่กี่ชั่วโมง

ที่มา: "Global Game Jam 2019 à Arles" — Yannickvernet, CC BY-SA 4.0, Wikimedia Commons
สิ่งที่กลุ่มเราเพิ่งทำ — ตั้ง scope · ทำ MVP · ปล่อย Release ให้คนอื่นเล่น — คือสิ่งเดียวกับที่นักพัฒนาเกมจริงทำ ต่างกันแค่บอร์ดของเราคือ PSoC Edge
เครื่องยุคนี้รวม CPU + GPU + memory controller ไว้บน SoC die เดียว + มี SDK/OS ซ่อนความซับซ้อนให้นักพัฒนา
เชื่อมกับวันนี้: บอร์ด PSoC Edge ของน้องก็คือ SoC แบบนั้น (tri-core M33+M55) ที่น้องเพิ่ง ship เกม ลงไป
ที่มา: ESP32 SoC block diagram · Brian Krent (CC0) · en.wikipedia.org/wiki/System_on_a_chip
| อาการ | วิธีแก้ |
|---|---|
| เลือกเกมใหญ่เกิน (Pacman เต็ม / Tetris มีทุกบล็อก) | ตัดเหลือ MVP: Pacman = กิน dot + ผี 1 ตัว |
| เขียน 70% ว่า "เขียน game loop" | loop = game.run() ให้มาแล้ว · 70% = "เรียก" ไม่ใช่ "เขียน" |
| 30% map ลง 3 ช่องไม่ได้ | เกิน 3 ช่อง = scope ใหญ่ไป ตัดจนเหลือ 3 |
เริ่มแก้ bentogame.py |
ห้ามแตะ 70% core — แก้ได้แค่ 3 ช่องใน mygame.py |
| Release ลืมแนบไฟล์ | เพื่อนโหลดไม่ได้ = Release ไม่ผ่าน ต้องแนบ mygame.py |
| Issue ไม่มี checklist | ใช้ - [ ] ทุกข้อ 30% |
วิธีคิดที่ใช้ได้กับทุกเกม เริ่มจากไอเดียเต็ม แล้ว ตัดของที่ยังไม่จำเป็นออก จนกว่า 30% จะลงพอดี 3 ช่องใน mygame.py:

เป้าหมายไม่ใช่เกมที่ครบเครื่อง แต่เป็นเกมที่ เล่นจบได้จริงในคาบเดียว — ของที่ตัดออกไป เก็บไว้ทำต่อเป็น v0.2 หลังคาบได้เสมอ
game. call · 30% map 3 ช่องครบmygame.py ที่กลุ่มอื่นโหลดเล่นได้DESIGN.md)
เกมที่กลุ่มเพิ่งส่ง ดูเหมือนแค่กล่องวิ่งบนจอ แต่ข้างใต้มันคือ 4 ศาสตร์ที่เราฝึกมาทั้งคอร์ส:
State คือ "ตัวแปร" ที่เกมต้องจำ · update() คือ "ฟังก์ชัน" ที่ engine เรียกซ้ำ ๆ (callback) · กระดานเก็บเป็น list แล้วหาช่องด้วย index — ของพื้นฐานทั้งนั้นgame.hit(a, b) ข้างในคือสูตรตรวจสี่เหลี่ยมทับกัน (AABB) · เกมกริดใช้ row*COLS+col แปลง 2 มิติเป็น list เดียว — นี่คืออัลกอริทึมจริง(x, y) บนจอ 792×398 · Box.move() ก็คือการย้ายพิกัดแล้ว clamp ไม่ให้หลุดขอบgame.sfx() — ไม่ใช่ simulator
นี่คือ "การต่อจุด" — เกมทำให้แนวคิดยาก ๆ จับต้องได้ พอเข้าใจแล้วน้องเอาไปทำสินค้าจริงต่อได้ ไม่ใช่แค่เกม
น้อง ๆ ไม่ได้เริ่มจากศูนย์เลย เราแค่เติม 30% ที่ฝึกมาแล้ว 4 รอบ ลงใน Game Engine เดิมที่คุ้นมือ แล้วปล่อยออกไปให้คนอื่นเล่น
ทำเอง 30% หลังคาบ: ลองเพิ่ม feature เล็ก ๆ ของกลุ่มเอง (เช่น เพิ่มศัตรูตัวที่ 2, เพิ่มเลเวล, เปลี่ยน Box เป็น game.Sprite) แล้ว push เป็น Release v0.2
และนี่คือจังหวะที่น้อง ๆ จะรู้สึกจริง ๆ ว่า "เกมนี้เป็นของเรา" — แล้วเจอกันคอร์สหน้า Developer II
mygame.py — อ่านให้เข้าใจ แล้วปิดไฟล์พิมพ์เองเฉลยนี้ไม่ได้มีไว้ลอกวางส่ง คะแนนของ Capstone อยู่ที่ใบออกแบบ 70/30 กับเกมที่กลุ่มอธิบายได้ด้วยคำพูดตัวเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจทุกบรรทัด ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือ ตอนพิมพ์เองนี่แหละที่สมองจำโครงได้ ที่สำคัญ mygame.py ทั้งไฟล์คือ "เกมตัวอย่างเต็ม" ที่ยืนอยู่บน engine bentogame.py (70% ที่ห้ามแตะ) เราแตะแค่ 3 ช่องเท่านั้น จากนี้เราจะแกะทีละก้อน ไม่ใช่ดูรวดเดียว เพราะทุกก้อนมีเหตุผลออกแบบของมัน
ก้อนแรก — ตั้งค่าคงที่ก่อนเริ่ม:
import bentogame as game
import random
# --- ค่าเกม ----------------------------------------------------------------
PLAYER_SPEED = 8
GOAL = 5
import bentogame as game (:8) ดึง engine 70% ทั้งก้อนเข้ามาใต้ชื่อสั้น game. — ตรงกับกฎทองสไลด์ "70% engine + 30%": ทุกอย่างที่ขึ้นต้นด้วย game. คือของที่มีให้แล้ว เราแค่เรียก ไม่เขียนใหม่PLAYER_SPEED = 8 กับ GOAL = 5 (:12-13) ยก "ค่า" ขึ้นมาตั้งชื่อไว้บนสุด ไม่ฝังเลข 8 หรือ 5 ลอย ๆ กลางลูป นี่คือบทเรียน "แยกค่าออกจากโครง" — ปรับความไวผู้เล่นหรือเงื่อนไขชนะได้ที่จุดเดียวmove() พอวันปรับสมดุลเกมต้องไล่แก้หลายที่ เสี่ยงแก้ไม่ครบ การตั้งชื่อค่าคือนิสัยกันบั๊กที่จะติดตัวไปทั้งคอร์สimport random (:9) เตรียมของไว้สุ่มตำแหน่งเป้า ซึ่งเป็นส่วน State ที่ต้อง "ไม่ซ้ำที่เดิม" ทุกครั้งที่ได้แต้มก้อนสอง — ฟังก์ชันสุ่มตำแหน่งเป้า:
def random_spot():
"""สุ่มตำแหน่งเป้าให้อยู่ในจอ (เผื่อขอบ)"""
x = random.randint(0, game.WIDTH - 24)
y = random.randint(0, game.HEIGHT - 24)
return x, y
random_spot() เป็นฟังก์ชันเดียว (:16-20) เพราะถูกเรียกซ้ำ 2 ที่ — ตอนวาง target ครั้งแรก และตอนย้ายเป้าหลังได้แต้ม เขียนกติกาสุ่มไว้ที่เดียว แก้ทีเดียวมีผลทั้งเกมgame.WIDTH - 24 และ game.HEIGHT - 24 (:18-19) เผื่อขอบเท่าขนาดเป้า 24px กันไม่ให้สุ่มไปโผล่ครึ่งตัวนอกจอจนกดไม่โดน — เป็นแนวคิด clamp เดียวกับที่ Box.move() ทำให้ผู้เล่น แต่ที่นี่เราคุมขอบเองgame.WIDTH จาก engine แทนพิมพ์เลข 792 ตรง ๆ — นิสัย "ถามฮาร์ดแวร์ก่อน อย่าเดา" เผื่อบอร์ดหรือจอต่างขนาด โค้ดชุดเดิมก็ยังรันถูกอ่านเฉลยแบบนี้ให้จับ "เหตุผล" ไม่ใช่จับ "ตัวอักษร" — พอเข้าใจว่าทำไมถึงตั้ง
PLAYER_SPEEDเป็นค่าคงที่ น้องจะเขียนเกมของกลุ่มเองได้โดยไม่ต้องเปิดเฉลย
ช่องแรกใน 3 ช่องที่เราเขียนเอง ตรงกับ ขั้นที่ 1 ใน 4 ขั้นคิด ที่วางไว้ต้นคาบ: "เกมต้องจำอะไรบ้าง" ทุกตัวใน State สร้าง ครั้งเดียวก่อนลูป
game.title("MY GAME") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
# 1) STATE — สิ่งที่เกมต้องจำ
player = game.Box(game.WIDTH // 2, game.HEIGHT // 2, 30, 30, game.CYAN)
tx, ty = random_spot()
target = game.Box(tx, ty, 24, 24, game.YELLOW)
score = 0
label = game.Text("Score: 0", 10, 10, game.WHITE)
game.title("MY GAME") (:23) เรียก engine ทำหน้า Start ให้ครบ — ปลุกจอ ปลุกจอย รอปุ่ม Start/Back ให้เอง เราไม่ต้องเขียนหน้าเมนูสักบรรทัด นี่คือ 70% กำลังทำงานแทนเรา:26-30) ตรงกับผัง STATE(1 ครั้ง) ในสไลด์ — ถ้าเผลอย้าย game.Box(...) เข้าไปใน update() จะเกิดวิดเจ็ตใหม่ทุกเฟรม จอมีงบได้สูงสุด 32 ชิ้น เต็มเมื่อไรเกมพังทันทีplayer, target, label เก็บลงตัวแปรทั้งหมด เพราะทุกตัวต้อง "อ้างถึงซ้ำ" ใน update() (ขยับ player, ย้าย target, label.set(...)) — บทเรียนเดิม: อะไรที่ต้องแก้ทีหลัง เก็บลงตัวแปรplayer เป็นกล่อง 30x30 สี game.CYAN, target 24x24 สี game.YELLOW — ขนาดกับสีคือ State ที่ตาเห็น ผู้เล่นใหญ่กว่านิดให้จับง่าย เป้าเล็กกว่าให้ท้าทายscore = 0 (:29) ตัวแปรจำนวนธรรมดา แต่คือหัวใจของ State — "เกมต้องจำคะแนน" ถ้าไม่มีตัวนี้ เงื่อนไขชนะในช่อง 3 ก็ไม่มีอะไรให้เทียบสังเกตว่า State ทั้งก้อนไม่มี logic อะไรเลย มันแค่ "ตั้งของที่เกมต้องจำ" ให้ครบก่อนลูปเริ่ม ยิ่ง State ชัด ช่อง Tick กับ Win/Lose ยิ่งเขียนง่าย
สองช่องที่เหลืออยู่ใน update() ที่ engine เรียกซ้ำทุกเฟรม ตรงกับ ขั้น 2 (Tick) และ ขั้น 3 (Win/Lose) ของ 4 ขั้นคิด:
def update():
global score
k = game.keys()
# (Back=ออก / Start=เริ่มใหม่ — game.run() จัดการให้)
# 2) TICK — ขยับผู้เล่นตามปุ่มจอย (Box.move clamp ขอบให้เองแล้ว)
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)
global score (:34) ต้องประกาศเพราะ update() จะ แก้ ตัวแปรระดับบนสุด ไม่ใช่แค่อ่าน — ถ้าลืมบรรทัดนี้ Python จะสร้าง score ตัว local เงียบ ๆ แล้วคะแนนจะไม่เพิ่มจริง เป็นบั๊กคลาสสิกที่เจอกันบ่อยk = game.keys() (:36) อ่าน snapshot ปุ่มครั้งเดียวต่อเฟรม แล้วใช้ซ้ำทั้งเฟรม — จังหวะ input จึงคงที่ ไม่ใช่อ่านใหม่ทุกบรรทัดจนค่าเพี้ยนกลางเฟรมplayer.move(...) (:40-47) — โค้ด clamp กันหลุดจออยู่ใน bentogame.py (engine 70%) เราแค่บอกทิศ ไม่เขียนเงื่อนไข if ขอบซ้าย/ขวา/บน/ล่างเอง ตรงกับที่เห็นในสไลด์ tick_moveif แยกทุกทิศ (ไม่ใช่ elif) ตั้งใจให้กด left+up พร้อมกันแล้วเลื่อนทแยงได้ — การออกแบบ input เล็ก ๆ ที่ทำให้เกมลื่นขึ้น # 3) WIN / LOSE — ชนเป้าได้คะแนน ครบ GOAL แล้วชนะ
if game.hit(player, target):
score += 1
label.set("Score: %d" % score)
game.sfx("point")
nx, ny = random_spot()
target.move_to(nx, ny)
if score >= GOAL:
label.set("YOU WIN!")
game.sfx("win")
return False
return True # เล่นต่อ
game.run(update, fps=30)
game.hit(player, target) (:50) ข้างในคือ AABB overlap ที่เราพิสูจน์สมการมาแล้วในคาบ 11–12 (4 อสมการขอบเหลื่อมกัน) — engine ทำให้หมด เราแค่ถามว่า "ทับกันไหม" ไม่เขียนเงื่อนไขชนเอง:51-55): score += 1 (เลข), game.sfx("point") (เสียง), random_spot() + move_to (เป้าโผล่ที่ใหม่) — ตรงกับสไลด์ "เสียง feedback" ที่ให้กลุ่มออกแบบเองif score >= GOAL: ... return False (:56-59) คือทางออกทางเดียวที่ทำให้ลูปจบ ส่วน return True (:61) แปลว่าเล่นต่อ — ตรงเป๊ะกับผังลูปทั้งตัวที่มี "ทางจบทางเดียว"game.run(update, fps=30) (:64) ส่งฟังก์ชัน update ของเราให้ engine ไปวน 30 เฟรม/วินาที นี่คือ callback: เราเขียน "จะทำอะไรต่อเฟรม" ส่วน engine คุม "เมื่อไรและบ่อยแค่ไหน"ช่อง Tick กับ Win/Lose รวมกันไม่ถึง 20 บรรทัด แต่มันคือ "กติกาทั้งเกม" — เพราะงานหนักอย่าง loop, clamp, collision, เสียง อยู่ใน engine หมดแล้ว เราลงแรงเฉพาะสิ่งที่เป็นเกมของเราจริง ๆ
ถอยมามองทั้งไฟล์ จะเห็นว่าแต่ละก้อนเล็กมาก แต่ประกอบกันเป็นเกมที่เล่นจบได้จริง หัวใจอยู่ที่คอลัมน์ "เรียก engine อะไร" — เกือบทุกช่องคือการ เรียก ของที่มีแล้ว ไม่ใช่ เขียนใหม่:
| ก้อน | บรรทัด | หน้าที่ | เรียก engine 70% | ถ้าทำแบบมือใหม่จะพังยังไง |
|---|---|---|---|---|
| ค่าคงที่ | :12-13 |
ยกค่าออกมาตั้งชื่อ | — | ฝังเลขในลูป ปรับความยากไม่ครบ |
random_spot |
:16-20 |
สุ่มเป้าเผื่อขอบ | game.WIDTH/HEIGHT |
เป้าโผล่นอกจอ กดไม่โดน |
| 1) STATE | :26-30 |
เกมจำอะไร | game.title/Box/Text |
สร้างใน loop จอเต็ม 32 ชิ้น |
| 2) TICK | :39-47 |
ขยับตามปุ่ม | game.keys · Box.move |
เขียน clamp ขอบเอง ยาว+พลาด |
| 3) WIN/LOSE | :49-59 |
ชน/ได้แต้ม/ชนะ | game.hit · game.sfx |
เขียน AABB เอง หรือไม่มีทางจบ |
| ลูป | :64 |
มอบลูปให้ engine | game.run |
เขียน while+timing เอง เพี้ยน fps |
เห็นรูปแบบเดียวกับที่คาบ 1 เราต่อไฟ LED ไหม — เริ่มจากของเล็กที่สุดที่รันได้ แล้วเติมทีละแนวคิด: ตั้งค่า → เตรียม State → อ่านปุ่มขยับ → เช็กชนแล้วตัดสินแพ้ชนะ ไม่มีก้อนไหนกระโดดไปเขียนทั้งเกมรวดเดียว
ถ้าจับหลักนี้ได้ น้องจะสร้างเกมใด ๆ ในแคตตาล็อกได้ด้วยโครงเดิม — เปลี่ยนแค่ "ของ" ใน 3 ช่อง ส่วน engine 70% ไม่ต้องแตะเลยสักบรรทัด
อยากให้หยุดคิดสักครู่ก่อนปิดคอร์ส ไม่ใช่เรื่องโค้ด แต่เรื่อง "มองให้ทะลุ" ว่าเกมกล่องวิ่งชนเป้าที่เพิ่งทำ จริง ๆ แล้วเป็นชิ้นส่วนของอะไร
ที่มา — 3 ช่องนี้กลั่นมาจากไหน ย้อนไปสไลด์ "30% = 4 ขั้นคิด" กับ 13 คาบที่เราถอด Snake / Flappy / Pong / Shooter ทีละเกม ทุกเกมที่ถอดมามีโครงเดียวกันเป๊ะ: State → Tick → Win/Lose → Draw ตอนนั้นเราถอดของคนอื่น วันนี้ mygame.py คือแม่แบบที่กลั่นจาก 4 เกมนั้นให้เหลือ 3 ช่องที่เราเขียนเอง (Draw เอนจินวาดให้) ส่วน game.hit ที่ตัดสินการชน ก็คือ AABB ที่เราพิสูจน์สมการกันมาแล้วในคาบ 11–12 — ไม่มีอะไรวันนี้ที่โผล่มาจากอากาศ
ที่ไป — 3 ช่องนี้จะโตเป็นอะไร โครงนี้ไม่ได้ใช้กับเกม catch ตัวเดียว มันคือ "พิมพ์เขียว" ของทุกเกมในแคตตาล็อกจากสไลด์ "เลือกเกม" (X-O → Tetris → Pacman) เปลี่ยนแค่ของใน 3 ช่อง งานทำเอง v0.2 หลังคาบก็คือการเติมช่องเดิม และคอร์ส Developer II จะเอาโครงนี้ไปห่อด้วย DevOps/CI/AI ตัวเต็ม — ในงานจริงมันคือ prototype-first ที่สตูดิโอเกมทั้งโลกใช้
ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
target เป็น "ศัตรูที่ขยับเอง" จะยังเป็น 3 ช่องเดิมไหม (ใบ้: State เพิ่ม 1 ตัว, Tick ขยับมันด้วย)game.hit(player, target) วันนี้บอก "ชนเป้า → ได้แต้ม" ได้ พรุ่งนี้ game.hit(player, enemy) จะบอก "โดนศัตรู → แพ้" ได้ไหม มันคือสมการ AABB เดียวกันหรือเปล่าupdate() ที่ engine เรียกซ้ำทุกเฟรม มันคือเรื่องเดียวกับ game loop ที่เราพูดถึงตั้งแต่คาบแรก ต่างแค่ตอนนี้เราเขียน "ข้างในลูป" เองได้แล้วถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด: เกมทั้งเกมคือ State + ลูป + การชน ที่โตขึ้น น้องถือกุญแจครบแล้ว
เทคนิคในเกมกล่องวิ่งชนเป้า ไม่ใช่แค่แบบฝึกหัด มันคือสิ่งที่วงการเกมและซอฟต์แวร์จริงใช้กันทุกวัน:
v0.2game.run(update, fps=30) คือแนวคิดเดียวกับ Update() ใน Unity, Tick() ใน Unreal, _process() ใน Godot: เอนจินเรียกฟังก์ชันเราซ้ำต่อเฟรม เราไม่เขียน while เองgame.hit ที่ตรวจสี่เหลี่ยมทับกัน คือด่านแรกของ physics และ UI จริง: hit-test ปุ่มบนจอสัมผัส, ตัวละครชนกำแพง, hitbox กระสุน ล้วนเริ่มจาก 4 อสมการเดียวกับที่พิสูจน์คาบ 11–12random_spot() ที่ย้ายเป้าไปที่ใหม่ คือ spawn logic ในเกมจริง: ไอเทมดรอป, ศัตรูเกิดสุ่ม, จุดเกิดผู้เล่นใหม่ ทุกที่ต้อง "เผื่อขอบ" ไม่ให้เกิดนอกจอ แบบที่เราลบ 24pxห้าอย่างที่ประกอบกันเป็นเกมเล็ก ๆ วันนี้ ไม่มีอันไหนเป็นของสมมติเลย — ทุกอันมีเอนจินเกมระดับโลกที่ทำงานด้วยหลักการเดียวกันอยู่จริง
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาเกมของกลุ่มเองและโปรเจกต์จริงได้:
target เลื่อนหนีผู้เล่นแทนที่จะอยู่นิ่ง จะเพิ่มลง 3 ช่องไหน (State เก็บทิศของเป้า? Tick ขยับเป้าด้วย?) โดยไม่แตะ bentogame.py เลยgame.hit) จะออกแบบให้มี return False สองความหมาย (ชนะเมื่อครบ GOAL / แพ้เมื่อโดนศัตรู) ให้โค้ดยังอ่านง่ายได้ยังไงgame.Box เป็น game.Sprite ให้ตัวละครเป็นพิกเซลอาร์ตจริงแบบ 4 เกมบนเครื่อง โครง 3 ช่องต้องเปลี่ยนตรงไหนบ้าง และตรงไหนที่ไม่ต้องแตะเลยmygame.py เข้า Release ด้วยมือ ถ้าอยากให้ทุกครั้งที่ git push แล้วระบบ build + ปล่อย Release ให้เองโดยไม่ต้องแนบไฟล์มือ จะออกแบบขั้นตอนยังไง นี่คือ GitHub Actions / DevOps ตัวเต็มที่เรารอเปิดกันในคอร์ส Developer IIเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากเกมที่กลุ่มพิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นวิศวกร ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css