อาการที่เจอ: ฉากไหนที่ของเยอะ ๆ (เกิดศัตรู + ระเบิด + ดาว พร้อมกัน) บางเฟรม CM33 ส่งจดหมายไปวาดเร็วกว่าที่ CM55 จะรับไหว CM55 เลยตอบว่า "not ready" ถ้าเราปล่อยให้ error นี้เด้งขึ้นมา (raise) → เกมค้างตายกลางเฟรม
ความจริงของฮาร์ดแวร์: "not ready" ไม่ใช่ความผิดพลาด มันคือ backpressure — สัญญาณว่า "ช้าลงหน่อย เดี๋ยวฉันตามทัน" สภาวะแบบนี้เป็นเรื่องปกติและชั่วคราว รอไม่กี่มิลลิวินาทีมันก็หายเอง
error จริง ๆ (เช่นชื่อ widget ผิด) ยังต้อง
raiseเหมือนเดิม เรา retry เฉพาะ อาการ "not ready" ที่รู้ว่าเป็นของชั่วคราวเท่านั้น — ไม่ใช่กลืน error ทุกชนิด
_r() ใน bentogame.pyทุกคำสั่งที่ engine ส่งไป CM55 ถูกห่อด้วย helper ตัวเดียวชื่อ _r() (bentogame.py:117) — คำว่า _r ย่อจาก retry:
def _r(fn, *a, **k):
"""Retry a ui call while CM55 signals transient "not ready" backpressure
(the same tolerance start() already uses for ui.screen). A heavy scene can
briefly outrun the CM55 graphics core; retrying instead of raising keeps the
game from crashing mid-frame."""
for _ in range(40): # ลองใหม่ได้สูงสุด 40 ครั้ง = bounded loop
try:
return fn(*a, **k) # สำเร็จ → คืนค่าออกทันที
except Exception as _e:
if "not ready" in str(_e): # เฉพาะ backpressure ชั่วคราวเท่านั้น
time.sleep_ms(15) # ถอยหลัง 15ms ให้ CM55 ตามทัน
continue
raise # error อื่น ๆ = ของจริง ปล่อยให้เด้ง
return fn(*a, **k) # ครบ 40 ครั้งยังไม่ได้ = ยอมให้ error จริง
เวลาเราเรียก box.move_to(x, y) ข้างในมันคือ _r(self._panel.pos, ...) (bentogame.py:234) — เท่ากับว่า ทุกการขยับ widget ผ่านตาข่ายนิรภัยตัวนี้หมด โดยที่โค้ดเกม (30% ที่เราเขียน) ไม่ต้องรู้เรื่องเลย
ทำไมต้อง "bounded" (
range(40)) ไม่ใช่while True? เพราะถ้า CM55 ตายจริง ๆ เราต้องยอมให้ error หลุดออกมา ไม่ใช่ค้างวนไม่รู้จบ — retry ที่ดีต้องมีเพดานเสมอ
อาการที่เจอ: เกม Shooter เวอร์ชัน Python รู้สึก "หนืด" — ทั้งที่เกม C บนบอร์ดเดียวกันลื่นปรื๊ด ทำไม? มันคือ CPU เดียวกันนี่นา
สาเหตุที่แท้จริง: ไม่ใช่ Python คำนวณช้า — แต่เป็นจำนวนจดหมายข้าม core ต่อเฟรม ทุก move_to หนึ่งครั้ง = IPC หนึ่งฉบับ CM33→CM55 เกมมีศัตรู + กระสุน + ระเบิด ยิ่งของบนจอเยอะ จดหมายต่อเฟรมยิ่งเยอะ → คอขวดอยู่ที่ จำนวน IPC round-trip ไม่ใช่ที่ความเร็ว CPU
fix จริงอยู่ที่ shooter_full.py:30 — ลดจำนวนศัตรูกับกระสุนสูงสุดลง:
# ก่อนแก้: EMAX, BMAX = 8, 6 ← เท่าเกม C (แต่ Python ส่ง IPC ต่อ object ไม่ไหว → หนืด)
EMAX, BMAX, BOOMMAX, STARS = 5, 4, 3, 5 # จุดสมดุล: object น้อยพอให้ลื่น แต่ยังมี action
comment ในไฟล์ยังคำนวณ "งบ widget" ไว้ให้เห็น (shooter_full.py:25–29) — เกม C ถือได้ ศัตรู 8 + กระสุน 6 เพราะมันวาดเองบน CM55 ไม่ต้องส่ง IPC ต่อชิ้น แต่ Python ต้องส่ง เราจึงเลือก budget จำนวน object บนจอ ให้พอดีกับ IPC ที่ไหวใน 1 เฟรม
โยงกับคาบ 11 (Object Pool): pool ช่วยไม่ให้ สร้าง object ใหม่ทุกเฟรม (ประหยัด RAM) ส่วนบทเรียนนี้บอกให้ budget จำนวน object ที่ขยับพร้อมกัน (ประหยัด IPC) — สองเรื่องนี้ทำงานคู่กัน: จำนวน widget คงที่ และ ไม่มากเกินงบ IPC ต่อเฟรม
อาการที่เจอ: ท่อในเกม Flappy เคยถูก resize() ทุกเฟรม เพื่อ "ตัด" ความสูงให้พอดีขอบจอ ผลคือ CM55 วาดท่อขนาดใหม่ทับ แต่ ไม่ได้เคลียร์พิกเซลเดิมที่ท่อเคยกินไว้ → เกิด รอยเขียวลากเฉียง + เส้นดำขวางจอ (scanline)
เหตุใด resize จึงเปื้อน: resize() เรียก ui.size (bentogame.py:254) เปลี่ยนขนาด widget — พื้นที่ที่ widget "หด" ออกไป CM55 ไม่ได้ถือว่าเป็นหน้าที่ต้องทาสีพื้นหลังทับให้ พิกเซลเก่าจึงค้างเป็นรอย
fix ใน flappy_full.py เปลี่ยนวิธีคิดเรื่องท่อทั้งหมด — ท่อเป็นกล่อง "สูงเต็มสนาม" สร้างครั้งเดียว ไม่ resize อีกเลย (flappy_full.py:22, 66):
PIPE_W = 64 # ความกว้างท่อ (คงที่ — ท่อเป็นกล่องสูงเต็มจอ ไม่ resize เลย)
# ท่อบน/ล่างเป็นกล่อง "สูงเต็มสนาม" (PIPE_W x PLAY_H) สร้างครั้งเดียว ไม่ resize อีกเลย
parts = {"top": game.Box(-200, 0, PIPE_W, PLAY_H, game.GB_DARK),
"bot": game.Box(-200, 0, PIPE_W, PLAY_H, game.GB_DARK), ...}
ช่องว่างระหว่างท่อไม่ได้เกิดจากการ ย่อ ท่อ แต่เกิดจากการ เลื่อน ท่อยาว ๆ ให้โผล่จากขอบบน/ขอบล่าง แล้วส่วนที่ล้นออกนอกจอ ปล่อยให้ฮาร์ดแวร์ clip ทิ้ง — ทุกเฟรมทำแค่ move_to (flappy_full.py:113–128):
def render_pipe(pipe):
# ต่อเฟรม: "เลื่อนอย่างเดียว" ไม่ resize — ท่อสูงเต็มจอ ส่วนพ้นขอบให้ฮาร์ดแวร์ตัด
pipe["top"].move_to(x, pipe["gap_top"] - PLAY_H) # ขอบล่างของท่อบน = ขอบช่องบน
pipe["bot"].move_to(x, pipe["gap_bot"]) # ขอบบนของท่อล่าง = ขอบช่องล่าง
อาการที่เจอ: พอปิดเมนูเลือกโหมด ฉากเกมต้อง recomposite ใหม่ใช้เวลา ~3–4 วินาที ระหว่างนั้นถ้าเกมเริ่มนับฟิสิกส์ทันที นกจะร่วง + ตายก่อนที่ผู้เล่นจะทันเห็นจอด้วยซ้ำ — แพ้แบบงง ๆ ทั้งที่ยังไม่ได้เล่น
วิธีแก้ 2 ชั้น ใน flappy_full.py:
started flag, flappy_full.py:150, 180)flappy_full.py:186)started + gracestarted = False # ยังไม่เริ่มบิน — นกลอยรอจนกระพือครั้งแรก (:150)
start_ms = 0 # เวลาที่ออกตัว — ใช้คุมช่วง grace 4 วิแรก (นกตายไม่ได้)
def update():
...
if not started: # ยังไม่ออกตัว: นกลอยรอกลางจอ ไม่มีแรงโน้มถ่วง
if not (flap_a or flap_up):
return # ให้ผู้เล่นพร้อม (ฉากวาดครบตั้งแต่หน้าเมนูแล้ว)
started = True # กระพือครั้งแรก = ออกบิน
start_ms = now # เริ่มจับเวลา grace 4 วิ
grace = time.ticks_diff(now, start_ms) < 4000 # 4 วิแรก: นกตายไม่ได้ (ครอบช่วงจอกำลังวาด)
...
if bird_y > PLAY_H - BIRD_BOX / 2: # ร่วงถึงพื้น
if grace: # ช่วง grace: ไม่ตาย แค่หยุดที่พื้น
bird_y, velocity_y = PLAY_H - BIRD_BOX / 2, 0.0
else:
return False # หมด grace แล้วถึงตายจริง
...
if hit_pipe(pipe) and not grace: # ช่วง grace 4 วิแรก: ชนท่อไม่ตาย (:220)
return False
นี่คือดีไซน์ Flappy คลาสสิก "tap to start" — เกมไม่เริ่มเดินจนกว่าผู้เล่นจะพร้อม แล้วให้ช่วงผ่อนผันตอนต้นเกม เพื่อกลบ latency ของจอจริง
| อาการ (symptom) | สาเหตุจริง (root cause) | วิธีแก้ (fix) | บทเรียนที่ใช้ต่อได้ |
|---|---|---|---|
| เกมค้างตายกลางเฟรมตอนฉากหนัก | CM55 ส่ง backpressure "not ready" ชั่วคราว แล้วโค้ด raise |
_r() bounded retry 40 ครั้ง × 15ms เฉพาะ "not ready" (bentogame.py:117) |
สภาวะ "ไม่พร้อมชั่วคราว" ของ HW เป็นเรื่องปกติ → bounded retry ไม่ใช่ crash |
| Shooter Python หนืด แต่ C ลื่น | IPC ต่อ object เยอะเกินงบต่อเฟรม (ไม่ใช่ CPU ช้า) | ลด EMAX,BMAX 8,6 → 5,4 (shooter_full.py:30) |
คอขวดคือ จำนวน cross-core call ต่อเฟรม → budget จำนวน object |
| จอเปื้อนรอยเขียวเฉียง + เส้นดำ | resize() ทุกเฟรม → พิกเซลเก่าไม่ถูกเคลียร์ |
ท่อกล่องสูงเต็มจอ สร้างครั้งเดียว + move_to เท่านั้น (flappy_full.py:66,113) |
fixed-size + reposition สะอาดกว่า resize ทุกเฟรม ให้ HW clip |
| นกตกตายก่อนผู้เล่นเห็นจอ | จอ recomposite ~4s หลังปิดเมนู (latency) | started hover + grace 4s กันตาย (flappy_full.py:150,186) |
จอมี latency → ออกแบบวินาทีแรกให้ให้อภัย (tap to start) |
ครั้งหน้าที่น้อง ๆ เจอเกม (หรือระบบ embedded อะไรก็ตาม) ทำงานแปลก ๆ ลองถามตัวเอง 4 คำถามนี้: มีจังหวะ ไม่พร้อมชั่วคราว ไหม? · ส่งข้าม core กี่ครั้งต่อเฟรม? · วาดทับแล้ว เคลียร์ของเก่าหรือยัง? · เผื่อ latency ให้จอแล้วหรือยัง?
bentogame.py คือ "เฉลยระดับเครื่องยนต์"ตลอดคอร์สนี้ 30% ที่น้องเขียนเองนั่งอยู่บน engine ตัวนี้ ไฟล์ bentogame.py จึงเป็นเฉลยอีกแบบ ไม่ใช่เฉลยไว้ลอกวางส่ง แต่ไว้ เปิดอ่านให้เข้าใจว่าทำไมมันเขียนแบบนี้ แล้วปิดไฟล์ ลองนึกว่าถ้าเราต้องเขียน engine เองจะกันพลาดตรงไหน โค้ด engine เราไม่ต้องท่อง แต่ต้องอ่านออกว่า "ทุก guard ในไฟล์นี้มีแผลจริงอยู่เบื้องหลัง" คือแผลที่เราเพิ่งเล่าไป 4 บทเรียน
ต่อจากนี้เราจะแกะ engine ทีละชิ้นที่ยัง ไม่ได้ เปิดในสี่บทเรียน แต่เกิดจากรากเดียวกัน คือ "ฮาร์ดแวร์จริงไม่ใช่ตัวจำลองในอุดมคติ"
ชิ้นแรก — degrade gracefully: ไม่มีฮาร์ดแวร์ ก็ไม่ล้ม ตัวสร้าง Sprite เช็คก่อนว่าเฟิร์มแวร์มี pixel-art ให้ไหม (_HAS_SPRITE, bentogame.py:98) ถ้าไม่มี ถอยไปวาดกล่องเขียว 16px แทน:
def __init__(self, name, x=0, y=0):
self.x, self.y, self.name = x, y, name
if _HAS_SPRITE:
self._w = _r(ui.Sprite, SPR[name], int(x), int(y))
else:
# logic-only fallback so the desktop sim still runs (a 16px box)
self._w = _r(ui.Panel, x=int(x), y=int(y), w=16, h=16, color=GB_DARK,
min=GB_DARK, max=1, value=0)
raise เมื่อไม่มี sprite? เพราะ logic ของเกม (ชน / นับแต้ม / แพ้ชนะ) ไม่ได้ขึ้นกับว่าภาพเป็น pixel-art หรือกล่อง เกมยังรันได้บน desktop sim หรือเฟิร์มแวร์เก่า นี่คือ ญาติของบทเรียน 1: "ไม่พร้อม" ไม่เท่ากับ "พัง" (:139-144)_r(...) ตัวเดิม ตาข่ายนิรภัย retry ครอบทั้งสองทาง ไม่ว่าจะวาด Sprite จริงหรือกล่องสำรองsfx() เงียบถ้าเฟิร์มแวร์ไม่มีเสียง (:75), pot() คืน -1.0 ถ้าบอร์ดไม่มีปุ่มหมุน (:503-505) รวมกันแปลว่า "ไฟล์เกมไฟล์เดียว รันได้ทุกบอร์ด" ตามที่หัวไฟล์ตั้งใจไว้ (:12-16)เขียน engine ที่ดีคือเผื่อไว้ว่า "ของชิ้นนี้อาจไม่มีอยู่จริงบนบอร์ดตรงหน้า" แล้วเลือกค่ากลาง ๆ ที่ไม่ทำให้ทั้งเกมล้ม ดีกว่ายืนกรานว่าต้องมีทุกอย่างครบก่อนถึงจะรันได้
pool(): จองวิดเจ็ตล่วงหน้า อย่าสร้างกลางเฟรมบทเรียน 2 (IPC มีราคา) สรุปว่าคอขวดคือ จำนวน object บนจอ และอ้างถึง Object Pool ของคาบ 11 นี่คือโค้ดจริงของมันใน engine (bentogame.py:176-184):
def pool(name, count):
"""Make `count` hidden sprites of one kind ONCE (e.g. snake body segments).
Show/move the ones you need each frame, hide the rest — exactly like the C
game. Keeps you under the 32-sprite limit and runs fast.
NEVER create sprites inside your update() loop; size a pool here instead."""
sprites = [Sprite(name, -100, -100) for _ in range(count)]
for sprite in sprites:
sprite.hide()
return sprites
(-100, -100) นอกจอ ระหว่างเล่นแค่ show() / move_to() ตัวที่ต้องใช้ ไม่มีการ สร้าง วิดเจ็ตใหม่กลางเกมเลยpool() แก้ที่ RAM + สร้างครั้งเดียว, budget จำนวน object แก้ที่ IPC ต่อเฟรม pool() คือครึ่งแรกของคู่นั้น"อย่าสร้างของกลาง loop" เป็นกฎที่ข้ามภาษา ทั้ง C, Python และเกมเอนจินระดับ AAA ล้วนจองทรัพยากรไว้เป็น pool ล่วงหน้าแล้ว recycle เพราะการจอง/คืนกลางเฟรมคือศัตรูของจังหวะที่คงที่
บทเรียน 4 พูดถึง "กระพือครั้งแรก = ออกตัว" ซึ่งก็คือการจับ ขอบขาขึ้น ของปุ่ม (เพิ่งกด ไม่ใช่กดค้าง) engine ทำเรื่องนี้ให้ด้วย pressed_once() ที่จำสถานะเฟรมก่อนไว้ใน _edge_prev (bentogame.py:359, 374-378):
k = snapshot if snapshot is not None else keys()
now = bool(getattr(k, key, False))
fired = now and not _edge_prev.get(key, False)
_edge_prev[key] = now
return fired
fired = now and not _edge_prev.get(key, False) เป็นจริง เฉพาะ เฟรมที่ปุ่มเปลี่ยนจาก "ไม่กด" เป็น "กด" กดค้างเฟรมถัดไป _edge_prev[key] เป็น True แล้ว จึงไม่ยิงซ้ำ_edge_prev, :359)? เพราะ update() วิ่ง ~30 ครั้งต่อวินาที ถ้าใช้ "กดอยู่ไหม" ตรง ๆ กับปุ่มยิง คนกดค้างครั้งเดียวจะยิงรัว 30 นัดต่อวินาที เกมพัง edge detection แปลง "สถานะต่อเนื่อง" ให้เป็น "เหตุการณ์ครั้งเดียว"prev_start ใน run() ที่กันปุ่ม Start ค้างจากหน้าเมนูไม่ให้สั่ง Pause ทันที (:961, :973)ปุ่มเดียวกันตอบได้สองคำถามที่ต่างกันสิ้นเชิง: "กดค้างอยู่ไหม" (เดินหน้า) กับ "เพิ่งกดหรือเปล่า" (ยิง / กระโดด) วิศวกรต้องรู้ว่ากำลังถามข้อไหน แล้วหยิบเครื่องมือให้ถูก
engine ไม่ได้กันพลาดแบบสุ่ม แต่ละ guard แก้ "ความจริงของฮาร์ดแวร์" คนละข้อ แล้ววางซ้อนกันเป็นชั้น ๆ:
| ชิ้นส่วน engine | บรรทัด | กันความจริงข้อไหนของฮาร์ดแวร์ | โยงกับบทเรียน |
|---|---|---|---|
_r() retry |
:117-130 |
จังหวะ "not ready" ชั่วคราวของ CM55 | บทเรียน 1 |
Sprite / sfx / pot fallback |
:139-144, :75, :503 |
ฮาร์ดแวร์บางชิ้นไม่มีบนบอร์ดนี้ | ญาติบทเรียน 1 (degrade ไม่ crash) |
pool() |
:176-184 |
RAM + งบวิดเจ็ต 32 ชิ้นจำกัด | บทเรียน 2 (budget) |
Box.move() clamp + move_to |
:240-252 |
move ถูกกว่า resize · ให้ HW clip ขอบ | บทเรียน 3 |
pressed_once() edge |
:359, :374-378 |
อ่านปุ่มได้แค่ "ตอนนี้" ต้องจำเฟรมก่อนเอง | บทเรียน 4 (first flap) |
run() frame-budget sleep |
:962-999 |
latency จอ + คุมจังหวะให้ตรง fps | บทเรียน 4 |
ครั้งหน้าที่น้องเปิด engine ของใครก็ตาม ลองมองหา guard พวกนี้ retry · fallback · pool · edge · budget แต่ละตัวคือรอยแผลของ bug ที่เคยเกิดจริง engine ที่ดีก็คือประวัติศาสตร์ของ bug ที่ถูกแก้แล้ว เขียนออกมาเป็นโค้ด
ที่มา แนวคิดของคาบนี้ไม่ได้เกิดวันนี้ มันโตมาจากสไลด์เปิด "บอร์ดของเรามีสองสมองที่คุยกันด้วยจดหมาย" ที่วางกติกาไว้ว่าทุก move_to คือจดหมายข้าม core หนึ่งฉบับ ไม่ใช่การเขียนพิกเซลฟรี ๆ พอยอมรับความจริงข้อนี้ ทุกบทเรียนถัดมา (retry, budget, move-ไม่-resize, grace) ก็คือ วิธีอยู่กับ ราคาและจังหวะของจดหมายนั้น และวันนี้เราเห็นว่ามันไม่ใช่สี่ทริกแยกกัน แต่เป็นชั้น guard ชั้นเดียวที่ engine แบกไว้แทนเรา
ที่ไป ทักษะ "ดันความยุ่งยากของฮาร์ดแวร์ลงไปซ่อนใต้ชั้น abstraction" คือสิ่งที่น้องจะทำซ้ำไปทั้งอาชีพ ไม่ว่าจะขึ้นไปเขียน C game SDK (แนวคิด sprite / move / input / collision map 1:1 ตามที่หัวไฟล์ bentogame.py เขียนไว้, :9-10), เขียน driver, หรือทำ cloud service งานคือ "รับ error / latency / ของขาดไว้ที่ชั้นล่าง แล้วยื่น API ที่สะอาดให้ชั้นบน" engine เล็ก ๆ ของวันนี้คือแบบจำลองย่อของสถาปัตยกรรมซอฟต์แวร์ทั้งวงการ
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
_r() ในบทเรียน 1 ได้ไหม ถ้าลบมันออกแล้วบังคับให้โค้ดเกมทุกไฟล์ต้อง try/except "not ready" เองทุกบรรทัดที่ขยับของ โค้ดเกม 30% จะสะอาดขึ้นหรือรกขึ้น? (ใบ้: ลองนับว่าต้องเพิ่มกี่บรรทัดต่อการเรียกหนึ่งครั้ง)game.Box ได้ พรุ่งนี้ตอนขึ้น C เราจะซ่อน "DMA + double-buffer + IPC" ไว้ใต้ sprite_draw() แบบเดียวกันได้ไหม?pool() คือจองความยุ่งยากไว้ล่วงหน้าที่ชั้นล่าง เพื่อให้ชั้นบน "แค่หยิบมาใช้"? abstraction กับ pooling คือคนละหน้าของเหรียญเดียวกันถ้าตอบได้ว่า "อ๋อ สี่บทเรียนกับ engine ทั้งไฟล์ มันคือทักษะเดียว คือรับความจริงของฮาร์ดแวร์ไว้ที่ชั้นล่าง" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด เพราะทักษะนี้จะติดตัวน้องไปไกลกว่าเกมมาก
เทคนิคของ engine วันนี้ไม่ได้อยู่แค่ในเกมฝึก มันคือหน้าที่ประจำของซอฟต์แวร์ชั้นล่างในระบบจริง:
_r() / Sprite คืองานประจำ: I2C เจอ NAK ก็ retry, sensor ไม่ตอบก็คืนค่าเก่า ไม่ล้มทั้งระบบ — บทเรียน 1 เป๊ะpool() ของเรา (บทเรียน 2)pressed_once() กันกดครั้งเดียวถูกนับหลายครั้ง คือ บทเรียน 4 ในฮาร์ดแวร์ที่น้องจับทุกวันไม่มีข้อไหนในสี่ข้อนี้เป็นของสมมติเลย engine เล็ก ๆ ที่เราถอดกันวันนี้ ใช้หลักการเดียวกับซอฟต์แวร์ระดับล้านผู้ใช้ ต่างแค่ขนาด ไม่ใช่แนวคิด
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหางานวิศวกรรมจริงได้:
try/except รอบ game_over() ที่ยอมให้ป้ายบางชิ้นไม่ขึ้นแต่ปุ่มยังทำงาน, :1043-1060)pool() ต้องแชร์ข้ามชนิดสไปรต์ เกมที่มีศัตรูสามแบบ จองแยก pool ละชนิดจะกินงบ 32 เร็ว จะออกแบบ pool รวมที่ "ยืม-คืน" ข้ามชนิดยังไง โดยไม่สร้างสไปรต์กลางเฟรม_r() ให้รายงาน "จำนวนจดหมายต่อเฟรม" ออกมาดูได้ไหม แล้วใช้เลขจริงตัดสิน budget แทนการเดา (โยงบทเรียน 2: วัดก่อน แล้วค่อย optimize)bentogame.py เขียนไว้ว่า sprite / move / input / collision map 1:1 ไปยัง C game SDK ของคอร์ส (:9-10) ลองเปิด SDK ฝั่ง C แล้วตามหา guard ทั้งหกที่เราถอดวันนี้ ตัวไหนหายไป (เพราะ C วาดบน CM55 ตรง ๆ ไม่มี IPC ต่อ object)? ตัวไหนยังอยู่แต่เปลี่ยนรูป? นี่คือการบ้านที่พาน้องข้ามจาก Python on-ramp ไปสู่ C จริงเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจาก engine ที่เราเพิ่งแกะด้วยกัน ตรงนั้นแหละคือจุดที่น้องเปลี่ยนจาก "คนใช้ engine" เป็น "คนเขียน engine"
fit-css