สิ่งที่ต้องจำคือ จุด อยู่มุมซ้ายบน และ ที่มากขึ้นหมายถึงลงล่าง ไม่ใช่ขึ้นบนอย่างที่เคยชินในวิชาคณิต ส่วนการขยับก็คือการบวกเวกเตอร์เข้าไป:
คลิปสั้นเสริม (คนละเอนจิน แต่อธิบายแกนเดียวกับเราเป๊ะ: จุดเริ่ม มุมซ้ายบน และ ชี้ลงล่าง):
Game Development Tutorials For Beginners - 2. Coordinate Systems — Adebayo
เปิดไฟล์ practise_codes/s03_box_joystick.py↗ ใน BENTO IDE สามบรรทัดบนสุดมีให้แล้ว:
import bentogame as game # ดึง Game Engine 70%
game.title("MOVE BOX") # หน้าเริ่ม (Start=เล่น Back=ออก) + เคลียร์จอ + ปลุกจอย
hero = game.Box(380, 180, 40, 40, game.CYAN) # กล่องฟ้า กลางจอ
game.Box(x, y, w, h, color) คือกล่องสี่เหลี่ยม ตำแหน่งซ้ายบนอยู่ที่ กว้าง สูง นี่คือของชิ้นแรกที่เราสั่งให้บอร์ดวาดออกมา

hero.move(60, 0) # ขยับขวา 60 px
hero.move(0, -40) # ขยับขึ้น 40 px
hero.move_to(380, 180) # ดีดกลับกลางจอ (พิกัดตรง ๆ)
game.CYAN ที่เราส่งให้กล่องที่ s03_box_joystick.py:16 ไม่ใช่แค่ "ชื่อสี" แต่คือเลขฐานสิบหก 24 บิตที่บอกความสว่างของ 3 ช่องสี R, G, B ช่องละ 8 บิต (ค่า 0–255):
bentogame.py:27-34): RED=0xFF5555 · GREEN=0x50FA7B · PINK=0xFF79C6 · WHITE=0xFFFFFFลองใส่เลข hex ของเราเองแทน
game.CYANเช่นgame.Box(380,180,40,40, 0xFF8800)(ส้ม) แล้ว Program to Device — นี่คือการผสมสีด้วยเลขสามช่องตรง ๆ
move() หนีบค่าให้อยู่ในจอเสมอ ด้วยสูตร clamp:
โดย คือขนาดจอ และ คือขนาดกล่อง
ข้อควรระวัง: move_to() ไม่ได้ clamp ให้ ถ้าใส่พิกัดติดลบหรือเกินขอบจอ กล่องจะหายไปเลย ฉะนั้นเวลาอยากให้กันหลุดขอบ ให้ใช้ move(dx, dy) แทน
ภาพนิ่งบอกผลลัพธ์ได้ แต่ลองดูตอนมันขยับจริง: กล่องวิ่งไปทางขวาเรื่อย ๆ พอถึงขอบ move() ก็ตรึงไว้ที่ พอดี ไม่ทะลุออกไป — เพราะ min() ในสูตรคุมไว้ ไม่ว่าเราสั่ง dx มากแค่ไหน

นี่คือเหตุผลที่เราใช้ move() ไม่ใช่ move_to() ในงานวันนี้ — เราไม่ต้องเขียนเช็คขอบเองสักบรรทัด แค่บอกทิศ แล้ว 70% จัดการเรื่องขอบให้
ก่อนจะให้กล่องขยับเอง เราควรเข้าใจก่อนว่าจอยส่งค่าอะไรกลับมาบ้าง — game.keys() คืน "สแน็ปช็อต" ของจอยในเฟรมนี้:
pressed = game.keys() # สแน็ปช็อตของจอย "เฟรมนี้"
print(pressed.left, pressed.right, pressed.up, pressed.down, pressed.start)
จอยของเราเป็น USB HID gamepad — ก้าน/ปุ่มส่งค่ามาทาง USB แต่ Game Engine แปลงให้เราเรียบร้อย เหลือแค่ True/False ต่อทิศ ไม่ต้องไปยุ่งกับค่าดิบเอง
ภาพบนจอมาจาก framebuffer (พื้นที่ memory) — เกมวาดลง buffer แล้วบอร์ดส่งให้จอผ่าน MIPI-DSI (สายความเร็วสูง; จอเล็กทั่วไปใช้ SPI ที่ช้ากว่า)

game.keys() อ่านมาให้game.keys() รับเลขพวกนั้นมาแปลงให้เหลือ True/False ต่อทิศ (ซ้าย/ขวา/ขึ้น/ลง) ให้เราใช้ง่าย ๆบอร์ดมี ADC ไว้อ่าน analog sensor (เช่น potentiometer ที่ส่งแรงดันต่อเนื่อง) — แต่จอย USB ของเราเป็น digital ส่งเลขมาเลย ไม่ผ่าน ADC
ที่มา: "Trust GM-1500 gamepad controller" — Multicherry, CC BY-SA 4.0, Wikimedia Commons
ลองไล่ดูทั้งเส้นทางในภาพเดียว จะเห็นว่าทำไมเราถึงเขียนโค้ดสั้นได้ขนาดนี้:

นิ้วกด D-pad → จอยส่งเลขดิบมาทาง USB → game.keys() แปลงเลขพวกนั้นให้เหลือ True/False ต่อทิศ ทั้งสามขั้นนี้คือ 70% ที่ Game Engine ทำให้ เราเข้ามาเขียนจริงแค่ขั้นสุดท้าย — "ถ้ากดทิศนี้ ให้กล่องขยับทางนี้" ซึ่งก็คือ 30% ของเรา
Atari 2600 มี RAM แค่ 128 ไบต์ และ ไม่มี framebuffer — CPU (MOS 6507) ต้องวาดภาพทีละเส้นให้ทันลำอิเล็กตรอนของจอ CRT ("racing the beam")

เชื่อมกับวันนี้: บอร์ดน้องมี RAM เยอะกว่ามหาศาล แต่กับดัก "memory บีบ" ยังจริงเสมอในงาน embedded
ที่มา: "Atari 2600" — Evan-Amos, Public domain, Wikimedia Commons
game.run(update) จะเรียกฟังก์ชัน update() ของเราทุกเฟรม นี่คือที่ที่เราเขียน 30% ของเราเอง โครงแบบง่ายสุด คือความเร็วคงที่:
def update():
pressed = game.keys() # 70%: อ่านจอยเฟรมนี้
if pressed.left: hero.move(-8, 0) # 30%: กติกาของเรา — ซ้ายให้ขยับซ้าย
if pressed.right: hero.move(8, 0)
if pressed.up: hero.move(0, -8)
if pressed.down: hero.move(0, 8)
# Back/Start จัดการให้โดย game.run() แล้ว — ไม่ต้องเช็คเอง
game.run(update) # 70%: วิ่งทุกเฟรม (Back=ออก · Start=เริ่มใหม่)

while True: ที่ไม่มีทางออก เพราะจอจะค้าง ให้ใช้ game.run(update) เท่านั้น มันจะจัดการปุ่ม Back (ออก) และ Start (เริ่มใหม่) ให้ แล้วจบลูปเองเมื่อ update() คืนค่า return False (คาบนี้ยังไม่ต้อง return เพราะกล่องขยับได้เรื่อย ๆ)
ความเร็วคงที่ 8 ก็ใช้ได้ แต่ไฟล์งานของเราต่อยอดให้ "กดค้างยิ่งนานยิ่งเร็ว" สามตัวแปรบนสุดมีให้แล้ว:
BASE_SPEED = 6 # ความเร็วเริ่มต้น (พิกเซลต่อเฟรม) ตอนเพิ่งแตะจอย
MAX_SPEED = 30 # ความเร็วสูงสุดเมื่อกดค้างนานพอ
hold_frames = 0 # กดค้างมากี่เฟรมแล้ว (ยิ่งมาก ยิ่งเร็ว)
แนวคิด: ถ้าเฟรมนี้มีการกดทิศใดทิศหนึ่ง ให้สะสม hold_frames ขึ้นทีละ 1 ถ้าปล่อยจอยให้รีเซ็ตกลับเป็น 0 แล้วคำนวณ speed จาก BASE_SPEED + hold_frames โดยคุมเพดานไม่ให้เกิน MAX_SPEED
ความเร็วก็คือระยะที่ขยับต่อหนึ่งเฟรม ยิ่ง speed มาก กล่องก็ยิ่งวิ่งไกลขึ้นเมื่อกดค้างเท่าเดิม กราฟข้างล่างคือสูตรเดียวกันนี้: เส้นค่อย ๆ ไต่ขึ้นตอนกดค้าง พอถึง 24 เฟรมก็ชน MAX_SPEED แล้วราบ — min() คือตัวที่ตรึงไม่ให้เกินเพดาน

ในไฟล์ practise_codes/s03_box_joystick.py↗ ส่วน update() เว้นช่องไว้ให้เราเติม มี STEP บอกใบ้:
def update():
global hold_frames
pressed = game.keys() # อ่านจอยเฟรมนี้
# STEP 1: เฟรมนี้มีการกดทิศใดไหม → ถ้ากด ให้ hold_frames += 1
# ถ้าไม่กดเลย รีเซ็ต hold_frames = 0 (กดค้างนาน = ยิ่งเร็ว)
# STEP 2: คำนวณ speed จาก BASE_SPEED + hold_frames คุมเพดานด้วย min(..., MAX_SPEED)
# STEP 3: เรียก hero.move(...) ทีละทิศ
# ซ้าย/ขวา = แกน x · ขึ้น/ลง = แกน y
# ขึ้นกับซ้ายใช้ค่าลบ · ลงกับขวาใช้ค่าบวก
pass # <- ลบ pass ออกเมื่อเริ่มเขียน
โครงของช่องที่ต้องเติมมีแค่ 3 ขั้น เรียงต่อกันตรง ๆ ตาม STEP ในโค้ด:
ลองคิดสูตรเองก่อน ติดตรงไหนยกมือถามได้เลย เดี๋ยวเฉลยในห้อง อาจารย์อยากให้น้อง ๆ ได้ลองพลาดเองก่อน เพราะนั่นคือตอนที่เราจำได้แม่นที่สุด
หลังเติมครบ ทั้งฟังก์ชันหน้าตาแบบนี้ (ตรงกับ solution_codes/s03_box_joystick.py:23-44):
def update():
global hold_frames
pressed = game.keys() # :25 อ่านจอยเฟรมนี้
moving = pressed.left or pressed.right or pressed.up or pressed.down
if moving:
hold_frames += 1 # :29 กดค้าง → สะสมขึ้นเรื่อย ๆ
else:
hold_frames = 0 # :31 ปล่อยจอย → รีเซ็ต
speed = min(BASE_SPEED + hold_frames, MAX_SPEED) # :34 กดค้างยิ่งนานยิ่งเร็ว (เพดาน MAX)
if pressed.left: hero.move(-speed, 0) # :36 ซ้าย
if pressed.right: hero.move(speed, 0) # :37 ขวา
if pressed.up: hero.move(0, -speed) # :38 ขึ้น
if pressed.down: hero.move(0, speed) # :39 ลง
game.run(update) # :44 วิ่งทุกเฟรมจนกว่าจะ return False
moving (:27) รวม 4 ทิศเป็นเงื่อนเดียว — กดทิศไหนก็นับว่า "กำลังขยับ":34) คือสูตร min(BASE+hold, MAX) ในสไลด์ก่อน มาลงเป็นโค้ดบรรทัดเดียวพอดีreturn False — กล่องขยับได้เรื่อย ๆ ออกด้วยปุ่ม Back (เอนจินจัดการให้)game.run(update) วนเรียก update() ทุกเฟรม ข้อมูลไหลจาก จอย → speed → ตำแหน่ง → ภาพ โดยมีจุดตัดสินใจที่ "เฟรมนี้กดทิศไหม":
จุดตัดสินใจมีจุดเดียว: "เฟรมนี้กดทิศไหม" ถ้ากด
hold_framesสะสมขึ้น (เร่ง) ถ้าปล่อยก็รีเซ็ต ที่เหลือคือบวก speed เข้าตำแหน่งแล้ววาด — แล้ววนใหม่
เปิด s03_box_joystick.py ใน BENTO IDE แล้วกด Program to Device

วางโค้ดใน BENTO IDE → กด Program to Device → โค้ดรันบนบอร์ดทันที ไม่ต้องพิมพ์ทีละบรรทัด ไม่ต้อง build/flash เฟิร์มแวร์
อยากให้ทั้งสามคนในกลุ่มผลัดกันลองคุมกล่อง พอทุกคนเล่นแล้ว ค่อยเซฟไฟล์และ commit ขึ้น repo ของทีม น้อง ๆ ทำได้แน่นอน
| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| จอค้าง / กดอะไรก็ไม่ตอบ | เขียน while True: ไม่มีทางออก |
ใช้ game.run(); ถ้าค้างแล้วให้รีเซ็ตบอร์ด |
| กดจอยกล่องไม่ขยับ | ลืม game.start() / ลืมลบ pass |
เรียก start() ก่อน + ลบ pass ใน update() |
NameError: game |
ลืม import bentogame as game |
พิมพ์ import บรรทัดแรก |
move() ทำกล่องหาย |
ใช้ move_to() พิกัดเกินจอ |
move_to ไม่ clamp — ใช้ move(dx, dy) |
| กดค้างแล้วกล่องไม่เร่ง | ลืม global hold_frames / ลืม += 1 |
ประกาศ global hold_frames + สะสมทุกเฟรม |
| กด Program to Device แล้วไม่รัน | ยังไม่ได้เปิดไฟล์ที่ถูกต้องใน BENTO IDE | เปิด s03_box_joystick.py แล้วกด Program to Device อีกครั้ง |
ลองแก้แค่ตัวเลขหรือสี แล้วรันใหม่ดูทันที พอน้อง ๆ แก้แล้วผลเปลี่ยนตามใจได้ ก็แปลว่าน้อง ๆ เป็นเจ้าของโค้ดนี้จริง ๆ แล้ว:
if pressed.a: hero.move_to(380, 180) # กด A = ดีดกล่องกลับกลางจอ
ลองทำกล่องสองใบ คนละสี ขยับพร้อมกันด้วยจอยเดียวก็สนุกดี ใครคิดลูกเล่นใหม่ ๆ ได้ เอามาโชว์เพื่อนในกลุ่มก่อนได้เลย
กล่องฟ้าที่ขยับตามจอยวันนี้ ดูเหมือนง่าย แต่ข้างใต้คือรากฐานที่เราจะใช้ซ้ำทั้งคอร์ส:

ฝั่ง Embedded / ฮาร์ดแวร์
ฝั่ง Python
update() แล้วยื่นให้ game.run() เรียกแทนเราทุกเฟรม (ส่งฟังก์ชันเป็นพารามิเตอร์)hold_frames ต้องจำค่าข้ามเฟรม จึงต้องประกาศ globalฝั่ง Algorithms / คณิต
ฝั่ง Graphics
วันนี้เราจับครบทั้งสี่ฝั่งด้วยโปรแกรมไม่กี่บรรทัด นี่แหละคือสิ่งที่เราจะต่อยอดไปสร้างเกมและผลิตภัณฑ์จริงต่อไป
ทำเอง 30% (ส่งงาน): เติม update() ใน practise_codes/s03_box_joystick.py↗ ให้กล่องขยับครบ 4 ทิศ + กดค้างยิ่งเร็ว แล้วรันบนบอร์ดจริง → เซฟไฟล์ + commit ขึ้น repo ทีม + แนบรูป/คลิปกล่องขยับ
วันนี้เราทำครบทั้งสามเสาหลักของทุกเกมได้ด้วยกล่องเพียงใบเดียว ทำได้ดีมาก คาบหน้า (คาบ 4) เราจะเติม game.hit() สำหรับการชน และ game.Text สำหรับคะแนน เพื่อให้กล่องเก็บของแล้วได้แต้ม กลายเป็นเกมที่เล่นจบเป็นรอบได้จริง
s03_box_joystick.py — อ่านให้เข้าใจ แล้วปิดไฟล์พิมพ์เองเฉลยนี้ไม่ได้มีไว้ให้ลอกวางส่ง คะแนนของคาบอยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง ไม่ใช่ที่โค้ดตรงกับเฉลย วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อไปเราจะแกะทีละส่วน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ส่วนแรก — ตั้งเวที: ปลุกเอนจิน วางหน้าเริ่ม แล้ววางกล่อง
import bentogame as game
game.title("MOVE BOX") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
# สร้างสี่เหลี่ยมตัวแรก: x, y, กว้าง, สูง, สี
hero = game.Box(380, 180, 40, 40, game.CYAN)
game.title("MOVE BOX") (:13) ทำสามอย่างในบรรทัดเดียว — หน้าเริ่ม (Start/Back) + เคลียร์จอ + ปลุกจอย นี่คือ 70% ที่เอนจินเตรียมเวทีให้ ตรงกับเส้นแบ่งในสไลด์ "70/30 ครั้งแรกของคอร์ส" เราไม่ต้องเขียนเองhero = game.Box(380, 180, 40, 40, game.CYAN) (:16) วางกล่องที่ ค่านี้ใกล้กลางจอ แต่ไม่ใช่กลางเป๊ะ เพราะ คือ มุมซ้ายบน ของกล่อง ไม่ใช่จุดกึ่งกลาง (ระบบพิกัดจากสไลด์ "ระบบพิกัดหน้าจอ": origin อยู่มุมซ้ายบน)hero? เพราะทุกเฟรมใน update() จะสั่ง hero.move(...) ซ้ำ ถ้าไม่ตั้งชื่อไว้ก็อ้างถึงกล่องเดิมทีหลังไม่ได้ กล่องจึงถูกสร้างที่ระดับ module เพื่อให้ทั้งไฟล์มองเห็นgame.CYAN (:16) ไม่ใช่แค่ "ชื่อสี" แต่คือเลข hex 24 บิต 0x00E0FF (สไลด์ "RGB สามช่อง") อยากเปลี่ยนสีก็ใส่เลขเองแทนได้เลยเฉลยมีไว้เทียบ ไม่ใช่ไว้ลอก อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนั่นแหละสมองจำ pattern ได้จริง
BASE_SPEED = 6 # ความเร็วเริ่มต้น (พิกเซลต่อเฟรม) ตอนเพิ่งแตะจอย
MAX_SPEED = 30 # ความเร็วสูงสุดเมื่อกดค้างนานพอ
hold_frames = 0 # กดค้างมากี่เฟรมแล้ว (ยิ่งมาก ยิ่งเร็ว)
def update():
global hold_frames
pressed = game.keys() # อ่านจอยเฟรมนี้
moving = pressed.left or pressed.right or pressed.up or pressed.down
if moving:
hold_frames += 1 # กดค้าง → สะสมขึ้นเรื่อย ๆ
else:
hold_frames = 0 # ปล่อยจอย → รีเซ็ตกลับช้า
BASE_SPEED / MAX_SPEED (:18-19) ตั้งชื่อไว้บนสุด ไม่ฝังเลข 6 กับ 30 ลงกลางโค้ด อยากปรับความรู้สึกการคุมกล่อง แก้ที่เดียวจบ นี่คือนิสัยแยก "ค่า" ออกจาก "โครง"global hold_frames (:24) ต้องมี เพราะบรรทัด :29 กับ :31 กำหนดค่าใหม่ ให้ตัวแปรนี้ ถ้าไม่ประกาศ global Python จะสร้างตัวแปร local ใหม่ทุกเฟรม ค่าสะสมหายเกลี้ยง กล่องจะเร่งไม่ขึ้น นี่คือ "สถานะข้ามเฟรม" จากสไลด์ "เชื่อมโยงรากฐาน"game.keys() (:25) คืนสแน็ปช็อตของจอยเฉพาะเฟรมนี้ (สไลด์ "ขั้น 2 — อ่านจอยให้เห็นค่าจริง") เรารวมสี่ทิศเป็น moving เงื่อนเดียว (:27) กดทิศไหนก็นับว่า "กำลังขยับ"+= 1, ปล่อย → รีเซ็ต = 0 (:28-31) นี่คือ accumulator: ความจำที่โตขึ้นเรื่อย ๆ ตราบใดที่ยังกดค้าง ยิ่งกดนาน hold_frames ยิ่งมาก แล้วเดี๋ยวเอาไปคิดความเร็วในส่วนถัดไปความจำข้ามเฟรมดูเป็นรายละเอียดเล็ก แต่มันคือหัวใจ เกมทั้งเกมคือ "จำสถานะไว้ แล้วแก้มันทีละเฟรม" ลืม
globalเมื่อไร กล่องจะไม่เร่งให้เห็นเลย
# ยิ่งกดค้างนาน speed ยิ่งมากขึ้น แต่ไม่เกิน MAX_SPEED
speed = min(BASE_SPEED + hold_frames, MAX_SPEED)
if pressed.left: hero.move(-speed, 0) # ซ้าย
if pressed.right: hero.move(speed, 0) # ขวา
if pressed.up: hero.move(0, -speed) # ขึ้น
if pressed.down: hero.move(0, speed) # ลง
# Back = ออกกลับ Playground · Start = เริ่มเกมใหม่ — game.run() จัดการให้ก่อนถึง update()
# game.Box.move() กัน "หลุดจอ" ให้อัตโนมัติ (clamp ขอบ)
game.run(update) # วิ่งทุกเฟรมจนกว่าจะ return False
speed = min(BASE_SPEED + hold_frames, MAX_SPEED) (:34) — min() คือตัวคุมเพดาน สะสมไปเรื่อยแต่ไม่มีวันเกิน MAX_SPEED นี่คือ clamp ฝั่งบน หลักเดียวกับสไลด์ "clamp" ที่ใช้ min/max คุมค่าให้อยู่ในช่วง ต่างแค่คราวนี้คุม ความเร็ว ไม่ใช่ตำแหน่งif แยกทีละแกน (:36-39) ซ้าย/ขวาแตะแกน x, ขึ้น/ลงแตะแกน y; ขึ้นกับซ้ายเป็นค่าลบ ลงกับขวาเป็นบวก เพราะ ชี้ลง (สไลด์ "ระบบพิกัดหน้าจอ") การขยับคือบวกเวกเตอร์ if สี่ตัวแยกกัน ไม่ใช่ elif — กดทแยง (เช่น ขวา+ลง) พร้อมกันได้ ทั้งสองแกนขยับในเฟรมเดียวreturn False เลย (:44) กล่องจึงขยับได้เรื่อย ๆ ออกด้วยปุ่ม Back ที่ game.run() จัดการให้ (สไลด์ "render-loop") และ move() ก็ clamp ขอบจอให้ฟรี เราไม่ต้องเขียนเช็คขอบเองสักบรรทัดจับหลักนี้ไว้: เราไม่เคยเขียนวงลูปเอง ไม่เช็คขอบจอเอง ไม่จัดการปุ่ม Back เอง เขียนแค่ "กดทิศไหน ขยับทางนั้น เร็วเท่าไร" นั่นคือ 30% ของเราจริง ๆ
ทั้งไฟล์มีไม่ถึงยี่สิบบรรทัด แต่สี่ชิ้นถูกวางให้ ต่อยอดกันทีละขั้น โดยยังใช้โครงเดิมที่เพิ่งวางไป นี่คือวิธีเดียวกับที่เราจะค่อย ๆ ต่อเกมทั้งเครื่องขึ้นมา:
| ชิ้น | บรรทัด | ทำอะไร | ต่อยอดจากชิ้นก่อน |
|---|---|---|---|
| วางกล่อง | :16 |
ของบนจอหนึ่งชิ้น ที่พิกัดคงที่ | เริ่มจากศูนย์ — origin มุมซ้ายบน |
| อ่านจอย | :25,27 |
keys() → รวมสี่ทิศเป็น moving |
ใช้กล่องเดิม เพิ่ม "รับ input" |
| สะสม + คิดเร็ว | :29-34 |
hold_frames โต → speed = min(...) |
ใช้ moving เดิม เพิ่ม "ความจำ + เพดาน" |
| ขยับทีละแกน | :36-39 |
move(±speed, …) บวกเวกเตอร์ + clamp |
ใช้ speed เดิม เพิ่ม "ลงมือขยับจริง" |
game.keys() เข้ามาอ่านจอยspeed ที่คิดไว้ กลายเป็นระยะจริงบนแกน x/y ผ่านการบวกเวกเตอร์ที่ move() clamp ขอบให้ฟรีอย่ากระโดดเขียนทั้งเกมรวดเดียว เริ่มจากกล่องนิ่ง ๆ ให้ขึ้นจอก่อน แล้วค่อยเติมทีละชิ้นเหมือนสี่บรรทัดนี้ ทุกเกมในคอร์สโตขึ้นด้วยจังหวะเดียวกัน
อยากให้หยุดคิดสักครู่ ไม่ใช่เรื่องโค้ด แต่เรื่อง "มองให้ทะลุ" ว่ากล่องที่เพิ่งขยับได้ จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — วันนี้ต่อยอดมาจากไหน ย้อนกลับไปสไลด์ "ระบบพิกัดหน้าจอ" ที่เราเขียนสมการ ค้างไว้เฉย ๆ วันนี้มันไม่ใช่สมการบนกระดานอีกต่อไป ทุกครั้งที่กล่องขยับหนึ่งก้าว คือการบวกเวกเตอร์นั้นจริง ๆ หนึ่งครั้ง และย้อนไปสไลด์ Atari 2600 ที่เกมทั้งเกมคือ "จำสถานะแล้ววาดใหม่ทุกเฟรม" — hold_frames ที่เราสะสมไว้ก็คือสถานะที่ถูกจำข้ามเฟรมแบบเดียวกัน ต่างแค่เราจำด้วยตัวแปร ไม่ใช่ต่อสายฮาร์ดแวร์
ที่ไป — ทักษะเล็กนี้จะกลายเป็นอะไร hold_frames คือความจำข้ามเฟรมชิ้นแรกของเรา คาบหน้า (คาบ 4) เราจะเก็บ "คะแนน" ไว้ข้ามเฟรมด้วยหลักเดียวกัน โชว์ผ่าน game.Text และเช็คการชนด้วย game.hit() ที่ต้องรู้ตำแหน่ง hero ตลอดเวลา (ซึ่งเรารู้อยู่แล้ว) ถัดจากนั้น ถ้าเปลี่ยน hold_frames ให้เก็บ "ความเร็ว vx, vy" แทน "จำนวนเฟรม" เราจะได้แรงเฉื่อยและแรงโน้มถ่วง กล่องใบนี้คือเมล็ดของ physics ทั้งหมด
ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
game.keys() ที่คืนสแน็ปช็อตต่อเฟรมได้ไหม (สไลด์ "ขั้น 2") ถ้าเฟรมนี้อ่านค่าได้ เฟรมหน้าอ่านใหม่ได้ นั่นแปลว่าเราเห็น "การเปลี่ยนแปลงตามเวลา" อยู่แล้วใช่ไหมhold_frames ข้ามเฟรมได้ พรุ่งนี้เราจะเก็บ "คะแนน" หรือ "ความเร็ว" ข้ามเฟรมแบบเดียวกันได้ไหม (ใบ้: global ตัวเดิมที่บรรทัด :24)hero.move(-speed, 0) กับสมการ ในสไลด์ระบบพิกัด มันคือเรื่องเดียวกันเป๊ะ ต่างแค่มี clamp ห่อไว้กันหลุดขอบถ้าตอบได้ว่า "อ๋อ
hold_framesวันนี้ กับคะแนนคาบหน้า มันคือความจำข้ามเฟรมอันเดียวกัน" นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด กล่องใบเล็กวันนี้ไม่ได้เล็กเลย
"อ่าน input ต่อเฟรม → สะสมสถานะ → ขยับแบบมีขอบ" คือโครงเดียวกับที่คุมเครื่องจริงจำนวนมาก:
min(max(x+dx, 0), W-w) ที่กันกล่องหลุดขอบจอ แค่ขอบจอกลายเป็นลิมิตของแกนจริงspeed = min(BASE_SPEED + hold_frames, MAX_SPEED) ตัวเดียวกัน: เริ่มช้าให้จูนละเอียด กดค้างแล้วเร่งให้ถึงไวhold_frames กับขอบจอเห็นไหมว่าไม่มีข้อไหนเป็นของสมมติเลย ทุกข้อมีเครื่องจริงในโลกที่ทำงานด้วยหลักการเดียวกับกล่องที่น้องเพิ่งเขียนวันนี้
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
hold_frames = 0 ทันทีที่ปล่อย ลองเก็บ "ความเร็วปัจจุบัน" ไว้แล้วค่อย ๆ ลดลงทีละเฟรม นี่คือประตูสู่แรงเฉื่อย (inertia)min/max หนีบไว้เฉย ๆ ถ้าอยากเด้ง ต้องรู้ให้ได้ว่า "ชนขอบแล้ว" แล้วกลับทิศความเร็ว จะเก็บทิศไว้ในตัวแปรแบบไหนhold_frames ตัวเดียวใช้ร่วมกันไม่ได้อีกต่อไปแล้วhero ตลอดเวลา (ข้อนี้เรารู้อยู่แล้ว) และต้องมี "ตัวนับคะแนน" ที่จำค่าข้ามเฟรม คาบ 4 เราจะใช้ game.hit() เช็คการชน และ game.Text โชว์คะแนน ลองคิดล่วงหน้าว่าตัวแปรคะแนนควรประกาศแบบไหน (global เหมือน hold_frames ไหม?)เลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องถูกทั้งหมด ขอแค่คิดต่อจากกล่องที่น้องพิมพ์เองวันนี้ ตรงนั้นแหละที่น้องเริ่มเป็นคนออกแบบเกม ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css