As part of the work done in the Arcades workshop that I am teaching at Sesc São Paulo, I needed to develop a series of functions for detecting collisions, by color, by coordinates and also to detect the movement of the joystick. These functions are used in some way in all games, and as a teaching resource, I allocated them in a single test file, which moves a rectangle on the Neopixel screen.
During the workshop we will produce six different games, with increasing levels of complexity. They will be: Snake, Maze, Arkanoid, Tetris, Space Invaders and Enduro Racer.
The source code of all games can now be found in the workshop repository: https://github.com/djairjr/oficina_CircuitPython
Treating the JoystickThe Analog Joystick is actually an arrangement of two potentiometers and a switch and can be handled using the Analogio and Digitalio modules.
The analog input reading range will return a value between 0 and 65535, ideally. It's a good idea to do some testing first to get an idea of the real range of values.
Here are two examples. The first contains only the auxiliary functions. And the second, exemplifies movement on the screen using the joystick.
This first example prints the joystick_x and joystick_y raw values:
# Treating Joystick
import board, time
from analogio import AnalogIn
# Prepare Joystick in Analog Pins
joystick_x = AnalogIn(board.A0)
joystick_y = AnalogIn(board.A1)
while True:
print (joystick_x.value, joystick_y.value)
time.sleep (0.05)
In the case of our games, the joystick will move the character to the right, adding one to the horizontal coordinate. Or to the left, subtracting one from the horizontal coordinate. For the vertical coordinate, the same happens, we add one when we move down and we subtract 1 when we move the joystick up.
To do this, I wrote the get_joystick() function, which returns two values, one for each axis.
You can also detect the trigger press, just setting up a Digital Input, with pull UP as follows:
# Treating Joystick
import board, time
from analogio import AnalogIn
# Prepare Joystick in Analog Pins
joystick_x = AnalogIn(board.A0)
joystick_y = AnalogIn(board.A1)
# Prepare Trigger Switch
trigger = DigitalInOut (board.D2)
trigger.direction = Direction.INPUT
trigger.pull = Pull.UP
def get_joystick():
# Returns -1 0 or 1 depending on joystick position
# You can change the values 200 and 65335 to get better response. In my case,
# this numbers allways get results between -1 and 1
x_coord = int (map_range (joystick_x.value, 200, 65535, - 2 , 2))
y_coord = int (map_range (joystick_y.value, 200, 65535, - 2 , 2))
return x_coord, y_coord
while True:
print (get_joystick())
# Detect trigger press - active low
if not trigger.value:
print ('Shot')
time.sleep (0.05)
Color Collision DetectionAnother important routine is collision detection. In some games this is done simply by identifying whether the two objects are in the same position (if the object's x and y coordinates are the same).
But in others, it is interesting to do this based on the pixel color. I use this routine in the game Maze, for example.
The code bellow, detects collision between red pixel and the two blue rectangles.
'''
Check Collision by Color
Wroted by Djair Guilherme (Nicolau dos Brinquedos)
For the "Recriating Arcade Games in Circuitpython, Neopixel and Seeed Xiao RP2040"
SESC Workshop - São Paulo - Brazil - May 2024
Requirements:
custom tilegrid, tile_framebuf, my_framebuf libraries
and simpleio, adafruit_pixel_framebuf, adafruit_rtttl, neopixel_spi
'''
# GC is a memory manager - Sometimes framebuffer get full
import gc
import board, random, time
import neopixel_spi as neopixel
from rainbowio import colorwheel
# Treating Joystick
from analogio import AnalogIn
from digitalio import DigitalInOut, Direction, Pull
from simpleio import map_range
# My custom version of Library, using two 8 x 32 pannels
'''from tile_framebuf import TileFramebuffer'''
# This is the original version of library when using 16x16 Panels
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
spi = board.SPI()
pixel_pin = board.D10 #board.MOSI
# Prepare Joystick in Analog Pins
joystick_x = AnalogIn(board.A0)
joystick_y = AnalogIn(board.A1)
# Prepare Trigger Switch
trigger = DigitalInOut (board.D2)
trigger.direction = Direction.INPUT
trigger.pull = Pull.UP
# Setup Buzzer. Can be any digital Pin
buzzer = board.D3
'''
# Setting up the Neopixel Pannel - 8 x 32 Version
num_tiles = 2
panel_height = 8
pixel_width = 32
pixel_height = panel_height * num_tiles
'''
# Setting up the Neopixel Pannel - 16 x 16 Version
pixel_width = 32
pixel_height = 16
num_pixels = pixel_width * pixel_height
pixels = neopixel.NeoPixel_SPI(
spi,
num_pixels, # dont forget to multiply for num_tiles
brightness=0.2,
auto_write=False,
)
'''
# Prepare PixelFramebuffer Screen 8 x 32 Version
screen = TileFramebuffer(
pixels,
pixel_width,
panel_height,
num_tiles,
rotation = 1 # 0 Default.
)
'''
# When 16x16 Using original Adafruit_Pixel_Framebuf Library
screen = PixelFramebuffer(
pixels,
pixel_width,
pixel_height,
rotation = 0,
reverse_x=True,
orientation=VERTICAL,
)
def get_joystick():
# Returns -1 0 or 1 depending on joystick position
x_coord = int (map_range (joystick_x.value, 200, 65535, - 2 , 2))
y_coord = int (map_range (joystick_y.value, 200, 65535, - 2 , 2))
return x_coord, y_coord
def get_pixel_color(x, y):
# Check if coordinates are within valid limits
if (0 <= x < screen.width) and (0 <= y < screen.height):
# Get pixel color
rgbint = screen.pixel(x, y)
return (rgbint >> 16 & 0xFF, rgbint >> 8 & 0xFF, rgbint & 0xFF)
# Return black (0, 0, 0) if out of bounds
return (0, 0, 0)
def check_wall(x, y, wall_color):
# Check Screen Limits First
if x < 0 or x >= screen._width or y < 0 or y >= screen._height:
return False
# Then check color
color = get_pixel_color(x, y)
return color != wall_color
def check_color(x, y, colorcheck):
# Only check a color
color = get_pixel_color(x, y)
return color == colorcheck
# Starting position of pixel
old_x = pixel_width // 2
old_y = pixel_height // 2
# Draw Blue Rectangles at diagonals
screen.fill(0x000000)
screen.pixel (old_x, old_y, 0xff0000)
screen.fill_rect(28, 14, 4, 2, 0x0000FF)
screen.fill_rect(0, 0, 4, 2, 0x0000FF)
screen.display()
while True:
if not trigger.value:
screen.fill(0x000000)
else:
new_x, new_y = get_joystick()
x_pos = old_x + new_x
y_pos = old_y + new_y
# Check Screen Limits
if x_pos < 0:
x_pos = 0
if x_pos>=pixel_width - 1:
x_pos = pixel_width -1
if y_pos < 0:
y_pos = 0
if y_pos >= pixel_height - 1:
y_pos = pixel_height - 1
if (x_pos != old_x) or (y_pos != old_y):
# If moves, clear screen first
screen.fill(0x000000)
screen.fill_rect(28, 14, 4, 2, 0x0000FF)
screen.fill_rect(0, 0, 4, 2, 0x0000FF)
# Print coordinates and color check result
if not check_color(x_pos, y_pos, (0, 0, 255)):
screen.pixel(x_pos, y_pos, 0xff0000)
old_x = x_pos
old_y = y_pos
else:
screen.pixel(old_x, old_y, 0xff0000)
print ('Collision at ', x_pos, y_pos)
screen.display()
Another usefull function is a Countdown.
'''
Simple Countdown Routine
'''
import board, random, time
# Using HT16K33 as Score and Message Display
from adafruit_ht16k33 import segments
# This is HT16K33 Four Digits 14 Segment Display
display = segments.Seg14x4(board.I2C())
class Countdown ():
""" Countdown Class """
def __init__ (self, time_sec):
self.time_sec = time_sec
self.update()
def update(self):
current = 0
while self.time_sec:
now = time.monotonic()
mins, secs = divmod(self.time_sec, 60)
timeformat = '{:02d}:{:02d}'.format(mins, secs)
print(timeformat, end='\r')
display.print (timeformat)
if now >= current + 1:
self.time_sec -= 1
current = now
time.sleep(0.1)
display.marquee ("KABOOM " , loop=False)
time.sleep(0.1)
countdown = Countdown (60)
Now, the moving rectangle example:
'''
Moving rectangle Example. Testing Joystick and Draw functions
Wroted by Djair Guilherme (Nicolau dos Brinquedos)
For the "Recriating Arcade Games in Circuitpython, Neopixel and Seeed Xiao RP2040"
SESC Workshop - São Paulo - Brazil - May 2024
Requirements: custom tilegrid, tile_framebuf, my_framebuf libraries
Available at: https://github.com/djairjr/oficina_CircuitPython/tree/main/aula_6_Neopixel/libraries
'''
import board, time, os
from analogio import AnalogIn
from digitalio import DigitalInOut, Direction, Pull
from simpleio import map_range
import neopixel_spi as neopixel
# This is the original version of library, not used..
from adafruit_pixel_framebuf import PixelFramebuffer, VERTICAL
from rainbowio import colorwheel
import framebufferio
# My custom version
from tile_framebuf import TileFramebuffer
spi = board.SPI()
pixel_pin = board.D10 # Not used - using SPI
pixel_width = 32
pixel_height = 8
num_tiles = 2
joystick_x = AnalogIn(board.A0)
joystick_y = AnalogIn(board.A1)
trigger = DigitalInOut (board.D2)
trigger.direction = Direction.INPUT
trigger.pull = Pull.UP
pixels = neopixel.NeoPixel_SPI(
spi,
pixel_width * pixel_height * num_tiles, # dont forget to multiply for num_tiles
brightness=0.1,
auto_write=False,
)
screen = TileFramebuffer(
pixels,
pixel_width,
pixel_height,
num_tiles,
rotation = 3
)
def get_joystick():
# Returns -1 0 or 1 depending on joystick position
x_coord = int (map_range (joystick_x.value, 200, 65535, - 2 , 2))
y_coord = int (map_range (joystick_y.value, 200, 65535, - 2 , 2))
return x_coord, y_coord
old_x = pixel_width //2
old_y = pixel_height * num_tiles // 2
while True:
if (not trigger.value):
screen.fill (0x000000)
else:
new_x, new_y = get_joystick()
x_pos = old_x + new_x
y_pos = old_y + new_y
# Draw rect
screen.fill_rect (y_pos, x_pos, 2, 2, colorwheel((time.monotonic()*50)%255))
screen.display()
if ((x_pos != old_x) or (y_pos != old_y)):
# If moves, clear screen first
screen.fill (0x000000)
old_x = x_pos
old_y = y_pos
""" Tratando os limites da tela """
if (old_x < 3):
old_x = 3
if (old_x > pixel_width - 3):
old_x = pixel_width - 3
if (old_y < 3):
old_y = 3
if (old_y > pixel_height * num_tiles - 3):
old_y = pixel_height * num_tiles - 3
Comments