แทนที่จะเขียน ship_x += 8 (ที่เลื่อนคงที่ทุกเฟรม) เราเก็บตัวแปร ship_speed ไว้ แล้วบวกมันเข้า ship_x ทุกเฟรม วิธีนี้ทำให้ความเร็วค่อย ๆ เปลี่ยนได้
นี่คือเรื่องเดียวกับไม้ตี Pong ที่เราทำในคาบก่อน accel / friction / clamp ชุดเดิม เปลี่ยนแค่จาก "พายขึ้น-ลง" มาเป็น "ยานซ้าย-ขวา" เป็นทักษะที่เราฝึกมาแล้ว แค่นำกลับมาใช้
ลองดูค่าจริงของ ship_speed เมื่อ กดค้างแล้วปล่อย ตามสมการสองตัวบนหน้าก่อน เห็นได้ว่าสองช่วงคนละรูปทรงกัน:
+ACCEL คงที่ทุกเฟรม ความเร็วจึงไต่ขึ้น เป็นเส้นตรง จนชน MAX_SPEED = 13 แล้วราบ (clamp คุมเพดาน)×0.80 ทุกเฟรม ความเร็วจึง โค้งลงแบบ exponential — ลดเยอะตอนเร็ว แล้วค่อย ๆ เฉียดศูนย์ นี่คือ "ความลื่น" ที่เรารู้สึกได้จำง่าย ๆ ว่า บวก = เส้นตรง, คูณ = เส้นโค้ง ใครอยากให้ยานลื่นยาวก็ดัน เข้าใกล้ 1 (เส้นโค้งแบนลง หางยาวขึ้น) ใครอยากให้หยุดไวก็ลด ลง
ส่วน 30% ที่อาจารย์เขียนให้ดูเป็นตัวอย่าง ทุกบรรทัดน้อง ๆ เคยเจอใน Catch/Pong มาแล้ว:
import bentogame as game
game.title("SHOOTER") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
ship = game.Box(365, 352, 62, 24, game.GREEN) # ยานกว้าง 62 สูง 24 กลาง-ล่าง
ship_x = 365.0 # ตำแหน่ง x ของยาน (เก็บเป็น float)
score, lives = 0, 3
hud = game.Text("Score: 0 Lives: 3", 10, 8, game.WHITE)
def on_frame():
pass # ยังไม่มี logic — ภาพนิ่งก่อน
game.run(on_frame, fps=30) # Back = ออก / Start = เริ่มใหม่ (เอนจินจัดการเอง)
ไฟล์เว้นช่อง:
practise_codes/shooter_step1.py↗ — โค้ดข้างบนให้ผลแบบจอด้านขวา: ยานเขียวกลาง-ล่าง + HUD มุมบน ยังเป็นภาพนิ่ง
ship_x ต้องเป็น float?เราเก็บตำแหน่งเป็นทศนิยม เพื่อให้สะสมเศษความเร็วได้ ถ้าเก็บเป็น int มันจะปัดเศษทิ้งทุกเฟรม จนยานขยับกระตุก:
ship_x = 365.0 (มีจุดทศนิยม) → เศษ 0.4 ถูกเก็บไว้ พอครบ ๆ ก็ขยับเพิ่มอีก 1 พิกเซลเองship_x = 365 (ไม่มีจุด) → ทุกเฟรมโดนปัดทิ้ง ยานเหมือนไม่ขยับ หรือกระตุกเรื่องเล็ก ๆ นี้คือกับดักที่เจอบ่อยที่สุดของคาบนี้ ใน step 2 เราจะบวกความเร็วที่เป็นเศษส่วน (เช่น 1.4) เข้าไป ถ้า
ship_xเป็น int ทุกอย่างจะพังเงียบ ๆ
เปิด practise_codes/shooter_step1.py↗ ใน BENTO IDE แล้วกดปุ่ม Program to Device
ต้องเห็น ยานเขียวกลาง-ล่าง + ข้อความ Score/Lives ด้านบน ตอนนี้ยังเป็นภาพนิ่ง ยังไม่ขยับ ซึ่งถูกต้องแล้ว · กด Back เพื่อออก (Start = เริ่มใหม่)
ภาพปลายทางอ่านได้แบบนี้: ยานเขียวกลาง-ล่าง คือตัวที่เราวางในคาบนี้ ·
Score / Livesมุมซ้ายบน คือ HUD ที่เราวางใน step 1 · กระสุนและศัตรู เป็นของคาบถัดไป
ค่าฟิสิกส์เราวางไว้บนสุด เป็น ตัวเลขจริงจากเกมที่ใช้งานจริง:
ACCEL, MAX_SPEED, FRICTION = 1.4, 13.0, 0.80 # ค่าฟิสิกส์เดียวกับเกมจริง
วิธีอัปเดตความเร็วคือ กดปุ่มก็บวก ACCEL, ปล่อยก็คูณ FRICTION, แล้วครอบไว้ไม่ให้เกินเพดาน:
บล็อก 30% ที่เป็นงานของเรา อยู่ใน on_frame():
def on_frame():
global ship_x, ship_speed
keys = game.keys()
# ปุ่มออก/เริ่มใหม่ เอนจินจัดการเอง: Back = ออก · Start = เริ่มใหม่
if keys.left: ship_speed -= ACCEL # (1) กดซ้าย = เร่งไปทางซ้าย
elif keys.right: ship_speed += ACCEL # (1) กดขวา = เร่งไปทางขวา
else: ship_speed *= FRICTION # (2) ปล่อย = คูณ 0.80 → ลื่น
ship_speed = max(-MAX_SPEED, min(MAX_SPEED, ship_speed)) # (3) จำกัดความเร็ว
ship_x = max(0, min(game.WIDTH - ship.w, ship_x + ship_speed)) # (4) ขยับ + clamp
ship.move_to(ship_x, 352)
ลองดูยานทำงานจริงจาก 4 บรรทัดเมื่อกี้: กดค้าง ยานเร่งจนเต็มความเร็ว (แถบฟ้าด้านล่าง) → ปล่อยปุ่ม เปลวดับ แต่ยานยัง ลื่นต่อ ด้วยแรงเฉื่อย ค่อย ๆ ช้าลงจน ชนขอบจอแล้วหยุด ไม่หลุดออก

แถบความเร็วด้านล่างคือ ship_speed ตอนปล่อยปุ่ม มันไม่ได้ตกเป็นศูนย์ทันที แต่หดลงทีละ 20% ทุกเฟรม (×0.80) ความ "ลื่นต่อทั้งที่ไม่ได้กด" นี่แหละคือ โมเมนตัม ที่ทำให้บังคับยานรู้สึกมีน้ำหนัก ไม่แข็งทื่อ
เปรียบกับเข็นรถเข็นบนพื้นเรียบ ผลักแล้วปล่อยมือ รถยังไถลต่อเองอีกพักหนึ่ง ฟิสิกส์ในเกมก็เลียนแบบความรู้สึกนั้นด้วยเลขแค่ไม่กี่ตัว
speed ± ACCELspeed *= FRICTIONmax / minx += speedclamp ขอบจอ: ship_x = max(0, min(game.WIDTH - ship.w, ship_x + ship_speed))
อ่านบรรทัดนี้แบบนี้: ถ้ายานเลย 0 ไปทางซ้าย ก็ดึงกลับมาที่ 0 ถ้าเลยขอบขวา ก็ดึงกลับมาที่ WIDTH - ship.w ที่ต้องลบ ship.w ด้วย เพราะไม่งั้นยานครึ่งลำจะหลุดขอบขวาไป
on_frame() หนึ่งครั้ง · ข้อมูลไหลไปทางไหนทุกเฟรม (30 ครั้งต่อวินาที) ข้อมูลวิ่งจาก ปุ่มจอย → ความเร็ว → ตำแหน่ง → ภาพบนจอ เป็นทางเดียว แล้ววนกลับมาเริ่มใหม่:
สังเกตว่า ปุ่มไม่ได้ขยับยานโดยตรง แต่ไปแก้ ship_speed ก่อน แล้วความเร็วค่อยไปดันตำแหน่ง นี่คือเหตุผลที่ยาน "เร่ง" และ "ลื่น" ได้ ถ้าปุ่มสั่ง ship_x ตรง ๆ ยานจะกระโดดทันทีแบบ Catch ไม่มีน้ำหนัก
เส้นประที่วิ่งกลับ คือหัวใจของทุกเกม:
game.run()เรียกon_frame()ซ้ำ ๆ ให้เราเอง เราแค่เขียน "หนึ่งเฟรมทำอะไร" ที่เหลือเอนจินวนให้
เปิด practise_codes/shooter_step2.py↗ ใน BENTO IDE แล้วกดปุ่ม Program to Device แล้วลองแก้ค่าด้านบน สังเกตว่าความรู้สึกในการบังคับเปลี่ยนไปอย่างไร:
| ลองเปลี่ยน | จะรู้สึกว่า |
|---|---|
FRICTION = 0.95 |
ยาน ลื่นยาวมาก (เหมือนน้ำแข็งจริง ๆ) |
FRICTION = 0.50 |
ยานหยุด เกือบทันที ที่ปล่อยปุ่ม |
ACCEL = 3.0 |
ยาน พุ่งเร็วขึ้นมาก ทันทีที่กด |
MAX_SPEED = 5.0 |
ยานวิ่งช้าลง (เพดานความเร็วต่ำ) |
clamp ขอบจอ คือการบีบตำแหน่งให้อยู่ในช่วงที่ยานยังเห็นเต็มลำ:
จุดเช็คผ่าน: ขับยานไปแตะ ขอบซ้ายและขอบขวา ให้ครบ โดยยานไม่หลุดจอ แล้วถ่ายรูปจอ 1 ใบเก็บไว้เป็นหลักฐานว่า clamp ทำงาน (จอด้านขวาคือหน้าตาที่ควรเห็น — ยานชิดขอบขวาพอดี ไม่หลุดออก)
global ship_x, ship_speed ยานจะไม่ขยับเลย เพราะ Python ไปสร้างตัวแปร local ใหม่แทน ต้องมีบรรทัด global ไว้บนสุดของฟังก์ชันship_x เป็น int (365 ไม่มีจุดทศนิยม) ยานจะกระตุกหรือไม่ขยับ เพราะ 1.4 ถูกปัดทิ้ง ต้องเขียนเป็น float 365.0max(0, min(WIDTH - ship.w, ...)) และอย่าลืมลบ ship.w ด้วยship_speed = 0 แทน *= FRICTION จำไว้ว่า friction คือการ คูณ ไม่ใช่ตั้งค่าเป็นศูนย์game.run() จัดการให้แล้ว: Back = ออก · Start = เริ่มใหม่acceptance

รูปส่งงานควรหน้าตาประมาณนี้: ยานชิดขอบ + HUD ครบ
ส่งงาน (commit/PR)
shooter_step2.py (เวอร์ชันจูนค่าฟิสิกส์ของกลุ่ม)feat(shooter): step1-2 ship + accel/friction steeringค่าคงที่ทั้ง 3 ตัว (
ACCEL 1.4 / MAX_SPEED 13 / FRICTION 0.80) เป็นของจริงจากเฟิร์มแวร์ ลองจูนได้ แต่จด "ความรู้สึก" ที่เปลี่ยนไปด้วย
ยาน "เร่ง-ลื่น" ที่เราเพิ่งเขียน ไม่ใช่ของใหม่ เกม Asteroids (Atari, 1979) ในตู้ข้าง ๆ นี้ บังคับยานด้วยแรงเฉื่อยแบบเดียวกัน คือบวกความเร็วเข้าตำแหน่งทุกเฟรม เกม shooter ทั้งสายสืบทอดแนวคิดนี้มา
ฝั่ง Algorithm / ฟิสิกส์
x += v และ v += a คือการประมาณการเคลื่อนที่ทีละเฟรม รากฐานเดียวกับฟิสิกส์เอนจินจริงshooter_step2.py เคลื่อนที่บนแกน x แกนเดียว (ship_x += ship_speed ที่ shooter_step2.py:32) จึงเป็น "จลนศาสตร์ 1 มิติ" ต่างจากลูก Pong ที่วิ่ง 2 แกน (2D); สมการเดียวกันเป๊ะ แค่ที่นี่ใช้แกนเดียว×FRICTION ทุกเฟรม = ความเร็วลดแบบ exponential คือสมการการสลายตัวที่เจอในฟิสิกส์/DSPฝั่ง Python
ship_x เป็นทศนิยมเพื่อสะสมเศษ คือเรื่อง data type ที่ส่งผลจริงกับการเคลื่อนที่global + สถานะข้ามเฟรม — ship_speed ต้องอยู่รอดข้ามการเรียก on_frame() แต่ละครั้ง คือ scope ของตัวแปรฝั่ง Embedded + Graphics
game.run() วน on_frame() 30 Hz เหมือน main loop ของเฟิร์มแวร์ที่อ่าน sensor แล้วสั่งงานเป็นรอบ ๆx ให้อยู่ใน 0 .. WIDTH−ship.w คือการทำงานกับระบบพิกัดของจอโดยตรงACCELERATION and FRICTION in Under 5 Minutes — John Ivess (คนละเอนจิน แต่แนวคิด accel + friction ตรงกับ 4 บรรทัดของเราเป๊ะ)
ที่มา: "Asteroids cabinet" — Joho345, CC BY 4.0, Wikimedia Commons
practise_codes/shooter_step2.py↗ แล้วเติมฟิสิกส์ 4 ขั้น (เร่ง / ลื่น / จำกัด / clamp) ด้วยตัวเอง ห้ามลอกเฉลยทั้งก้อน — ลองเขียนก่อน ติดตรงไหนยกมือถามได้เลย เดี๋ยวเฉลยในห้อง
shooter_step1.py — อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เองเฉลยชุดนี้มีไว้ให้เทียบ ไม่ได้มีไว้ลอกวางส่ง คะแนนของคาบอยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อไปเราจะแกะทีละส่วน เพราะทั้ง step 1 มีแค่ไม่กี่บรรทัด แต่ทุกบรรทัดวางไว้มีเหตุผล
ส่วนแรก — เปิดฉากด้วยโครงมาตรฐานเดิม:
import bentogame as game
game.title("SHOOTER") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
import bentogame as game (:11) — ตั้ง alias สั้น ๆ ว่า game แล้วเรียก 70% core ทั้งหมดผ่านคำเดียวนี้ (game.Box, game.Text, game.run) ตรงกับสไลด์ 70/30 — ของเดิมที่คุ้นมือ ที่ย้ำว่าไม่มี API ใหม่เลย ทุกอย่างเรียกผ่าน game.game.title("SHOOTER") (:13) — เอนจินสร้างหน้า Start ให้ในตัว (Start = เล่น, Back = ออก) เราจึงไม่ต้องเขียน state machine ของหน้าเริ่มเอง นี่คือหนึ่งใน 70% ที่ให้มาแล้ว เหมือนที่ Catch กับ Pong ขึ้นด้วย title เป็นบรรทัดแรกเช่นกันimport ... as game แล้วพิมพ์ bentogame. เต็มทุกบรรทัด โค้ดยาวขึ้นโดยไม่ได้อะไร; ข้าม game.title เกมเด้งเข้าฉากเล่นทันทีโดยไม่มีหน้าเริ่มให้กดอาจารย์อยากให้สังเกตว่าสองบรรทัดนี้ยังไม่มีอะไรของ Shooter เลย มันคือโครงเดิมของทุกเกม เราแค่หยิบมาวางซ้ำ
# ----- เติมส่วนนี้เอง: วางยานของผู้เล่น + ป้าย HUD -----
# ยานกว้าง 62 สูง 24 วางกลางแนวนอน (365) ใกล้ขอบล่าง (y=352)
# สี = game.GREEN (เท่ากับ BP_GREEN ฝั่ง C — ยานลำเดียวกันสีเดียวกัน)
ship = game.Box(365, 352, 62, 24, game.GREEN)
ship_x = 365.0 # ตำแหน่ง x ของยาน (เก็บเป็น float)
score, lives = 0, 3
hud = game.Text("Score: 0 Lives: 3", 10, 8, game.WHITE)
ship = game.Box(365, 352, 62, 24, game.GREEN) (:18) — สร้าง widget ยานแล้ว เก็บลงตัวแปร ship เพราะคาบหน้าเราต้องเรียก ship.move_to(...) ซ้ำทุกเฟรม ถ้าไม่เก็บ object ไว้ก็อ้างถึงมันทีหลังไม่ได้ · 365 มาจากการจัดกลางแนวนอน (จอกว้างราว 792, ยานกว้าง 62 → (792-62)/2 ≈ 365) · y=352 วางใกล้ขอบล่างที่ยานจะยิงพุ่งขึ้น · game.GREEN คือสีเดียวกับ BP_GREEN ฝั่ง C ยานลำเดียวกันบนจอ (ตามคอมเมนต์ :17)ship_x = 365.0 (:19) — เก็บตำแหน่ง x เป็น ตัวแปร float แยกต่างหาก จากตัว widget ทั้งที่ยานยังไม่ขยับในคาบนี้ เพราะ step 2 จะบวกความเร็วที่เป็นเศษ (เช่น 1.4) สะสมลงตรงนี้ ถ้าเก็บเป็น int เศษถูกปัดทิ้งทุกเฟรมจนยานกระตุก ตรงกับสไลด์ ทำไม ship_x ต้องเป็น float? เป๊ะ เราวางนิสัยนี้ไว้ก่อนที่จะได้ใช้จริงscore, lives = 0, 3 (:20) — state ของเกมสองตัว ประกาศพร้อมกันบรรทัดเดียว (multiple assignment) ยังไม่ถูกใช้ตอนนี้ แต่ HUD อ่านค่ามาแสดง คาบถัดไปจะ score += ... เมื่อยิงโดน และ lives -= 1 เมื่อโดนชนhud = game.Text("Score: 0 Lives: 3", 10, 8, game.WHITE) (:21) — ป้าย HUD มุมซ้ายบน (x=10, y=8) เก็บลงตัวแปร hud เพราะจะอัปเดตข้อความทีหลัง คือ HUD Score / Lives ที่เห็นในเดโมต้นคาบ · ถ้า hard-code เลขในสตริงแทนการผูกกับ score/lives พอค่าจริงเปลี่ยน ป้ายจะโกหกจับหลักเดิมจาก session ก่อน ๆ ไว้: อะไรที่ต้องอ้างถึงซ้ำ (
ship,hud) เก็บลงตัวแปร ส่วนship_xเป็น float ตั้งแต่วันนี้ เพราะพรุ่งนี้มันจะต้องสะสมเศษความเร็ว
# ภาพนิ่ง — คาบหน้าทำให้ยานขยับ. on_frame() ที่นี่ยังไม่ขยับอะไร.
# ปุ่มออก/เริ่มใหม่ เอนจิน game.run() จัดการให้: Back = ออก · Start = เริ่มใหม่.
def on_frame():
pass # ยังไม่มี logic — ภาพนิ่งก่อน
game.run(on_frame, fps=30) # Back = ออก / Start = เริ่มใหม่ (เอนจินจัดการเอง)
def on_frame(): แล้ว pass (:26-27) — ประกาศฟังก์ชัน "หนึ่งเฟรมทำอะไร" ไว้ แต่ตอนนี้ยัง pass (ยังไม่ทำอะไร) ภาพจึงนิ่งซึ่งถูกต้องแล้ว นี่คือ smallest runnable step: เริ่มจากของเล็กที่สุดที่รันได้ก่อน แล้วค่อยเติมทีละแนวคิด ตามหลัก 70/30 ที่เราคุยกันต้นคาบon_frame ทั้งที่ว่างเปล่า? เพราะ game.run() ต้องการฟังก์ชันไปเรียกทุกเฟรม ถ้าไม่ส่งอะไรให้ เอนจินไม่รู้จะทำอะไรต่อ นี่คือ "สัญญา" ระหว่างโค้ดเรากับเอนจินgame.run(on_frame, fps=30) (:29) — มอบ on_frame ให้เอนจินเรียกซ้ำ 30 ครั้งต่อวินาที ตรงกับสไลด์ ภายใน on_frame() หนึ่งครั้ง ที่เส้นประวิ่งกลับคือ game loop 30 Hz หรือ super-loop ของเฟิร์มแวร์นั่นเอง:24-25)pass ในฟังก์ชันว่าง Python ฟ้อง IndentationError ทันที; หรือเรียก game.run() ก่อนสร้าง ship ยานจะไม่โผล่บนจอลูปเปล่า ๆ บรรทัดนี้ดูเหมือนไม่มีค่า แต่มันคือ "บ้าน" ที่คาบหน้าเราจะย้ายฟิสิกส์ กระสุน และศัตรูเข้าไปอยู่ เราสร้างบ้านก่อน แล้วค่อยเชิญคนเข้า
shooter_step1.py มีโค้ดจริงแค่ห้าบรรทัด แต่เรียงเป็นโครงที่ทุกเกมใช้ซ้ำ แต่ละชิ้นทำงานวันนี้พอประมาณ พร้อม เปิดที่ว่างไว้ให้ step ถัดไปมาเติม โดยไม่ต้องรื้อของเดิม:
| ชิ้นส่วน | บรรทัด | ทำหน้าที่วันนี้ | เปิดทางให้ step ถัดไป |
|---|---|---|---|
| หน้าเริ่ม | :13 |
game.title สร้าง Start/Back ให้ในตัว |
โครงเดิมทุกเกม ไม่ต้องแตะอีก |
| ตัวละคร | :18 |
game.Box วางยานเขียวกลาง-ล่าง |
ship.move_to() ใน step 2 |
| ตำแหน่ง float | :19 |
ship_x = 365.0 เก็บเศษได้ |
ship_x += ship_speed (เร่ง/ลื่น) |
| state + HUD | :20-21 |
score/lives + game.Text |
score += เมื่อยิงโดน · lives -= เมื่อชน |
| ลูปเปล่า | :26-29 |
on_frame = pass + run 30 Hz |
เติมฟิสิกส์/กระสุน/ศัตรูใน on_frame |
ship_x เป็น float ตั้งแต่บรรทัดที่ยานยังไม่ขยับ ก็เพราะเรามองเห็นล่วงหน้าแล้วว่าคอลัมน์ขวาสุดต้องการมันถ้าจับจังหวะการต่อชิ้นนี้ได้ น้องจะเลิกกลัวเกมใหญ่ ๆ เพราะทุกเกมก็คือ step 1 ที่โตขึ้นทีละคาบเท่านั้นเอง
ที่มา ย้อนไปสไลด์ 70/30 — ของเดิมที่คุ้นมือ ต้นคาบ ที่บอกว่า Shooter ไม่มี API ใหม่เลย ยานลำนี้ใช้ game.Box กับ game.Text ชุดเดียวกับที่เราวางไม้ตีใน Pong และตะกร้าใน Catch การ "วางตัวละคร + HUD ให้เห็นภาพนิ่งถูกก่อน แล้วค่อยขยับ" ไม่ใช่ท่าใหม่ มันคือ pattern เดียวที่เราทำมาทุกเกม ปัญหาที่มันแก้แต่แรกคือการ แยกของที่อยู่นิ่งออกจากตรรกะที่ขยับ — พิสูจน์ว่าฉากถูกก่อน ค่อยเติมการเคลื่อนไหว จะได้ไม่ต้องเดาสองเรื่องพร้อมกันเวลาบั๊ก
ที่ไป ship_x = 365.0 ที่เราวางวันนี้ทั้งที่ยานยังไม่ขยับ คือเมล็ดของสไลด์ ทำไม ship_x ต้องเป็น float? — คาบหน้า (step 2) เราจะบวก ship_speed ลงตรงนี้ทุกเฟรม ยานจึงเร่งและลื่นได้ ส่วน on_frame() ที่ยัง pass วันนี้ จะกลายเป็นบ้านของฟิสิกส์ 4 บรรทัด แล้วคาบ 11 จะย้ายกระสุนกับศัตรูเข้ามาในลูปเดียวกันนี้ ยานนิ่งลำนี้คือ เฟรมที่ศูนย์ ของเกมยิงเต็มรูปแบบ
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
game.Box, game.Text, game.run ใน step 1 นี้ ว่าอันไหนเพิ่งเจอครั้งแรกวันนี้ (ใบ้: ไม่มีเลยสักตัว)ship_x เป็น float ได้ทั้งที่ยานยังนิ่ง พรุ่งนี้พอบวก 1.4 ลงไปทุกเฟรม เราจะได้การเคลื่อนที่ที่ลื่นแทนกระตุกเลยไหม (ใบ้: เปิดสไลด์ float vs int ดูช่องว่างที่ int ตามหลัง)on_frame() ที่ pass เฉย ๆ วันนี้ คือช่องเดียวกับที่ Pong เอาไว้ใส่ฟิสิกส์ไม้ตี มันคือเรื่องเดียวกับ game loop 30 Hz ที่เส้นประวิ่งกลับในสไลด์ ภายใน on_frame()ถ้าตอบสามคำถามได้ว่า "อ๋อ มันคือเรื่องเดียวกันกับที่ทำมาแล้ว" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด ยานนิ่งลำเดียวที่ดูเล็กวันนี้ คือ frame 0 ของทั้งเกม เราแค่ยังไม่ได้กด play
ท่าของ step 1 (วางของนิ่งให้ครบ → เก็บ state แยกจาก UI → ปล่อยลูปคงที่หมุน) ไม่ใช่แค่แบบฝึกหัด มันคือวิธีทำงานของระบบจริงหลายอย่าง:
Score: 0 Lives: 3 ที่เราวาง (:21) คือหลักเดียวกับหน้าปัดที่โชว์ความเร็ว/ชีพจร: ค่าจริงเก็บใน state (score, lives ที่ :20) แล้ว label อ่านมาแสดง วาง layout ให้นิ่งครบก่อน ค่อย feed ค่า realtime ทีหลังon_frame() ที่ pass ไว้ก่อน (:26-27) เป็นการ de-risk ให้เห็นว่าจอทำงานก่อนไปเดาบั๊กของ logicgame.run(on_frame, fps=30) (:29) คือ fixed-rate update loop แบบเดียวกับ FixedUpdate ของ Unity หรือ tick ของเซิร์ฟเวอร์เกม และการเก็บพิกัดเป็น float เพื่อสะสมเศษ (:19) เป็นมาตรฐานของฟิสิกส์เอนจินทุกตัวscore, lives (:20) เป็นข้อมูล ส่วน hud เป็นการแสดงผล แอปจริงทำแบบนี้เพื่อให้ "แก้ที่ตัวแปรแล้ว UI ตามเอง" ไม่ต้องไล่แก้ทั้งสองที่ทั้งสี่ตัวอย่างไม่มีอันไหนสมมติ ยานนิ่งกับป้าย HUD ที่ดูเหมือนของเล่นวันนี้ คือโครงเดียวกับหน้าจอในเครื่องจริงที่คนใช้กันทุกวัน
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหางานจริงได้:
ship2 กับ ship2_x แยกอีกชุด หรือเก็บยานทั้งหมดเป็น list แล้ววนจัดการทีเดียวดีกว่าscore, lives, และเวลาที่เหลือ จะวาง game.Text หลายป้ายยังไงบนจอเล็กให้อ่านง่าย ค่าไหนควรอยู่มุมไหน และควรผูกแต่ละป้ายกับ state ตัวไหน365 ตายตัว จะเก็บค่าเริ่มไว้ตรงไหน และต้องระวังอะไรไม่ให้ยานโผล่นอกจอ (นึกถึง clamp ขอบจอที่เจอใน step 2)ship_x ทุกเฟรมจากใน on_frame() ที่ตอนนี้ยัง pass อยู่ ฟังก์ชันต้องประกาศอะไรเพิ่มถึงจะแก้ตัวแปรข้างนอกได้ (ใบ้: คำเดียวที่โผล่ในสไลด์ กับดักที่เจอบ่อย)เลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากยานที่พิมพ์เองวันนี้ ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css