Fundamental of Embedded Systems Developer II

คาบ 8 — Flappy #1: นก + แรงโน้มถ่วง + กระพือ

ช่วง Flappy (ฟิสิกส์เกม) · คาบที่ 8 จาก 13

เกมที่สองของคอร์สแล้วนะ Snake สอนเราเรื่อง "กริด" ส่วน Flappy คาบนี้เราจะมาเรียนฟิสิกส์การเคลื่อนที่กัน

ดูของจริงก่อน — เกม Flappy Bird บนบอร์ด BENTO

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

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

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

  • step 1: นกลอยนิ่งกลางฟ้า + คะแนน 0
  • step 2: นกตกเอง + กด A/UP กระพือเด้งขึ้น + ตกพื้น = GAME OVER

เช็กพอยต์: step 1 รันได้ (นกนิ่ง) และ step 2 รันได้ (ตก + กระพือ + GAME OVER) ให้ทั้งกลุ่มเล่นบนบอร์ดของตัวเองได้ก่อน ส่วนท่อเก็บไว้คาบหน้า

vy+=gy+=vyกระพือ: vyvflap (<0)v_y \mathrel{+}= g \qquad y \mathrel{+}= v_y \qquad \text{กระพือ:}\ v_y \leftarrow v_{\text{flap}}\ (<0)

จะได้เรียนรู้ — 3 แนวคิดหลัก

velocity (ความเร็ว)
ตัวเลข "ขยับเท่าไรต่อเฟรม" ไม่ใช่ตำแหน่ง
vy = Δy / เฟรม
gravity (แรงโน้มถ่วง)
บวกความเร็วลงทุกเฟรม ยิ่งตกยิ่งเร็ว
vy += g
impulse (กระพือ)
เซ็ตความเร็วเป็นค่าลบทันที
vyvflap

สามคำนี้ = สามสัญลักษณ์ — velocity คือ "ขยับต่อเฟรม" (vy=Δy/เฟรมv_y=\Delta y/\text{เฟรม}), gravity คือบวก gg เข้าไปทุกเฟรม (vy+=gv_y\mathrel{+}=g) ส่วน impulse คือกระโดดค่า vyv_y ไปเป็น vflapv_{\text{flap}} ทันที จำสมการสั้น ๆ พวกนี้ไว้ เดี๋ยวมันกลายเป็นโค้ด 2–3 บรรทัด


  • velocity ต่างจาก positionvelocity คือ "ระยะที่จะขยับต่อ 1 เฟรม" ส่วน position คือ "ตอนนี้อยู่ตรงไหน" ทุกเฟรมเราเอา velocity ไปบวกเข้า position
  • เฟรมลูป (game.run) คือหัวใจของทุกอย่าง on_each_frame() ถูกเรียกวินาทีละ 50 ครั้ง
  • ขอบจอเป็นกติกา ชนเพดาน (y<0) ความเร็วเป็นศูนย์, ตกพื้น = จบเกม

เกร็ด: ทำไม embedded สำคัญ — Game Boy (1989)

Game Boy ใช้ LCD สะท้อนแสง (ไม่มีไฟส่อง) + CPU จิ๋ว (Sharp LR35902 @ 4.19 MHz) เล่นได้ ~15 ชม. บนถ่าน AA — power efficiency ชนะ spec ดิบ

เชื่อมกับวันนี้: จอ TFT ของน้องคนละ pipeline กับ CRT — ไม่มีลำแสงวิ่ง แต่ต้อง refresh ทั้งเฟรม การคิดเรื่อง power/refresh เริ่มจากยุคนี้

ที่มา: Game Boy DMG-01 · ภาพ Raimond Spekking (CC BY-SA 4.0) · en.wikipedia.org/wiki/Game_Boy

ฟิสิกส์ของคาบนี้ (เขียนบนกระดานก่อนโค้ด)

ทุกเฟรม (Δt=150\Delta t = \frac{1}{50} วินาที) อัปเดต 2 สมการ:

vy+=g(แรงโน้มถ่วงเพิ่มความเร็วลง)v_{y} \mathrel{+}= g \qquad\text{(แรงโน้มถ่วงเพิ่มความเร็วลง)}

y+=vy(ตำแหน่งเลื่อนตามความเร็ว)y \mathrel{+}= v_{y} \qquad\text{(ตำแหน่งเลื่อนตามความเร็ว)}

เมื่อกดกระพือ — ตั้งความเร็วใหม่ทันที (impulse):

vyvflap(vflap<0, ลบ=ขึ้น)v_{y} \leftarrow v_{\text{flap}} \quad (v_{\text{flap}} < 0,\ \text{ลบ} = \text{ขึ้น})

โดย gg คือ ความเร่งโน้มถ่วง (gravity) — โลกจริงมีค่า ≈ 9.81 m/s² ส่วนในเกมเราย่อเป็นค่าคงที่เล็ก ๆ ที่บวกเข้า v_y ทุกเฟรม

ค่าจริงในเกมนี้: g=0.45g = 0.45, vflap=7.4v_{\text{flap}} = -7.4 หน่วยเกม/เฟรม (ตรงกับ full_games/flappy_full.py)

velocity ≠ position — ทำไมต้องแยกสองตัว

เฟรม 0 เฟรม 1 เฟรม 2 y=180 y=180.45 y=181.35 v=0 v=0.45 v=0.90 v บวกเข้า y g บวกเข้า v y ยิ่งร่วงเร็ว
position (bird_y) บอกว่าตอนนี้นกอยู่สูงเท่าไร

velocity (bird_velocity) บอกว่าเฟรมหน้านกจะขยับลงเท่าไร

ทุกเฟรมเราทำ 2 ขั้น: v += g (โน้มถ่วงสะสมความเร็ว) แล้ว y += v (ความเร็วพา y ลง) นี่คือเหตุผลที่นกร่วงเร็วขึ้นเรื่อย ๆ

ทำไม "ลบ = ขึ้น"? เพราะ Y นับจากบนลงล่าง

y = 0 (ขอบบน) y เพิ่ม ↓ v ลบ = ขึ้น v บวก = ลง
พิกัด Y บนจอ นับจากบนลงล่าง ตรง y=0 คือขอบบนสุด

ดังนั้นความเร็ว ลบ (bird_y ลดลง) จะทำให้นก ลอยขึ้น

ตรงนี้แหละที่หลายคนงงกันบ่อย เราเลยอยากให้น้อง ๆ เห็นเป็นภาพก่อนลงมือเขียนโค้ด

70 / 30 — น้อง ๆ เขียนแค่ฟิสิกส์

70% core (มีให้แล้ว — จาก Snake)
  • game.start() — ล้างจอ + ปลุกจอย
  • game.Box / game.Text — นก + คะแนน
  • game.run(fn, fps) — ลูปเกม 50 fps
  • game.keys() / game.sfx()
30% น้อง ๆ เขียน
  • ตัวแปร bird_velocity
  • 2 บรรทัดฟิสิกส์
  • เงื่อนไขขอบจอ

ของใหม่จริง ๆ ของคาบนี้มีแค่ตัวแปร bird_velocity (ความเร็วแนวตั้ง) กับสมการ 2 บรรทัด เท่านั้นเอง

Step 8.1 — นกลอยนิ่ง + คะแนน (step 1)

ไฟล์เด็กเล่น: practise_codes/flappy_step1.py (เว้นช่อง bird_y)

    import bentogame as game

    game.title("FLAPPY")                            # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)

    # ----- เติมส่วนนี้เอง: ตำแหน่งเริ่มต้นของนก -----
    bird_y = 180.0                                  # ความสูงของนก (พิกเซลจากขอบบน)
    bird = game.Box(150, bird_y, 34, 34, game.GB_LIGHTEST)   # ตัวนก = กล่องสว่างสุด
    score = 0
    score_label = game.Text("0", 384, 16, game.WHITE)

    def on_each_frame():
        # ทุกเฟรม วาดนกไว้ที่ความสูง bird_y (ยังไม่ขยับ — เป็นภาพนิ่ง)
        bird.move_to(bird.x, bird_y)
        # (Back=ออก / Start=เริ่มใหม่ — game.run() จัดการให้)

    game.run(on_each_frame, fps=50)                 # ลูปเกม 50 fps

เช็ก: กล่องนกนิ่งกลางฟ้า กับเลข 0 ที่มุมบน ลองเปลี่ยน bird_y เป็น 50 หรือ 300 ดูสิ นกจะย้ายที่ตาม นี่แหละคือ 30% ที่เป็นงานของน้อง ๆ

หน้าจอ step 1 ควรหน้าตาแบบนี้ — นก (Box สว่างสุด) อยู่นิ่งที่ (150, 180) และ Score: 0 ยังไม่มีฟิสิกส์มาขยับ

ภาพจำลองหน้าจอ step 1 (course render) เทียบกับผลจริงบนบอร์ด — กรอบ BENTO Playground 800×480

Step 8.2 — แรงโน้มถ่วง: นกตกเอง

ไฟล์: practise_codes/flappy_step2.py (เว้นบล็อกฟิสิกส์)

    GRAVITY = 0.45                                  # แรงโน้มถ่วง (ดึงลงต่อเฟรม)
    bird_y = 180.0                                  # ความสูงของนก
    bird_velocity = 0.0                             # ความเร็วแนวตั้ง (+ = ลง, - = ขึ้น)

    def on_each_frame():
        global bird_y, bird_velocity
        # ----- เติมส่วนนี้เอง: ฟิสิกส์ 2 บรรทัด -----
        bird_velocity += GRAVITY                     # โน้มถ่วงดึงลงทุกเฟรม
        bird_y += bird_velocity                      # เลื่อนนกตามความเร็ว
        bird.move_to(bird.x, bird_y)

สังเกตว่านกจะร่วงเร็วขึ้นเรื่อย ๆ เพราะความเร็วมันสะสมไปทุกเฟรม:

v:0.450.901.351.80v: 0.45 \to 0.90 \to 1.35 \to 1.80 \dots

ยังไม่กระพือ — ตกอย่างเดียว ระยะห่างแต่ละเฟรมกว้างขึ้น = เร่ง
ภาพนี้คือสิ่งที่ฟิสิกส์ 2 บรรทัด ของ step 2 ทำ ก่อนใส่กระพือ — นกร่วงลงอย่างเดียว แต่ ตกเร็วขึ้นเรื่อย ๆ

จุดจาง ๆ คือตำแหน่งนกในเฟรมก่อน ๆ ยิ่งลงล่าง ช่องห่างยิ่งกว้าง — เพราะ v += GRAVITY สะสมความเร็วทุกเฟรม นี่แหละความหมายของคำว่า "เร่ง"

กราฟของสมการสองบรรทัด — เห็นว่า "ทำไมยิ่งตกยิ่งเร็ว"

ลองวาดค่าจริงออกมาดู: velocity เพิ่มเป็นเส้นตรง (บวก gg คงที่ทุกเฟรม) แต่ y ที่เอา velocity ไปบวกสะสม กลายเป็นเส้นโค้ง ร่วงเร็วขึ้นเรื่อย ๆ พอกดกระพือ velocity ถูกตั้งใหม่เป็นค่าลบทันที (impulse) — y เลยเด้งกลับขึ้น

velocity เป็นเส้นตรงเพราะเราบวก gg เท่าเดิมทุกเฟรม ส่วน y โค้งเพราะมัน "สะสม" velocity ที่โตขึ้นเรื่อย ๆ นี่คือ Euler integration แบบง่ายสุด — บวกทีละก้าวเล็ก ๆ เร็ว ๆ จนได้การเคลื่อนที่ที่ดูเป็นธรรมชาติ

Math for Game Developers - Jumping and Gravity (Time Delta, Game Loop) — Jorge Rodriguez · อธิบาย velocity + gravity ในเฟรมลูปแบบเดียวกับที่เราเขียนเป๊ะ

Step 8.3 — กระพือ + ขอบจอ (นกมีชีวิต)

y<0vy=0 (ชนเพดาน)y>HhGAME OVERy < 0 \Rightarrow v_y{=}0\ (\text{ชนเพดาน}) \qquad y > H - h \Rightarrow \text{GAME OVER}

    GRAVITY = 0.45
    FLAP = -7.4                                      # แรงกระพือ (ดีดขึ้น)

    def on_each_frame():
        global bird_y, bird_velocity
        keys = game.keys()
        if keys.a or keys.up:
            bird_velocity = FLAP                     # กระพือ = ดีดขึ้น (ความเร็วติดลบ)
            game.sfx("flap")
        bird_velocity += GRAVITY                     # โน้มถ่วงดึงลงทุกเฟรม
        bird_y += bird_velocity                      # เลื่อนนกตามความเร็ว
        if bird_y < 0:                               # ชนเพดานบน → หยุดความเร็ว
            bird_y, bird_velocity = 0, 0
        if bird_y > game.HEIGHT - bird.h:            # ตกพื้น → จบเกม
            game.Text("GAME OVER", 320, 180, game.RED)
            return False
        bird.move_to(bird.x, bird_y)

ถึงตรงนี้นกจะร่วงลง พอกด A/UP มันก็เด้งขึ้นพร้อมเสียง เราคุมจังหวะให้มันลอยอยู่กลางอากาศได้แล้ว

เสียงในคาบนี้ — Flappy คาบนี้มีสองเสียง: game.sfx("flap") ตอนกระพือเด้งขึ้น และเสียงตกพื้นตอน GAME OVER (กดเล่นฟังได้จริง):

flappy_flap (flap)

flappy_die (fall)

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

กระพือแบบ level-trigger — ทำไมกด A ค้างแล้วนกลอยค้าง

บรรทัดกระพือ flappy_step2.py:34-35 ทำงาน ทุกเฟรม ตราบใดที่ปุ่มยังถูกกด:

    if keys.a or keys.up:          # flappy_step2.py:34
        bird_velocity = FLAP       # :35 — เซ็ตใหม่ทุกเฟรมที่ปุ่มค้าง
  • level-trigger (โค้ดตอนนี้): ดูสถานะปุ่มว่า "กำลังกดอยู่ไหม" — ถ้ากดค้าง ทุกเฟรม bird_velocity จะถูกรีเซ็ตเป็น FLAP (−7.4) ก่อน แล้วโน้มถ่วงบวกกลับนิดเดียว (+GRAVITY = +0.45 ที่ :37) → นกจึง ลอยพุ่งขึ้นค้าง ตราบใดที่ยังกด (โกงเกมได้ด้วยการกดค้าง)
  • edge-trigger (ของจริง Flappy Bird): จับเฉพาะจังหวะเปลี่ยน ไม่กด→กด (rising edge) กด 1 ครั้ง = กระพือ 1 ที ต้องปล่อยแล้วกดใหม่ถึงจะกระพืออีก:
    if (keys.a or keys.up) and not prev_a:   # จับ rising edge เท่านั้น
        bird_velocity = FLAP
    prev_a = keys.a or keys.up               # จำสถานะปุ่มของเฟรมก่อน

นี่คือแนวคิด edge vs level detection เดียวกับปุ่ม FSM คาบ 1–2 (pressed and not prev) — level ทำเกมง่ายเกิน (กดค้างลอยฟรี) ส่วน edge บังคับให้ "แตะเป็นจังหวะ" คือความยากคลาสสิกของ Flappy Bird · theory → code: flap = level-trigger → flappy_step2.py:34-36

วิธีรันบนบอร์ด — Program to Device

  1. เปิดไฟล์ flappy_step2.py ใน BENTO IDE
  2. เสียบบอร์ด PSoC Edge เข้ากับเครื่อง
  3. กดปุ่ม Program to Device
  4. ดูนกบนจอบอร์ด ลองกด A/UP กระพือเลย

เราไม่ได้ใช้ exec(open(...)) เพื่อรันโค้ดนะ วิธีของคอร์สนี้คือกด Program to Device ใน IDE ให้ไฟล์ทั้งก้อนไปรันบนบอร์ดทีเดียว

ดูภาพรวมก่อน: สำหรับคนที่เพิ่งเริ่ม คลิปสั้นนี้แสดงลำดับ "flash เฟิร์มแวร์ → ส่งโค้ด → รันบนบอร์ด" ของ MicroPython ให้เห็นเป็นจังหวะ — แนวคิดเดียวกับปุ่ม Program to Device ของ BENTO IDE (ต่างยี่ห้อบอร์ดเท่านั้น)

MicroPython for Beginners: Flash Firmware, Upload Code & Run! — SparkFun Electronics · เห็นลำดับ flash → upload → run จริง ๆ เป็นแนวทางเดียวกับ Program to Device

render-loop: ฟิสิกส์เกิดจากการทำซ้ำเร็ว ๆ

อ่านปุ่ม (keys) v += GRAVITY y += v เช็กขอบจอ วาดนก (move_to) ทำซ้ำ 50 ครั้ง/วินาที (fps=50)

ฟิสิกส์ทั้งเกมเกิดจากการ "ทำซ้ำเล็ก ๆ เร็ว ๆ" ไม่มีสูตรลับอะไรเลย มีแค่ 2 บรรทัดที่วนซ้ำกันไป

ผังงานทั้งโปรแกรม · Flappy render-loop พร้อมกิ่งกระพือ (flappy_step2.py:30-46)

ผังก่อนหน้าเป็นลูปแบบย่อ — นี่คือ ทั้งเฟรมจริง รวมกิ่ง กระพือ และกิ่ง ชนเพดาน / ตกพื้น · สัญกรณ์: มนเทา = เริ่ม/จบ · เขียว = อ่าน/วาด · น้ำเงิน = เซ็ตค่า · ส้ม = ฟิสิกส์ · ข้าวหลามตัดม่วง = ตัดสินใจ

1 เฟรมgame.run :46 อ่าน keys:33 กด A/UP?:34 v = FLAP · sfx('flap'):35-36 v += GRAVITY:37 y += v:38 เช็กขอบ ↓ Yes No (จากฟิสิกส์) y < 0? เพดาน:39 y=0 · v=0:40 y > H−h? พื้น:41 GAME OVER · return False:42-43 จบเกม (ออกจาก loop) move_to วาดนก:44 Yes No Yes No วนเฟรมถัดไป (50 fps) → อ่าน keys

theory → code: impulse v ← FLAP (กิ่ง Yes) → :35 · Euler v += g:37, y += v:38 · ชนเพดาน clamp → :39-40 · ตกพื้น = game over → :41-43 · render move_to:44

มีจุดตัดสินใจ 3 จุดต่อเฟรม: กด A/UP? (กระพือ) → ชนเพดาน? (หยุด v) → ตกพื้น? (จบเกม) เส้นประคือ game.run() วน on_each_frame() 50 fps มีเพียงกิ่ง "ตกพื้น" ที่ return False

ลองดูภาพเคลื่อนไหว — โน้มถ่วงดึงลง กระพือดีดขึ้น

แท่งทางขวาคือ bird_velocity: เมื่อนกร่วง แท่งสีแดงโตลง (v บวก) ยิ่งร่วงนานยิ่งยาว พอกระพือ ค่ากลายเป็นลบทันที (แท่งเด้งไปฝั่งเขียว) นกจึงพุ่งขึ้น

ภาพนี้คือสิ่งที่โค้ด 2 บรรทัดของน้องทำในแต่ละเฟรม — เราไม่ได้สั่งให้นก "ตก" ตรง ๆ เราแค่บวกความเร็วแล้วบวกตำแหน่ง การร่วงที่ดูสมจริงโผล่ขึ้นมาเอง

Timer ต่อยอด — จังหวะเกมที่นิ่ง (fixed tick + spawn)

บางอย่างในเกมต้องเกิด "ทุก N มิลลิวินาทีเป๊ะ" ไม่ว่าเฟรมจะหนักแค่ไหน — ยกออกจาก game.run loop ไปไว้ที่ machine.Timer:

    from machine import Timer
    # เลื่อนฉาก/ขยับท่อทุก 500 ms เป๊ะ — เกมไม่กระตุกตามภาระเฟรม
    Timer(0, mode=Timer.PERIODIC, period=500, callback=scroll_world)
    # ปล่อยท่อใหม่ทุก 2 วินาที (spawn) — เก็บไว้ใช้ตอนเพิ่มท่อคาบหน้า
    Timer(1, mode=Timer.PERIODIC, period=2000, callback=spawn_pipe)
  • fixed tick: จังหวะมาจากฮาร์ดแวร์ ไม่ดริฟต์ → Snake/Flappy ความเร็วคงที่ทุกเครื่อง
  • spawn / คูลดาวน์: ปล่อยท่อ/ไอเทมเป็นจังหวะ · ตั้งเวลายิงซ้ำ · นับถอยหลังจบเกม

callback ควร สั้น (ตั้ง flag / อัปเดตตัวแปร) แล้วให้ on_each_frame() ใน game.run วาด — หลักเดียวกับ interrupt handler คาบ 1 (Eva Game มี Timer id 0–2 ให้ใช้)

Step 8.4 — จูนค่าให้เป็น "เกมของเรา"

ลองปรับ ทีละค่า ในไฟล์ แล้วกด Program to Device ใหม่ ดูความรู้สึกที่เปลี่ยนไป:

ลองเปลี่ยน ผลที่เห็น
GRAVITY = 0.30 นกตกช้า เล่นง่าย
GRAVITY = 0.70 นกตกเร็ว เล่นยาก
FLAP = -5.0 กระพือเด้งขึ้นน้อย
FLAP = -10.0 กระพือพุ่งขึ้นสูง

พอเราแยก GRAVITY กับ FLAP ออกมาเป็นค่าคงที่ เราก็เปลี่ยน "ความรู้สึก" ของเกมได้ โดยไม่ต้องไปยุ่งกับตรรกะเลย

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

ผิด: FLAP = +7.4 กดแล้วพุ่งลง
ถูก: FLAP = -7.4 กดแล้วเด้งขึ้น (ลบ = ขึ้น)
  • ลืม global bird_y, bird_velocity ทำให้ Python มองเป็นตัวแปรใหม่ นกเลยไม่ขยับ
  • ตั้ง FLAP เป็นบวก กดแล้วนกพุ่งลง (จำไว้นะว่า ลบ = ขึ้น)
  • เซ็ต bird_velocity = 0 แทนที่จะเป็น = FLAP นกจะหยุดนิ่งกลางอากาศ ไม่เด้ง
  • ลืม if bird_y > HEIGHT - bird.h นกจะทะลุขอบล่างหายไป
  • return False ใช้ตัวเล็ก เกมจะไม่หยุด (ต้องเป็น F ตัวใหญ่ คืนค่าจาก on_each_frame)
  • จอยกดไม่ติด ลองถอดแล้วเสียบสาย USB ใหม่ อันนี้เป็นอาการปกติ ไม่ใช่บั๊กในโค้ดของน้อง ๆ

ข้อควรระวัง: game.tilt() (เอียงบอร์ดคุมนก) มีอยู่ใน API ก็จริง แต่ BMI270 ยังไม่ทำงานบนเฟิร์มแวร์ Game เพราะฉะนั้นให้คุมด้วยปุ่ม A/UP เท่านั้น อย่าเพิ่งไปสัญญาน้อง ๆ ว่าเอียงเล่นได้

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

ฟิสิกส์นกที่น้องเพิ่งเขียน ไม่ใช่ของเล่นแยกจากงานจริง มันคือรากฐานเดียวกับที่เราใช้ทำผลิตภัณฑ์:

Algorithms · ฟิสิกส์
v += g แล้ว y += v คือ numerical integration (Euler) ตัวเดียวกับที่ใช้ประมาณตำแหน่งจากความเร่ง
Embedded · เซนเซอร์จริง
เกมสมมติ g เอง แต่ของจริง BMI270 (accelerometer) วัด g ได้จริง ผ่านบัส I2C (SCB0) — สมการแปลงค่าคือตัวเดียวกัน
Graphics · พิกัด
Y นับจากบนลงล่าง "ลบ = ขึ้น" เป็นกติกาพื้นฐานของทุก framebuffer ไม่ใช่แค่เกมนี้
นี่คือ IMU breakout จริง (accelerometer + gyroscope 3 แกน) — ชิปคลาสเดียวกับ BMI270 ที่อยู่บนบอร์ดของน้อง สังเกตขา SDA/SCL นั่นคือบัส I2C 2 เส้นที่ส่งค่า g ออกมาเป็นตัวเลข — สมการแปลงค่าเป็นมุมเอียงก็คือ integration ก้อนเดียวกับฟิสิกส์นกในคาบนี้
ที่มา: "GY-521 MPU-6050 Module 3 Axis Gyroscope + Accelerometer" — Nevit Dilmen, CC BY-SA 3.0, Wikimedia Commons
และ Timer/interrupt ที่ยกจังหวะออกจากเฟรมลูป (สไลด์ก่อนหน้า) คือหลักการเดียวกับ periodic interrupt ในระบบ control จริง — งานเร่งด่วนแยกออกมา ทำให้สั้น แล้วค่อยให้ลูปหลักวาด

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

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

เกณฑ์ผ่าน
  • step 1: นกนิ่ง + คะแนน 0
  • step 2 ครึ่งแรก: นกตกเร็วขึ้นเรื่อย ๆ
  • step 2 เต็ม: กระพือเด้ง + เสียง
  • ตกพื้น → GAME OVER
  • อธิบาย velocity / +=GRAVITY / FLAP ลบ ได้
งานทำเอง (30%)
  • เติม bird_velocity + 2 บรรทัดฟิสิกส์เองในไฟล์ practise
  • คลิป 10 วิ นกตก + กระพือเด้ง
  • บันทึกค่า GRAVITY/FLAP ที่จูน + เหตุผล
  • เซฟ flappy_step2.py ที่จูนแล้ว (ใช้ต่อคาบ 9)
ความต่อเนื่อง: คาบ 8 (คาบนี้) คือนก + ฟิสิกส์, คาบหน้า เพิ่มท่อ + การชน + คะแนน จนกลายเป็น Flappy เต็มเกมใน full_games/flappy_full.py

เฉลย flappy_step1.py : อ่านให้เข้าใจ แล้วปิดไฟล์พิมพ์เอง

เฉลยนี้มีไว้ เทียบ ไม่ได้มีไว้ลอก คะแนนของคาบอยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง ไม่ใช่โค้ดตรงเฉลย วิธีใช้ให้ได้ผลจริงคือ ลองเองก่อนอย่างน้อย 15 นาที ติดตรงไหนอ่านให้เข้าใจ แล้ว ปิดไฟล์ กลับไปพิมพ์ใหม่ด้วยมือตัวเอง — ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อไปเราจะแกะทีละก้อน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน

ก้อนแรก คือดึง core แล้วเปิดหน้า Start:

import bentogame as game

game.title("FLAPPY")                          # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
  • import bentogame as game (:13) ดึง core ที่ให้มาแล้ว 70% เข้ามาก้อนเดียว: Box, Text, run, keys อยู่ในนี้หมด ตรงกับสไลด์ "70 / 30" ที่บอกว่าน้องเขียนแค่ 30% ที่เหลือ core จัดให้
  • game.title("FLAPPY") (:15) สร้างหน้า Start ให้ในตัว กด Start เริ่ม กด Back ออก เราไม่ต้องเขียน state machine ของเมนูเอง นี่คือสิ่งที่คอมเมนต์หมายถึง "ทำ start ให้ในตัว"
  • สังเกตว่า step 1 เรายัง ไม่แตะฟิสิกส์เลย เป้าหมายแคบ ๆ คือพานกขึ้นจอให้ได้ก่อน แล้วคาบหน้าค่อยทำให้ตก — ไต่ทีละขั้นแบบเดียวกับที่ Snake สอนเรื่องกริดมาก่อนหน้านี้
  • ถ้ากระโดดไปเขียนทั้งเกมรวดเดียว พอมันไม่รันน้องจะไม่รู้ว่าพังตรงไหน การเริ่มจากก้อนเล็กสุดที่รันได้ทำให้ debug ตรงจุด

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

เฉลย · วางนกกับป้ายคะแนนบนจอ (ยังไม่ขยับ)

# ----- เติมส่วนนี้เอง: ตำแหน่งเริ่มต้นของนก -----
bird_y = 180.0                                  # ความสูงของนก (พิกเซลจากขอบบน)
bird = game.Box(150, bird_y, 34, 34, game.GB_LIGHTEST)   # ตัวนก = กล่องสว่างสุด
score = 0
score_label = game.Text("0", 384, 16, game.WHITE)
  • bird_y = 180.0 เป็น ทศนิยม ไม่ใช่ 180 เต็ม เพราะคาบหน้า bird_y += bird_velocity จะบวกค่าเศษ (0.45, 0.90 …) ถ้าเก็บเป็น int ตั้งแต่แรก การตกจะกระตุกเป็นขั้น ตรงนี้ตรงกับสไลด์ "velocity ≠ position" ที่ย้ำว่า position ต้องละเอียดพอจะสะสมความเร็วได้ (:18)
  • game.Box(150, bird_y, 34, 34, ...): x คงที่ที่ 150, y ผูกกับตัวแปร bird_y, กล่อง 34×34 นก ไม่ขยับซ้าย-ขวา เลยทั้งเกม ที่ขยับมีแต่แกน y นี่คือเหตุผลที่ Flappy คุมแค่ "ขึ้น-ลง" มิติเดียว (:19)
  • game.GB_LIGHTEST คือสีสว่างสุดในจาน 4 เฉดของ Game Boy จานเดียวกับที่ใช้ใน Snake เลือกเฉดสว่างสุดให้นกตัดกับพื้นหลัง โยงกับเกร็ด Game Boy (1989) ที่เราคุยเรื่องจอ 4 เฉด
  • score = 0 กับ score_label เป็นการวาง นั่งร้าน (scaffold) ของคะแนนไว้ก่อน ทั้งที่คาบนี้ยังไม่มีอะไรให้นับ เพราะคาบหน้าพอเพิ่มท่อ เราแค่ score += 1 กับป้ายที่มีอยู่แล้ว ไม่ต้องรื้อ — นิสัย "เตรียมที่ไว้ให้ของที่กำลังจะมา" (:20-21)

เลือกชนิดข้อมูลตามอนาคตของมัน bird_y เป็น float เพราะมันกำลังจะสะสมความเร็ว ส่วน score เป็น int เพราะมันจะถูกนับทีละหนึ่ง

เฉลย · เฟรมลูป: วาดซ้ำทุกเฟรม แม้ยังไม่ขยับ

def on_each_frame():
    # ทุกเฟรม วาดนกไว้ที่ความสูง bird_y (ยังไม่ขยับ — เป็นภาพนิ่ง)
    bird.move_to(bird.x, bird_y)
    if game.keys().start:        # กด Start เพื่อออกจากเกม (เหมือนทุกเกม)
        return False
    # คาบหน้า: เพิ่มแรงโน้มถ่วงให้นกตก แล้วกด A เพื่อ "กระพือ" ขึ้น

game.run(on_each_frame, fps=50)                 # bentogame.py:353 — ลูปเกม 50 fps
  • bird.move_to(bird.x, bird_y) ถูกเรียก ทุกเฟรม ทั้งที่ค่ายังไม่เปลี่ยน ดูเหมือนเปลืองเปล่า ๆ แต่นี่คือการวาง นิสัยของ render-loop ไว้ตั้งแต่ต้น พอคาบหน้า bird_y เริ่มเปลี่ยนทุกเฟรม บรรทัดเดิมบรรทัดนี้จะกลายเป็นภาพเคลื่อนไหวทันที โดยไม่ต้องเพิ่มโค้ดวาดใหม่ ตรงกับสไลด์ "render-loop: ฟิสิกส์เกิดจากการทำซ้ำเร็ว ๆ" (:25)
  • if game.keys().start: return False อ่านปุ่มด้วย game.keys() (ของ core 70%) แล้ว คืน False เพื่อออกจากลูป เป็น pattern เดียวกับตอนจบเกมใน step หลัง ที่ return False ตอนตกพื้น จำหลักนี้ไว้: คืน False = บอก game.run ว่า "พอแล้ว" (:26-27)
  • คอมเมนต์ :28 จงใจเขียนเตือนตัวเองว่าคาบหน้าจะเติมอะไรตรงนี้ — โน้มถ่วง + กระพือ นี่คือ "ช่องว่างที่ยังไม่เติม" ที่จะกลายเป็นฟิสิกส์ 2 บรรทัดในสไลด์ "ฟิสิกส์ของคาบนี้"
  • game.run(on_each_frame, fps=50) (:30) ส่งฟังก์ชันของเราให้ core เรียกวินาทีละ 50 ครั้ง เราไม่เขียน while True เองเพราะ core ต้องแทรกงานวาดจอ/อ่านจอยระหว่างเฟรมให้ด้วย — หลักเดียวกับที่สไลด์ Timer พูดถึงการแยกจังหวะออกจากงานหนัก

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

เฉลย · สามก้อนประกอบกันเป็น "เกมที่รันได้ตัวเล็กที่สุด"

ก้อน บรรทัด หน้าที่ตอนนี้ (step 1) จะโตเป็นอะไรคาบหน้า
import + title :13,:15 ดึง core + เปิดหน้า Start เท่าเดิม (ฐานที่ไม่ต้องแตะ)
bird_y + Box :18-19 วางนกนิ่งที่ y=180 bird_y += bird_velocity → นกตกเอง
score scaffold :20-21 โชว์ "0" เฉย ๆ score += 1 เมื่อผ่านท่อ
on_each_frame + move_to :23-25 วาดซ้ำภาพนิ่ง บรรทัดเดิมกลายเป็นภาพเคลื่อนไหว
keys().startFalse :26-27 กด Start ออกจากเกม + กิ่งตกพื้น = GAME OVER
game.run(fps=50) :30 ลูป 50 fps เท่าเดิม (หัวใจของทุก step)
  • ทุกก้อนที่เขียนวันนี้ ไม่มีก้อนไหนถูกทิ้ง คาบหน้าเราแค่เติมเนื้อในกิ่งที่เว้นไว้ นี่คือเหตุผลที่เริ่มจาก step เล็กสุดที่รันได้ ไม่กระโดดเขียนทั้งเกมรวดเดียว — pattern เดียวกับที่ Snake ค่อย ๆ ต่อจากกริดว่าง ๆ
  • แกนเดียวที่ยัง "นิ่ง" อยู่คือ bird_y ของใหม่จริง ๆ คาบหน้าคือตัวแปร bird_velocity กับสมการ 2 บรรทัด ตรงกับที่สไลด์ "70 / 30" วาดกรอบไว้เป๊ะ

จับจังหวะการไต่ระดับนี้ให้ได้ แล้วน้องจะเห็นว่าเกมทั้งเกมสร้างแบบเดียวกันหมด — เริ่มจากของเล็กสุดที่รันได้ แล้วเติมทีละแนวคิด

เชื่อมจุด — นกนิ่งวันนี้ คือโครงของเกมที่ขยับได้พรุ่งนี้

ที่มา: ย้อนไปสไลด์ "render-loop: ฟิสิกส์เกิดจากการทำซ้ำเร็ว ๆ" และก่อนหน้านั้นคือ Snake ที่สอนเราวาดของลงจอทุกเฟรม โครง on_each_frame + game.run(fps=50) ไม่ใช่ของใหม่ของ Flappy มันคือ game loop ตัวเดียวกับ Snake เราแค่เอานก 1 กล่องมาวางแทนงูทั้งตัว ลำดับ "วาดก่อน ค่อยขยับทีหลัง" คือสิ่งที่ทำให้ debug ง่าย เพราะเห็นนกอยู่ถูกที่บนจอก่อน แล้วค่อยไปเถียงกับฟิสิกส์คาบหน้า

ที่ไป: bird_y ที่วันนี้เป็นค่าคงที่ 180.0 คือ ตัวแปรตัวเดียว ที่คาบหน้าจะมีชีวิต พอเราเติม bird_velocity += GRAVITY แล้ว bird_y += bird_velocity (สไลด์ "ฟิสิกส์ของคาบนี้") บรรทัด move_to เดิมจะทำให้นกตกเองโดยไม่ต้องแตะอะไรอีก ส่วน score_label ที่โชว์ 0 เฉย ๆ วันนี้ จะกลายเป็นคะแนนจริงตอนใส่ท่อในคาบ 9 จนครบเป็น full_games/flappy_full.py

ที่มา game loop วาดทุกเฟรม (Snake / render-loop) วันนี้ นกนิ่ง + นั่งร้านคะแนน (bird_y คงที่) ที่ไป ตก + กระพือ + ท่อ (flappy เต็มเกม)

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

  • จำ game.run(on_each_frame, fps=50) จากสไลด์ render-loop ได้ไหม ถ้า move_to ถูกเรียก 50 ครั้งต่อวินาทีอยู่แล้ว เราต้องเพิ่มโค้ด "วาด" อีกไหมตอนนกเริ่มขยับ? (ใบ้: ไม่ต้องเลย เปลี่ยนแค่ค่า bird_y)
  • ถ้าวันนี้เราวางนกนิ่งบนจอได้ พรุ่งนี้เราแค่ทำให้ bird_y เปลี่ยนทุกเฟรม นกก็ตกเองได้ไหม? (ใบ้: v += g แล้ว y += v สองบรรทัด)
  • สังเกตไหมว่า move_to ทุกเฟรมของนก คือเรื่องเดียวกับไฟวิ่ง knight-rider ที่กวาดทีละดวงในลูป และ Snake ที่วาดใหม่ทุกเฟรม? มันคือ "ลบของเก่า วาดของใหม่ เร็ว ๆ" อันเดียวกัน

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

ใช้จริงที่ไหน: วาง UI นิ่งก่อน + เฟรมลูป + นั่งร้านค่า

เทคนิควันนี้ไม่ใช่แค่แบบฝึกหัด มันคือลำดับการทำงานที่วิศวกรจริงใช้ทุกวัน:

แผงหน้าปัด HMI — โชว์ "0" ก่อนต่อเซนเซอร์ 0 Temp 0 RPM 0 จอ refresh — วาดทั้งเฟรมซ้ำ 30–60 fps ค่าเท่าเดิมก็วาดใหม่ทุกเฟรม นั่งร้าน UI — ค่าปลอม 0 / — ก่อน backend 0 mockup โชว์ค่าปลอม ทดสอบ layout ได้ก่อน ข้อมูลจริงค่อยมาทีหลัง ทางออกสู่สถานะรู้แน่ — return False game loop on_each_frame return False สถานะปลอดภัย ปิดมอเตอร์ · ดับไฟ
  • หน้าจอ HMI / แผงหน้าปัดก่อนต่อเซนเซอร์: วิศวกรวาง layout ให้ครบแล้วโชว์ "ค่า 0" ก่อน ค่อยต่อสายเซนเซอร์จริงทีหลัง เหมือนที่เราวาง bird + score_label ("0") ไว้ก่อนใส่ฟิสิกส์ — เห็นหน้าจอถูกก่อน ค่อยเถียงกับข้อมูล
  • จอ refresh ที่วาดซ้ำทุกเฟรมแม้ภาพไม่เปลี่ยน: เครื่องมือแพทย์ dashboard รถ เกมคอนโซล วาดทั้งเฟรมใหม่ 30–60 ครั้ง/วินาที ต่อให้ค่าเท่าเดิม ตรงกับ move_to(bird.x, bird_y) ทุกเฟรมของเรา — pipeline เดียวกับที่เกร็ด Game Boy พูดถึง refresh ทั้งเฟรม
  • placeholder / นั่งร้านในงานพัฒนา: mockup โชว์ค่าปลอม (0, "—") ก่อนต่อ backend ทำให้ทีมทดสอบ layout ได้ก่อนข้อมูลจริงมา score = 0 ที่ยังไม่มีใครนับคือ placeholder แบบเดียวกันเป๊ะ
  • ทางออกที่คืนเครื่องสู่สถานะรู้แน่ (known state): game.keys().start แล้ว return False คือการออกจากลูปอย่างเป็นระเบียบ ระบบ embedded จริงต้องมีทางออกที่คืนเครื่องสู่สถานะปลอดภัยเสมอ (ปิดมอเตอร์ ดับไฟ) ไม่ปล่อยค้างกลางอากาศ

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

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

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

วันนี้ — bird_y คงที่ (นกนิ่ง) ทุกเฟรมนกอยู่ y=180 เท่าเดิม — ภาพนิ่ง เพิ่มตัวแปรเดียว: bird_velocity v += g แล้ว y += v (ฟิสิกส์ 2 บรรทัด) พรุ่งนี้ — bird_y += bird_velocity (นกตกเอง) แค่บวก v เข้า y ทุกเฟรม นกร่วงเร็วขึ้นเอง
  • นกขยับเบา ๆ ตอนนิ่ง (idle bob): จะทำให้นกลอยแกว่งขึ้น-ลงนิด ๆ ให้จอไม่ดูตายสนิท ยังไง โดย ยังไม่ใส่ฟิสิกส์จริง? (ใบ้: ให้ bird_y แกว่งด้วย sin ของเลขเฟรม ไม่ต้องสะสมเหมือนแรงโน้มถ่วง)
  • สามระดับความยากที่เปลี่ยนแค่จุดเริ่ม: อยากมีนกเริ่มสูง/กลาง/ต่ำ โดยแตะแค่ bird_y เริ่มต้น จะออกแบบให้ผู้เล่นเลือกก่อนเข้าเกมยังไง โดยไม่ยุ่งกับโครง on_each_frame?
  • พื้นหลัง/พื้นดินก่อนใส่ท่อ: ถ้าจะเพิ่ม Box เป็นพื้นดิน ควรวาดมันในลูปทุกเฟรม หรือวางครั้งเดียวนอกลูป เพราะอะไร? (โยงกับเรื่อง move_to ทุกเฟรมที่เราเพิ่งคุยกัน)
  • (สะพานสู่คาบหน้า) ตอนนี้ bird_y เป็นค่าคงที่ คาบหน้าเราต้องเพิ่ม bird_velocity แล้วทำให้ทั้งคู่เปลี่ยนใน on_each_frame ลองคิดล่วงหน้า: ทำไมสองตัวนี้ต้องประกาศ global ในฟังก์ชัน แล้วถ้าลืมจะเกิดอะไรกับนก? (เปิดสไลด์ "กับดักที่เจอบ่อย" ทวนได้)

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

fit-css

Marp deck — Fundamental of Embedded Systems Developer II — คาบ 8 (จาก 13 คาบ) ช่วง Flappy (ฟิสิกส์เกม) — คาบที่ 1 — MicroPython only Render: npx @marp-team/marp-cli session-08.md -o session-08.html

← Roadmap (TOC)