คาบ 12 — Capstone

ออกแบบ "เกมของตัวเอง" (grid / physics)

คาบรองสุดท้ายของคอร์ส (คาบ 12 จาก 13) — จุดที่ทุกอย่างมารวมกัน

คาถาประจำคาบ: "เราไม่ได้สร้าง Game Engine ใหม่ เราแค่เปลี่ยน 4 ช่อง: State · Tick · Win/Lose · Draw."

11 คาบที่ผ่านมา = ถอด Snake (grid) + Flappy (physics) จนเข้าใจ · วันนี้ = น้อง ๆ ออกแบบเกมใหม่ของกลุ่มเอง

ดูของจริงก่อน — เกมตัวอย่างที่ทำได้จริง (Space Shooter) บนบอร์ด BENTO

เดโมรันบนบอร์ด BENTO จริง — โดย อ.วิรุฬห์ ศรีบริรักษ์ (YouTube)

เป้าหมายของคาบนี้

จบคาบทุกกลุ่มต้อง ตัดสินใจให้เสร็จ แล้วเขียนออกมาเป็น แผน 1 หน้า + เปิด MVP จากโครงให้ได้:

  1. เลือก แนวเกม: grid (แบบ Snake) หรือ physics (แบบ Flappy)
  2. แยกให้ออกว่าอะไรคือ 70% ที่ reuse และอะไรคือ 30% ที่เราเขียนเอง
  3. เปิด GitHub Issue ของกลุ่ม เป็น checklist
  4. เปิด mygame.py จาก practise แล้วกด Program to Device ให้ MVP ขึ้นจอ
สิ่งที่ต้องเห็นปลายคาบ: เอกสารออกแบบ + Issue บน GitHub ที่ตอบได้ชัดว่า "MVP = อะไรที่ขึ้นจอแล้วเล่นได้" และ mygame.py รันบนบอร์ดได้

คาบนี้เน้น "คิดแบบ architect" — reuse 70% เขียนเอง 30% — ไม่ใช่เขียนเกมให้เสร็จในคาบเดียว

กฎทอง: 70% engine + 30% กติกา

70% CORE — ของให้ (ห้ามแก้) screen · keys() · hit() · sound · run() · palette 30% กติกาเกมเรา

ทั้ง Snake และ Flappy ที่เราถอดมา ใช้ Game Engine ชุดเดียวกัน ~70% ต่างกันแค่ กติกา ~30% เท่านั้น

สูตรสร้างเกมใหม่: หยิบ 70% มาใช้เลย แล้วเขียนแค่ 4 อย่างที่เป็น 30%

ทุกอย่างอยู่ใน bentogame.py แล้ว เราแค่เรียกใช้ — ห้ามแตะไฟล์นี้

เกร็ด: ทำไม embedded สำคัญ — SNES Super FX (coprocessor)

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

ที่มา: "SNES-Mod1-Console-Set" — Evan-Amos, Public domain, Wikimedia Commons

เชื่อมกับวันนี้: งานวาดหนัก ๆ ของน้องก็ฝากให้ C core (coprocessor ของเรา) ทำ ส่วน Python คุม logic

SNES Super FX chip วาด polygon (ในตลับ) ส่งผลกลับ SNES CPU คุมเกม (logic) ≡ แบบเดียวกัน BENTO C core (CM55) วาด Box/Text IPC Python (CM33) คุมเกม (logic)

ตลับเกมมี Super FX ช่วยวาด ↔ บอร์ดเรามี C core (CM55) ช่วยวาด — coprocessor ตัวเดียวกันคนละยุค

30% = 4 ขั้นคิดเท่านั้น

1) State เกมต้องจำอะไร? (ตัวแปร) Snake: หัวงู + ลำตัว + อาหาร 2) Rule-in-tick ทุกเฟรมอัปเดตยังไง? Flappy: โน้มถ่วงดึงลง + กดกระพือ 3) Collision / win-lose ชนแล้วเกิดอะไร? แพ้/ชนะเมื่อไหร่? Flappy: ลอดท่อ→+1 / ชนท่อ→แพ้ 4) Draw สไปรท์ไหนอยู่ตรงไหน? เอนจินวาดให้ตามพิกัด Box/Text

เกมใหม่ทุกตัวคิดตาม 4 ขั้นเดิมนี้ ไม่มีอะไรเกินนี้ และในไฟล์เราเขียนจริงแค่ 3 ช่อง (Draw เอนจินวาดให้เอง)

2 แนวให้เลือก: grid หรือ physics

แนว grid — แบบ Snake
ตำแหน่งเป็น ช่อง (row, col) ขยับทีละช่อง
เก็บกระดานเป็น list เดียว
ชน = เช็คว่าช่องนั้นมีอะไรอยู่
เหมาะกับ: เกมเดินตาราง วางหมาก เก็บของ
แนว physics — แบบ Flappy
ตำแหน่งเป็น พิกเซลต่อเนื่อง
มีความเร็ว + แรง (โน้มถ่วง/เด้ง)
ชน = กล่องทับกัน game.hit()
เหมาะกับ: เกมเคลื่อนลื่น ตก/กระโดด/หลบ

grid จริง: Snake บนบอร์ด (คาบ 4-6)

physics จริง: Flappy บนบอร์ด (คาบ 7-10)

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

ทั้งสองแนวใช้ engine ชุดเดียวกัน ต่างกันแค่ "30% คิดเป็นช่อง" หรือ "30% คิดเป็นความเร็ว" — เลือกตามไอเดียของกลุ่มได้เลย

พิกัดกริด / พิกเซล (สำหรับแนว grid)

ถ้าเลือกแนว grid (เดินทีละช่องแบบ Snake) แปลงช่อง / จุดบนจอด้วยสูตรเดียว:

xpx=col×CELL,ypx=row×CELLx_{px} = col \times CELL, \qquad y_{px} = row \times CELL

col=xpxCELL,row=ypxCELLcol = \left\lfloor \frac{x_{px}}{CELL} \right\rfloor, \qquad row = \left\lfloor \frac{y_{px}}{CELL} \right\rfloor

เก็บกระดานเป็น list เดียว (ดัชนีช่องแบบ 1 มิติ):

index=row×COLS+colindex = row \times COLS + col

board[row * COLS + col] — กริด 3×3 = list ขนาด 9 (Snake ก็เก็บลำตัวงูเป็น list ของช่องแบบนี้)

ความเร็ว / แรง (สำหรับแนว physics)

ถ้าเลือกแนว physics (เคลื่อนลื่นแบบ Flappy) ทุกเฟรมคิดเป็น 2 บรรทัดเดิม:

v+=a(แรงสะสมเข้าความเร็ว)y+=v(ความเร็วสะสมเข้าตำแหน่ง)v \mathrel{+}= a \quad(\text{แรงสะสมเข้าความเร็ว}) \qquad y \mathrel{+}= v \quad(\text{ความเร็วสะสมเข้าตำแหน่ง})

a (โน้มถ่วง/แรง) v เล็ก v โตขึ้น (ตกเร็วขึ้น) กดปุ่ม = ใส่ความเร็วสวนทาง (Flappy: กระพือ = v พุ่งขึ้น) เด้ง = พลิกเครื่องหมายของ v
a = +0.5 (โน้มถ่วง)
v = 0.0 · y = 60
ปล่อยไว้ = ตกเร็วขึ้น (v โต) · กด = v เด้งขึ้น

Flappy ใช้สูตรนี้กับแกน y อย่างเดียว เกมของน้อง ๆ จะใช้ 1 แกนหรือ 2 แกนก็ได้

ทำไม 2 บรรทัดนี้ถึงได้ "อาร์ค" สวย ๆ

ลองกาง สองบรรทัดนั้นออกมาทีละเฟรม จะเห็นว่าเลขมันโตเป็นแบบไหน:

vn=v0+na(เส้นตรง)yn=y0+k=1nvk    y0+12an2(พาราโบลา)v_n = v_0 + n\,a \quad(\text{เส้นตรง}) \qquad y_n = y_0 + \sum_{k=1}^{n} v_k \;\approx\; y_0 + \tfrac{1}{2}a\,n^2 \quad(\text{พาราโบลา})

a คงที่ ทำให้ v โตเป็นเส้นตรง (เฟรมหลัง ๆ ตกเร็วขึ้น) — พอเอา v ที่โตขึ้นเรื่อย ๆ ไปบวกสะสมเข้า y ผลรวมจึงโค้งเป็น พาราโบลา นี่คือ "ตกแบบมีน้ำหนัก" ที่ทำให้ Flappy รู้สึกเป็นธรรมชาติ (จริง ๆ คือ Euler integration ของ a=ga=g)

v (เส้นตรง) เฟรมที่ 0 → v เพิ่มทีละ a เท่า ๆ กัน → ระยะตกต่อเฟรมยิ่งมากขึ้น (โค้งลง)

กดปุ่ม = เซ็ต v เป็นค่าลบก้อนเดียว → เส้นพาราโบลาเด้งหัวขึ้นชั่วครู่ แล้วโน้มถ่วงก็ดึงลงต่อ

ขั้น 1 — รัน 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 เฉพาะตอน "ชนะ/แพ้จริง" เท่านั้น)

ขั้น 1 (ต่อ) — engine 70% ครบ เหลือเติม 3 ช่อง (30%)

พอโครงพร้อมแบบนี้ แปลว่า engine (70%) ครบแล้ว เหลือแค่เราเติม 3 ช่อง (30%):

เกมใหม่  =  bentogame.py70% ห้ามแตะ  +  i=13ช่องi30% น้อง ๆ เขียน\text{เกมใหม่} \;=\; \underbrace{\texttt{bentogame.py}}_{70\%\ \text{ห้ามแตะ}} \;+\; \underbrace{\sum_{i=1}^{3}\text{ช่อง}_i}_{30\%\ \text{น้อง ๆ เขียน}}

ผลงานขั้นนี้: ทุกคนเห็นเกมตัวอย่างวิ่งบนจอ และชี้ "3 ช่องว่าง" ในไฟล์ได้

ขั้น 1 (ต่อ) — โครง 3 ช่อง (1 ก่อนลูป + 2 ใน update())

1) STATE ครั้งเดียว ก่อนลูป ⟶ game.run(update) ⟲ 2)+3) ในลูป tick + win/lose ทุกเฟรม
    # 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 — ยังไม่เฉลย)

init · STATE (ครั้งเดียว)title·Box·Text·score=0 :23-31 game.run(update, fps):72 อ่านปุ่ม keys():39 player.move ตามปุ่ม:40-47 TODO: ชน?Issue #2 TODO: game.hit → scoreชนะ/แพ้ → return False :52-58 ใช่ (เติมเอง) return True → เฟรมถัดไป (30 Hz) :68

สัญกรณ์: ▭ฟ้า = ประมวลผล · ▭เขียว = อ่าน input · ◇ม่วง = ตัดสินใจ · ▭เทามน = เริ่ม/จบ · เส้นประ = 30% ที่น้องเติมเอง (ยังไม่เฉลย)

โครงที่ให้มาแล้ว (practise_codes/mygame.py): State :23-31 · keys :39 · move :40-47 · loop :72 — 3 ช่องที่ต้องเติมคือ # 1) # 2) # 3) ที่ map ลงกล่องเส้นประนี้พอดี

ขั้น 2 — เลือกแนวเกมของกลุ่ม (30 นาที)

ไอเดียเกม แนว 30% ที่ต้องเขียนเอง เหมาะกับ
เก็บของให้ครบ grid วาง item ในช่อง, เดินเก็บ, นับครบ กลุ่มเริ่มต้น
เขาวงกตเดินถึงเส้นชัย grid กำแพงเป็นช่อง, เดินชนกำแพงไม่ได้ กลุ่มกลาง
หลบสิ่งกีดขวางตก physics ของตกลงมา, เลื่อนซ้ายขวาหลบ กลุ่มกลาง
กระโดดข้ามหลุม physics โน้มถ่วง + กระโดด + พื้นเลื่อน กลุ่มเก่ง

กฎ: ทุกเกมไม่ต้องเขียน engine ใหม่ ต่างกันแค่ "30% เป็น grid หรือ physics" และ "ยากแค่ไหน" เลือกตามกำลังของกลุ่มได้เลย

ผลงานขั้นนี้: กลุ่มตกลงไอเดียเกม 1 อัน + ระบุว่าเป็นแนว grid หรือ physics พร้อมเหตุผล

ขั้น 3 — ใบออกแบบ 1 หน้า: แยก 70/30 (40 นาที)

นี่คือผลงานหลักของคาบ ลองดูตัวอย่างที่กรอกด้วยเกมแนว grid:

70% REUSE — game. call เท่านั้น30% → 3 ช่อง

    === ใบออกแบบเกมของกลุ่ม ===
    ชื่อเกม: 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 ต้องเล็กพอจนทำเสร็จได้

ขั้น 4 — แปลงใบออกแบบ → GitHub Issue (40 นาที)

ขั้นนี้เราต่อยอด 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 เล็ก" ถึงสำคัญกว่าที่คิด

ภาพนี้คือสิ่งที่เกิดขึ้นจริงทุกครั้งที่ทำโปรเจกต์ — ทาง MVP รีบทำแก่นเดียวให้ ส่งได้ก่อน แล้วค่อยต่อยอด ส่วนทาง scope creep มัวแต่กองฟีเจอร์จน ไม่ได้ส่งสักที:

นักพัฒนามืออาชีพไม่ได้เก่งกว่าตรงที่เขียนเร็วกว่า แต่เก่งตรงที่ "ตัด" เป็น — รู้ว่าอะไรคือแก่นที่ต้องมี และอะไรรอได้

Making Your First Game: Minimum Viable Product - Scope Small, Start Right — Extra Credits

กับดักที่เจอบ่อย

Scope creep คือศัตรูหมายเลข 1 ของคาบนี้
อาการ วิธีแก้
เลือกเกมใหญ่เกิน (มีหลายด่าน / มี 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%

เช็คผ่าน + ส่งงาน

เช็คผ่าน (ทุกข้อต้องใช่)
  • เลือกไอเดียเกม 1 อัน + ระบุแนว grid/physics
  • ใบออกแบบ 1 หน้า ครบทุกหัวข้อ
  • 70% = game. call · 30% map 3 ช่องครบ
  • เปิด 1 GitHub Issue + checklist 30% ครบ
  • mygame.py รันบนบอร์ดได้ (Program to Device)
ส่งงานคาบนี้
  1. ลิงก์ GitHub Issue (1 อัน)
  2. ใบออกแบบ 1 หน้า (รูป / DESIGN.md)
  3. รูปจอบอร์ดตอน mygame.py รัน

เชื่อมโยงรากฐาน — เกมของน้องคือวิศวกรรมจริง

เกมที่น้องกำลังออกแบบ ไม่ใช่ของเล่นแยกจากวิชาวิศวกรรม — มันคือ 4 รากฐาน ที่เรียนมา มาประกอบกันพอดี:

Python
State = ตัวแปร · Tick = ฟังก์ชัน ที่ run() เรียกแบบ callback · กระดาน grid = list/indexing
Algorithms
physics v+=a; y+=v = Euler integration · grid = พีชคณิตพิกัด · ชน = เงื่อนไข/ค้นหา
Graphics
Box/Text ตามพิกัด = 2D coordinate system · ทุกเฟรมวาดใหม่ = frame/animation
Embedded / MCU
Python คุม logic บน CM33 · งานวาดหนักฝากให้ C core (CM55) ผ่าน IPC — แบ่งงานแบบ coprocessor
Python Algorithms Graphics Embedded / MCU เกมของ กลุ่ม

ออกแบบเกมเป็น = ใช้ทั้ง 4 รากฐานเป็น ทักษะนี้ตรง ๆ คือทักษะสร้าง "ผลิตภัณฑ์ฝังตัว" จริง

สรุป + ทำเอง 30%

คาบ 1-11ปู 70/30 + Snake + Flappy คาบ 12 (วันนี้)ออกแบบเกมของฉัน คาบ 13สร้าง MVP จริง

เป้าหมายสำคัญที่สุดของทั้งวิชา: การที่น้อง ๆ ออกแบบเกมของตัวเองได้ ไม่ใช่แค่ลอกตาม

น้อง ๆ ไม่ได้เริ่มจากศูนย์เลย เขาแค่เติม 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-15
  • FPS = 30 คือความถี่ที่ update() ถูกเรียกซ้ำ ตรงกับ "30 Hz" ในผัง mygame.py ที่เราดูไป (return True → เฟรมถัดไป) ทั้ง Snake และ Flappy ก็เต้นด้วยลูปความถี่คงที่แบบนี้ :14
  • WIN_SCORE = 5 คือการ บังคับ MVP ให้เล็ก ถ้าตั้ง 50 เกมจะยืดจนเทสต์ยาก เลข 5 ทำให้เล่นจบเห็นผลได้ในไม่กี่วินาที ตรงกับสไลด์ "ทำไม MVP เล็กถึงสำคัญ" :15
  • game.title("MY GAME") คือของ 70% core ล้วน ๆ เอนจินทำหน้า Start/Back ให้เองครบ เราไม่ต้องเขียน state machine ของเมนูเลย นี่คือสิ่งที่หมายถึง "reuse ก่อนเขียน" :17

ค่าคงที่สามตัวบนหัวไฟล์ไม่ใช่แค่ความสวยงาม มันคือ "ปุ่มปรับกติกา" ที่แยกออกจากเครื่องยนต์ เปลี่ยนความรู้สึกของเกมได้โดยไม่ต้องแตะลูปสักบรรทัด

เฉลย · ก้อนที่ 2 — STATE: เกมจำอะไรบ้าง (ช่อง 1)

นี่คือ "ขั้นคิดที่ 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 = ตัวแปร" ที่เราสรุปไว้ตอนท้ายเดค :20
  • player วางที่ game.WIDTH // 2, game.HEIGHT // 2 คือกลางจอพอดี เก็บ object ลงตัวแปรเพราะทุกเฟรมต้อง player.move(...) ซ้ำ ถ้าไม่เก็บก็อ้างถึงมันทีหลังไม่ได้ พิกัดเป็น พิกเซลต่อเนื่อง — นี่คือเกม แนว physics ไม่ใช่ grid :21
  • game.CYAN / game.YELLOW / game.WHITE มาจาก palette ใน 70% core เราไม่ตั้งเลขสีเอง ใช้ชื่อที่เอนจินเตรียมไว้ :21-23
  • game.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-30

State คือ "ความจำ" ของเกม ของที่ต้องอยู่ข้ามเฟรมเก็บลงตัวแปรก่อนลูป ของที่ใช้ครั้งเดียวไม่ต้องเก็บ ตัดสินใจแบบนี้เหมือนกันทุกเกม

เฉลย · ก้อนที่ 3 — TICK + WIN/LOSE: หัวใจของลูป (ช่อง 2 + 3)

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 จะมองว่าเป็นตัวแปรใหม่ในเครื่อง แล้วคะแนนจะไม่สะสม :35
  • ใช้ if สี่ตัวแยกกัน (ไม่ใช่ elif) จงใจ กด left+up พร้อมกันแล้วเดินทแยงได้ ขยับทีละ PLAYER_SPEED พิกเซล = แนว physics เต็มตัว ต่างจาก Flappy ที่คุมแกน y แกนเดียว เกมนี้คุมทั้ง x และ y :39-46
  • ยังไม่ return ในก้อนนี้ เพราะ 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% ยังไม่เฉลย ตอนนี้เราเติมมันแล้ว :51
  • ชนแล้วทำ 4 อย่างเรียงกัน: score += 1 (อัปเดต state) → label.set(...) (บอกผลบนจอ) → game.sfx("point") (เสียงจาก 70%) → _move_target() (ย้ายเป้าใหม่) นี่คือ "ชนแล้วเกิดอะไร" ของขั้นคิดที่ 3 :52-55
  • if score >= WIN_SCORE ซ้อนอยู่ใน if hit เพราะเงื่อนไขชนะเช็คเฉพาะตอนเพิ่งได้แต้ม ไม่ต้องเช็คทุกเฟรม แล้ว return False คือสัญญาณเดียวที่บอก game.run() ให้ "จบเกม" ตรงกับ return False ในผัง :56-59
  • game.run(update, fps=FPS) ส่ง update เข้าไปเป็น callback — เอนจินวนเรียกให้เอง 30 ครั้ง/วินาที เราไม่ได้เขียน while True เอง นี่คือรากฐาน "Tick = ฟังก์ชันที่ run() เรียกแบบ callback" :64

TICK เปลี่ยน state จาก input, WIN/LOSE ตัดสินผลแล้วส่งสัญญาณจบด้วย return False แยกสองหน้าที่นี้ให้ขาด แล้วลูปจะอ่านง่ายตลอด ไม่ว่าเกมจะโตแค่ไหน

เฉลย · ภาพรวม: 3 ช่องประกอบกันเป็นเกมยังไง

ทั้งไฟล์คือ 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 ขั้นคิด + กฎ 70/30 (Snake grid · Flappy physics) วันนี้ mygame.py = 3 ช่องเต็ม (เกมเล่นได้จริง) ที่ไป MVP ของกลุ่ม (คาบ 13 + งานจริง)

ถ้าตอบได้ว่า "อ๋อ เฉลยนี้คือ 4 ขั้นคิดที่เติมเต็มแล้ว" — นั่นคือการหยั่งรู้ที่อยากให้เกิด เกมของกลุ่มไม่ได้เริ่มจากศูนย์ มันคือไฟล์นี้ที่เปลี่ยนแค่ 30%

ใช้จริงที่ไหน — callback loop · collision · reuse-first

เทคนิคในเฉลยนี้ไม่ใช่ของเล่นในคาบ มันคือสิ่งที่ซอฟต์แวร์จริงใช้ทุกวัน:

callback loop · game.run(update, fps) วนซ้ำทุกเฟรม update() 30 ครั้ง/วินาที = 30 Hz hit-testing · game.hit(a, b) player target กล่องทับกัน = ชน (AABB overlap) super-loop ของเฟิร์มแวร์ อ่าน input อัปเดต state วาดผล วนใหม่ทุกเฟรม reuse-first · 70/30 บน framework 30% ตรรกะเฉพาะงาน (เราเขียน) 70% framework / BSP / RTOS (reuse)
  • fixed-timestep game loopgame.run(update, fps=FPS) เรียก update() 30 ครั้ง/วินาที คือหัวใจเดียวกับ game engine จริง (Unity Update(), Godot _process) ตรงกับ ช่อง loop :64
  • hit-testing บน UIgame.hit(player, target) คือ AABB overlap ตัวเดียวกับที่เฟรมเวิร์ก UI ใช้ตัดสินว่า "นิ้วแตะโดนปุ่มไหน" บนจอสัมผัส ตรงกับ ช่อง 3 Win/Lose :51
  • event loop ของเฟิร์มแวร์ — โครง "อ่าน input → อัปเดต state → วาดผล → วนใหม่" คือ super-loop ที่ระบบ embedded จริงรันตลอด เกมเราคือ event loop ตัวจิ๋ว ตรงกับ ช่อง 2 Tick :33
  • สร้างของบน framework ที่มีอยู่ — วิศวกรจริงแทบไม่เขียนทุกอย่างจากศูนย์ เขาต่อยอดบน BSP/RTOS/ไลบรารี แล้วเขียนแค่ "ตรรกะเฉพาะงาน" คือหลัก 70/30 ที่ mygame.py สอน ตรงกับ กฎทอง

เขียนเกมเล็ก ๆ จบหนึ่งไฟล์วันนี้ น้องได้ฝึกโครงเดียวกับที่ทีมพัฒนาเกมและทีมเฟิร์มแวร์มืออาชีพใช้ ต่างกันแค่ขนาด ไม่ใช่หลักการ

ต่อยอด — คิดต่อเอง

ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาเกมของกลุ่มได้:

เฉลยวันนี้ · mygame.py 1) State — player + target 2) Tick — อ่านปุ่ม → player.move 3) Win/Lose — hit(target) → +1 แต้ม เปลี่ยนแค่ 30% engine 70% เดิม เกมของกลุ่ม (30% ใหม่) 1) State — player + ศัตรู / เหรียญ 2) Tick — grid (row,col) หรือ v+=a 3) Win/Lose — hit(enemy) → แพ้
  • เพิ่มศัตรูที่ทำให้แพ้ — ถ้าอยากมีของที่ชนแล้ว แพ้ (ไม่ใช่ได้แต้ม) จะออกแบบยังไง? (ใบ้: เพิ่ม game.Box อีกใบใน State แล้วเพิ่ม if game.hit(player, enemy): return False ในช่อง 3 — ตรงข้ามกับ target)
  • สลับ physics เป็น grid — ถ้าเปลี่ยนเกมนี้เป็นแนว grid (เดินทีละช่องแบบ Snake) จะแก้ช่องไหนบ้าง? ตำแหน่งเก็บเป็น (row, col) แทนพิกเซลไหม แล้ว _move_target ใช้สูตรดัชนี row*COLS+col แทน % ไหม
  • ระดับความยากด้วยค่าคงที่ 3 ตัว — อยากให้เกม "ยากขึ้นแบบรู้สึกได้" โดยแตะแค่หัวไฟล์ จะขยับ PLAYER_SPEED / FPS / WIN_SCORE ตัวไหน โดยไม่แตะลูปเลย?
  • (สะพานไปคาบ 13) หยิบ ใบออกแบบ + GitHub Issue ของกลุ่ม มาวางข้าง 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 + คะแนน

← Roadmap (TOC)