ลองดูว่า player_speed เปลี่ยนยังไงในแต่ละเฟรม ถ้าเรากดค้างแล้วปล่อย — จุดส้มคือค่าความเร็ว ณ เฟรมนั้น วิ่งไปตามเวลาจริง:
ช่วงกด ความเร็วโตเป็น เส้นตรง (บวก ACCEL คงที่ทุกเฟรม) จนชน เพดาน 12 แล้วถูก clamp ให้แบนราบ พอปล่อยปุ่ม การคูณ FRICTION = 0.78 ซ้ำ ๆ ทำให้เป็น เส้นโค้งลู่เข้าศูนย์ (geometric decay): หลังปล่อย 1 เฟรมเหลือ 78%, 2 เฟรมเหลือ ~61%, 3 เฟรมเหลือ ~47%
นี่คือเหตุผลที่ไม้ "ไถลแล้วค่อยหยุด" ไม่ใช่หยุดผึง การคูณด้วยค่าน้อยกว่า 1 ซ้ำ ๆ คือสูตรเดียวกับลูกบอลที่เด้งเตี้ยลงเรื่อย ๆ หรือเสียงที่ค่อย ๆ เบาลง
clamp มีสองชั้น: ชั้นแรกจำกัด ความเร็ว ไม่ให้ไม้เร็วเกินจนคุมไม่อยู่ ชั้นที่สองจำกัด ตำแหน่ง ไม่ให้ไม้หลุดขอบจอ ลืมชั้นไหนก็เห็นบั๊กคนละแบบ
player_y, player_speed = float(PADDLE_START_Y), 0.0 # ตำแหน่ง + ความเร็วไม้
def on_each_frame():
global player_y, player_speed, ball_x, ball_y, ball_vx, ball_vy
keys = game.keys()
if keys.up and not keys.down: player_speed -= ACCEL # กดขึ้น = เร่งขึ้น
elif keys.down and not keys.up: player_speed += ACCEL # กดลง = เร่งลง
else: player_speed *= FRICTION # ปล่อย = ไถลช้าลง
player_speed = max(-MAX_SPEED, min(MAX_SPEED, player_speed)) # clamp ความเร็ว
player_y = max(0, min(game.HEIGHT - PADDLE_H, player_y + player_speed)) # clamp ตำแหน่ง
player_paddle.move_to(20, player_y)
เหตุที่เราใช้
player_speedแทนการเขียนplayer_y += 8ตรง ๆ ก็เพราะการสะสมความเร็วทำให้ไม้ค่อย ๆ เร่งขึ้นและไถลหยุดเอง ได้ความรู้สึกแบบเกมจริง — โค้ดเต็มอยู่ที่practise_codes/pong_step2.py↗

แถบทางขวาคือ player_speed แบบเรียลไทม์: ตอนกดค้างมันโตขึ้นจนเต็มเพดาน (แดง = ชน clamp) พอปล่อยปุ่มแถบหดลงทีละนิด ไม้จึง ไถลต่ออีกหน่อย ก่อนนิ่ง
สังเกตว่าตอนปล่อย ไม้ไม่ได้หยุดทันทีที่เฟรมนั้น มันยังขยับต่อด้วยความเร็วที่เหลือ นี่แหละคือ "น้ำหนัก" ที่เราสร้างจากตัวเลขแค่สามตัว (ACCEL / MAX_SPEED / FRICTION)
on_each_frame() ~60 ครั้ง/วินาที
game.run(on_each_frame, fps=60)คือคนสั่งให้ฟังก์ชันของเราถูกเรียกซ้ำ ๆ ทุกเฟรม เราแค่บอกว่า "ในหนึ่งเฟรมต้องทำอะไร" ที่เหลือ core จัดการจังหวะให้เอง
AABB = Axis-Aligned Bounding Box คือ "กล่องสี่เหลี่ยมตรงแกน" สองกล่องชนกันก็ต่อเมื่อ ซ้อนทับกันทั้งแกน X และแกน Y พร้อมกัน
คาบนี้เราเข้าใจ "หลักการ" ของ AABB ก่อน ส่วนการเรียกใช้จริง
game.hit(ball, paddle)เพื่อตีลูกกลับ จะลงมือทำเต็ม ๆ ในคาบถัดไป (Pong #3)
Simple 2D Collision Detection for Games (AABB Tutorial) — Dylan Falconer — อธิบายเงื่อนไขซ้อนทับ 4 บรรทัดเดียวกับที่เราเพิ่งเห็น ใช้ได้กับทุกภาษา/ทุกเกม
เสียงในคาบนี้ — Pong จับจังหวะการชนด้วยเสียง: ลูกชนไม้ (
pong_paddle), ลูกเด้งขอบบน/ล่าง (pong_wallคือตัวที่โค้ดเรียกgame.sfx("wall")อยู่แล้ว), และได้แต้ม (pong_score) — กดเล่นฟังได้จริง:
เมื่อลูกชนไม้ ถ้าไม้ "กำลังเคลื่อน" อยู่ตอนตี เราเอาความเร็วไม้ส่วนหนึ่งไปบวกเข้ากับความเร็วลูก = สปิน
สมการนี้ลงเป็นโค้ดจริงหนึ่งบรรทัดเป๊ะ ๆ ที่ pong_step3.py:63 → ball_vy += player_speed * SPIN (ค่า SPIN = 0.28 ประกาศไว้ที่ :23) — player_speed คือความเร็วไม้ตัวเดียวกับที่เราสะสมไว้ตั้งแต่ต้นคาบ
นี่คือเหตุผลที่เราเก็บไม้เป็น
player_speedตั้งแต่ต้น — พอถึงคาบหน้าเรามีความเร็วไม้พร้อมใช้ใส่สปินได้ทันที (ค่าจริงSPIN = 0.28)
pong_step3.py:56-64คาบนี้เราวางพื้นครบแล้ว (accel/friction/clamp + หลัก AABB + สปิน) คาบหน้าจะเอาทั้งหมดมารวมเป็น "ตีลูกกลับ" จริง — นี่คือบล็อกที่น้อง ๆ จะเติมเอง (ส่วน 30% ของ pong_step3.py) แกะทีละบรรทัด:
if ball_vx < 0 and game.hit(ball, player_paddle): # :58 เช็กทิศก่อน แล้วค่อยเช็กชน (AABB)
hit_offset = ((ball_y + BALL_SIZE/2) - (player_y + PADDLE_H/2)) / (PADDLE_H/2) # :60 โดนบน/กลาง/ล่าง = -1..1
ball_vx = min(-ball_vx + SPEEDUP, BALL_CAP) # :61 กลับทิศ + เร่ง (มีเพดาน BALL_CAP)
ball_vy += hit_offset * 2.0 # :62 มุมเด้งตามจุดสัมผัส (contact angle)
ball_vy += player_speed * SPIN # :63 สปินจากไม้ที่กำลังเคลื่อน
game.sfx("paddle") # :64 เสียงตีโดน
ball_vx < 0 ก่อน game.hit() (:58) — เช็ก ทิศทางก่อนเสมอ กันลูกที่เพิ่งเด้งออกไปโดนตีซ้ำจนติดหนึบในไม้hit_offset (:60) — สมการ contact-angle เดียวกับที่เราเพิ่งดู แปลง "โดนตรงไหนของไม้" เป็นเลข -1..1min(..., BALL_CAP) (:61) — เพดานความเร็ว (BALL_CAP = 14.0 ที่ :22) กันลูกเร็วขึ้นเรื่อย ๆ จนตีไม่ทันplayer_speed * SPIN (:63) — บรรทัดสปินที่เราโยงไว้เมื่อกี้ ค่าไม้ที่เคลื่อนอยู่กลายเป็นการหมุนของลูกคาบนี้เราแค่ "อ่านให้เข้าใจ" ว่า 6 บรรทัดนี้ทำงานยังไง — คาบ 7 (Pong #3) เราจะลงมือเติมมันเองจริง ๆ พร้อม AI คู่ต่อสู้ · โค้ดเต็ม:
practise_codes/pong_step3.py↗
on_each_frame() ของ Pong ทำอะไรบ้างต่อจากลูป render ที่เห็นแล้ว นี่คือ ผังทั้งโปรแกรม ของหนึ่งเฟรมใน pong_step3.py — เห็นครบตั้งแต่รับปุ่ม → ฟิสิกส์ไม้ → ขยับลูก → จุดตัดสินใจ (◇) ทั้งสามจุด → วาดจอ แล้ววนกลับ:
สัญกรณ์ผัง (ใช้เหมือนกันทั้งคอร์ส): กล่องมนเทา = จุดเริ่ม/จบ · สี่เหลี่ยมฟ้า/ส้ม = ประมวลผล · สี่เหลี่ยมเขียว = รับปุ่ม/วาดจอ (I/O) · สี่เหลี่ยมข้าวหลามตัดม่วง = จุดตัดสินใจ (มีทาง Yes/No) · เส้นประ = ทางวนกลับ
จุดสำคัญที่อยากให้เห็น: หนึ่งเฟรมไม่ได้ "ตรงไปตรงมา" มันมี จุดตัดสินใจ 3 จุด (◇) เสียบอยู่กลางทาง — ชนขอบไหม, ชนไม้ไหม, ถึงกำแพงขวาไหม — แต่ละจุดถ้า "ใช่" ก็แวะแก้ความเร็วแล้วค่อยไปวาดจอ นี่คือโครงเดียวกับทุกเกมในคอร์ส
pong_step2.py ใน BENTO IDEเราไม่ใช้วิธี copy โค้ดไปแปะที่ REPL หรือ
exec(open(...))นะครับ ทุกครั้งให้เปิดไฟล์ใน BENTO IDE แล้วกด Program to Device อย่างเดียว เพื่อให้ทุกคนทำงานบนวิธีเดียวกัน
รัน
pong_step2.pyได้ครบทุกข้อ แล้วโชว์อาจารย์ที่หน้าจอ ถือว่าผ่าน
กับดักยอดฮิตของแถวล่าง "ไม้หลุดขอบจอ" — ลืม clamp ตำแหน่ง ไม้ก็ไถลเลยขอบบนหายไปจากจอ ใส่ max(0, …) กลับมาอยู่ที่ขอบพอดี:
player_y += speed (ไม่จำกัด)player_y = max(0, min(H−PADDLE_H, …))| อาการ | สาเหตุ | วิธีแก้ |
|---|---|---|
| ไม้กระตุกไม่ลื่น | เขียน player_y += ACCEL ตรง ๆ |
สะสมใน player_speed ก่อน |
| ไม้ยิ่งกดยิ่งเร็วไม่หยุด | ลืม clamp ความเร็ว | max(-MAX_SPEED, min(MAX_SPEED, ...)) |
| ไม้หลุดขอบจอ | ลืม clamp ตำแหน่ง | max(0, min(HEIGHT - PADDLE_H, ...)) |
| ปล่อยปุ่มแล้วไม้ไม่หยุด | ลืม *= FRICTION ใน else |
ใส่กรณี "ไม่กดอะไร" คูณ FRICTION |
| กด UP+DOWN พร้อมกันเพี้ยน | ไม่กันปุ่มสวนกัน | keys.up and not keys.down |
| ไม้เลื่อนผิดทาง | สับสนทิศแกน y | ค่าน้อย = บน, ค่ามาก = ล่าง |
| ทำสิ่งนี้ | คำสั่งใน bentogame |
|---|---|
| เริ่ม + ล้างจอ | game.start() |
| สร้างกล่อง (ไม้/ลูก) | game.Box(x, y, w, h, สี) |
| ขยับไปตำแหน่ง | box.move_to(x, y) |
| อ่านจอย ขึ้น/ลง | game.keys() → .up / .down |
| เสียงชนกำแพง | game.sfx("wall") |
| ลูปต่อเฟรม | game.run(update, fps) |
ทั้งหมดนี้ core เตรียมไว้ให้แล้วใน
bentogame.pyหน้าที่เราคือ "ใช้มันให้เป็น" ในส่วน 30% — ไม่ต้องไปแก้ตัว core
Pong เครื่องแรกของ Atari ปี 1972 ใช้แค่วงจรลอจิก ไม่มี CPU ด้วยซ้ำ แต่ฟิสิกส์ไม้ตีที่เราเขียนวันนี้คือหัวใจเดียวกัน และมันต่อยอดไปงานวิศวกรรมจริงหลายชั้น:
Embedded / MCU — on_each_frame() ถูกเรียกตาม timer tick ~16 ms ของบอร์ด คือ control loop แบบเดียวกับที่อ่านเซนเซอร์/สั่งมอเตอร์เป็นรอบคงที่ การกด UP/DOWN คืออ่าน digital input ของจอย
Python — เก็บสถานะใน ตัวแปร (player_speed) แล้วอัปเดตทุกเฟรม; on_each_frame ที่ส่งให้ game.run() คือ callback; การกันปุ่มสวนใช้ เงื่อนไข and not
Algorithms — position += velocity ทุกเฟรมคือ Euler integration; FRICTION ซ้ำ ๆ คือ geometric decay; clamp คือ saturation; AABB คือเทสต์ช่วงซ้อนทับ — ทั้งหมดคือคณิต/ฟิสิกส์ที่ใช้ใน DSP และระบบควบคุมจริง
Graphics — ระบบพิกัด y น้อย = บน, y มาก = ล่าง; bounding box คือพื้นฐานของ hit-test และ UI ทุกชนิด
ที่มา: "Signed Pong Cabinet" — Chris Rand, CC BY-SA 3.0, Wikimedia Commons
Stretch (สำหรับกลุ่มที่ทำเสร็จเร็ว):
ACCEL / FRICTION / MAX_SPEED แล้วสังเกตน้ำหนักของไม้ball_vx, ball_vy เริ่มต้นgame.Box(...) เป็นเส้นประpong_step3.py แล้วเดาว่า game.hit() จะถูกเอาไปใช้ทำอะไรส่งงาน (ก่อนเลิก):
pong_step2.py รันบนบอร์ดpractise_codes/pong_step2.py↗player_speed + AABB เช็คอะไรบ้างgame.hit() ทำให้ไม้ ตีลูกกลับ จริง พร้อมมุมเด้งและสปินpong_step2.py — อ่านให้เข้าใจแล้วพิมพ์เองเฉลยชุดนี้มีไว้ให้ เทียบ ไม่ได้มีไว้ให้ลอกวางส่ง คะแนนของคาบนี้อยู่ที่ช่อง 30% ที่เติมเองกับสองบรรทัดที่อธิบายด้วยคำของน้อง ไม่ใช่ที่โค้ดตรงกับเฉลย วิธีใช้ให้ได้ของจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะทีละส่วน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ส่วนแรก — ตั้งค่าคงที่ก่อนเขียนฟิสิกส์:
PADDLE_W, PADDLE_H, BALL_SIZE = 14, 90, 14
PADDLE_START_Y = game.HEIGHT // 2 - PADDLE_H // 2 # วางไม้กลางจอตอนเริ่ม
ACCEL = 1.5 # เร่งต่อเฟรมเมื่อกดค้าง
MAX_SPEED = 12.0 # ความเร็วสูงสุดของไม้
FRICTION = 0.78 # หน่วงเมื่อปล่อยปุ่ม (คูณความเร็วให้ค่อย ๆ เหลือศูนย์)
:20-22) แทนที่จะฝังเลข 1.5 12.0 0.78 ลงกลางลูป? เพราะนี่คือ "สามปุ่มปรับฟีลของไม้" ทั้งหมดของคาบนี้ อยากให้ไม้หนักขึ้น/เบาลง แก้ที่นี่ที่เดียว ไม่ต้องไปไล่หาในโค้ด นี่คือบทเรียน แยกค่าออกจากโครง ที่เราย้ำมาตั้งแต่สไลด์ "70 / 30 — เราเขียนแค่หัวใจฟิสิกส์"PADDLE_START_Y = game.HEIGHT // 2 - PADDLE_H // 2 (:18) ไม่ได้ใส่เลขตายตัวว่าไม้เริ่มที่ y เท่าไร แต่ คำนวณจากความสูงจอจริง เพื่อวางไม้ให้อยู่กึ่งกลางเสมอ ไม่ว่าจอจะสูงกี่พิกเซล นิสัยเดียวกับ "ถามฮาร์ดแวร์ก่อน อย่าเดา"MAX_SPEED = 12.0 กับ FRICTION = 0.78 คือค่าที่เราเห็นเป็นเส้นโค้งไปแล้วในสไลด์ "กราฟความเร็วของไม้" — 12.0 คือระดับที่เส้นแบนราบ (เพดาน) 0.78 คือความชันของหางที่ไถลลู่เข้าศูนย์ค่าคงที่ที่ตั้งชื่อดี ๆ คือคอมเมนต์ที่ไม่มีวันล้าสมัย อ่าน
ACCELFRICTIONแล้วรู้เลยว่าแต่ละตัวทำอะไร โดยไม่ต้องเดาจากเลขลอย ๆ
game.title("PONG") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
player_paddle = game.Box(20, PADDLE_START_Y, PADDLE_W, PADDLE_H, game.GB_LIGHT)
ball = game.Box(game.WIDTH // 2, game.HEIGHT // 2, BALL_SIZE, BALL_SIZE, game.GB_LIGHTEST)
game.Text("Move your paddle: UP / DOWN", 16, 12, game.WHITE)
ball_x, ball_y = float(game.WIDTH // 2), float(game.HEIGHT // 2)
ball_vx, ball_vy = 6.2, 3.4
player_y, player_speed = float(PADDLE_START_Y), 0.0 # ตำแหน่ง + ความเร็วไม้ของเรา
player_paddle กับ ball เรา เก็บลงตัวแปร เพราะทุกเฟรมต้องเรียก .move_to(...) ขยับมัน ส่วน game.Text(...) (:28) เป็นป้ายคงที่ ไม่ต้องแก้ทีหลัง เลยไม่ต้องตั้งชื่อ นี่คือหลักเดิม: อะไรที่อ้างถึงซ้ำเก็บลงตัวแปร อะไรที่วางแล้วจบไม่ต้อง:32 — เราเปิด ตัวแปรสถานะสองตัวคู่กัน: player_y (ไม้อยู่ไหน) และ player_speed (ไม้กำลังไปเร็วแค่ไหน) เริ่มที่ 0.0 การแยก "ตำแหน่ง" ออกจาก "ความเร็ว" นี่แหละคือหัวใจที่ทำให้ไม้มีน้ำหนัก ถ้าเก็บแค่ player_y ตัวเดียวจะทำ accel/friction ไม่ได้เลยfloat(...) ทั้ง ball_x และ player_y? เพราะฟิสิกส์เราบวกทีละ 1.5 หรือคูณ 0.78 ค่าจึงมีทศนิยม ถ้าเก็บเป็น int ตั้งแต่แรก เศษจะถูกปัดทิ้งทุกเฟรม ไม้กับลูกจะกระตุกแทนที่จะลื่น — ตรงกับที่เราเห็นในกราฟว่าความเร็วเป็นเส้นโค้งเนียน ไม่ใช่ขั้นบันได
player_yคือ "อยู่ไหน"player_speedคือ "จะไปทางไหนเร็วแค่ไหน" สองคำถามนี้แยกกันเสมอในทุกเกม จำคู่นี้ไว้ให้ขึ้นใจ
นี่คือช่องที่น้องเติมเอง ทั้งคาบสรุปอยู่ใน 11 บรรทัดนี้ แกะทีละก้อน:
# --- ไม้ตีแบบเร่ง+เสียดทาน (ฟีลเดียวกับยานใน Shooter) ---
# ----- เติมส่วนนี้เอง: กดค้าง = เร่ง, ปล่อย = ไถลช้าลง, จำกัดความเร็ว, กันออกนอกจอ -----
if keys.up and not keys.down:
player_speed -= ACCEL
elif keys.down and not keys.up:
player_speed += ACCEL
else:
player_speed *= FRICTION # ปล่อยปุ่ม -> ค่อย ๆ หยุด
player_speed = max(-MAX_SPEED, min(MAX_SPEED, player_speed)) # จำกัดความเร็ว
player_y = max(0, min(game.HEIGHT - PADDLE_H, player_y + player_speed)) # clamp ไม้อยู่ในสนาม
player_paddle.move_to(20, player_y)
keys.up and not keys.down (:41,43) — เช็ก "กดขึ้นโดยไม่กดลงพร้อมกัน" ไม่ใช่แค่ keys.up เฉย ๆ เพราะถ้ากดสวนทั้งสองปุ่ม เราอยากให้มัน "หักล้าง" ตกไปเข้า else ไม่ใช่เร่งมั่ว นี่คือกับดัก "กด UP+DOWN พร้อมกันเพี้ยน" ในตารางกับดักที่เราดูไปแล้วplayer_speed -= ACCEL เมื่อกดขึ้น (:42) — จำระบบพิกัดจากสไลด์ AABB ได้ไหม y น้อย = บน ดังนั้น "เลื่อนขึ้น" คือทำให้ y ลดลง จึงลบ ไม่ใช่บวก คนพลาดตรงนี้ไม้จะเลื่อนสวนทางelse: player_speed *= FRICTION (:46) — บรรทัดที่สร้าง "น้ำหนัก" ทั้งหมด ไม่มีปุ่มไหนถูกกด เราคูณด้วย 0.78 ซ้ำทุกเฟรม ความเร็วจึงลู่เข้าศูนย์แบบ geometric decay เดียวกับกราฟ ลืมบรรทัดนี้ไม้จะไถลไม่หยุด:47-48) — ชั้นแรกจำกัด ความเร็ว max(-MAX_SPEED, min(MAX_SPEED, ...)) ชั้นสองจำกัด ตำแหน่ง max(0, min(game.HEIGHT - PADDLE_H, ...)) ตรงกับสไลด์ "ฟิสิกส์ #2 — clamp" เป๊ะ ๆ ลืมชั้นบนไม้เร็วจนคุมไม่อยู่ ลืมชั้นล่างไม้หลุดขอบจอ
position += velocityแล้ว clamp — โครงนี้จะกลับมาทุกเกมที่เหลือในคอร์ส วันนี้เราแค่เขียนมันครั้งแรกกับไม้ตี
ลูกบอลยังใช้โครงเดียวกับ Pong #1 เป๊ะ (ต่างที่คาบนี้เราเพิ่ม game.sfx("wall") ให้มีเสียงตอนเด้ง):
# ลูกบอล (ยังเด้งทั้ง 4 ด้าน — คาบหน้าทำให้ตีกลับด้วยไม้)
ball_x += ball_vx
ball_y += ball_vy
if ball_y <= 0 or ball_y >= game.HEIGHT - BALL_SIZE:
ball_vy = -ball_vy
game.sfx("wall")
if ball_x <= 0 or ball_x >= game.WIDTH - BALL_SIZE:
ball_vx = -ball_vx
ball.move_to(ball_x, ball_y)
สังเกตว่าไม้กับลูกใช้ สูตรเดียวกัน — ต่างแค่ไม้เอาความเร็วมาจากปุ่ม (accel) ส่วนลูกความเร็วคงที่แล้วกลับทิศเมื่อชนขอบ (ball_vx = -ball_vx, :58) นี่คือ position += velocity ตัวเดียวกันทั้งคู่ ทีนี้มองทั้งไฟล์เป็นชั้น ๆ ว่าแต่ละส่วนต่อยอดกันยังไง:
| ส่วนของไฟล์ | บรรทัด | แนวคิดใหม่ที่เพิ่ม | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| ค่าคงที่ | :20-22 |
สามปุ่มปรับฟีล (ACCEL/MAX_SPEED/FRICTION) | แยกค่าออกจากโครง (คาบ 1) |
| ตัวแปรสถานะ | :32 |
คู่ player_y + player_speed |
เก็บ state ในตัวแปร |
| accel | :41-44 |
กดปุ่ม → เร่งความเร็วสะสม | อ่าน keys() + เงื่อนไข |
| friction | :46 |
ปล่อย → คูณ <1 ลู่เข้าศูนย์ |
geometric decay (กราฟ) |
| clamp | :47-48 |
จำกัดความเร็ว และ ตำแหน่ง | saturation สองชั้น |
| ลูกบอล | :52-59 |
game.sfx("wall") ตอนเด้ง |
reflect ของ Pong #1 |
เห็นไหมว่าไม้ตีที่ "รู้สึกมีน้ำหนัก" ไม่ได้มาจากบรรทัดวิเศษบรรทัดเดียว แต่มาจากค่าคงที่ + ตัวแปรคู่ + สามเงื่อนไข ประกอบกันพอดี ถอดชิ้นไหนออกฟีลก็เพี้ยนทันที
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่อง "มองให้ทะลุ" ว่าไม้ตีที่เพิ่งเขียนจริง ๆ แล้วเป็นชิ้นส่วนของอะไร
ที่มา — ไม้ตีอันนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "ทบทวน Pong #1 → ต่อยอดคาบนี้" คาบที่แล้วเราเล่นกับ "ความเร็วของลูกบอล" ball_x += ball_vx แล้วกลับทิศเมื่อชนขอบ วันนี้เราหยิบสูตร position += velocity อันเดิมเป๊ะ มาใช้กับไม้ตี เพิ่มแค่สองอย่าง: ปุ่มมาป้อนความเร็ว (accel) กับแรงหน่วงตอนปล่อย (friction) นี่คือเหตุผลที่คาบนี้ไม่ได้เริ่มจากศูนย์ — เราแค่เอาเครื่องมือเดิมมาใช้กับของชิ้นใหม่
ที่ไป — ไม้ตีอันนี้จะโตเป็นอะไร ดูสไลด์ "สปิน — ไม้ที่กำลังเคลื่อนใส่หมุนให้ลูก" กับ "แกะบล็อกการชนจริง — pong_step3.py:56-64" ให้ดี คาบหน้า (คาบ 7, Pong #3) เราจะเรียก game.hit(ball, player_paddle) ให้ไม้ ตีลูกกลับ ได้จริง แล้วเอา player_speed ตัวเดียวที่เราสะสมไว้วันนี้ ไปคูณ SPIN ใส่หมุนให้ลูก ถ้าวันนี้เราไม่เก็บไม้เป็น "ความเร็ว" คาบหน้าก็ทำสปินไม่ได้เลย
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
ball_vx = -ball_vx ที่กลับทิศลูกตอนชนกำแพงได้ไหม การ "ตีลูกกลับด้วยไม้" คาบหน้าคือเรื่องเดียวกันไหม ต่างแค่เงื่อนไขเป็น game.hit() แทน ball_x <= 0?player_speed ของไม้ได้ พรุ่งนี้เราจะเอาความเร็วนั้นไปใส่ให้ลูก (สปิน) ได้ไหม? (ใบ้: ball_vy += player_speed * SPIN):39 พูดถึง — น้ำหนักของยานกับน้ำหนักของไม้ใช้สูตรเดียวกันเป๊ะ?ถ้าตอบสามคำถามได้ว่า "อ๋อ มันคือ
position += velocityเรื่องเดียวกัน" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด ไม้ตีอันเดียววันนี้คือเมล็ดของทั้งฟิสิกส์เกม
สี่เทคนิควันนี้ไม่ใช่แค่แบบฝึกหัด มันคือสิ่งที่ระบบจริงใช้กันทุกวัน:
ACCEL กับ FRICTION ตรง ๆ กันเฟืองสึกและงานสะบัดplayer_speed *= FRICTION (:46) ความเร็วถูกคูณด้วยค่าน้อยกว่า 1 ทุกเฟรมจนลู่เข้าศูนย์max(-MAX_SPEED, min(MAX_SPEED, ...)) (:47) ชั้นความปลอดภัยพื้นฐานของทุก actuatorgame.hit(ball, paddle) ที่เราเข้าใจหลักไปแล้วไม่มีเทคนิคไหนวันนี้เป็นของสมมติเลย accel/friction/clamp/AABB ทั้งสี่มีเครื่องจริงในโลกที่เดินด้วยหลักการเดียวกันอยู่จริง
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาของจริงได้:
ACCEL / MAX_SPEED / FRICTION สามค่ายังไงให้รู้สึกต่างกันชัด ลองจดค่าที่ชอบไว้บนใบงาน:48 เป็นอะไร คิดว่าควรทำ player_speed ยังไงตอนแตะขอบball_vx < 0 ก่อน game.hit() ทำไมถึงต้องเช็กทิศก่อน ถ้าไม่เช็กแล้วลูกที่เพิ่งเด้งออกจะเกิดอะไรขึ้น ลองวาดเหตุการณ์บนกระดาษดูkeys.up/keys.down มาป้อนความเร็ว แล้ว AI จะเอา อะไร มาแทนปุ่มเพื่อตัดสินใจเร่งขึ้น/ลง? ตอบข้อนี้ได้ คาบ 7 จะง่ายขึ้นมากเขียนไอเดียของน้องเองลงใบงานอย่างน้อยหนึ่งข้อ ไม่ต้องกลัวผิด ของที่คิดเองแล้วลองบนบอร์ดจริงคือของที่จำได้นานที่สุด
fit-css
ภาพปลายทาง: Pong จริงบนบอร์ด BENTO
CAPTURE: pong_step2.png — รัน pong_step2.py บนบอร์ด, กดค้าง UP ให้ไม้สีเขียวเลื่อนขึ้นพร้อมลูกบอลกลางจอ