เราเก็บทิศไว้เป็น step_col, step_row แล้วบวกเข้ากับหัวงู ก็จะได้ช่องถัดไปที่งูจะก้าวไป:
# ทิศที่งูกำลังเดิน — เริ่มไปขวา
step_col, step_row = 1, 0
on_each_frame() หัวใจของเกมฟังก์ชันนี้จะถูกเรียก ซ้ำทุกจังหวะ เราใส่ global เพื่อให้แก้ค่าทิศจากข้างในได้ แล้วเริ่มด้วยการอ่านจอยก่อน:
def on_each_frame(): # ถูกเรียกซ้ำทุกจังหวะ = หัวใจของเกม
global step_col, step_row
keys = game.keys() # อ่านปุ่มจอยตอนนี้
# Back = ออกจากเกม · Start = เริ่มใหม่ — game.run() จัดการให้แล้ว
ระวัง: ถ้าลืม
globalทิศจะไม่เปลี่ยนเวลากดจอย อันนี้เป็นกับดักที่เจอกันบ่อยมาก
เรายอมให้เลี้ยวได้ เฉพาะตอนแกนนั้นกำลังนิ่ง (=0) เพื่อไม่ให้งูหักกลับมาทับตัวเอง:
if keys.left and step_col == 0: step_col, step_row = -1, 0
if keys.right and step_col == 0: step_col, step_row = 1, 0
if keys.up and step_row == 0: step_col, step_row = 0, -1
if keys.down and step_row == 0: step_col, step_row = 0, 1

เรียกชื่อมันให้ถูก — นี่คือ finite-state machine (FSM) 4 สถานะ: ทิศการเดินมีได้แค่ 4 สถานะ RIGHT
(1,0)· LEFT(-1,0)· UP(0,-1)· DOWN(0,1)เก็บสถานะปัจจุบันไว้ใน(step_col, step_row)[snake_step2.py:23] ส่วนเงื่อนไขif keys.X and axis == 0[snake_step2.py:30-33] คือ transition แบบมี guard — อนุญาตให้เปลี่ยนสถานะได้เฉพาะเมื่อไม่ใช่ทิศตรงข้าม (RIGHTLEFT, UP
DOWN ต้องห้าม) กฎ "แกนตั้งฉากต้องนิ่ง (=0)" ก็คือ guard ตัวนี้แหละ · แนวคิด FSM + guarded transition นี้เป็นรากฐานเดียวกับ state machine ที่ใช้คุมโหมดอุปกรณ์ฝังตัวในงานจริง
เคล็ดลับคลาสสิกของ Snake ที่ทุกคนใช้กัน คือ เติมหัว แล้วตัดหาง:
next_head = [snake_body[0][0] + step_col,
snake_body[0][1] + step_row]
snake_body.insert(0, next_head) # เติมหัวใหม่หน้าสุด
snake_body.pop() # ตัดหางทิ้ง 1 ช่อง
ดูทีละจังหวะ — หัวใหม่ (เขียว) โผล่หน้าสุด พร้อมกับหางเก่า (แดง) หายไปท้ายสุด ส่วนที่เหลือนิ่งสนิท แตะแค่ 2 ปลายเท่านั้น:
โครงเว้นช่องอยู่ใน
practise_codes/snake_step2.py↗ (มี TODO 1–3)
snake_body คือ list ของพิกัด แต่เราใช้มันแบบ คิวสองปลาย (deque): เติมของที่ปลายหนึ่ง เอาออกอีกปลายหนึ่ง

ถ้าเราขยับงูแบบตรงไปตรงมา คือไล่เขียนทุกปล้องว่าปล้องนี้ไปไหน เราต้องแตะทุกช่อง — งูยาว 100 ปล้องก็ทำงาน 100 ครั้งต่อเฟรม แต่เคล็ดลับ insert+pop ทำให้เรา แตะแค่ 2 ปลายเสมอ ไม่ว่างูจะยาวแค่ไหน นี่คือเหตุผลที่ Snake ลื่นแม้บนเครื่องเล็ก ๆ
เขียนเป็นต้นทุนงานต่อเฟรม ( = จำนวนปล้อง) จะเห็นชัด:
ที่ : วิธี insert+pop ทำงาน 2 ครั้ง ส่วนวิธีเขียนใหม่ทำงาน 100 ครั้ง ต่อเฟรม — ต่างกัน 50 เท่า และยิ่งห่างขึ้นเรื่อย ๆ เมื่องูยาวขึ้น
เดิน 1 จังหวะ เราทำ 2 อย่างที่หักล้างกันพอดี: เติมหัว +1 แล้วตัดหาง −1

เส้นเขียวคือกรณีที่เราทำครบทั้ง insert กับ pop — ความยาวนิ่งอยู่ที่ 3 ตลอด ส่วนเส้นแดงคือ ถ้าลืม pop() งูจะยืดขึ้นทีละปล้องทุกเฟรมจนเต็มจอ (เป็นบั๊กยอดฮิตของคาบนี้เลย) คาบหน้าเวลาเราจะ "ให้งูโตตอนกินอาหาร" เราก็แค่ ข้าม pop() หนึ่งจังหวะ เท่านั้นเอง
ทีนี้เราย้ายกล่องบนจอให้ไปตรงกับช่องใหม่ โดยให้หัวเป็นสีอ่อน ลำตัวเป็นสีเข้ม:
for index, cell in enumerate(snake_body):
body_squares[index].set_color(
game.GB_LIGHT if index == 0 else game.GB_DARK)
body_squares[index].move_to(cell[0] * CELL_PX, cell[1] * CELL_PX)
แปลงช่องเป็นพิกเซลด้วย

แล้วบรรทัดสุดท้ายของไฟล์ คือการเปิด game loop:
game.run(on_each_frame, fps=9) # เรียก 9 ครั้ง/วินาที = จังหวะ Snake
snake_step2.py — 1 tick ทำอะไรบ้างผังนี้คือ 1 รอบของ on_each_frame() ตั้งแต่อ่านจอยจนวาดจบ หัวใจอยู่ที่ จุดตัดสินใจ turn-guard (◇) ของ direction FSM: กดจอยแล้วเลี้ยวได้ก็ต่อเมื่อ แกนตั้งฉากกำลังนิ่ง (=0) — ผ่านค่อยเปลี่ยนสถานะทิศ ไม่ผ่านก็คงทิศเดิม แล้วจึง "เติมหัว + ตัดหาง" เดินต่อ
เส้นทาง No (ไม่ผ่าน guard) กับ Yes (ผ่าน → เปลี่ยนทิศ) ไปบรรจบกันที่
next_headเหมือนกัน — เพราะไม่ว่าจะเลี้ยวหรือไม่ งูก็ยัง "เดินหน้า 1 ช่อง" ทุกเฟรมอยู่ดี guard แค่ตัดสินว่าทิศไหน
ขั้นตอนเดียวกับทุกคาบ:
snake_step2.py ใน BENTO IDEเราจะ ไม่ใช้
exec(open(...))หรือพิมพ์รันเองใน REPL — ทุกครั้งให้กด Program to Device เพื่อให้บอร์ดรันไฟล์ของเราโดยตรง เหมือนเครื่องเกมจริง
กด Program to Device แล้ว งูจะเลื้อยข้ามจอเอง ลองโยกจอยซ้าย/ขวา/ขึ้น/ลง แล้วเราจะเห็น งูเลี้ยวตามจอยจริง
บนบอร์ดจริงงูเลื้อยเร็วจนตาตามแทบไม่ทัน ลองดูภาพนี้ที่ทำให้ช้าลง จะเห็นว่าทุกจังหวะมีแค่ หัวใหม่ (กรอบส้ม) โผล่ข้างหน้า และ หางเก่า (กรอบแดง) หายไปหนึ่งช่อง — ที่เหลือนิ่งหมด

นี่แหละคือ insert(0, next_head) กับ pop() ที่กำลังทำงานทีละเฟรม พอเลื่อนเร็ว ๆ ตาเราจะเห็นเป็นงูตัวเดียวเลื้อยไปข้างหน้าอย่างต่อเนื่อง — เหมือนการ์ตูนที่พลิกทีละหน้า
global ทิศจะไม่เปลี่ยนเวลากดจอยpop() งูจะยาวขึ้นเรื่อย ๆ ไม่หยุดmove_to list ขยับแล้วแต่กล่องบนจอยังค้างที่เดิมงูที่เลื้อยตามจอยวันนี้ ดูเหมือนเล่นสนุก แต่ข้างใต้คือรากฐานที่เราจะใช้ซ้ำทั้งคอร์สและในงานจริง — เลื่อนปอกทีละชั้นจะเห็นวิศวกรรมที่ซ่อนอยู่:
ฝั่ง Embedded / ฮาร์ดแวร์
fps=9 คือการขอให้ระบบเรียกเราตรงเวลาทุก ๆ วินาที เบื้องหลังคือ timer + interrupt ของชิป ซึ่งเป็นกลไกเดียวกับที่ใช้คุมจังหวะมอเตอร์/เซนเซอร์ในงานควบคุมจริงgame.keys() ทุกเฟรมฝั่ง Python
insert(0, ...) + pop() คือการใช้โครงสร้างข้อมูลพื้นฐานให้ตรงงาน (เติม/ตัดที่ปลาย)on_each_frame ให้ game.run() เรียกแทนเรา และใช้ global เพื่อให้ทิศจำค่าข้ามเฟรมฝั่ง Algorithms / คณิต
keys.left/right คือการเทียบค่าแกนอนาล็อกกับ threshold () เปลี่ยนเลขดิบ ๆ ให้เป็นคำสั่งเลี้ยว นี่คือ sensor/joystick data processing แบบย่อฝั่ง Graphics
ดูคลิป (เสริม): game loop (อ่าน input → อัปเดต → วาด → วนซ้ำ) คือแบบเดียวกับ game.run() ที่เราใช้เป๊ะ แม้คลิปจะคนละภาษา/เอนจินก็ตาม
Game Loops Explained in 5 Minutes (With Code) — Dylan Falconer
สิ่งที่ได้คาบนี้: game loop ของจริง + เดิน 1 ช่อง/จังหวะ + เลี้ยวตามจอย + กฎกันถอยหลัง
30% ที่เป็นงานของน้อง ๆ คือ TODO 1–3 ใน practise_codes/snake_step2.py↗ — หาหัวใหม่, insert(0, ...), pop() ทั้งหมดอยู่ใน on_each_frame() · ติดตรงไหนยกมือถามได้เลย — เดี๋ยวเฉลยในห้อง
คาบหน้า: เราจะเติม อาหาร · การกิน · การโตของงู · นับแต้ม · การตาย ให้กลายเป็น Snake ที่เล่นได้จริง
snake_step2.py — อ่านให้เข้าใจ แล้วปิดไฟล์พิมพ์เองเฉลยนี้ไม่ได้มีไว้ลอกวางส่ง คะแนนของคาบนี้อยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง ไม่ใช่ที่โค้ดตรงกับเฉลย วิธีใช้ให้ได้ของจริงคือ ลองเติม TODO ให้สุดก่อนอย่างน้อย 15 นาที ถ้าติดค่อยเปิดเฉลยอ่านให้เข้าใจ แล้วปิดไฟล์ กลับไปพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะเฉลยทีละส่วน ไม่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน
ส่วนแรก — ตั้งเวทีก่อนงูเดิน:
import bentogame as game
CELL_PX = 26
GRID_COLS = game.WIDTH // CELL_PX
GRID_ROWS = game.HEIGHT // CELL_PX
game.title("SNAKE") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
snake_body = [[6, 8], [5, 8], [4, 8]] # หัวอยู่ตัวแรก
body_squares = [game.Box(col * CELL_PX, row * CELL_PX, CELL_PX - 2, CELL_PX - 2, game.GB_DARK)
for col, row in snake_body]
body_squares[0].set_color(game.GB_LIGHT)
score_text = game.Text("Score: 0", 10, 8, game.WHITE)
CELL_PX = 26 (:9) ตั้งครั้งเดียวเป็นค่าคงที่ แล้ว GRID_COLS/GRID_ROWS (:10-11) หารเอาจากขนาดจอจริง เราไม่ hard-code จำนวนช่อง เพราะถ้าย้ายไปจออื่นกริดจะปรับตามเอง นี่คือนิสัย "ถามฮาร์ดแวร์ก่อน อย่าเดา" แบบเดียวกับที่คอร์สย้ำมาตลอดgame.title("SNAKE") (:13) วางหน้าเริ่ม Start/Back ให้ในตัว ตรงกับสไลด์ วิธีรัน — Program to Device ที่บอกว่าเราให้บอร์ดรันไฟล์ตรง ๆ เหมือนเครื่องเกมจริง ไม่ต้องเขียนปุ่มเริ่มเองsnake_body (:15) เก็บพิกัด col,row ของทุกปล้อง หัวคือตัวแรก ส่วน body_squares (:16-17) คือกล่องบนจอ สร้างจาก list comprehension วนคู่ col, row — นี่คือการแยก "ข้อมูลตำแหน่ง" ออกจาก "ภาพบนจอ" สองชั้นนี้จะถูกจับให้ตรงกันทุกเฟรมในส่วนวาดbody_squares[0].set_color(game.GB_LIGHT) (:18) ย้อมหัวสีอ่อนไว้ก่อน คนเล่นจะได้แยกหัวออกจากลำตัวตั้งแต่เฟรมแรก ตรงกับ MVP ที่เราวางไว้ว่า "หัวเป็นสีอ่อนนำหน้า"ก่อนงูจะเดินได้สักก้าว เราต้องแยกให้ชัดว่าอะไรคือ ข้อมูล (snake_body) อะไรคือ ภาพ (body_squares) — จำเส้นแบ่งนี้ไว้ เดี๋ยวส่วนวาดจะเอามันมาเชื่อมกัน
# ทิศที่งูกำลังเดิน เก็บเป็น "ก้าวละกี่ช่องในแนวคอลัมน์/แถว"
# 1,0 = ขวา · -1,0 = ซ้าย · 0,-1 = ขึ้น · 0,1 = ลง
step_col, step_row = 1, 0
def on_each_frame(): # ฟังก์ชันนี้ถูกเรียกซ้ำทุกจังหวะ (= หัวใจของเกม)
global step_col, step_row
keys = game.keys()
# เลี้ยวตามจอย — แต่ "ห้ามถอยหลังกลับทับตัวเอง" (เลี้ยวได้เมื่อแกนนั้นกำลังนิ่ง = 0)
if keys.left and step_col == 0: step_col, step_row = -1, 0
if keys.right and step_col == 0: step_col, step_row = 1, 0
if keys.up and step_row == 0: step_col, step_row = 0, -1
if keys.down and step_row == 0: step_col, step_row = 0, 1
# Back = ออกจากเกม · Start = เริ่มใหม่ — game.run() จัดการให้ทั้งคู่อยู่แล้ว
step_col, step_row = 1, 0 (:23) คือ สถานะของ direction FSM ที่สไลด์ "สเต็ป 2" เรียกชื่อไว้ — ทิศมีได้แค่ 4 สถานะ เก็บทั้งหมดในเวกเตอร์เล็ก ๆ 2 ตัว ตั้งต้นให้ไปขวา งูจะได้เริ่มเดินทันทีโดยไม่ต้องรอให้กดจอยglobal step_col, step_row (:27) ต้องมี เพราะ on_each_frame จะเขียนทับสองตัวนี้ ถ้าลืม Python จะสร้างตัวแปร local ใหม่ ทิศที่เรากดจะหายไปเมื่อจบเฟรม — นี่คือ กับดัก global ที่สไลด์เตือนไว้ว่าเจอกันบ่อยที่สุดif keys.X and axis == 0 (:30-33) คือ guarded transition: เงื่อนไข step_col == 0 / step_row == 0 คือ guard ที่อนุญาตให้เลี้ยวได้เฉพาะตอนแกนตั้งฉากกำลังนิ่ง กันหัวพับกลับทับปล้องที่ 2 ทันที ถ้าเขียนแบบไร้เดียงสาว่า "กดซ้ายก็ไปซ้าย" งูจะฆ่าตัวตายเองตอนกำลังไปขวา:30-33) ไม่ใช่ความบังเอิญ — จัดให้อ่านออกทันทีว่าแต่ละปุ่มแมปไปทิศไหน โค้ดที่อ่านง่ายคือโค้ดที่ debug ง่าย
game.keys()(:28) เบื้องหลังคือ USB HID ที่เราคุยในสไลด์เชื่อมโยงรากฐาน — จอยส่งสถานะปุ่มมาให้อ่านทุกเฟรม เราแค่แปลง "ปุ่มที่กด" เป็น "การเปลี่ยนสถานะทิศ" ผ่าน guard
# ----- เติมส่วนนี้เอง: ทำให้งูเดิน 1 ช่อง -----
# 1) คำนวณช่องใหม่ของหัว = หัวเดิม + ทิศที่เดิน
next_head = [snake_body[0][0] + step_col, snake_body[0][1] + step_row]
# 2) เติมหัวใหม่ไว้ข้างหน้า + ตัดหางทิ้ง 1 ช่อง = เลื่อนไปข้างหน้า 1 ช่อง
snake_body.insert(0, next_head)
snake_body.pop()
# วาดทุกปล้องให้ไปอยู่ช่องที่ถูกต้อง (หัวสีอ่อน ลำตัวสีเข้ม)
for index, cell in enumerate(snake_body):
body_squares[index].set_color(game.GB_LIGHT if index == 0 else game.GB_DARK)
body_squares[index].move_to(cell[0] * CELL_PX, cell[1] * CELL_PX)
next_head (:38) คือการบวกเวกเตอร์ตรง ๆ: หัวเดิม + ทิศ = ช่องถัดไป ตรงกับสูตร ที่เราวาดไว้ตั้งแต่สไลด์ "ทิศการเดิน" ไม่มี if ยาว ๆ ว่าไปทิศไหนแล้วบวกเท่าไร เพราะทิศถูกเก็บเป็นตัวเลขไว้แล้วinsert(0, next_head) + pop() (:40-41) คือหัวใจ list-as-deque: เติมหัวหน้าสุด ตัดหางท้ายสุด แตะแค่ 2 ปลายเสมอ () ไม่ว่างูยาวแค่ไหน นี่คือเหตุผลที่ Snake ลื่นบนเครื่องเล็ก และสองบรรทัดนี้บวก/ลบหักกันพอดี ความยาวจึงคงที่ — ลืม pop() เมื่อไรงูยืดจนเต็มจอทันทีenumerate (:44-46) คือจุดที่ ข้อมูลกับภาพมาเจอกัน: ไล่ทุกปล้องใน snake_body แล้วสั่ง move_to กล่องคู่ของมันไปช่องใหม่ ถ้ามีแต่ insert/pop โดยไม่ move_to list ขยับแล้วแต่กล่องบนจอค้างที่เดิม — อีกหนึ่งกับดักที่สไลด์เตือนgame.GB_LIGHT if index == 0 else game.GB_DARK (:45) ย้อมหัวใหม่ทุกเฟรม ที่ต้องย้อมซ้ำเพราะปล้องที่เคยเป็นหัวเมื่อเฟรมก่อนตอนนี้กลายเป็นลำตัวแล้ว · cell[0] * CELL_PX (:46) คือสูตรแปลง col/row → พิกเซล ที่เราใช้ตั้งแต่ต้นคาบเขียนโค้ดสั่งฮาร์ดแวร์ให้จับหลักนี้: แก้ข้อมูลก่อน แล้วค่อยวาดภาพให้ตามข้อมูล อย่าขยับภาพกับข้อมูลปนกัน ไม่งั้นวันหนึ่งสองอย่างจะเพี้ยนไปคนละทาง
โครงของทั้งไฟล์ไม่ได้สุ่มมา มันคือ game loop ที่สไลด์ต้นคาบวาดไว้เป๊ะ ๆ อ่านหน้าคะแนน (:49) จะเห็นว่าทุกอย่างประกอบกันเป็นชั้น ๆ ที่ต่อยอดกัน:
| ส่วน | บรรทัด | แนวคิดใหม่ที่เพิ่มเข้ามา | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| setup เวที | :9-19 |
ค่าคงที่ + แยกข้อมูล/ภาพ + game.title |
col/row grid จาก Snake #1 |
| จำทิศ (FSM) | :23 |
เก็บสถานะทิศเป็นเวกเตอร์ 2 ตัว | ตัวแปรธรรมดา |
| เลี้ยว (guard) | :27, 30-33 |
global + guarded transition กันถอยหลัง |
game.keys() อ่านจอย |
| เดิน (deque) | :38, 40-41 |
insert+pop = เลื่อน + คงความยาว | เวกเตอร์ทิศบวกหัว |
| วาดตามข้อมูล | :44-46 |
จับภาพให้ตรง list ทุกเฟรม | col/row → พิกเซล |
game.run(on_each_frame, fps=9) # เรียก on_each_frame 9 ครั้ง/วินาที = จังหวะ Snake
game.run(on_each_frame, fps=9) (:49) คือตัวหมุนทั้งหมดนี้ให้เกิดขึ้น 9 ครั้ง/วินาที เราเขียนแค่ "หนึ่งเฟรมทำอะไร" ส่วนการวนซ้ำตรงเวลาเป็นหน้าที่ของ timer+interrupt เบื้องหลัง (fixed timestep)on_each_frame — เส้นแบ่ง "ทำครั้งเดียว vs ทำซ้ำทุกเฟรม" คือหัวใจของการออกแบบเกม วางผิดฝั่งเมื่อไรเกมช้าหรือเพี้ยนทันทีถ้าจับจังหวะ setup-นอก / update-ใน ได้ น้องจะเขียนเกมไหนก็วางโครงถูกตั้งแต่บรรทัดแรก เพราะทุกเกมคือ game loop อันเดียวกันนี้ที่โตขึ้น
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องโค้ด แต่เรื่อง "มองให้ทะลุ" ว่างูที่คืบทีละช่องวันนี้ จริง ๆ แล้วเป็นชิ้นส่วนของอะไร
ที่มา — งูเดินได้ ไม่ได้เริ่มจากศูนย์ สไลด์ game loop คืออะไร ต้นคาบวาดวงจร อ่านจอย → อัปเดต state → วาดใหม่ → รอ ไว้แล้ว วันนี้เราแค่เอาคาบที่แล้ว (Snake #1 งูภาพนิ่งบนกริด col/row) มาหย่อนเข้าในวงจรนั้น พอ on_each_frame ถูกเรียกซ้ำ ภาพนิ่งก็มีชีวิต และการ "เลี้ยวแบบมี guard" ที่สไลด์ สเต็ป 2 เรียกว่า direction FSM ก็คือคำตอบของปัญหาเก่าแก่ที่สุดของ Snake — งูฆ่าตัวตายเองตอนกดย้อนทิศ
ที่ไป — insert+pop ที่เราเพิ่งฝึกไม่ได้จบแค่ทำงูเดิน สไลด์ ความยาวงูคงที่ได้ยังไง ทิ้งกุญแจไว้แล้ว: การโตของงูตอนกินอาหารในคาบหน้า คือแค่ ข้าม pop() หนึ่งจังหวะ เท่านั้น และ game loop อันนี้จะไม่หายไปไหน มันคือโครงเดียวกับ Flappy, Pong, เกมยิง ที่เราจะเขียนต่อ — ต่างกันแค่ "หนึ่งเฟรมทำอะไร"
ลองตอบในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
on_each_frame ของเราทำครบทั้งสี่ขั้นในฟังก์ชันเดียว ลองชี้ทีละบรรทัดว่าบรรทัดไหนคือขั้นไหนpop()" พรุ่งนี้ถ้าอยากให้งูสั้นลง (โดนกับดัก) เราจะทำยังไง? (ใบ้: pop สองครั้ง)ถ้าตอบสามข้อได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" นั่นคือการหยั่งรู้ที่อยากให้เกิด — งูก้าวเล็ก ๆ วันนี้คือ game loop ตัวเต็มที่กำลังจะโต เราแค่ยังไม่ได้เติมกิน/ตายให้มัน
เทคนิคสี่อย่างของคาบนี้ไม่ได้อยู่แค่ในเกม มันคือของที่ระบบจริงใช้กันทุกวัน:
game.run(fps=9) ที่เรียกตรงเวลาทุก วินาที คือหลักเดียวกับ control loop ของโดรน/หุ่นยนต์/เครื่องพิมพ์ 3 มิติ: อ่านเซนเซอร์ → คำนวณ → สั่งมอเตอร์ ในจังหวะคงที่ เบื้องหลังคือ timer+interrupt ตัวเดียวกันsnake_body คือหัวใจของ คิวข้อมูลเสียง/เครือข่าย: เขียนเข้าหัว ดึงออกหาง แตะแค่ 2 ปลาย ไม่ต้องเลื่อนทั้งบัฟเฟอร์game.keys() ที่อ่านจอยทุกจังหวะ คือแบบเดียวกับ เมาส์/คีย์บอร์ด/จอยคอนโซล ที่ถูกโพลสถานะปุ่มเป็นรอบ ๆ ไม่ใช่รอ event ทีละครั้งสี่ท่าที่เราเล่นวันนี้ไม่มีท่าไหนเป็นของสมมติ ทุกท่ามีเครื่องจริงในโลกที่เดินด้วยหลักการเดียวกันอยู่ — เราแค่เล่นมันในร่างของงู
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาของจริงได้:
fps=9 คงที่ ถ้าอยากให้งูเร็วขึ้นเมื่อยาวเกินค่าหนึ่ง จะออกแบบยังไงให้จังหวะเปลี่ยนโดยไม่พังโครง game loop? เก็บความเร็วเป็นตัวแปรที่แก้ได้ระหว่างเล่นแบบไหนnext_head ตรงไหน? แล้วถ้าให้ชนขอบ = ตายล่ะ ออกแบบต่างกันยังไงpop() หนึ่งจังหวะ จริงไหม ลองวาดบนกระดาษดูเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากงูที่พิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นวิศวกร ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css
CAPTURE: snake_walking.png — รัน snake_step2.py แล้วถ่ายตอนงูกำลังเลี้ยวกลางจอ (หัวสีอ่อนนำหน้า)