คาบ 14 — Capstone

ออกแบบเกมเอง (70/30) + Showcase + Release

คาบสุดท้ายของคอร์ส — รวมทุกอย่างที่เราฝึกมาเป็นเกมของกลุ่มเราเอง

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

13 คาบที่ผ่านมา = ถอด 4 เกมจริง · วันนี้ = น้อง ๆ ออกแบบเกมใหม่ของกลุ่มเอง แล้วปล่อย Release ให้เพื่อนโหลด

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

คาบเดียวจบครบวงจร เราจะคิดแบบ architect แล้วลงมือทำจริง:

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

หัวใจของคาบนี้คือคำเดียว: MVP — ทำเกมเล็กที่ "เล่นจบได้" ก่อน แล้วค่อยต่อยอด นักพัฒนาเกมมืออาชีพก็ย้ำเรื่องนี้กับมือใหม่เสมอ

Making Your First Game: Minimum Viable Product — Extra Credits — ทำไมต้อง scope เล็กก่อน ตรงกับหัวใจคาบนี้: ทำ MVP ที่ "เล่นจบได้" ก่อน แล้วค่อยต่อยอด

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

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

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

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

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

70/30 ช่วยจริงไหม — ดูที่ "แรงที่ต้องลง"

ลองคิดเป็นตัวเลข ถ้าทั้งเกมคือแรง 1 หน่วย เราเลือก reuse 70% ที่หนักที่สุด (screen · loop · input · sound) เหลือเขียนเองแค่ 30%:

แรงที่เราต้องลง  =  1.0ทั้งเกม    0.7reuse engine  =  0.3\text{แรงที่เราต้องลง} \;=\; \underbrace{1.0}_{\text{ทั้งเกม}} \;-\; \underbrace{0.7}_{\text{reuse engine}} \;=\; 0.3

ลดงานลง ~70% — นั่นคือเหตุผลที่เกมหนึ่งเกมทำเสร็จได้ในคาบเดียว ไม่ใช่เพราะเกมมันเล็ก แต่เพราะเราลงแรงเฉพาะส่วนที่เป็นของเราจริง ๆ

มือใหม่มักรู้สึกว่า "เขียนเกม = เขียนทุกอย่างตั้งแต่ศูนย์" จุดที่ทำให้จบงานได้คือ เลือกลงแรงให้ถูกที่ ไม่ใช่ขยันขึ้น

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

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

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

ขั้น 1 — รัน 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() ของเราไปเรื่อย ๆ

ขั้น 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{น้อง ๆ เขียน}}

1) STATE ครั้งเดียว ก่อนลูป ⟶ game.run(update) ⟲ 2)+3) ในลูป tick + win/lose ทุกเฟรม

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

ขั้น 1 (ต่อ) — โครง 3 ช่อง (State ก่อนลูป + Tick/Win ใน 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 เรียกซ้ำทุกเฟรม:

# 1) STATE รันครั้งเดียว · ก่อนลูป game.run( ) update() ⟲ engine เรียกซ้ำทุกเฟรม (30 ครั้ง/วินาที) # 2) TICK ขยับผู้เล่นตามปุ่ม # 3) WIN/LOSE ชนเป้า/ครบ GOAL? ↻ จบเฟรมแล้ววนกลับมาทำใหม่

ไฟล์เว้นช่อง: practise_codes/mygame.py

ขั้น 2 — เลือกเกมจากแคตตาล็อก (20 นาที)

เกมใหม่ 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) แปลงช่อง / จุดบนจอด้วยสูตรเดียว:

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

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

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

X O

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 ไม่ใช่ดัชนีช่องกริด เลือกใช้ก็ต่อเมื่อกระดานเป็นตารางช่องจริง ๆ

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

นี่คือผังคิดของกลุ่ม ก่อนลงมือเขียนจริง ลองดูตัวอย่างที่กรอกด้วย X-O:

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

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

ขั้น 4 — เปิด GitHub Issue เป็น checklist (10 นาที)

แปลงใบออกแบบเป็น 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 เสร็จ

ขั้น 5 — สร้าง 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 ช่อง — เห็นผลทันที

ขั้น 5 (ต่อ) — ช่อง 3: Win/Lose + tick Issue ทีละข้อ

    # 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 = จบ)

START STATE (1 ครั้ง):26–30 game.keys():36 player.move():40–47 · clamp เอนจิน hit?:50 score+ · sfx · ย้ายเป้า:51–55 ≥GOAL?:56 YOU WIN · จบreturn False :57–59 Yes Yes No No return True → เฟรมถัดไป (30 Hz)

ทั้ง 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_select (แตะเป้า/ได้แต้ม)

sfx_ui_start (ครบ GOAL/ชนะ)

sfx_game_over (ปิดเกม)

ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer

เสียงไม่ได้มีแค่ "ได้ยิน" — มันมี รูปร่าง ด้วย นี่คือ waveform ของ sfx_ui_select (เสียงตอนแตะเป้า/ได้แต้ม) เสียงสั้นแหลมจะขึ้นต้นแรงแล้วจางเร็ว:


รูปคลื่น (waveform) ของ sfx_ui_select — แกนนอนคือเวลา แกนตั้งคือความดัง

แต่ละกลุ่มจับคู่ event ของเกมตัวเอง (ได้แต้ม / ชนะ / แพ้) กับเสียงที่เหมาะ แล้วลอง map ชื่อใน game.sfx("...") ให้ตรงกับโทนเกม

ขั้น 6 — Release ให้เพื่อนโหลด (DevOps แบบเบา)

จุดประสงค์: ทำให้กลุ่มอื่น โหลดเกมเราไปเล่นได้ ด้วย 3 ก้าวง่าย ๆ

1) fork / repo กลุ่ม ที่เก็บเกมของเรา 2) commit + push mygame.py เวอร์ชันเรา 3) Release v0.1 แนบ mygame.py

ที่ 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

ขั้น 7 — Game Jam Showcase (30 นาที)

ทุกกลุ่มได้เวที 3 นาที โชว์เกมบนบอร์ดจริง

กลุ่มที่โชว์ พูด 3 อย่าง
  • ชื่อเกม + กติกาใน 1 ประโยค
  • 70% reuse อะไร / 30% เราเขียนอะไร
  • เดโมสด: เล่นบนบอร์ดให้เห็นชนะ/จบ
กลุ่มที่ดู ทำ 1 อย่าง
  • โหลด mygame.py จาก Release ของเพื่อน
  • เปิดใน BENTO IDE → Program to Device
  • ลองเล่น แล้วให้ฟีดแบ็ก 1 ข้อ


ที่มา: "WikiGameJam NYC 2025 participants" — Kevin Payravi, CC BY-SA 4.0, Wikimedia Commons

บรรยากาศ showcase จริง — กลุ่มเล็ก ๆ ล้อมโต๊ะ โชว์เกมที่เพิ่งทำเสร็จให้กันดูและลองเล่น นี่คือบทพิสูจน์ว่า "ของเรารันบนเครื่องคนอื่นได้" — หัวใจของการ ship งานจริง

Game Jam ที่เราทำวันนี้ ไม่ได้ทำคนเดียวในโลก

ทั่วโลกมีงานแบบนี้จริง ๆ เช่น Global Game Jam ที่นักพัฒนาหลายพันคนทั่วโลกรวมตัวกันทำเกมให้เสร็จในเวลาจำกัด ภาพล่างคือบรรยากาศจริงงานหนึ่งที่ฝรั่งเศส — คนกลุ่มเล็ก ๆ ตั้งโจทย์ ลงมือ แล้วปล่อยผลงานในไม่กี่ชั่วโมง


ที่มา: "Global Game Jam 2019 à Arles" — Yannickvernet, CC BY-SA 4.0, Wikimedia Commons

สิ่งที่กลุ่มเราเพิ่งทำ — ตั้ง scope · ทำ MVP · ปล่อย Release ให้คนอื่นเล่น — คือสิ่งเดียวกับที่นักพัฒนาเกมจริงทำ ต่างกันแค่บอร์ดของเราคือ PSoC Edge

เกร็ด: ทำไม embedded สำคัญ — ยุค SoC (PS5 / Switch)

เครื่องยุคนี้รวม 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

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

Scope creep คือศัตรูหมายเลข 1 ของคาบนี้
งบ 30% = 3 ช่องเท่านั้น 1) State 2) Tick 3) Win/Lose ไอเดียเต็มที่บวมเกินงบ หมุน Tetris ผี AI ไล่ 2 ผู้เล่น เลเวล 10 ด่าน power-up ร้านค้า เก็บไฮสกอร์ โหมดเนื้อเรื่อง ของที่ล้นกล่อง = ตัดออก เก็บไว้ทำ v0.2
อาการ วิธีแก้
เลือกเกมใหญ่เกิน (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%

ยาแก้ scope creep: ตัดให้เหลือ MVP

วิธีคิดที่ใช้ได้กับทุกเกม เริ่มจากไอเดียเต็ม แล้ว ตัดของที่ยังไม่จำเป็นออก จนกว่า 30% จะลงพอดี 3 ช่องใน mygame.py:

เป้าหมายไม่ใช่เกมที่ครบเครื่อง แต่เป็นเกมที่ เล่นจบได้จริงในคาบเดียว — ของที่ตัดออกไป เก็บไว้ทำต่อเป็น v0.2 หลังคาบได้เสมอ

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

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

เชื่อมโยงรากฐาน — เกมเล็ก ๆ นี้ใช้วิศวกรรมจริงครบทุกด้าน

เกมที่กลุ่มเพิ่งส่ง ดูเหมือนแค่กล่องวิ่งบนจอ แต่ข้างใต้มันคือ 4 ศาสตร์ที่เราฝึกมาทั้งคอร์ส:

  • Python: State คือ "ตัวแปร" ที่เกมต้องจำ · update() คือ "ฟังก์ชัน" ที่ engine เรียกซ้ำ ๆ (callback) · กระดานเก็บเป็น list แล้วหาช่องด้วย index — ของพื้นฐานทั้งนั้น
  • Algorithms: game.hit(a, b) ข้างในคือสูตรตรวจสี่เหลี่ยมทับกัน (AABB) · เกมกริดใช้ row*COLS+col แปลง 2 มิติเป็น list เดียว — นี่คืออัลกอริทึมจริง
  • Graphics: ทุกตำแหน่งคือ "ระบบพิกัด" (x, y) บนจอ 792×398 · Box.move() ก็คือการย้ายพิกัดแล้ว clamp ไม่ให้หลุดขอบ
  • Embedded: ทั้งหมดนี้รันบน PSoC Edge SoC จริง ที่อ่านปุ่มจากจอย (GPIO/USB) ส่งภาพออกจอ และเล่นเสียงผ่าน game.sfx() — ไม่ใช่ simulator

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

ทำเอง 30% — ก้าวต่อจากวันนี้

คาบ 1-13ปู 70/30 + 4 เกม คาบ 14 (วันนี้)ออกแบบ + MVP + Release + Jam Developer IIDevOps/AI ตัวเต็ม

น้อง ๆ ไม่ได้เริ่มจากศูนย์เลย เราแค่เติม 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 ลอย ๆ กลางลูป นี่คือบทเรียน "แยกค่าออกจากโครง" — ปรับความไวผู้เล่นหรือเงื่อนไขชนะได้ที่จุดเดียว
  • ถ้าเผลอฝังเลข 8 ไว้ในทุกบรรทัด 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 เป็นค่าคงที่ น้องจะเขียนเกมของกลุ่มเองได้โดยไม่ต้องเปิดเฉลย

เฉลย · ช่อง 1 STATE — สิ่งที่เกมต้องจำ

ช่องแรกใน 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% กำลังทำงานแทนเรา
  • State สร้าง "ครั้งเดียวก่อนลูป" (: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 ยิ่งเขียนง่าย

เฉลย · ช่อง 2+3 — 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_move
  • ใช้ if แยกทุกทิศ (ไม่ใช่ 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 ทำให้หมด เราแค่ถามว่า "ทับกันไหม" ไม่เขียนเงื่อนไขชนเอง
  • แตะเป้าแล้วเกิด feedback ครบ 3 ทางในครั้งเดียว (: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 หมดแล้ว เราลงแรงเฉพาะสิ่งที่เป็นเกมของเราจริง ๆ

เฉลย · 3 ช่องไต่ระดับกันยังไง (ภาพรวม)

ถอยมามองทั้งไฟล์ จะเห็นว่าแต่ละก้อนเล็กมาก แต่ประกอบกันเป็นเกมที่เล่นจบได้จริง หัวใจอยู่ที่คอลัมน์ "เรียก 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 ช่องนี้มาจากไหน แล้วจะพาเราไปถึงไหน

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

ที่มา — 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 ที่สตูดิโอเกมทั้งโลกใช้

ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:

  • จำสไลด์ "70% engine + 30%" ได้ไหม ถ้าวันนี้เปลี่ยนแค่ของใน 3 ช่องแล้วได้เกมใหม่ พรุ่งนี้เปลี่ยน target เป็น "ศัตรูที่ขยับเอง" จะยังเป็น 3 ช่องเดิมไหม (ใบ้: State เพิ่ม 1 ตัว, Tick ขยับมันด้วย)
  • ถ้า game.hit(player, target) วันนี้บอก "ชนเป้า → ได้แต้ม" ได้ พรุ่งนี้ game.hit(player, enemy) จะบอก "โดนศัตรู → แพ้" ได้ไหม มันคือสมการ AABB เดียวกันหรือเปล่า
  • สังเกตไหมว่า update() ที่ engine เรียกซ้ำทุกเฟรม มันคือเรื่องเดียวกับ game loop ที่เราพูดถึงตั้งแต่คาบแรก ต่างแค่ตอนนี้เราเขียน "ข้างในลูป" เองได้แล้ว
ที่มา ถอด 4 เกม → 4 ขั้นคิด (คาบ 1–13) วันนี้ mygame.py = 3 ช่องเราเขียน (Capstone) ที่ไป ทุกเกม + DevOps + สินค้าจริง (Developer II)

ถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด: เกมทั้งเกมคือ State + ลูป + การชน ที่โตขึ้น น้องถือกุญแจครบแล้ว

ใช้จริงที่ไหน — MVP · game loop · AABB · respawn

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

prototype-first · MVP gray-box เล่นจบได้ แล้วค่อย ใส่ศิลป์ v0.2 scope เล็ก = 3 ช่อง fixed-timestep loop · fps=30 update() ทุกเฟรม เฟรมห่างเท่ากัน 1/30 วินาที · game.run() AABB collision · game.hit() player target สี่เหลี่ยมทับกัน 4 อสมการ = ชน respawn · spawn สุ่ม (เผื่อขอบ) เดิม ใหม่ ได้แต้ม → โผล่ที่สุ่มใหม่
  • prototype-first / MVP ในสตูดิโอเกมจริง — ทีม AAA และ indie เริ่มจาก "gray-box prototype" ที่เล่นจบได้ก่อน (ยังไม่มีกราฟิก) แล้วค่อยใส่ศิลป์ทีหลัง ตรงกับที่เราตัด scope เหลือ 3 ช่องให้เล่นจบในคาบเดียว — ของที่ยังไม่จำเป็นเก็บไว้ v0.2
  • fixed-timestep game loopgame.run(update, fps=30) คือแนวคิดเดียวกับ Update() ใน Unity, Tick() ใน Unreal, _process() ใน Godot: เอนจินเรียกฟังก์ชันเราซ้ำต่อเฟรม เราไม่เขียน while เอง
  • AABB collisiongame.hit ที่ตรวจสี่เหลี่ยมทับกัน คือด่านแรกของ physics และ UI จริง: hit-test ปุ่มบนจอสัมผัส, ตัวละครชนกำแพง, hitbox กระสุน ล้วนเริ่มจาก 4 อสมการเดียวกับที่พิสูจน์คาบ 11–12
  • respawn / procedural spawnrandom_spot() ที่ย้ายเป้าไปที่ใหม่ คือ spawn logic ในเกมจริง: ไอเทมดรอป, ศัตรูเกิดสุ่ม, จุดเกิดผู้เล่นใหม่ ทุกที่ต้อง "เผื่อขอบ" ไม่ให้เกิดนอกจอ แบบที่เราลบ 24px

ห้าอย่างที่ประกอบกันเป็นเกมเล็ก ๆ วันนี้ ไม่มีอันไหนเป็นของสมมติเลย — ทุกอันมีเอนจินเกมระดับโลกที่ทำงานด้วยหลักการเดียวกันอยู่จริง

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

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

1) State 2) Tick 3) Win / Lose วันนี้ player · target สิ่งที่เกมจำ ขยับผู้เล่นตามปุ่ม อัปเดตทุกเฟรม hit target → +แต้ม ครบ GOAL → ชนะ เปลี่ยนแค่ "ของ" ในช่องเดิม ต่อยอด + enemy · ทิศของเป้า จำเพิ่มอีกนิด + เป้าขยับหนี · ศัตรูเดิน ยังอยู่ในเฟรมเดิม hit target → ชนะ hit enemy → แพ้ engine 70% · bentogame.py — ไม่แตะเลยสักบรรทัด ทุกโจทย์ต่อยอด = เปลี่ยนแค่ของใน 3 ช่อง ส่วน 70% เหมือนเดิม
  • ถ้าเป้าต้องขยับหนี — อยากให้ 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

← Roadmap (TOC)