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