This article is an example of writing a game. Move the character to walk in the maze to collect flags that are randomly positioned as shown in Figure 1, where the character will walk in the specified channel and can’t penetrate the wall. With a warning sound when trying to walk in an impossible location and when walking in any direction will change the image of the character to turn the face to that direction. In addition, pressing A will randomize the position of the new flag, pressing B will randomize the player’s position, and pressing D will exit the program. The board for use is still dCoreML4M as before, let’s get started.
Data structure
Let’s start by designing the data collection of diagrams, walls, flags, and characters, as well as taking the information to display on the OLED to give players a visual representation of the map, flags and characters.
Map storing
The maze variable is a variable that is assigned to store map data. The map uses 8 sets of 2-byte data to store the values of walls and walkways. It is defined as follows:
- Which position is a corridor, set to 0
- Which position is a wall, set to 1
By defining the data of each channel as 1-bit data as it has only 2 states, and the display has a resolution of 128×64, it can display an 8×8 dot image with 16 columns and 8 rows. So it’s as follows
maze = [
0b1111000111111101,
0b1000010000100101,
0b0011000100000101,
0b1001001110010101,
0b1000001000010001,
0b1011110010111011,
0b0001000010000001,
0b110001100011111
]
An example of bringing the map data to draw is as follows, and the resulting image is as shown in Figure 2. In the drawing process, using the principle of bit-by-bit access for each row and if it is 1 A drawing of the wall will be drawn to the row r*8 and column c*8 with a 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 image storing
The created wall image is stored in the variable wall as a FrameBuffer object of 8 bytearray, representing 8 rows of data and 8 columns in each row. The conditions for bit interpretation are defined as follows:
- The value 0 is the point that does not need to be lit.
- The value 1 is the point that need to be lit.
When 8 sets of data are obtained representing all rows, the frame buffer size must be specified as 8×8, and storage should be framebuf.MONO_HLSB. This means that the data is stored as mono (values 0 or 1), arranged horizontally. The drawing method blits the entire data set to the display as mentioned in the article on double buffering.
wall = FrameBuffer(bytearray([
0b00011110,
0b11100011,
0b01000001,
0b10000010,
0b01000001,
0b10000010,
0b10000001,
0b01111110
]),8,8,framebuf.MONO_HLSB)
Flag image storing
The flag image is stored in the flag variable, stored like a wall. A one-member list variable flagPos is created to store the row and column values used to display flag images.
flag = FrameBuffer(bytearray([
0b01000000,
0b01111111,
0b01111110,
0b01110000,
0b01000000,
0b01000000,
0b01100000,
0b11110000
]),8,8,framebuf.MONO_HLSB)
flagPos = [0,0]
Character image storing
The characters consist of four images representing facing left, facing right, facing up and facing down. Thus, it is necessary to design 4 data collections and store the data in actor variable as 4 FrameBuffer as shown in the code below. Each set contains 8 8-bit data, stored in framebuf.MONO_HLSB format. For this reason, the drawing or blit must specify the order of the actor data in the drawing process.
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)
]
The direction of the character’s orientation is to move the lever left, right, up, or down, storing the direction in the actorDir variable and the position where the character is displayed in the actorPos variable.
Movement
The movement is done by moving a lever, which is written as follows:
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
In addition to the levers that are used to direct the character’s movement, the A, B, and m1 keys are assigned the following functions.
- Button b to randomize the position of the flag.
- Button b to randomize the position of the character.
- Button m1 to exit the program.
Random flag position
The method for randomizing the position of the flag is conditioned as follows:
- Randomize the row values.
- Randomize the column values.
- If the column position value of the randomized row does not use a wall, calculate the flagPos values of c*8 and r*8, otherwise, repeat the randomization.
You can write the code from the above condition as
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
Random character position
There is an additional condition to randomize the character’s position from the random flag, The position must not be the same as the flag. So the code of random character position is as follows.
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
Collision check
In the game there are two types:
- A collision with a flag results in a random flag repositioning.
- Collision with a wall or edge of the map will beep and do not move.
ตัAn example of coding is as follows.
move left
if (actorPos[0] > 0):
if (isWall(actorPos[0]-actorSize[0],actorPos[1])):
beep()
else:
actorPos[0] -= actorSize[0]
move right
if (actorPos[0] < scrWidth-actorSize[0]):
if (isWall(actorPos[0]+actorSize[0],actorPos[1])):
beep()
else:
actorPos[0] += actorSize[0]
move up
if (actorPos[1] > 0):
if (isWall(actorPos[0],actorPos[1]-actorSize[1])):
beep()
else:
actorPos[1] -= actorSize[1]
move down
if (actorPos[1] < scrHeight-actorSize[1]):
if (isWall(actorPos[0],actorPos[1]+actorSize[1])):
beep()
else:
actorPos[1] += actorSize[1]
Example Code
The snippet of code is as follows, from the above data storage structure and functions can be combined into the flag runner game code as follows:
#######################################################
### 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()
Conclusion
From this article, you will find that the basics of using switches and readings from the ADC can be used to determine direction or selection conditions within the game. And from the graphical display when drawing maps, characters and flags along with setting the conditions of movement and events can create a simple game of collecting flags. A good game will have a lot of other things to add, such as character or environment animations, sound effects, sound effects. and the important thing is conditions for passing scenes that must create challenges for players.
Finally, We hope that this article was helpful and gave some ideas to those who are interested in developing simple and easy-to-make games on their own. Have fun programming.
If you want to talk with us, feel free to leave comments below!!
(C) 2020-2021, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2021-12-12