
ฝั่งซ้ายของรูปคือ "โลกของช่อง" — นับเป็น จำนวนเต็ม (col, row) สบายสมอง ส่วนฝั่งขวาคือ "โลกของจอ" ที่เป็น พิกเซล จริง สูตรเดียวที่เชื่อมสองโลกคือคูณด้วย CELL_PX:
ใต้ลูกศรคือ "คูณ CELL_PX" · ใต้วงเล็บซ้าย = ช่อง (จำนวนเต็ม) · ใต้วงเล็บขวา = พิกเซลบนจอ
เชื่อมโยงรากฐาน
สูตร "คูณด้วยค่าคงที่เพื่อเปลี่ยนหน่วย" นี้คือ การแปลงค่า (unit conversion) ตัวเดียวกับที่ใช้ตอนอ่านเซนเซอร์ — เช่น แปลงค่าดิบ ADC เป็นโวลต์ หรือแปลงค่า IMU เป็น g (÷9.81) ส่วน "ระบบพิกัด col/row → x/y" คือพื้นฐาน graphics: coordinate system ของทุกเกม 2D
How to create a GRID in PYGAME - Pygame Snake Game #2 — Robot Maze · แนวคิด "จอแบ่งเป็นช่อง แล้วช่อง × ขนาดช่อง = พิกเซล" เดียวกับเรา แต่เขาเขียนด้วย Pygame
คาบนี้เราเขียนแค่ "งูเริ่มต้นอยู่ช่องไหน และเป็นสีอะไร" ส่วนการวาดกล่องและ IPC เป็นงานของ engine ที่อาจารย์เตรียมไว้ให้แล้ว
import bentogame as game
CELL_PX = 26 # 1 ช่องกริด = 26 พิกเซล
GRID_COLS = game.WIDTH // CELL_PX # หน้าจอกว้างกี่ช่อง (~30)
GRID_ROWS = game.HEIGHT // CELL_PX # หน้าจอสูงกี่ช่อง (~15)
game.title("SNAKE") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
# งู = list ของช่อง [คอลัมน์, แถว] — "หัวงูอยู่ช่องแรก" เสมอ
snake_body = [[6, 8], [5, 8], [4, 8]]
กฎที่ต้องจำให้ขึ้นใจ: หัวงู =
snake_body[0]เสมอ กฎนี้เราจะใช้ยาวไปจนจบ Snake
# แปลง "ช่องกริด" ของแต่ละปล้อง -> กล่องสีเขียว (ช่อง × 26 = ตำแหน่งจริง)
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) # หัวงูสีอ่อนกว่า
รันแล้วเราจะเห็นงูเขียว 3 ช่อง หัวสีอ่อน ลำตัวเข้ม
CELL_PX - 2 คือการเว้นขอบ 2px ให้เห็นแต่ละช่องแยกกันชัดเจน
บรรทัดนี้คือ list comprehension ซึ่งก็คือ for loop ที่สร้างกล่องทีละอัน เพียงแต่เขียนสั้นกว่า

ปล้องทุกปล้องเดินตามเส้นทางเดียวกันนี้ทุกครั้งที่วาด: เก็บเป็น ช่อง → คูณ
CELL_PXเป็น พิกเซล →game.Boxวาดเป็น กล่อง เราจับแค่ต้นทาง (ช่อง + สี) ที่เหลือ engine ทำให้

คาบนี้งูยัง นิ่ง แต่ลองดูภาพเคลื่อนไหวว่าคาบหน้ามันจะเดินยังไง: หัวงูขยับ ทีละช่อง (col +1) ส่วนพิกเซลกระโดดทีละ CELL_PX ไม่ค่อย ๆ ไหลทีละพิกเซล
นี่คือเหตุผลที่เราเก็บงูเป็น ช่อง ไม่ใช่พิกเซล — การเดิน การชน การวางอาหาร คิดบนตารางช่องง่ายกว่ามาก แล้วค่อยคูณเป็นพิกเซลตอนวาด
เชื่อมโยงรากฐาน: การขยับเป็น "ก้าวแบบไม่ต่อเนื่อง" (discrete step) คือหัวใจของ เกมลูป + การจัดการสถานะ ที่เราจะใช้ตลอดคอร์ส
food_square = game.Box(7 * CELL_PX, 4 * CELL_PX,
CELL_PX - 2, CELL_PX - 2, game.GB_LIGHTEST)
score = 0
score_text = game.Text("Score: 0", 10, 8, game.WHITE)
# ตาน้อง ๆ เขียน 30% ตรงนี้: ขยับ snake_body / เปลี่ยนสีหัว ดูจอเปลี่ยน
def update(): # ภาพนิ่ง — คาบนี้ยังไม่มีอะไรขยับ (คาบหน้าให้เดิน)
pass
game.run(update, fps=9)

update() ว่างgame.run()
fps=9ครบแล้วทั้งงู อาหาร และ Score: 0 — ลองเขียนเองให้สุดก่อนนะ เดี๋ยวเฉลยพร้อมกันในห้อง หน้าจอควรเหมือนภาพ
session-04.png
ขั้นตอนสั้น ๆ ที่เราใช้ทุกคาบของคอร์สนี้:
practise_codes/snake_step1.py↗ ใน BENTO IDEข้อตกลงของคอร์ส: เราไม่ใช้
exec(open(...))รันโค้ด แต่ใช้ Program to Device เสมอ เพื่อให้ทุกคนรันเหมือนกันและดีบักง่าย
ถ้าลืม
game.run(...)โปรแกรมจะจบทันที งูจะโผล่แว้บเดียวแล้วจอเด้งกลับ Home
snake_step1.py — setup แล้วลูป "นิ่ง"ก่อนหน้านี้กล่อง update() ยัง "ทึบ" — เปิดออกมาดูจริง ๆ พบว่าคาบนี้มันคือ pass (ไม่ทำอะไร) ผังนี้ไล่ทั้งไฟล์: ช่วง setup ทำงานหนักครั้งเดียว (สร้างงู/อาหาร/คะแนน) แล้ว game.run() วนเรียก update() ที่ไม่แก้ state อะไรเลย → ภาพเลย นิ่ง
คาบหน้าจะ "เปิด" กล่อง
update(): ใส่ตรรกะเดิน/เลี้ยวเข้าไปแทนpass— พอ state เปลี่ยนทุกเฟรม งูก็เริ่มเลื้อย ผังเดียวกันนี้แหละ เปลี่ยนแค่กล่องกลาง
| อาการ | สาเหตุ / แก้ |
|---|---|
NameError: game | ลืม import bentogame as game บรรทัดแรก |
| งูกองมุมซ้ายบน | ลืมคูณ CELL_PX → ต้อง col*CELL_PX, row*CELL_PX |
| หัวงูไม่เปลี่ยนสี | ลืม body_squares[0].set_color(...) (หัวคือ [0] เสมอ) |
| งูแว้บเดียวแล้วหาย | ลืม game.run(...) ปิดท้าย → โปรแกรมจบทันที |
list index out of range | snake_body ว่าง → ต้องมีอย่างน้อย 1 ปล้อง |
| โปรแกรมค้าง | กด Ctrl-C ที่ REPL (อย่าถอดสาย) |
Acceptance
Score: 0 มุมซ้ายบนsession-04.png[0]" ได้ส่วนที่น้อง ๆ ทำเอง (30%) — แก้ใน practise_codes/snake_step1.py↗
snake_body)body_squares[0].set_color(...))ลองต่อยอด: งูยาว 5–6 ปล้อง / งูแนวตั้ง / CELL_PX=20 / เปลี่ยนธีมสี
ตัวอย่าง "งูแนวตั้ง" — snake_body=[[6,8],[6,9],[6,10]]

หัว ([0]) อยู่บนสุด ลำตัวต่อลงล่าง
render จากเอนจิน BENTO Game · BDH & TESAIoT
สนาม Snake พร้อมแล้ว ก่อนจบคาบอาจารย์อยากให้น้อง ๆ รู้จัก เครื่องมือทำเมนูภาษาไทย ไว้ใช้กับเกมตัวเอง

ทบทวนของเดิม — game.Text ภาษาไทยได้อยู่แล้ว เราใช้มาตั้งแต่ป้าย Score คาบนี้ ลองเปลี่ยนข้อความเป็นไทยดู:
score_text = game.Text("คะแนน: 0", 10, 8, game.WHITE)
score_text.set("คะแนน: 30") # อัปเดตได้เหมือนเดิม
game.Textเหมาะกับ HUD / คะแนน ที่ลอยอยู่บนสนามเกม — ภาษาไทยขึ้นถูกอักขระ สระลอย วรรณยุกต์ครบ
ของใหม่จริง ๆ — เมนูเกมจาก rich UI widgets หน้าเมนู (เลือกระดับ, เปิดเสียง, ปุ่มเริ่ม) ไม่ใช่แค่ข้อความลอย แต่เป็น widget กดได้ เราใช้โมดูล ui แยกอีกตัว
import ui
ui.screen(792, 398) # ตั้งผืนจอเท่าจอเกม
ui.clear()
gameไว้วาดสนามเกม ·uiไว้ทำหน้าเมนู/ตั้งค่า — สอง import คนละหน้าที่ ใช้ร่วมกันได้
ภายใต้ firmware v1.2.0 font ไทย + การจัด cluster (สระ/วรรณยุกต์ซ้อน) ทำให้ในเฟิร์มแวร์แล้ว ฝั่ง Python เราแค่ส่งสตริงไทยเข้าไปตรง ๆ

| widget | สร้างยังไง (ข้อความเป็นไทยได้หมด) | ใช้ทำอะไรในเมนู |
|---|---|---|
ui.Label(text, x=, y=, color=, value=SIZE) |
ui.Label("ตั้งค่าเกมงู", x=40, y=24, value=28) |
หัวเรื่อง / ป้ายกำกับ |
ui.Button(text, x=, y=, w=, h=) |
ui.Button("เริ่มเกม", x=300, y=300, w=200, h=60) |
ปุ่มเริ่ม / ปุ่มออก |
ui.Dropdown(text, x=, y=) |
ui.Dropdown("ง่าย\nปานกลาง\nยาก", x=300, y=124) |
เลือกระดับความยาก |
ui.Checkbox(text, x=, y=, value=1) |
ui.Checkbox("เปิดเสียงในเกม", x=40, y=190, value=1) |
เปิด/ปิดตัวเลือก |
ui.Label — value คือ "ขนาดฟอนต์" เลือกได้ 14 / 16 / 20 / 24 / 28 ยิ่งเลขมากตัวยิ่งใหญ่ (ไม่ใส่ = ขนาดปกติ)ui.Dropdown ตัวเลือกเขียนต่อกันคั่นด้วย \n (ขึ้นบรรทัด) ส่งเป็น argument แรกui.Checkbox ใส่ value=1 เพื่อให้ ติ๊กไว้ก่อน ตั้งแต่เปิดหน้าระวังจุดต่าง: ใน
Label/Buttonค่าvalue=คือ ขนาดฟอนต์ แต่ในCheckboxค่าvalue=1คือ สถานะติ๊ก — ตัวเดียวกันคนละความหมายตาม widget
ทุกชิ้นบนจอนี้เป็น widget กดได้ ที่มีข้อความเป็นภาษาไทย — หัวเรื่อง dropdown checkbox และปุ่ม
สระ/วรรณยุกต์ซ้อน 2–3 ชั้นก็ขึ้นถูก ลองนึกถึงคำยาก ๆ:
ตัวอักษรพื้น + สระบน/ล่าง + วรรณยุกต์ วางซ้อนกันเป็นชั้น ๆ ได้ครบ ไม่หล่นไม่เหลื่อม
font shaping ทำในเฟิร์มแวร์ให้แล้ว — น้อง ๆ พิมพ์ไทยใน string ได้เลย ไม่ต้องตั้งค่าอะไรเพิ่ม
หน้าเมนูต้องรู้ว่า "ใครถูกกด" เราเทียบ handle จาก ui.poll() กับ .id() ของปุ่มที่จำไว้:
start_btn = ui.Button("เริ่มเกม", x=300, y=300, w=200, h=60)
start_id = start_btn.id() # จด id ไว้เทียบ
while True:
for ev in ui.poll(): # list เหตุการณ์ (ว่างได้)
if ev.get("handle") == start_id:
status.text("เริ่มเกม!")
time.sleep_ms(50)

ไฟล์ฝึก: practise_codes/s02_thai_menu.py↗ — มองหาคำว่า "เติม:" (มี 2 จุด)
ส่วนที่น้อง ๆ ทำเอง (30%)
ลองต่อยอด:
value= ดูตัวเล็ก-ใหญ่เมนูนี้คือ "หน้าแรก" ที่ดึงเข้าสนาม Snake — คาบหน้าเราจะให้งูเดินจริงในสนามที่วาดไว้
คาบนี้: ภาพนิ่ง
วันนี้เราจับ 3 อย่างให้อยู่หมัด: กริด → พิกเซล, งูคือ list ของช่อง, และ หัว = index 0
คาบ 3 — Snake ที่เล่นได้จริง:
กฎ "หัว = index 0" ที่เราจำวันนี้ คือหัวใจของคาบหน้า
เมื่อเข้าใจ "ช่อง → กล่อง" แล้ว คาบหน้าจะง่ายขึ้นมาก ลองทบทวนสูตรแปลงพิกัดมาก่อนนะ
snake_step1.py — อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เองเฉลยนี้ไม่ได้มีไว้ลอกวางส่ง คะแนนของคาบอยู่ที่ใบงานและการอธิบายด้วยคำพูดของน้องเอง วิธีใช้ให้ได้ผลจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง ตอนพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อจากนี้เราจะแกะเฉลยทีละส่วน ไม่ใช่ดูรวดเดียว เพราะทุกบรรทัดมีเหตุผลของมัน และทุกเหตุผลโยงกลับไปหาแนวคิดที่เราคุยกันไปแล้วในคาบนี้
ส่วนแรก — ตั้งค่ากริดก่อน โดยถามจอเอง ไม่เดา:
import bentogame as game
CELL_PX = 26 # 1 ช่องกริด = 26 พิกเซล
GRID_COLS = game.WIDTH // CELL_PX # หน้าจอกว้างกี่ช่อง
GRID_ROWS = game.HEIGHT // CELL_PX # หน้าจอสูงกี่ช่อง
game.title("SNAKE") # หน้าเริ่ม: Start=เล่น Back=ออก (ทำ start ให้ในตัว)
CELL_PX = 26 (:9) คือค่าคงที่ตัวเดียวที่คุมทั้งเกม — ตรงกับสไลด์ "สูตรเดียวที่ต้องจำ: ช่อง → พิกเซล" ถ้าแก้เลขนี้เลขเดียว กริดทั้งจอย่อ/ขยายตามหมด นี่คือเหตุผลที่เราตั้งชื่อมันไว้ ไม่โปรยเลข 26 กระจายทั่วโค้ดGRID_COLS = game.WIDTH // CELL_PX (:10-11) ใช้ // หารปัดลง เพราะ "จำนวนช่อง" ต้องเป็นจำนวนเต็ม เศษพิกเซลปัดทิ้ง ทำไมไม่พิมพ์ 30 ลงไปตรง ๆ? เพราะให้โค้ดคำนวณจากขนาดจอจริง (game.WIDTH) เอง — นิสัย "ถามฮาร์ดแวร์ก่อน อย่าเดา" อันเดียวกับที่คาบ 1 เราถาม gpio.num_leds() ก่อนสั่งไฟgame.title("SNAKE") (:13) ทำหน้าเริ่ม Start/Back ให้ในตัว — นี่คือส่วน 70% ของ engine จากสไลด์ 70/30 น้องได้หน้าเริ่มมาฟรี ไม่ต้องเขียนเองเห็นไหมว่าสามบรรทัดแรกไม่ได้วาดอะไรเลย มันแค่ "ตั้งหน่วยวัด" ของทั้งเกมให้ชัดก่อน แล้วที่เหลือค่อยพูดภาษาช่องได้สบาย
# งู = list ของช่อง [คอลัมน์, แถว] โดย "หัวงูอยู่ช่องแรกของ list" เสมอ — จำกฎนี้ไว้ให้ดี
snake_body = [[6, 8], [5, 8], [4, 8]]
# แปลง "ช่องกริด" ของแต่ละปล้อง -> กล่องสีเขียวบนจอ (ช่อง x 26 พิกเซล = ตำแหน่งจริง)
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) # หัวงูสีอ่อนกว่า ให้เห็นชัดว่าหัวอยู่ไหน
snake_body (:15-16) เป็น list ซ้อน list — ตรงกับแนวคิดที่ 3 "งูคือ list ของช่อง" หัว [6, 8] อยู่ index 0 ตามกฎ เราเก็บงูเป็น ช่อง (int) ไม่ใช่พิกเซล ด้วยเหตุผลจากสไลด์ "ทำไมต้องทีละช่อง": การเดิน/ชน/วางอาหาร คิดบนตารางช่องง่ายกว่ามากbody_squares (:19-20) คือ list comprehension (แนวคิดที่ 4) อ่านว่า "สำหรับทุก col, row ใน snake_body สร้าง game.Box หนึ่งกล่อง" — จุดนี้เองที่สูตร ช่อง → พิกเซล ทำงานจริง: col * CELL_PX, row * CELL_PX ถ้าลืมคูณตรงนี้ งูจะกองมุมซ้ายบน (ช่อง 6 กลายเป็นพิกเซล 6)CELL_PX - 2? เว้นขอบ 2px ให้เห็นแต่ละปล้องแยกกันชัด ถ้าใช้ 26 เต็ม กล่องจะชิดกันเป็นแท่งเดียว มองไม่ออกว่ากี่ปล้องbody_squares[0].set_color(game.GB_LIGHT) (:21) เปลี่ยนสีหัว หลัง สร้างเสร็จ ทำไมต้อง [0]? เพราะกฎ "หัว = ช่องแรก" ถ้าจะทำให้หัวสีอ่อนตั้งแต่ใน list-comp ต้องใส่ if ในลูป ซับซ้อนกว่า แก้ทีหลังบรรทัดเดียวอ่านง่ายกว่าจับจังหวะตรงนี้ให้ดี: เราเก็บ state เป็นช่อง (สมองอ่านง่าย) แล้วค่อยแปลงเป็น พิกเซล ตอนวาด สองโลกนี้แยกกันชัด — เป็นนิสัยที่จะติดตัวไปทุกเกม
food_square = game.Box(7 * CELL_PX, 4 * CELL_PX, CELL_PX - 2, CELL_PX - 2, game.GB_LIGHTEST)
score = 0
score_text = game.Text("Score: 0", 10, 8, game.WHITE)
# ----- เติมส่วนนี้เอง -----
# ลองขยับ snake_body เริ่มต้นไปวางตรงอื่น หรือเปลี่ยนสีหัวงู ดูว่าหน้าจอเปลี่ยนตามไหม
# ภาพยังนิ่งอยู่ คาบหน้าเราจะเติม logic เดินใน update() (Back = ออก / Start = เริ่มใหม่ จัดการให้โดย game.run())
def update():
pass # ทุกเฟรมไม่ต้องทำอะไร ภาพเลยนิ่ง
game.run(update, fps=9)
food_square (:23) ใช้สูตรช่อง → พิกเซลเดียวกัน 7 * CELL_PX, 4 * CELL_PX วางอาหารคนละช่องกับงู สี game.GB_LIGHTEST ให้ต่างจากตัวงู มองออกทันทีว่าอันไหนอาหารscore = 0 (:24) กับ score_text (:25) แยกกัน: score คือค่า (int) score_text คือข้อความบนจอ คาบหน้าพอกินอาหารจะ score += 1 แล้วค่อยสั่ง score_text อัปเดต — แยก "ค่า" ออกจาก "การแสดงผล" สังเกตว่า game.Text("Score: 0", 10, 8, ...) ใช้พิกัด พิกเซลตรง ๆ ไม่ใช่ช่อง เพราะ HUD ลอยอิสระ ไม่ได้อยู่บนกริดของสนาม# ----- เติมส่วนนี้เอง ----- (:27-28) คือ 30% ของน้อง จากสไลด์ 70/30 — แค่ขยับ snake_body หรือเปลี่ยนสีหัว ดูจอเปลี่ยนตามdef update(): pass (:31-32) — render loop วนเรียก update() ทุกเฟรม แต่ในนี้ไม่แก้ state อะไรเลย ภาพจึงนิ่ง (ตรงกับสไลด์ผังทั้งโปรแกรม) คาบหน้าเราจะ "เปิด" กล่องนี้ ใส่ตรรกะเดินแทน passgame.run(update, fps=9) (:34) ต้องปิดท้ายไฟล์ ถ้าลืม โปรแกรมจบทันที งูแว้บเดียวแล้วจอเด้งกลับ Home — ตรงกับกับดักที่เราเตือนไว้โปรแกรมนี้ครบทุกชิ้นของ "ฉากนิ่ง" แล้ว: กริด งู อาหาร คะแนน ลูป — ที่ยังไม่มีคือ "การเปลี่ยนแปลง" ซึ่งเราจงใจเว้นไว้ให้คาบหน้า
โค้ดทั้งไฟล์ไม่ได้กองรวมกันมั่ว ๆ มันไต่จาก "ตั้งหน่วยวัด" → "เก็บ state" → "แปลงเป็นภาพ" → "ฉากประกอบ" → "ลูป" ทีละชั้น แต่ละชั้นยืนบนชั้นก่อนหน้า:
| ชั้น | บรรทัด | สร้างอะไร | ต่อยอดจากชั้นก่อน |
|---|---|---|---|
| 1 หน่วยวัดกริด | :9-11 |
CELL_PX + GRID_COLS/ROWS |
ถาม game.WIDTH/HEIGHT ของ engine |
| 2 state ของงู | :15-16 |
snake_body (list ของช่อง) |
ใช้หน่วย "ช่อง" ที่ชั้น 1 นิยามไว้ |
| 3 แปลงเป็นภาพ | :19-21 |
body_squares (list-comp + ช่อง→พิกเซล) |
คูณ state ชั้น 2 ด้วย CELL_PX ชั้น 1 |
| 4 ฉากประกอบ | :23-25 |
อาหาร + คะแนน | สูตรเดิมชั้น 3 + game.Text |
| 5 ลูป | :31-34 |
update(): pass + game.run |
วาดทุกอย่างชั้น 2-4 ซ้ำทุกเฟรม |
update() ชั้นเดียว ชั้น 1-4 อยู่เหมือนเดิมทุกบรรทัด — เกมทั้งเกมโตขึ้นจากการ "เปิดกล่อง update()" ไม่ใช่รื้อของเก่าอยากให้เห็นแบบนี้: การสร้างเกมคือการ ต่อชั้น ไม่ใช่เขียนรวดเดียว เริ่มจากฉากนิ่งที่รันได้ก่อน แล้วค่อยเติมการเคลื่อนไหวทีละชั้น เหมือนที่คาบ 1 เราต่อไฟจากท่าเดียวไปห้าท่า
หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องไวยากรณ์ แต่เรื่อง "มองให้ทะลุ" ว่างูสามปล้องบนกริดที่เราเพิ่งวาด จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — ช่องนี้ยืนอยู่บนอะไร ย้อนไปสไลด์ "NES PPU: tile & nametable" ต้นคาบ NES วาดทั้งจอจาก tile ซ้ำ ๆ ที่อ้างด้วย "เลขช่อง" ใน nametable ไม่เก็บทุกพิกเซล เพราะ memory กับ timing บีบ ทีนี้ลองถามตัวเอง: snake_body ที่เก็บ [[6,8],[5,8],[4,8]] มันต่างจาก nametable ที่เก็บเลขช่องตรงไหน? คำตอบคือ แนวคิดเดียวกัน เราเก็บ "เลขช่อง" แล้วค่อยแปลงเป็นกล่องตอนวาด ผ่านสูตร ช่อง → พิกเซล จากสไลด์ "สูตรเดียวที่ต้องจำ" ซึ่งก็คือ unit conversion ตัวเดียวกับที่เราแปลงค่า ADC เป็นโวลต์ตอนอ่านเซนเซอร์
ที่ไป — ช่องนี้จะโตเป็นอะไร กฎ "หัว = index 0" กับ "งูคือ list ของช่อง" ที่เราจำวันนี้ ไม่ได้มีไว้แค่วาดภาพนิ่ง คาบหน้ามันจะกลายเป็นเครื่องยนต์ของการเดิน: แทรกช่องใหม่ที่ หัว (index 0) แล้วตัดช่องท้ายทิ้ง งูก็เลื้อยได้โดยไม่ต้องขยับทุกปล้อง เมื่อ update() เริ่มแก้ snake_body ทุกเฟรม ฉากนิ่งของเราก็มีชีวิต — นี่คือ การจัดการสถานะ (state management) ที่สไลด์ "ทำไมต้องทีละช่อง" เกริ่นไว้
ลองตอบสามคำถามนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
col * CELL_PX ที่แปลงช่องเป็นพิกเซลได้ไหม ถ้าวันหน้าเราวาด แผนที่ tile ทั้งจอ ด้วยสูตรเดียวกันนี้ มันคือเรื่องเดียวกับ nametable ของ NES หรือเปล่า?snake_body ได้ พรุ่งนี้พอ update() แทรกที่หัวแล้วตัดหาง งูจะ เดิน ได้ไหม โดยที่โค้ดวาด (ชั้น 3) ไม่ต้องแก้เลย?ถ้าตอบได้ว่า "อ๋อ nametable กับ snake_body คือเรื่องเดียวกัน" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด: การเก็บโลกเป็นเลขช่อง แล้วค่อยเรนเดอร์ คือรากของทั้งกราฟิกเกม ของที่ดูนิ่งวันนี้คือเมล็ดของงูที่จะเลื้อยพรุ่งนี้
เทคนิควันนี้ไม่ใช่แบบฝึกหัดลอย ๆ มันคือวิธีที่ระบบจริงใช้วาดภาพและจัดโลกกันทุกวัน:
snake_body ที่เก็บช่องแล้วค่อยวาด ตรงกับสไลด์ NES PPU — เก็บเลข ไม่เก็บพิกเซล ประหยัดทั้ง memory และเวลา(col, row) ก็คือสูตร ช่อง → พิกเซล (col * CELL_PX) ที่เราคูณวันนี้ ขยายจริงบนฮาร์ดแวร์ห้าสไลด์แนวคิดที่เราเรียนวันนี้ ไม่มีอันไหนเป็นของสมมติ ทุกอันมีระบบจริงที่วางภาพและวางโลกด้วย "ช่องเป็นเลข" แบบเดียวกันทำงานอยู่
เอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาของจริงได้ และข้อสุดท้ายคือสะพานไปคาบหน้า:
snake_body (list ของช่อง) ได้ไหม แล้ววาดด้วย list-comp จาก game.Box เหมือนตัวงูเลยหรือเปล่าgame.GB_DARK / GB_LIGHT / GB_LIGHTEST ถ้าอยากสลับธีมทั้งเกมด้วยการแก้ที่เดียว จะจัดโครงตัวแปรสียังไง (นึกถึง CELL_PX ที่คุมทั้งจอจากเลขเดียว)CELL_PX จาก 26 เป็น 20 หรือ 40 จำนวนช่อง (GRID_COLS/ROWS) กับความรู้สึกตอนเล่นจะเปลี่ยนไปทางไหน ช่องเล็กลง = ยากขึ้นหรือง่ายขึ้น ลองเดาก่อนแล้วรันจริงเทียบ[6, 8] แล้วเดินไปทางขวา ช่องใหม่ที่แทรกหน้า snake_body คือช่องอะไร แล้วเขียนแทรก/ตัดยังไงโดยไม่ต้องแตะโค้ดวาด (ชั้น 3)เลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากช่องที่เราวาดเองวันนี้ — โดยเฉพาะข้อสุดท้าย ถ้าคิดออกก่อน คาบหน้างูจะเลื้อยในหัวน้องก่อนเลื้อยบนจอ
fit-css