คาบ 11 — Shooter #2

กระสุน 6 นัด ยิงได้ไม่รู้จบ

Object Pool (ชุดหมุนเวียน) = สร้างของชุดเดียว แล้วหมุนใช้ซ้ำ
คูลดาวน์ = นับถอยหลังก่อนยิงนัดถัดไป

คาบที่แล้วยานของเราบินได้แบบมีน้ำหนักแล้ว วันนี้ยานจะ ยิงเลเซอร์ ได้ กด A ค้างไว้ก็ยิงได้เรื่อย ๆ ทั้งที่มีกระสุนจริงแค่ 6 นัด

เป้าหมายวันนี้ — กด A แล้วเลเซอร์พุ่ง

ภาพจำลองจากตรรกะ shooter_step3.py ไม่ใช่ภาพถ่ายจอ

สิ่งที่น้อง ๆ จะเห็นบนจอเมื่อทำเสร็จ:

  1. กด A หนึ่งครั้ง → เลเซอร์ฟ้า 1 นัดพุ่งขึ้นจากหัวยาน พร้อมเสียง
  2. กด A ค้าง → ยิงต่อเนื่องไม่หยุด นัดห่างกันเท่า ๆ กัน
  3. กระสุนมีแค่ 6 นัด บนจอจึงไม่มีทางเกิน 6
  4. ยานยังบินซ้าย-ขวาได้เหมือนคาบ 10

ไฟล์ของวันนี้มีไฟล์เดียว: practise_codes/shooter_step3.py↗ · ป้าย Score/Lives ยังไม่ขยับ ศัตรูกับคะแนนมาคาบ 12

ทายก่อน — ถ้าสร้างกระสุนใหม่ทุกครั้งที่กด A?

วิธีที่คิดง่ายที่สุดคือ กด A เมื่อไร ก็สร้างกล่องใหม่ 1 กล่อง:

    if keys.a:                                          # ตัวอย่างที่ "ไม่ควรทำ"
        game.Box(ship_x + 28, 340, 6, 14, game.CYAN)    # กล่องใหม่ทุกเฟรมที่กด A
  • กด A ค้าง = ได้กล่องใหม่ ทุกเฟรม
  • กล่องที่บินพ้นจอไปแล้ว ยังนับเป็นชิ้นบนจอ จนกว่าจะสั่ง delete()

คุยกับเพื่อนข้าง ๆ 1 นาที: เกมนี้จะเป็นอะไรไปก่อน?

(ก) ไม่เป็นไร เครื่องแรงพอ
(ข) เกมกระตุกเป็นพัก ๆ
(ค) สร้างกล่องต่อไม่ได้

คำตอบ — (ค) สร้างกล่องต่อไม่ได้

จอมีที่ให้ 32 ชิ้นรวมทุกอย่าง (bentogame.py:179)
ยาน + HUD + ป้ายปุ่มล่างจอ ใช้ไปแล้ว 3 ชิ้น → กล่องกระสุนได้อีกราว 29 กล่อง
กล่องถัดไป: RuntimeError: ui: max 32 widgets → เกมหยุดทันที · กด A ค้างราว 1 วินาทีก็ถึง
(ข) อาจเกิดด้วย แต่เป็นเรื่องรอง: สร้าง-ทิ้งบ่อย ตัวเก็บขยะ (garbage collector) อาจต้องหยุดเกมไปเก็บ — ยังไม่มีใครวัดบนบอร์ดนี้
ทางออก: สร้างกระสุน 6 นัดครั้งเดียว ก่อนเกมเริ่ม แล้วหมุนใช้ซ้ำไปเรื่อย ๆ
นี่คือ Object Pool (เอกสารไทยบางที่เขียน "พูล") · คาบนี้เรียกว่า ชุดหมุนเวียน

ไม่ใช่เรื่อง RAM เต็ม — เพดานที่ชนจริงคือ "จำนวนชิ้นบนจอ"

ภาพในหัว — ตู้ล็อกเกอร์ที่ห้องสมุดหรือสระว่ายน้ำ

ตู้ล็อกเกอร์ เกมของเรา โค้ด
มีตู้ 6 ใบ ติดตั้งครั้งเดียว กระสุน 6 นัด สร้างก่อนเกมเริ่ม bullets = [game.Box(...) for _ in range(6)]
เดินไล่หาตู้ที่ยังมีกุญแจเสียบ หากระสุนที่จอดรออยู่เหนือจอ find_free_bullet() ดูว่า bullet.y < -20
ไม่มีตู้ว่างเลย ไม่มีนัดว่าง return None
ใช้เสร็จ คืนกุญแจ กระสุนบินพ้นเส้น -20 → ว่างเอง bullet.y < -20 แล้วจึง hide() ให้มองไม่เห็น

จุดที่เปรียบไม่ตรง 2 จุด

  • คนต้องเดินมาคืนกุญแจเอง · กระสุน คืนตัวเอง ทันทีที่บินพ้นเส้น -20
  • ไม่มีตู้ว่าง คน ยืนรอ ได้ · เกม ไม่รอ เฟรมนั้นแค่ยิงไม่ออก แล้วลองใหม่เฟรมหน้า

ชุดกระสุนหมุนเวียนในเกมของเรา

  1. สร้าง 6 นัดก่อนเกมเริ่ม — list นี้ ไม่มีวันยาวขึ้น
  2. แต่ละนัดมีแค่ 2 สถานะ: ว่าง หรือ ใช้อยู่ ดูจากค่า y ค่าเดียว
  3. กด A = ยืมตัวว่าง · บินพ้นจอ = คืนเอง · ไม่มีตัวว่าง = เฟรมนั้นยิงไม่ออก

พูดตามกันดัง ๆ: ว่าง ก็ต่อเมื่อ bullet.y < -20

ภาพรวม — หนึ่งเฟรมของ shooter_step3.py

def on_frame():
    global ship_x, ship_speed, fire_cooldown   # ค่าที่ต้องจำข้ามเฟรม ลืม = UnboundLocalError

① ครั้งเดียวก่อนเกมเริ่ม · ④⑤ ทุกเฟรม · ②③ เฉพาะเฟรมที่กด A และคูลดาวน์เป็น 0 (หาตัวว่างไม่ได้ = ไม่ยิง)

โค้ด ① ② — ให้มาแล้วในไฟล์ อ่านให้เข้าใจ

# ① สร้างชุดครั้งเดียว: 6 นัด จอดรอที่ y = -50 (เหนือจอ)
bullets = [game.Box(0, -50, 6, 14, game.CYAN) for _ in range(MAX_BULLETS)]
for bullet in bullets:
    bullet.hide()                   # ซ่อนไว้ก่อน (ที่ทำให้ว่างคือ y = -50)

# ② หาตัวว่าง: ไล่ดูทีละนัด นัดแรกที่ y < -20 คือตัวว่าง
def find_free_bullet():
    for bullet in bullets:
        if bullet.y < -20:
            return bullet           # เจอแล้ว ส่งนัดนี้ออกไปใช้
    return None                     # ใช้อยู่ครบ 6 → ไม่มีตัวว่าง
  • บรรทัดที่ 2 อ่านว่า: ทำ game.Box(...) ซ้ำ MAX_BULLETS (= 6) รอบ แล้วเก็บทุกกล่องไว้ใน list bullets · _ คือตัวนับรอบที่เราไม่ได้ใช้
  • -50 มาจากตอนสร้าง game.Box(0, -50, ...) — hide() ไม่ได้เปลี่ยนค่า y
  • return None แปลว่า "ไม่มี" · คนเรียกต้องเช็ก if bullet: ก่อนใช้ทุกครั้ง

กระสุน 1 นัด อยู่ได้กี่เฟรม — ดูเป็นตัวเลข

yใหม่=yเดิม−9เช่น 331=340−9y_{\text{ใหม่}} = y_{\text{เดิม}} - 9 \qquad \text{เช่น } 331 = 340 - 9

  • เฟรม 0 ยิงออก ได้ y = 331 แล้วขึ้นเฟรมละ 9 px (ขึ้นข้างบน = y ลดลง)
  • เฟรม 39 ได้ -20 พอดี → ยังใช้อยู่ (ต้อง น้อยกว่า -20) · เฟรม 40 ได้ -29 → คืน
  • ใช้อยู่ตั้งแต่เฟรม 0 ถึงเฟรม 40 = 41 เฟรม ≈ 1.4 วินาที

ทายก่อน — find_free_bullet() จะส่งนัดไหนออกมา?

กด A ค้างมาตั้งแต่เฟรม 0 ตอนนี้เป็น เฟรม 48 · ค่า y ตอนที่ฟังก์ชันเริ่มไล่ดู:

นัด [0] [1] [2] [3] [4] [5]
y -29 -20 52 124 196 268
y < -20 ? ใช่ ? ไม่ ? ? ?
สถานะ ว่าง ? ใช้อยู่ ? ? ?
  1. เติม ? ให้ครบ แล้วตอบ: ฟังก์ชันคืนนัดไหน? ไล่ดูไปกี่นัดถึงเจอ?
  2. ถ้า y ของทั้ง 6 นัดเป็น 124, 160, 196, 232, 268, 304 ฟังก์ชันจะคืนอะไร? แล้วบนจอเกิดอะไรขึ้น?

เขียนคำตอบลงใบงานก่อน แล้วค่อยเปิดสไลด์ถัดไป

เฉลยการทาย

ข้อ 1 — คืน bullets[0]

  • [0] คือนัด แรก ที่ y < -20 ฟังก์ชันเจอแล้วหยุดทันที ดูไปแค่ 1 นัด
  • [1] ที่ y = -20 ยังไม่ว่าง เพราะ -20 ไม่น้อยกว่า -20 (เฟรมนี้มันจะขึ้นไป -29 แล้วค่อยว่าง)
  • ฟังก์ชันเริ่มดูจาก [0] เสมอ · ที่เห็นยืมวน 0 → 5 ก็เพราะนัดคืนตามลำดับที่ยิงออกไป

ข้อ 2 — คืน None

  • ไล่ดูครบ 6 นัด ไม่มีนัดไหน y < -20 เลย
  • if bullet: เป็นเท็จ → เฟรมนี้ยิงไม่ออก ไม่ error ไม่ค้าง แค่ไม่มีเลเซอร์ออกมา
  • จะได้เห็นของจริงในงาน "ลองแก้" ระหว่างแล็บ

คูลดาวน์ — นับถอยหลังก่อนยิงนัดถัดไป

เหมือน ปุ่มสกิลในเกมมือถือ กดแล้วไอคอนเป็นสีเทา รอครบถึงกดได้อีก · คล้าย debounce คาบ 2 แต่คาบ 2 ใช้ sleep หยุดรอ ส่วนคูลดาวน์นับเฟรมโดยเกมไม่หยุด

cใหม่=max⁡(0,  cเดิม−1)c=fire_cooldownc_{\text{ใหม่}} = \max(0,\; c_{\text{เดิม}} - 1) \qquad c = \texttt{fire\_cooldown}

คูลดาวน์ ยิงได้สูงสุด (ถ้าบอร์ดวิ่ง 30 เฟรม/วินาที) กระสุน 1 นัดใช้อยู่ ช่องที่ต้องมี ชุดเรามี
8 30 ÷ 8 ≈ 3.75 นัด/วินาที 41 เฟรม 41 ÷ 8 = 5.1 ปัดขึ้นเป็น 6 6 พอดี
  • ตอนจะยิง มีนัดเก่ายังใช้อยู่อย่างมาก 5 นัด (นัดที่เก่าสุดค้างที่ y = -20 มองไม่เห็นแล้ว) → มีช่องว่างรอเสมอ → กด A ค้างยิงได้ไม่รู้จบ · บนจอเห็นไม่เกิน 5 นัด

งานของเรา — เติม 2 ช่องใน shooter_step3.py

ในไฟล์ แต่ละช่องมีคอมเมนต์ใบ้กับ pass · ลบ pass แล้วเขียนโค้ดหน้าตาแบบนี้ (เติม ____ และ ... เอง):

งานของคุณ 1 — คูลดาวน์ + ③ ยืม

    fire_cooldown = ____(0, fire_cooldown - 1)          # ลด 1 ไม่ให้ต่ำกว่า 0
    if (keys.a or keys.up) and ______________:          # กดยิง และ พร้อมยิง
        bullet = find_free_bullet()                     # ② หาตัวว่าง
        if bullet:                                      # ได้ตัวว่าง (ไม่ใช่ None)
            ...                                         # ③ ยืม: 4 บรรทัด ดูใบ้ข้างล่าง

งานของคุณ 2 — ④ อัปเดตทุกเฟรม + ⑤ คืน

    for bullet in bullets:
        if bullet.y >= -20:                             # เฉพาะนัดที่ใช้อยู่
            ...                                         # ④ ขึ้น 9 px (x เท่าเดิม)
            if ______________:                          # ⑤ พ้นเส้น -20 แล้ว?
                ...                                     #    ซ่อน = คืน

ใบ้ ③ ยืม: .show() · ย้ายไปหัวยาน .move_to(x, 340) โดย x = กลางยาน ship_x + ship.w // 2 ถอยครึ่งความกว้างกระสุน 3 px · เสียง game.sfx("fire") · ตั้งคูลดาวน์ 8

รันบนบอร์ด + เช็กลิสต์

ขั้นตอน

  1. เปิด practise_codes/shooter_step3.py↗ ใน BENTO IDE (ไม่มีบอร์ดใช้ BENTO Emulator ที่ ide.tesaiot.dev ได้)
  2. เติมงาน 1 และงาน 2 แล้ว ลบ pass สองบรรทัดออก
  3. กด Program to Device
  4. หน้าชื่อเกม: กด Start เพื่อเล่น
  5. ระหว่างเล่น: Start = พักเกม · Back = ออก

ผ่านเมื่อ

  • ☐ กด A → เลเซอร์ฟ้า 1 นัดพุ่งจากหัวยาน + มีเสียง
  • ☐ กด A ค้าง → ยิงต่อเนื่องไม่หยุด นัดห่างเท่า ๆ กัน
  • ☐ บนจอไม่เคยเกิน 6 นัด
  • ☐ ยานยังบินซ้าย-ขวาได้เหมือนคาบ 10
  • ☐ ลองคูลดาวน์ 4 แล้วเห็นสายเลเซอร์ขาดช่วง จากนั้นแก้กลับเป็น 8

ระหว่างแล็บ อาจารย์จะเปิด 3 หน้าถัดไป: กับดักที่เจอบ่อย · ลองแก้คูลดาวน์ 4 · ผลจริง

ระหว่างแล็บ

กับดักที่เจอบ่อย · ลองแก้: คูลดาวน์ 8 → 4 (ทายก่อน) · ผลจริง

เปิดตอนน้อง ๆ ลงมือ ไม่ต้องบรรยายในช่วงสอน

กับดักที่เจอบ่อย

อาการ สาเหตุ วิธีแก้
ยิงได้ 6 นัดแรก แล้วยิงไม่ออกอีกเลย คืนด้วย hide() อย่างเดียว แต่ y ยังไม่ต่ำกว่า -20 (เช่น เลื่อนแค่ถึง y < 0 แล้วหยุด) เลื่อนต่อจน y < -20 · hide() แค่ซ่อน ไม่ได้ทำให้ว่าง
UnboundLocalError ชี้ที่ fire_cooldown ลืมบรรทัด global บรรทัดแรกของ on_frame: global ship_x, ship_speed, fire_cooldown
กด A ทีเดียว ได้หลายนัดติดกันเป็นแท่ง ไม่ได้ตั้งคูลดาวน์หลังยิง ตั้ง fire_cooldown = 8 ทุกครั้งที่ยิงออก
AttributeError: 'NoneType' object has no attribute 'show' ลืม if bullet: (เห็นตอนลองคูลดาวน์ 4) เช็กว่าได้ตัวว่างก่อนใช้ทุกครั้ง
ยิงไปราว 30 นัด แล้วเกมหยุดพร้อม RuntimeError: ui: max 32 widgets game.Box(...) อยู่ใน on_frame → กล่องเพิ่มทุกนัด ไม่มีใครลบ สร้างครั้งเดียวข้างนอก on_frame เท่านั้น

ลองแก้ — ลดคูลดาวน์จาก 8 เหลือ 4

แก้บรรทัด fire_cooldown = 8 เป็น fire_cooldown = 4 · ทายก่อนกดรัน:

  • ยิงถี่ขึ้นเท่าตัวจริงไหม? (30 ÷ 4 = 7.5 นัดต่อวินาที?)
  • สายเลเซอร์บนจอจะหน้าตาเป็นยังไง?
คูลดาวน์ กระสุน 1 นัดใช้อยู่ ช่องที่ต้องมี ชุดเรามี ผล
8 41 เฟรม 41 ÷ 8 ปัดขึ้น → 6 6 พอดี ยิงได้ไม่ขาด
4 41 เฟรม 41 ÷ 4 ปัดขึ้น → 11 6 ?

เขียนคำทายลงใบงาน แล้วรันบนบอร์ดจริง ดูสายเลเซอร์ให้ดี

ผลจริง — ชุดหมุนเวียนหมด สายเลเซอร์ขาดเป็นช่วง

  • เฟรม 0–20: ยิงออก 6 นัดติดกัน ห่างกัน 4 เฟรม
  • เฟรม 24–40: ทั้ง 6 นัดยังใช้อยู่ → find_free_bullet() คืน None → ยิงไม่ออก
  • เฟรม 40: [0] พ้นเส้น -20 → คืน · เฟรม 41: ยืม [0] ยิงต่อ
  • บนจอเห็น 6 นัดเป็นกลุ่ม แล้ว เว้นช่องว่าง 21 เฟรม (0.7 วินาที)
  • ได้จริง 6 นัดทุก 41 เฟรม ≈ 4.4 นัด/วินาที ไม่ใช่ 7.5

อยากยิงถี่จริงต้องเพิ่มช่องด้วย: ลอง MAX_BULLETS = 11 คู่กับคูลดาวน์ 4 แล้วดูว่าช่องว่างหายไหม · เสร็จแล้วแก้กลับเป็น 6 กับ 8

สรุป + คาบหน้า

  1. ชุดหมุนเวียน (Object Pool): สร้างของครั้งเดียว แล้ว ยืม → ใช้ → คืน ไม่สร้างใหม่ระหว่างเล่น
  2. ว่างหรือใช้อยู่ ดูจากค่าเดียว: ของเราใช้ bullet.y < -20
  3. คูลดาวน์: นับถอยหลังทุกเฟรม ทำได้เมื่อถึง 0 → ตั้งความถี่ สูงสุด ได้

เอาไปใช้ที่อื่น: เหรียญ ประกายไฟ ระเบิด ในเกมของน้องเอง = ชุดหมุนเวียน · เสียงเตือนตอนเอียงบอร์ด (คาบ 9) ไม่ให้ดังทุกเฟรม = คูลดาวน์ (ตารางเต็มในภาคผนวก)

คาบหน้า: ศัตรูใช้หลักเดียวกัน — ศัตรู 6 ตัวก็สร้างครั้งเดียวแล้วใช้ซ้ำทั้งเกม · เพิ่มการชนแบบกล่อง คะแนน ชีวิต GAME OVER

ลองคิดมาก่อน: ศัตรูที่ตกพ้นจอล่าง ควร "คืนแล้วรอให้ยืม" แบบกระสุน หรือ "วนกลับขึ้นไปข้างบน" เลย?

คาบ 12 ใช้ไฟล์ใหม่ shooter_step4.py ที่มีส่วนกระสุนของวันนี้ให้แล้ว

ภาคผนวก

ดูมันหมุน · list ฉบับเต็ม · เฉลย · คณิตเพิ่มเติม · ทำไมไม่สร้างใหม่ (ละเอียด) · เอาไปใช้ที่อื่น · ใช้จริงที่ไหน · แบบฝึกเรียงโค้ด · คิดต่อ

ไม่ต้องใช้ในแล็บ อ่านเพิ่มเมื่ออยากรู้ลึก

ดูมันหมุน — ช่องไหนถูกยืม ช่องไหนถูกคืน

สังเกต 4 จุด (กด A ค้าง คูลดาวน์ 8)

  • เฟรม 0, 8, 16, … ยืมทีละช่อง [0] [1] [2] … ห่างกัน 8 เฟรม
  • เฟรม 39: [0] อยู่ที่ y = -20 ยัง ใช้อยู่ เพราะต้อง น้อยกว่า -20
  • เฟรม 40: [0] ขึ้นไปถึง -29 → คืน
  • เฟรม 48: ยืม [0] ช่องเดิม อีกครั้ง

find_free_bullet() เริ่มดูจาก [0] ทุกครั้ง ที่เห็นยืมวน 0 → 5 เพราะนัดคืนตามลำดับที่ยิง ไม่ใช่เพราะมันจำว่าถึงคิวใคร

ภาพช้ากว่าจริง · ตัวเลขทุกตัวมาจากตรรกะของ shooter_step3.py

list ฉบับเต็ม — เขียนแบบยาวเทียบกัน

bullets = [game.Box(0, -50, 6, 14, game.CYAN) for _ in range(MAX_BULLETS)]
bullets = []                                               # เขียนแบบยาว ได้ผลเท่ากัน
for _ in range(MAX_BULLETS):                               # (ดูเทียบ ไม่ได้อยู่ในไฟล์)
    bullets.append(game.Box(0, -50, 6, 14, game.CYAN))
เขียนว่า แปลว่า
_ ตัวนับรอบที่เราไม่ได้ใช้ ตั้งชื่อ _ บอกคนอ่านว่า "ไม่สนเลขรอบ"
for bullet in bullets: หยิบทีละนัด ตั้งแต่นัดแรกถึงนัดสุดท้าย
bullets[0] นัดแรก (นับจาก 0) · bullets[5] คือนัดที่ 6

เฉลย · ส่วนหัวไฟล์ (บรรทัด 14–22)

ACCEL, MAX_SPEED, FRICTION = 1.4, 13.0, 0.80
MAX_BULLETS = 6                                 # ขนาดชุดหมุนเวียน ตั้งไว้ที่เดียว

game.title("SHOOTER")                          # หน้าชื่อเกม: Start = เล่น · Back = ออก

ship = game.Box(365, 352, 62, 24, game.GREEN)
ship_x, ship_speed, fire_cooldown = 365.0, 0.0, 0
score, lives = 0, 3                             # ยังไม่ใช้ในคาบนี้ (คาบ 12 ใช้)
hud = game.Text("Score: 0   Lives: 3", 10, 8, game.WHITE)
  • บรรทัด 15: ตัวเลขขนาดชุดอยู่ที่เดียว อยากเปลี่ยนเป็น 11 ก็แก้จุดเดียว
  • บรรทัด 20: fire_cooldown เริ่มที่ 0 → กด A ครั้งแรกยิงได้ทันที
  • บรรทัด 14, 19–20: ยานกับฟิสิกส์ยกมาจาก step 2 ของคาบ 10 ทั้งชุด
  • บรรทัด 24–34 คือ ① ② ที่อธิบายไปแล้วในสไลด์ "โค้ด ① ②"

เฉลย · งานของคุณ 1 — คูลดาวน์ + ③ ยืม (บรรทัด 50–57)

    fire_cooldown = max(0, fire_cooldown - 1)
    if (keys.a or keys.up) and fire_cooldown == 0:
        bullet = find_free_bullet()                        # ② หาตัวว่าง
        if bullet:                                         # ③ ยืม (ได้ตัวว่าง)
            bullet.show()
            bullet.move_to(ship_x + ship.w // 2 - 3, 340)  # ไปหัวยาน
            game.sfx("fire")                               # เสียงเลเซอร์
            fire_cooldown = 8                              # รอ 8 เฟรม
  • บรรทัด 50: ลด 1 ทุกเฟรม ไม่ว่าจะกดหรือไม่ · max(0, …) กันค่าติดลบ
  • บรรทัด 51: ดันขึ้น (keys.up) ก็ยิงได้ เผื่อคนถนัดแบบนั้น
  • บรรทัด 55: ship.w // 2 = 62 ÷ 2 = 31 (// หารแล้วปัดลง) · ลบ 3 = ครึ่งความกว้างกระสุน → กระสุนอยู่กลางยาน · y = 340 อยู่เหนือยาน (ยานอยู่ที่ 352)
  • บรรทัด 57: ตั้ง 8 → ยิงได้สูงสุด 30 ÷ 8 ≈ 3.75 นัดต่อวินาที

เฉลย · งานของคุณ 2 — ④ อัปเดต + ⑤ คืน (บรรทัด 60–64)

    for bullet in bullets:
        if bullet.y >= -20:                            # เฉพาะนัดที่ใช้อยู่
            bullet.move_to(bullet.x, bullet.y - 9)     # ④ ขึ้น 9 px
            if bullet.y < -20:                         # ⑤ พ้นเส้นแล้ว
                bullet.hide()                          #    ซ่อน = คืน
  • บรรทัด 61: ข้ามนัดที่ว่าง มันจอดอยู่ ไม่ต้องขยับ
  • บรรทัด 62: move_to เปลี่ยน bullet.y ทันที (bentogame.py:234) บรรทัด 63 จึงเช็กค่าที่เพิ่งขยับ
  • บรรทัด 63–64: ค่า y ที่ต่ำกว่า -20 คือสิ่งที่ทำให้ว่าง ส่วน hide() แค่ทำให้ผู้เล่นมองไม่เห็น
  • ทำไมเส้นคือ -20 ไม่ใช่ 0: กระสุนสูง 14 px ที่ y = -2 ปลายยังโผล่ให้เห็น พอถึง -20 ก็พ้นจอทั้งแท่ง เผื่อไว้อีก 6 px

ท่อนยิงแค่ "ยืม" ส่วนการพาทุกนัดขึ้นไปเป็นงานของลูปนี้ แยกหน้าที่กันชัด ๆ โค้ดจึงอ่านง่าย

คณิตเพิ่ม — ต้องมีกี่ช่อง และหาตัวว่างเร็วแค่ไหน

ต้องมีกี่ช่อง (คิดเอง ไม่ได้อยู่ในโค้ด):

ช่องที่ต้องมี = ปัดขึ้น( เฟรมที่กระสุน 1 นัดใช้อยู่ ÷ คูลดาวน์ )  ·  ตำราเขียนย่อว่า ⌈ 41 ÷ 8 ⌉
  • คูลดาวน์ 8: 41 ÷ 8 = 5.1 ปัดขึ้น = 6 · คูลดาวน์ 4: 41 ÷ 4 = 10.25 ปัดขึ้น = 11
  • 41 มาจาก: เฟรม 0 ถึง 39 ขึ้นรวม 40 × 9 = 360 px จาก 340 ถึง -20 พอดี · เฟรม 40 ได้ -29 → ใช้อยู่เฟรม 0–40 = 41 เฟรม

หาตัวว่างเร็วแค่ไหน:

  • find_free_bullet() ไล่ดูทีละช่องจาก [0] แย่สุดดูครบทุกช่อง งานจึงโตตามจำนวนช่อง (ตำราเขียนว่า O(n))
  • 6 ช่อง × ยิงสูงสุด 3.75 ครั้งต่อวินาที = ดูไม่เกิน ~23 ช่องต่อวินาที เบามาก
  • ถ้ามี 1,000 ช่อง: เก็บ "รายชื่อช่องว่าง" ไว้ต่างหาก (free list) ยืมตัวหัวแถวได้เลย ไม่ต้องไล่ดู

ทำไมไม่สร้างใหม่ — ฉบับละเอียด

  • สาเหตุหลัก — เพดานชิ้นบนจอ: เฟิร์มแวร์มีตารางชิ้นบนจอ 32 ช่อง (UI_MAX_WIDGETS = 32, ipc_ui_protocol.h:172) เต็มแล้วสร้างใหม่ → RuntimeError: ui: max 32 widgets (modui.c:628) · ช่องคืนก็ต่อเมื่อ delete() หรือ clear() เท่านั้น ตัวแปร Python หายไปไม่ได้คืนช่อง · ของเราใช้ 9 ชิ้น (กระสุน 6 + ยาน + HUD + ป้ายปุ่ม)
  • อาจเกิดด้วย (รอง ยังไม่ได้วัด): ตัวเก็บขยะหยุดโปรแกรมระหว่างเก็บ · หน่วยความจำแหว่งเป็นรู (fragmentation) · เอกสาร MicroPython แนะนำให้สร้างของที่อยู่ยาวไว้ตั้งแต่ต้น
  • ตามจริง: ลูปของเรายังจองของเล็ก ๆ ทุกเฟรมอยู่ดี — joystick.read() สร้าง dict ใหม่ (modjoystick.c:213) · Keys(...) ใหม่ทุกเฟรม (bentogame.py:353) · เลขทศนิยมก็เป็น object · ชุดหมุนเวียนตัดแค่ "กล่องกระสุนที่เกิดบ่อย"
  • กฎวิศวกรรม: NASA JPL "Power of Ten" ข้อ 3 ไม่จองหน่วยความจำใหม่หลังเริ่มระบบ — ชุดหมุนเวียนคือวินัยนี้สำหรับวัตถุในเกม ไม่ได้แปลว่าทั้งโปรแกรมไม่จองอะไรเลย · MISRA C:2012 ข้อ 21.3 ห้ามใช้ malloc/free ในภาษา C เด็ดขาด

เลข 32 และข้อความ error อ่านจากซอร์สเฟิร์มแวร์ (BENTO-TESAIoT-libraries/game/common/) ยังไม่ได้ลองบนบอร์ด

เอาไปใช้ที่อื่น

ใช้ที่ไหน เทคนิค ชื่อตัวแปรที่น่าจะใช้
เหรียญ ประกายไฟ ระเบิด ในเกมของน้องเอง ชุดหมุนเวียน coins, find_free_coin(), sparks
ของที่ตกลงมาใน Catch (คาบ 4) หลายชิ้นพร้อมกัน ชุดหมุนเวียน items, find_free_item()
ป้ายแจ้งเตือนบนจอ ขึ้นได้ทีละ 3 ป้าย ชุดหมุนเวียน toasts = [game.Text(...) for _ in range(3)]
เสียงเตือนตอนเอียงบอร์ดเกิน (คาบ 9) ไม่ให้ดังทุกเฟรม คูลดาวน์ alarm_cooldown = 30 (เฟรม)
ปุ่มเปลี่ยนสถานะ FSM (คาบ 2) แบบเกมไม่หยุด คูลดาวน์ นับเฟรม button_cooldown = 5 (เฟรม)
  • คาบ 2 ใช้ time.sleep_ms(50) ซึ่ง หยุดทั้งลูป รอ · คูลดาวน์นับเฟรมทำงานอื่นต่อได้ระหว่างรอ
  • ตัวเก็บค่าเซนเซอร์แบบวน (ring buffer) เป็นญาติกัน: จองครั้งเดียวเหมือนกัน แต่เขียนทับช่องถัดไปตามลำดับ ไม่มีการหาตัวว่าง

ใช้จริงที่ไหน

ที่ ของที่หมุนเวียน เทียบกับเกมเรา
เอนจินเกม เช่น Unity (ObjectPool) กระสุน อนุภาค ศัตรู เหมือนกันทุกอย่าง แค่ใหญ่กว่า
lwIP (TCP/IP บนไมโครคอนโทรลเลอร์) บัฟเฟอร์แพ็กเก็ต จองเป็นชุดตั้งแต่เริ่ม ชุดหมุนเวียนของหน่วยความจำ
ไดรเวอร์ DMA ช่องรับ-ส่งข้อมูลจำนวนคงที่ วนใช้ช่องเดิม 6 ช่องของเรา
เว็บ / ฐานข้อมูล (connection pool) การเชื่อมต่อ ไม่มีว่าง = รอ เหมือน None
API rate limit · ปุ่มลิฟต์ จำนวนครั้งต่อเวลา คูลดาวน์

อ่านต่อ:

แบบฝึกเรียงโค้ด — มีบรรทัดหลอก 1 บรรทัด

เรียงบรรทัดให้เป็น งานของคุณ 2 (④ อัปเดต + ⑤ คืน) พร้อมย่อหน้าให้ถูก · บรรทัดไหนไม่ควรอยู่ใน on_frame?

A   bullet.hide()
B   if bullet.y >= -20:
C   bullet = game.Box(ship_x + 28, 340, 6, 14, game.CYAN)
D   for bullet in bullets:
E   bullet.move_to(bullet.x, bullet.y - 9)
F   if bullet.y < -20:

เฉลย: D → B → E → F → A (ย่อหน้าลึกขึ้นทีละขั้น)
C คือบรรทัดหลอก — สร้างกล่องใหม่ใน on_frame ทุกเฟรม ตรงข้ามกับสิ่งที่คาบนี้สอน

คิดต่อ

  • ชุดที่ใหญ่ขึ้นตอนสู้บอส — 6 นัดไม่พอ: สร้างอีก 6 นัด ครั้งเดียว ตอนเข้าฉากบอส ไม่ใช่เพิ่มทีละนัดในลูป · นับชิ้นรวมแล้วยังต่ำกว่า 32 ไหม?
  • กระสุน 2 แบบ (เร็ว / แรง) — ใช้ชุดเดียวกันหรือแยกสองชุด? การหาตัวว่างยุ่งขึ้นแค่ไหน
  • คูลดาวน์ตามคะแนน — ยิ่งคะแนนสูงยิ่งยิงเร็ว แต่ต้องคำนวณช่องที่ต้องมีให้ไม่เกิน MAX_BULLETS
  • เกมเต็มทำต่างจากเรา — full_games/shooter_full.py ใช้ list ธง bact (True/False) บอกว่าใช้อยู่ แทนการดู y · ข้อดีข้อเสียต่างกันยังไง?
  • วัดเอง — จับเวลาแต่ละเฟรมด้วย time.ticks_ms() เทียบแบบ "สร้างแล้ว delete() ทุกนัด" กับแบบชุดหมุนเวียน เฟรมไหนนานผิดปกติบ้าง?
  • สะพานสู่คาบ 12 — ศัตรู 6 ตัว ควรใช้ y บอกว่าว่างแบบกระสุน หรือใช้ธงแบบเกมเต็ม?

เลือกมาหนึ่งข้อ เขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ก่อนแก้โค้ด

fit-css

core

core

core

core

core

core

core

core

core

core

core

core

core

core

core

← Roadmap (TOC)