บทความนี้เป็นตัวอย่างการเขียนเกมขยับตัวละครให้เดินไปในเขาวงกตเพื่อเก็บธงที่ถูกสุ่มตำแหน่ง ดังภาพที่ 1 ซึ่งตัวละครจะเดินในช่องที่กำหนดไม่สามารถทะลุกำแพงได้ โดยมีเสียงร้องเตือนเมื่อพยายามเดินไปในตำแหน่งที่ไม่สามารถไปได้ และเมื่อเดินไปทิศใดจะเปลี่ยนภาพของตัวละครให้หันไปทางทิศนั้น นอกจากนี้กำหนดให้การกดปุ่ม A ให้เป็นการสุ่มตำแหน่งของธงใหม่ การกดปุ่ม B ให้ทำการสุ่มตำแหน่งของผู้เล่น และถ้ากดปุ่ม D ให้ออกจากโปรแกรม โดยบอร์fสำหรับใช้งานยังคงเป็น dCoreML4M เช่นเดิม มาเริ่มกันครับ

ภาพที่ 1 ตัวอย่างเกมของบทความนี้

โครงสร้างข้อมูล

มาเริ่มกันที่การออกแบบการจัดเก้บข้อมูลของแผนภาพ กำแพง ธงและตัวละคร พร้อมทั้งนำข้อมูลไป แสดงผลที่จอแสดงผล OLED เพื่อให้ผู้เล่นได้เห็นภาพของแผนที่ ธงและตัวละคร

การเก็บแผนที่

ตัวแปร maze เป็นตัวแปรที่ถูกกำหนดขึ้นมาเพื่อเก็บข้อมูลแผนที่ โดยแผนที่ใช้ข้อมูลขนาด 2 ไบต์จำนวน 8 ชุด เพื่อใช้เก็บค่าของกำแพงและทางเดิน โดยกำหนดไว้ดังนี้

  • ช่องใดเป็นทางเดินให้กำหนดเป็น 0
  • ช่องใดเป็นกำแพงให้กำหนดเป็น 1

จากการกำหนดทำให้จัดเก็บข้อมูลของแต่ละช่องเป็นข้อมูลขนาด 1 บิตเนื่องจากมีเพียง 2 สถานะ และจอแสดวผบมีความละเอียด 128×64 ทำให้สามารถแสดงภาพขนาด 8×8 จุดได้ 16 คอลัมน์ และ 8 แถว ดังนั้น ตัวอย่างของแผนที่ที่สร้างขึ้นจึงเป็นดังนี้่

maze = [
    0b1111000111111101,
    0b1000010000100101,
    0b0011000100000101,
    0b1001001110010101,
    0b1000001000010001,
    0b1011110010111011,
    0b0001000010000001,
    0b110001100011111
]

ตัวอย่างการนำข้อมูลแผนที่ไปวาดเป็นดังนี้และได้ภาพผลลัพธ์ดังภาพที่ 2 โดยในขั้นตอนการวาดนั้นใช้หลักการเข้าถึงทีละบิตของแต่ละแถวและถ้าเป็น1 จะทำการวาดภาพของกำแพงลงไปที่ตำแหน่ง แถวที่ r*8 และคอลัมน์ที่ c*8 ด้วยการ blit

    for r in range(8):
        mask = 0b1000000000000000
        for c in range(16):
            if (maze[r] & mask):
                oled.blit(wall,c*8, r*8)
            mask >>= 1
ภาพที่ 2 ตัวอย่างผลลัพธ์การวาดแผนที่

การเก็บภาพกำแพง

ภาพกำแพงที่สร้างขึ้นจัดเก็บในตัวแปร wall เป็นวัตถุประเภท FrameBuffer ของ bytearray จำนวน 8 ชุด เพื่อแทนข้อมูลจำนวน 8 แถว และแต่ละแถวมี 8 คอลัมน์ พร้อมทั้งกำหนดเงื่อนไขของการตีความบิตไว้ดังนี้

  • บิตค่า 0 เป็นตำแหน่งที่ไม่ต้องลงจุดสว่าง
  • บิตค่า 1 เป็นตำแหน่งที่แสดงจุดสว่าง

เมื่อได้ข้อมูลจำนวน 8 ชุดแทนแถวทั้ง 8 ต้องระบุขนาดของเฟรมบัฟเฟอร์เป็น 8×8 และเลือกใช้การจัดเก็บแบบ framebuf.MONO_HLSB ซึ่งหมายถึงเก็ยข้อมูลเป็นแบบโมโน (ค่า 0 หรือ 1) แบบเรียงตามแนวนอน โดยวิธีการวาดใช้การ blit ข้อมูลทั้งชุดลงจอแสดงผลดังที่เคยได้กล่าวไปแล้วในบทความเกี่ยวกับการทำดับเบิลบัฟเฟอร์

wall = FrameBuffer(bytearray([
    0b00011110,
    0b11100011,
    0b01000001,
    0b10000010,
    0b01000001,
    0b10000010,
    0b10000001,
    0b01111110
]),8,8,framebuf.MONO_HLSB)

การเก็บภาพธง

ภาพของธงเก็บไว้ในตัวแปร flag โดยจัดเก็บแบบเดียวกับกำแพง และมีสร้างตัวแปร flagPos แบบลิสต์จำนวน 1 สมาชิกสำหรับเก็บค่าแถวและคอลัมน์ที่ใช้แสดงภาพธง

flag = FrameBuffer(bytearray([
0b01000000,
0b01111111,
0b01111110,
0b01110000,
0b01000000,
0b01000000,
0b01100000,
0b11110000
]),8,8,framebuf.MONO_HLSB)
flagPos = [0,0]

การเก็บภาพตัวละคร

ตัวละครประกอบด้วยภาพ 4 ภาพแทนการหันไปทางซ้าย หันไปทางขวา หันขึ้นบนและหันลงล่าง ทำให้ต้องออกแบบการเก็บข้อมูล 4 ชุด จึงจัดเก็บข้อมูลในตัวแปร actor เป็น FrameBuffer จำนวน 4 ตัวดังโค้ดด้านล่าง โดยแต่ละชุดเป็นข้อมูลขนาด 8 บิตจำนวน 8 ชุด จัดเก็บแบบ framebuf.MONO_HLSB เหมือนกันทั้งหมด ด้วยเหตุนี้ การวาดหรือ blit จึงต้องระบุลำดับของข้อมูลใน actor ในขั้นตอนการวาด

actor = [
    FrameBuffer(bytearray([
        0b00000000,
        0b00001000,
        0b00011000,
        0b00111000,
        0b11111000,
        0b00111000,
        0b00011000,
        0b00001000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00100000,
        0b00110000,
        0b00111000,
        0b00111110,
        0b00111000,
        0b00110000,
        0b00100000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00010000,
        0b00010000,
        0b00111000,
        0b01111100,
        0b11111110,
        0b00000000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00000000,
        0b11111110,
        0b01111100,
        0b00111000,
        0b00010000,
        0b00010000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB)
]

สิ่งที่ใช้กำหนดทิศทางของการหันของตัวละครคือการโยกคันโยกไปทางซ้าย ขวา บน หรือล่าง โดยเก็บค่าทิศในตัวแปร actorDir และตำแหน่งที่แสดงตัวละครในตัวแปร actorPos

การเคลื่อนที่

การเคลื่อนที่ทำได้จากการโยกคันโยกซึ่งเขียนวิธีการตรวจสอบทิศทางของการโยกและภาพการหันหัวของตัวละครเอาไว้ดังนี้

def getInput():
...
    jx = 4095-jst[0].read()
    jy = jst[1].read()
    
    if (jx < 1200):
        left = True
    elif (jx > 3000):
        right = True
    if (jy < 1200):
        up = True
    elif (jy > 3000):
        down = True

...

        (l,r,u,d,a,b,m1,m2)=getInput()

...

        if (l):
            actorDir = 0

        if (r):
            actorDir = 1

        if (u):
            actorDir = 2
    
        if (d):
            actorDir = 3

นอกจากคันโยกที่ถูกใช้เพื่อกำหนดทิศทางการเดินของตัวละครแล้ว ปุ่ม A, B และ m1 ถูกกำหนดหน้าที่ไว้ดังนี้

  • ปุ่ม a สำหรับสุ่มตำแหน่งของธง
  • ปุ่ม b สำหรับสุ่มตำแหน่งของตัวละคร
  • ปุ่ม m1 สำหรับออกจากโปรแกรม

การสุ่มตำแหน่งธง

วิธีการสุ่มตำแหน่งของธงกำหนดเงื่อนไขไว้ดังนี้

  • สุ่มค่าแถว
  • สุ่มค่าคอลัมน์
  • ถ้าค่าตำแหน่งที่คอลัมน์ของแถวที่สุ่มไม่ใช้กำแพงให้คำนวณค่า flagPos เป็น c*8 และ r*8 แต่ถ้าไม่ใช่ให้ทำการสุ่มใหม่อีกครั้ง

เขียนโค้ดจากเงื่อนไขด้านบนได้คือ

    while True:
        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            flagPos[0] = c*8
            flagPos[1] = r*8
            break

การสุ่มตำแหน่งตัวละคร

การสุ่มตำแหน่งของตัวละครมีเงื่อนไขเพิ่มเติมจากการสุ่มธงคือ จะต้องไม่เป็นตำแหน่งเดียวกับธง จึงได้โค้ดของการสุ่มตำแหน่งตัวละครได้ดังนี้

        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            if ((c*8 != flagPos[0]) and (r*8 != flagPos[1])): # ต้องไม่ใช่ที่เดียวกับธง
                actorPos[0] = c*8
                actorPos[1] = r*8
                break

การตรวจสอบการชน

ในเกมมีการบน 2 แบบ คือ

  • การชนกับธง ซึ่งส่งผลให้ทำการสุ่มค่าตำแหน่งใหม่ของธง
  • การชนกับกำแพงหรือขอบของแผนที่จะเล่นเสียงบี๊ปและไม่ขยับตำแหน่ง

ตัวอย่างการเขียนโค้ดเป็นดังนี้

กรณีขยับซ้าย
            if (actorPos[0] > 0):
                if (isWall(actorPos[0]-actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] -= actorSize[0]
กรณีขยับขวา
            if (actorPos[0] < scrWidth-actorSize[0]):
                if (isWall(actorPos[0]+actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] += actorSize[0]
กรณีขยับขึ้นด้านบน
            if (actorPos[1] > 0):
                if (isWall(actorPos[0],actorPos[1]-actorSize[1])):
                    beep()
                else:
                    actorPos[1] -= actorSize[1]
กรณีเลื่อนลงด้านล่าง
            if (actorPos[1] < scrHeight-actorSize[1]):
                if (isWall(actorPos[0],actorPos[1]+actorSize[1])):
                    beep()
                else:
                    actorPos[1] += actorSize[1]

ตัวอย่างโปรแกรม

จากโครงสร้างของการจัดเก็บข้อมูลและการทำงานข้างต้นสามารถนำมารวมกันเป็นโค้ดของเกมวิ่งเก็บธงได้ดังนี้

#######################################################
### sprite
### board: ML4M
### (C) 2021, JarutEx
#######################################################
from machine import Pin,I2C,ADC, DAC
import math
import machine
import gc
import ssd1306
import random
import time
import sys
import framebuf
from framebuf import FrameBuffer


#######################################################
gc.enable()
gc.collect()
machine.freq(240000000)

#######################################################
sclPin = Pin(22)
sdaPin = Pin(21)
spkPin = DAC(Pin(25, Pin.OUT))
i2c = I2C(0,scl=sclPin, sda=sdaPin, freq=400000)
oled = ssd1306.SSD1306_I2C(128,64,i2c)
oled.poweron()
oled.contrast(255)
oled.init_display()
oled.fill(0)
oled.show()

#######################################################
swA = Pin(32, Pin.IN)
swB = Pin(33, Pin.IN)
swC = Pin(34, Pin.IN, Pin.PULL_UP) # select
swD = Pin(35, Pin.IN) # start
jst = (ADC(Pin(39)), ADC(Pin(36))) # X,Y
jst[0].width( ADC.WIDTH_12BIT ) # 12bit
jst[0].atten( ADC.ATTN_11DB ) # 3.3V
jst[1].width( ADC.WIDTH_12BIT ) # 12bit
jst[1].atten( ADC.ATTN_11DB ) # 3.3V

#######################################################
actor = [
    FrameBuffer(bytearray([
        0b00000000,
        0b00001000,
        0b00011000,
        0b00111000,
        0b11111000,
        0b00111000,
        0b00011000,
        0b00001000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00100000,
        0b00110000,
        0b00111000,
        0b00111110,
        0b00111000,
        0b00110000,
        0b00100000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00010000,
        0b00010000,
        0b00111000,
        0b01111100,
        0b11111110,
        0b00000000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB),
    FrameBuffer(bytearray([
        0b00000000,
        0b00000000,
        0b11111110,
        0b01111100,
        0b00111000,
        0b00010000,
        0b00010000,
        0b00000000
    ]),8,8,framebuf.MONO_HLSB)
]
scrWidth = 128
scrHeight = 64
actorSize = (8,8)
actorPos = [scrWidth//2-4,scrHeight//2-4]
actorDir = 0 #0,1,2,3:left,right,up,down

flag = FrameBuffer(bytearray([
0b01000000,
0b01111111,
0b01111110,
0b01110000,
0b01000000,
0b01000000,
0b01100000,
0b11110000
]),8,8,framebuf.MONO_HLSB)
flagPos = [0,0]

wall = FrameBuffer(bytearray([
    0b00011110,
    0b11100011,
    0b01000001,
    0b10000010,
    0b01000001,
    0b10000010,
    0b10000001,
    0b01111110
]),8,8,framebuf.MONO_HLSB)

maze = [
    0b1111000111111101,
    0b1000010000100101,
    0b0011000100000101,
    0b1001001110010101,
    0b1000001000010001,
    0b1011110010111011,
    0b0001000010000001,
    0b110001100011111
]

#######################################################
def beep():
    spkPin.write(255)
    time.sleep_ms(20)
    spkPin.write(0)
    
#######################################################
def getInput():
    left = False
    right = False
    up = False
    down = False
    button1 = False
    button2 = False
    button3 = False
    button4 = False
    #### Joystick
    jx = 4095-jst[0].read()
    jy = jst[1].read()
    
    if (jx < 1200):
        left = True
    elif (jx > 3000):
        right = True
    if (jy < 1200):
        up = True
    elif (jy > 3000):
        down = True
    # switch
    a = swA.value()
    b = swB.value()
    c = 1-swC.value()
    d = swD.value()
    if (a):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        a2 = swA.value()
        if (a == a2):
            button1 = True
    if (b):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        b2 = swB.value()
        if (b == b2):
            button2 = True
    if (c):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        c2 = swC.value()
        if (c == c2):
            button3 = True
    if (d):
        t0 = time.ticks_ms()
        time.sleep_ms(25)
        d2 = swD.value()
        if (d == d2):
            button4 = True
    return (left,right,up,down,button1, button2, button3, button4)

#######################################################
def drawMaze():
    for r in range(8):
        mask = 0b1000000000000000
        for c in range(16):
            if (maze[r] & mask):
                oled.blit(wall,c*8, r*8)
            mask >>= 1
    oled.blit(flag, flagPos[0],flagPos[1])

#######################################################
def randFlag():
    while True:
        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            flagPos[0] = c*8
            flagPos[1] = r*8
            break
    
#######################################################
def randActor():
    while True:
        r = random.getrandbits(3) # 0..7
        c = random.getrandbits(4) # 0..15
        mask = (0b1000000000000000 >> c)
        if (maze[r] & mask):
            pass
        else:
            if ((c*8 != flagPos[0]) and (r*8 != flagPos[1])): # ต้องไม่ใช่ที่เดียวกับธง
                actorPos[0] = c*8
                actorPos[1] = r*8
                break
            

#######################################################
def isWall(c,r):
    c //= 8
    r //= 8
    mask = (0b1000000000000000 >> c)
    if (maze[r] & mask):
        return True
    return False

#######################################################
### Main program
#######################################################
beep()
try:
    randFlag()
    randActor()
    while True:
        (l,r,u,d,a,b,m1,m2)=getInput()
        if (m1):
            break
        if (a):
            randFlag()
        if (b):
            randActor()
        if (l):
            actorDir = 0
            if (actorPos[0] > 0):
                if (isWall(actorPos[0]-actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] -= actorSize[0]

        if (r):
            actorDir = 1
            if (actorPos[0] < scrWidth-actorSize[0]):
                if (isWall(actorPos[0]+actorSize[0],actorPos[1])):
                    beep()
                else:
                    actorPos[0] += actorSize[0]

        if (u):
            actorDir = 2
            if (actorPos[1] > 0):
                if (isWall(actorPos[0],actorPos[1]-actorSize[1])):
                    beep()
                else:
                    actorPos[1] -= actorSize[1]
    
        if (d):
            actorDir = 3
            if (actorPos[1] < scrHeight-actorSize[1]):
                if (isWall(actorPos[0],actorPos[1]+actorSize[1])):
                    beep()
                else:
                    actorPos[1] += actorSize[1]

        oled.fill(0)
        drawMaze()
        oled.blit(actor[actorDir], actorPos[0], actorPos[1])
        oled.show()
        time.sleep_ms(100)
except KeyboardInterrupt:
    pass
beep()
oled.poweroff()

สรุป

จากบทความนี้จะพบว่า จากพื้นฐานของการใช้สวิตช์ และการอ่านค่าจาก ADC สามารถนำมาประยุกต์ใช้ในการกำหนดทิศทางหรือสภานะการเลือกภายในเกม และจากจอแสดงผลกราฟฟิกเมื่อนำมาวาดเป็นแผนที่ ตัวละคร และธง พร้อมกับการกำหนดเงื่อนไขของการเคลื่อนที่และเหตุการณ์สามารถสร้างเกมเก็บธงแบบง่าย ๆ ขึ้นมาได้ แต่อย่างไรก็ดี เกมที่ดีจะมีสิ่งอื่น ๆ อีกหลายสิ่งที่ต้องนำเข้ามาประกอบ เช่น การทำเอนิเมชันของตัวละครหรือสิ่งแวดล้อม เสียงเอฟเฟค เสียงประกอบฉาก และที่สำคัญคือ เงื่อนไขการผ่านฉากที่ต้องสร้างความท้ายทายให้กับผู้เล่น

สุดท้ายนี้หวังว่าตัวอย่างของบทความนี้คงมีประโยชน์และสร้างแนวคิดให้กับผู้ที่สนใจพัฒนาเกมแบบทำเองง่าย ๆ และสามารถทำได้ด้วยตนเอง ขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ได้เลยครับ

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-09-21, 2021-12-02