Extra — Co-op ยิง Boss ร่วมกัน

WiFi + MQTT: เลือดบอสก้อนเดียว ทุกบอร์ดช่วยกันยิง

โปรเจกต์เสริม — ต่อยอดจากคาบ WiFi + MQTT
หลายบอร์ด · 1 broker · เกมเดียวที่ทุกคนเล่นพร้อมกัน

จากคาบที่แล้วเราส่ง "คะแนน" หากันได้ คราวนี้เราจะแชร์ "เลือดบอส" ก้อนเดียวกัน ทุกคนยิงตัวเดียวกันให้ล้ม

How Multiplayer Games Work — Nick Maltbie · เกมหลายผู้เล่นแชร์สถานะก้อนเดียวกันได้อย่างไร — แนวคิดเดียวกับที่ทุกบอร์ดช่วยกันยิงบอสตัวเดียว

เป้าหมายวันนี้

เราจะทำเกม co-op ง่าย ๆ ที่บอร์ดทุกตัว "ช่วยกันยิง Boss ตัวเดียว" ผ่านเครือข่าย

กดยิง = ส่งดาเมจ
กดปุ่ม keys().apublish ดาเมจไป game/boss/dmg
ทุกบอร์ดหัก HP เดียวกัน
subscribe topic เดียวกัน → หัก HP ที่แชร์ → วาดหลอดเลือดให้ตรงกันทุกจอ

use case จริง: นี่คือแก่นของ multiplayer แบบ shared state — ผู้เล่นหลายคนแก้ข้อมูลก้อนเดียวกันแบบ real-time เหมือน raid boss ในเกมออนไลน์ หรือแดชบอร์ดโรงงานที่หลายสถานีอัปเดตตัวเลขรวมพร้อมกัน

ตอนจบ: ทุกจอเห็นหลอด HP ลดพร้อมกัน และขึ้น "BOSS DOWN!" พร้อมกันเมื่อเลือดหมด

ภาพรวม: pub/sub กับ shared HP

ไม่มีบอร์ดไหนเป็นเจ้าของเลือด ทุกบอร์ดส่งดาเมจเข้า broker แล้ว broker กระจายให้ทุกคนหักเท่ากัน

บอร์ด A กดยิง -10 Broker game/boss/dmg :1883 บอร์ด B กดยิง -10 publish dmg publish dmg broker เด้งดาเมจกลับให้ทุกคน (เส้นประ) → ทุกบอร์ดหัก HP เท่ากัน → หลอดเลือดตรงกัน

payload ที่ส่งคือสตริงสั้น ๆ: "board-01:10" = ใครยิง : ดาเมจเท่าไร

กับดักที่ต้องเข้าใจ: หัก HP ตอน "รับ" ไม่ใช่ตอน "กด"

จุดที่ทำให้ทุกจอเห็นเลขตรงกันเป๊ะ อยู่ตรงนี้ — เรา ไม่หัก HP ตอนกดปุ่ม แต่หักตอนข้อความเด้งกลับมาจาก broker

เพราะ broker ส่งดาเมจของเราเองกลับมาด้วย (echo) ถ้าเราหักตอนกดอีก HP จะโดนหักสองเด้ง แล้วเลขจะเพี้ยนจากเพื่อน เราจึงเลือกหัก "ที่เดียว" คือตอนรับ ทุกบอร์ดจึงเดินเลขเดียวกันเสมอ

นี่คือหลัก single source of truth — มี "ทางเข้า" ของการเปลี่ยนสถานะแค่ทางเดียว ทุกคนถึงไม่ทะเลาะกันเรื่องเลข

เดินโค้ด (1) — ต่อเน็ตแล้วสมัครรับ topic

ต่อ WiFi ก่อนเสมอ แล้วค่อยต่อ MQTT จากนั้น subscribe ทั้ง topic ดาเมจและ topic HP

    if not mqtt.connect(BROKER, PORT, client_id=PLAYER):
        print("ต่อ broker ไม่ได้")
        raise SystemExit
    mqtt.subscribe(TOPIC_DMG)
    mqtt.subscribe(TOPIC_HP)
    print("ต่อ broker แล้ว ทุกบอร์ดอยู่ห้องเดียวกัน")

client_id=PLAYER ต้องไม่ซ้ำกันทั้งห้อง ไม่งั้น broker เตะกันออก

ทางลัด: ต่อ WiFi ผ่านเมนู Wi-Fi บนจอก่อนครั้งเดียว แล้ว wifi.connect() ในโค้ดจะผ่านทันที

เดินโค้ด (2) — กดยิง = publish ดาเมจ

อ่านปุ่มยิงด้วย game.keys().a แล้วนับเฉพาะ "ขอบขาลง" เพื่อยิงทีละครั้ง (กดค้างไม่รัว)

    fire = game.keys().a
    if fire and not prev_fire:
        mqtt.publish(TOPIC_DMG, "%s:%d" % (PLAYER, HIT_DAMAGE))
        game.sfx("fire")
    prev_fire = fire

prev_fire คือเทคนิค edge-detection เดียวกับปุ่ม FSM ในคาบต้น ๆ — กันยิงซ้ำตอนกดค้าง

เดินโค้ด (3) — รับดาเมจแล้วหัก HP ร่วมกัน

get_message() ไม่บล็อก (ไม่มีของก็คืน None) เกมจึงลื่น เราหัก HP แล้ววาดหลอดใหม่

    msg = mqtt.get_message()
    if msg is not None:
        topic, data = msg
        if topic == TOPIC_DMG:
            _, dmg = data.decode().split(":")
            apply_damage(int(dmg))
            game.sfx("hit")

apply_damage() หัก boss_hp แล้วย่อความกว้างหลอดด้วย bar_fg.resize(...) ให้ตรงกันทุกจอ

หมายเหตุ: ดาเมจของเราเองก็เด้งกลับมาทาง broker เราจึงหัก HP จาก "ข้อความที่รับ" ที่เดียว — ทุกบอร์ดเห็นเลขเดียวกันเป๊ะ

เสียงทำให้ Co-op มีชีวิต

โค้ดเราเรียกเสียงสองจังหวะ: game.sfx("fire") ตอนกดยิง และ game.sfx("hit") ตอนดาเมจเด้งกลับมาหัก HP เสียงทุกคนจึงดังพร้อมกันเวลาช่วยกันรัว ตอนเลือดบอสหมดค่อยปิดท้ายด้วยเสียงระเบิด

เสียงในคาบนี้ — เกมยิง Boss ใช้เสียงตอนกดยิง (fire) ตอนยิงโดน (hit) และเสียงระเบิดตอนบอสล้ม (กดเล่นฟังได้จริง):

shoot_fire

shoot_hit

shoot_explode

ฟังครบ 21 ตัว + รูปคลื่น/ซูม/spectrogram → Sound Explorer

เสียงไม่ได้มีแค่ "ดัง" — มันมี รูปร่าง ดูเสียงระเบิดตอนบอสล้มเป็น spectrogram (แกนนอน=เวลา แกนตั้ง=ความถี่ สีสว่าง=ดัง) จะเห็นพลังงานปะทุเป็นชั้น ๆ แล้วค่อยจางหายไป:


ที่มา: spectrogram ของเสียง shoot_explode สร้างจาก bento_sfx (BENTO course asset)

ลองสลับเสียงระเบิดไปใส่ตอน "BOSS DOWN!" เพื่อให้จังหวะปิดเกมหนักแน่นขึ้น — ทุกจอจะได้ยินพร้อมกันเพราะวาดจากเลขก้อนเดียวกัน

วิธีรัน + ต้องใช้กี่บอร์ด

วิธีรัน: เปิด codes/ex_a4_coop_boss.py ใน BENTO IDE แล้วกด Program to Device (ปุ่มเขียวมุมซ้ายบน) — โค้ดเดียวกันนี้ลงได้ทุกบอร์ด


ปุ่ม Program to Device มุมซ้ายบนของ BENTO IDE — กดแล้วเซฟเป็น main.py ลงบอร์ดและรันทันที (เห็น "Programming main.py to device" ใน Terminal) · ที่มา: BENTO IDE (course asset)

อย่างน้อย 2 บอร์ด
ตั้ง PLAYER คนละชื่อ ("board-01", "board-02")
TOPIC และ BROKER เหมือนกันทุกตัว
ยิ่งหลายบอร์ดยิ่งสนุก
HP บอส = 500, ยิงทีละ 10
หลายมือช่วยกันยิง บอสล้มเร็วขึ้น

เกณฑ์ผ่าน: กดยิงแล้วหลอด HP ทุกจอ ลดพร้อมกัน และขึ้น "BOSS DOWN!" พร้อมกันเมื่อเลือดหมด

ยิ่งหลายบอร์ด ยิ่งล้มเร็ว — ดูเป็น "เลข"

หลอด HP ที่เราวาด คือ HP ที่เหลือเทียบกับเต็ม คิดเป็นสมการความกว้างตรง ๆ:

wหลอด=600×hpHPmax(พิกเซล)w_{\text{หลอด}} = 600 \times \dfrac{hp}{HP_{\max}} \qquad(\text{พิกเซล})

ดาเมจรวมต่อวินาที = จำนวนบอร์ด NN คูณดาเมจต่อนัด dd คูณความถี่ยิง ff ดังนั้นเวลาล้มบอส:

tkill=HPmaxNdft_{\text{kill}} = \dfrac{HP_{\max}}{N \cdot d \cdot f}

เพราะ NN อยู่ที่ "ตัวหาร" เวลาจึงลดแบบ 1/N1/N — จาก 1 เป็น 2 บอร์ดเวลาหายครึ่ง แต่จาก 5 เป็น 6 แทบไม่ต่าง นี่คือเหตุผลว่าทำไม "เพื่อนคนแรก ๆ ที่มาช่วย" สำคัญที่สุด

หน้าตาเวลารัน: หลอดเดียว ลดพร้อมกัน

ทุกจอวาดจากเลขเดียวกัน (HP ที่รับจาก broker) หลอดจึงเดินตรงกันเป๊ะ จนขึ้น BOSS DOWN พร้อมกัน

บอร์ด A หรือ B ยิงทีไหน ดาเมจวิ่งผ่าน broker แล้วเด้งกลับให้ทุกคนหัก HP ก้อนเดียวกัน — ไม่มีใครเห็นเลขต่างกันเลย

ลองจินตนาการเปลี่ยน "หลอด HP" เป็น "ยอดผลิตรวมของไลน์" หรือ "อุณหภูมิเฉลี่ยของฟาร์ม" ภาพก็เหมือนกันเป๊ะ

ต่อยอด + โลกจริง

สิ่งที่เราเพิ่งทำคือ shared real-time state ผ่าน MQTT — แก่นเดียวกับงานจริงหลายแบบ

งาน LAN party (DreamHack) — คนนับพันต่อเครื่องเข้าเครือข่ายเดียว เล่นเกมเดียวกันพร้อมกัน คือภาพจริงของ "หลาย node หนึ่งเครือข่าย หนึ่งสถานะร่วม"
ที่มา: "Winter 2004 DreamHack LAN Party" — Toffelginkgo, CC BY-SA 3.0, Wikimedia Commons
  • เกมออนไลน์: raid boss / co-op หลายคนตีมอนสเตอร์ตัวเดียวกัน คือ pattern เป๊ะ ๆ แบบนี้
  • Industry 4.0: หลายสถานีในไลน์ผลิตอัปเดต "ยอดรวม/สถานะเครื่อง" เข้า topic เดียว แดชบอร์ดเห็นพร้อมกัน
  • IoT บ้าน: หลายสวิตช์/เซนเซอร์คุมหลอดไฟดวงเดียวกันผ่าน broker

ลองทำเพิ่ม (เลือก 1):

  • ใช้ game.tilt() แทนปุ่ม: เอียงบอร์ดแรง = ดาเมจมากขึ้น
  • เพิ่ม HP สูงสุดตามจำนวนผู้เล่น แล้วโชว์ชื่อคนที่ยิงทีสุดท้าย (MVP)
  • เปิด LED ด้วย gpio.led(0) กะพริบทุกครั้งที่ยิงโดน

โปรเจกต์นี้พิสูจน์ว่าบอร์ดเล็ก ๆ ก็เป็น node ในระบบ multiplayer/IoT จริงได้ ด้วยโค้ดไม่ถึง 80 บรรทัด

เชื่อมโยงรากฐาน

เกม co-op เกมเดียวนี้ ร้อยวิชาพื้นฐานเข้าด้วยกันหมด — ลองมองทะลุเกมไปที่วิศวกรรมข้างใต้:

เกม Co-op ยิง Boss ร่วมกัน Python string-parse · edge-detect update() = callback Algorithms single source of truth สเกล 1/N Embedded / Networking GPIO → pub/sub
Python
data.decode().split(":") คือ string parsing · fire and not prev_fire คือ edge-detection ด้วยตัวแปรจำสถานะ · update() ที่ game.run เรียกทุกเฟรม คือ callback
Algorithms
single source of truth + การลู่เข้าของ shared state (ทุก node ได้เลขเดียวกัน) · เวลาล้มบอสแบบ $1/N$ คือการวิเคราะห์สเกลของระบบ
Embedded / Networking — บอร์ด PSoC Edge E84 ต่อ WiFi เป็น node บนเครือข่าย แล้วคุยกันด้วย pub/sub ผ่าน broker · ปุ่มยิงคือ GPIO อ่าน digital input · เลเยอร์เดียวกับที่เซนเซอร์โรงงานส่งค่าขึ้นห้องควบคุม

เล่นเป็นเกมวันนี้ พรุ่งนี้สเกลขึ้นเป็นระบบจริงได้เลย เพราะรากฐานมันตัวเดียวกัน

fit-css

← Extra Index