ทุกครั้งที่เครื่องเกมเพิ่มช่องทาง I/O ใหม่ (CD-ROM บน PlayStation, WiFi บน DS) มันเปิดเกมแบบใหม่ทั้งหมดที่เครื่องเดิมทำไม่ได้
เชื่อมกับวันนี้: การที่บอร์ดน้องคุยกันผ่าน network (WiFi/MQTT) ก็เปิดเกม multiplayer ที่เครื่องเดี่ยวทำไม่ได้

PlayStation รุ่นแรก (SCPH-1000, ปี 1994) — ฝาด้านบนคือช่อง CD-ROM ซึ่งเป็น I/O ใหม่ที่จุเกมได้ใหญ่กว่าตลับมาก เปิดทางให้เกมมีหนัง เสียงพากย์ และฉาก 3D · ที่มา: "PlayStation SCPH-1000 with Controller" — Evan-Amos, Public domain, Wikimedia Commons
The Original PlayStation Changed Gaming Forever! — RES - Retrospective · ดูว่าการเปลี่ยนจากตลับมาเป็น CD-ROM (I/O ใหม่) เปิดเกมแบบใหม่ทั้งหมดได้อย่างไร — หลักการเดียวกับที่ network เปิดเกม multiplayer ให้บอร์ดเรา · ที่มา: en.wikipedia.org/wiki/PlayStation_technical_specifications
นี่คือตู้ PONG ของ Atari ตัวจริง เกมที่เราจะทำวันนี้ก็คือเกมนี้แหละ สังเกตแผงควบคุม: มี PLAYER 1 กับ PLAYER 2 อยู่ติดกันบนตู้เดียว ปุ่มหมุนคนละอันคุมไม้ตีคนละข้าง

ที่มา: "Atari Pong arcade game front" — spablab, CC BY 2.0, Wikimedia Commons
สมัยนั้นสองคนต้องมายืน ข้างกัน หน้าตู้เดียว สิ่งที่เราจะทำต่างออกไปนิดเดียวแต่สำคัญมาก คือย้าย P1 กับ P2 ไปอยู่ คนละบอร์ด คนละจอ แล้วให้คุยกันผ่าน network — หลักการเล่นเหมือนเดิมเป๊ะ เปลี่ยนแค่ "ตำแหน่งไม้" เดินทางผ่านสายแทนที่จะอยู่ในเครื่องเดียว
Pong (1972 Atari Arcade Game) — Gameplay, 50th Anniversary — AlphaYellow · ไม้สองข้างกับลูกบอลที่เด้งไปมา — นี่คือการเคลื่อนไหวที่เราจะสร้างขึ้นใหม่ให้เล่นข้ามสองบอร์ด
หัวใจคือ topic สลับฝั่ง: P1 ส่งเข้า /p1 และฟัง /p2, ส่วน P2 ทำสลับกันเป๊ะ
ถ้าจำสองบรรทัดนี้ได้ก็เข้าใจทั้งโปรเจกต์: ฉันส่งเข้า topic ของฉัน, ฉันฟัง topic ของอีกคน
ลองดูข้อความ วิ่งจริง — แพ็กเก็ต y:120 เดินทางจากบอร์ดหนึ่งผ่าน broker ไปอีกบอร์ด แล้วของอีกฝั่งก็สวนกลับมาในเส้นทางสลับฝั่ง:
ก้อนน้ำเงินคือไม้ของ P1 วิ่งไปขึ้นจอ P2, ก้อนชมพูคือไม้ของ P2 สวนกลับมาขึ้นจอ P1 — คนละ topic แต่ broker ตัวเดียว เป็นตัวกลางส่งต่อให้
test.mosquitto.org ไม่ใช่กล่องในรูปวาด แต่เป็น เซิร์ฟเวอร์จริง ที่รันอยู่ใน data center แบบนี้ คอยรับ-ส่งข้อความให้ใครก็ได้บนอินเทอร์เน็ต · ที่มา: "Datacenter Server Racks" — Carl Lender, CC BY 2.0, Wikimedia Commons
MQTT QoS - Quality of Service | MQTT Essentials Part 7 — HiveMQ — อธิบายระดับ QoS 0/1/2 ว่าต่างกันอย่างไร เลือกใช้แบบไหนตอนส่งตำแหน่งไม้ตีให้คู่ต่อสู้ทันเกม
ลำดับเหมือนคาบที่แล้ว — ต่อ WiFi ก่อน แล้วค่อย MQTT แล้ว subscribe topic ของคู่ต่อสู้
TOPIC_ME = "game/room1/p1" # P1 ส่งตำแหน่งไม้ตัวเองที่นี่
TOPIC_YOU = "game/room1/p2" # P1 ฟังไม้ของ P2 ที่นี่
CLIENT_ID = "bento-pong-p1" # ต้องไม่ซ้ำกับอีกบอร์ด
# ต่อ WiFi (ถ้า auto-connect อยู่แล้วจะผ่านเร็ว)
for _ in range(6):
if wifi.is_connected() or wifi.connect(SSID, PASSWORD):
break
time.sleep(2)
# ต่อ MQTT แล้วขอฟังไม้ของอีกฝั่ง
mqtt.connect(BROKER, PORT, client_id=CLIENT_ID)
mqtt.subscribe(TOPIC_YOU)
ลำดับ 3 ขั้นนี้ ห้ามสลับ — ต้องมี IP จาก WiFi ก่อน แล้วค่อยต่อ MQTT แล้วจึงขอ subscribe ฟังไม้ของคู่ต่อสู้:
ทางลัด: ต่อ WiFi ผ่านเมนู Wi-Fi บนจอ ครั้งเดียว (เซฟรหัสไว้) แล้ว
wifi.connect()จะผ่านทันทีเพราะบอร์ด auto-connect ให้แล้ว
หัวใจอยู่ใน update() ที่ game.run เรียกทุกเฟรม: คุมไม้ตัวเอง → ส่งเมื่อ เปลี่ยน → รับของเพื่อนมาวาด
def update():
global last_sent
k = game.keys()
if k.up: my_paddle.move(0, -SPEED)
if k.down: my_paddle.move(0, SPEED)
# ส่งเฉพาะตอน y เปลี่ยน จะได้ไม่ถล่ม broker ทุกเฟรม
y = int(my_paddle.y)
if y != last_sent:
mqtt.publish(TOPIC_ME, "y:%d" % y)
last_sent = y
# รับไม้ของอีกฝั่ง (ไม่บล็อก: ไม่มีของก็คืน None)
msg = mqtt.get_message()
if msg is not None:
_topic, data = msg
text = data.decode() # payload เป็น bytes
if text.startswith("y:"):
opp_paddle.move_to(opp_paddle.x, int(text[2:]))
game.run(update, fps=30)
ไฟล์ p2 คือกระจกเงา: แค่สลับ TOPIC_ME/TOPIC_YOU และวางไม้ตัวเองไว้ฝั่งขวา
โค้ดเมื่อกี้อ่านเป็นบรรทัด ๆ อาจจะยังนึกภาพไม่ออกว่ามันวนยังไง ลองดูเป็นแผนภาพ — ทุกเฟรม game.run เรียก update() หนึ่งครั้ง แล้วทำ 3 อย่างตามนี้

ลองดูลำดับ หนึ่งเฟรม เดินทีละขั้น (กล่องที่ "ทำงานอยู่" จะสว่างขึ้น) แล้ววนกลับไปเฟรมถัดไป:
จุดที่อยากให้สังเกตคือ ทางแยกตรงกลาง: เราไม่ได้ publish ทุกเฟรม แต่ publish เฉพาะตอน y เปลี่ยนเท่านั้น ส่วนการรับใช้ get_message() ที่ ไม่บล็อก — ถ้ายังไม่มีข้อความเข้ามาก็คืน None เกมเดินต่อได้ทันที ไม่ค้างรอเพื่อน
ลองคิดเลขกัน ถ้าเรา publish ทุกเฟรมที่ fps = 30 จำนวนข้อความต่อวินาทีคือ
ทั้งที่ไม้ตี ส่วนใหญ่อยู่นิ่ง ไม่ได้ขยับ ค่าที่ส่งซ้ำ ๆ ก็เหมือนเดิมเปล่าประโยชน์ พอใส่เงื่อนไข if y != last_sent: จำนวนข้อความจริงจะเหลือเท่ากับ "จำนวนเฟรมที่ขยับ" เท่านั้น

ลองดูแบบ เคลื่อนไหว — เส้นกวาดเวลาวิ่งจากซ้ายไปขวา: เส้นแดงเต็ม 30 ตลอด ส่วนเส้นเขียว (ของเรา) พุ่งขึ้นเฉพาะช่วงที่ไม้ขยับ แล้วหล่นกลับมา 0 ทันทีที่ปล่อยนิ้ว:
เส้นแดงคือถ้าส่งทุกเฟรม (เต็ม 30 ตลอด), เส้นเขียวคือของเราที่พุ่งขึ้นเฉพาะตอนนิ้วกดขยับ แล้วกลับมา 0 ตอนปล่อย นี่คือเทคนิคเดียวกับที่ระบบ IoT จริงใช้ลด traffic เรียกว่า send-on-change (ส่งเมื่อค่าเปลี่ยน) ไม่ใช่ส่งตลอดเวลา
พอเล่นจริงน้องจะสังเกตว่า ไม้ของเราขยับ ทันที (เพราะอ่านปุ่มในเครื่องเรา) แต่ไม้ของเพื่อนบนจอเรา ตามมาช้ากว่านิดหน่อย — ไม่ใช่บั๊ก แต่เป็นธรรมชาติของ network

ตำแหน่งไม้ของเพื่อนต้องเดินทาง บอร์ดเขา → broker → บอร์ดเรา กว่าจะถึงก็ใช้เวลาหลักสิบมิลลิวินาที สิ่งที่เราวาดจึงเป็น "ค่าที่เพิ่งรับมา" ซึ่งช้ากว่าตำแหน่งจริงของเขาตอนนี้นิดนึง (เงาจางในภาพ)
ดูเส้นทางเป็นไทม์ไลน์: ดีเลย์ที่เราเห็นคือ ผลรวมของทุกช่วงต่อ ไม่ใช่จุดเดียว
แต่ละช่วงราวหลักมิลลิวินาทีถึงหลักสิบ พอบวกกันก็เลยเห็นไม้เพื่อน "ตามมาช้านิดนึง" เป็นธรรมดา — เกมออนไลน์ตัวจริงเจอเรื่องนี้เหมือนกัน เขาถึงมีเทคนิคอย่าง interpolation กับ prediction มากลบดีเลย์ให้เนียนขึ้น
Lag Compensation in Online Games Explained — Netduma · เกมใหญ่ใช้ interpolation/prediction กลบดีเลย์ของ network แบบเดียวกับที่อธิบายข้างบนอย่างไร
ต้องใช้ 2 บอร์ด (อย่างน้อย 2 เครื่อง คนละไฟล์):
ex_a2_pong_p1.py กด Program to Deviceex_a2_pong_p2.py กด Program to DeviceCLIENT_ID ไม่ซ้ำกัน
ต่อยอด (เลือก 1):
"y:120;bx:400;by:200" ให้ P1 เป็นเจ้าของลูก แล้ว P2 วาดตามroom1 เป็นชื่อห้องของกลุ่มตัวเอง จะได้แข่งหลายโต๊ะพร้อมกันไม่ชนกันรูปแบบ "ส่ง state ของฉัน–รับ state ของคนอื่น" นี้ขยายไปได้ถึงเกมออนไลน์หลายคน และระบบ IoT/Industry 4.0 ที่อุปกรณ์หลายตัวมองเห็นสถานะกันผ่าน broker กลางตัวเดียว
เกมเล็ก ๆ นี้ดูเหมือนแค่ "ขยับไม้ตี" แต่จริง ๆ มันคือการฝึกรากฐานวิศวกรรมที่ใช้ในงานจริงครบทั้งสี่ด้าน
Embedded/MCU — การสื่อสารผ่าน network stack (WiFi radio → TCP → MQTT) คือ I/O แบบเดียวกับที่เซนเซอร์อุตสาหกรรมส่งค่าขึ้น cloud ส่วน CYW55500 ที่ทำ WiFi บนบอร์ดก็คือชิป radio จริงที่เรากำลังโปรแกรมคุม
Python — update() ที่ถูกเรียกทุกเฟรมคือ callback; การแกะ "y:120" เป็นตัวเลขคือ string indexing + แปลง type (int(text[2:])); การจำค่าเก่าด้วย last_sent คือการใช้ตัวแปร state ข้ามรอบการเรียก
Algorithms — เทคนิค send-on-change (ส่งเมื่อค่าเปลี่ยน) กับการรับแบบ non-blocking คือหัวใจของระบบ real-time ที่ต้องประหยัด bandwidth และห้ามค้างรอ I/O
Graphics — การวาดไม้คู่ต่อสู้คือการ map ค่าตัวเลข y กลับเป็น พิกัดบนจอ และการที่ P2 เป็น "กระจกเงา" ของ P1 คือการสะท้อนระบบพิกัดข้ามแกน
ขยับไม้ตีให้สนุกก่อน แล้วค่อย ๆ เห็นว่าเบื้องหลังมันคือ network + callback + พิกัด — พื้นฐานชุดเดียวกับที่ใช้สร้างผลิตภัณฑ์ IoT จริง
fit-css