CPWii (Retro Wii-style Mariokart Game on LED Matrix)
by flanaghi in Circuits > Microcontrollers
80 Views, 1 Favorites, 0 Comments
CPWii (Retro Wii-style Mariokart Game on LED Matrix)
If you were like me growing up, you had a Nintendo Wii and spent countless hours playing video games on it. One of those games being the classic Mario Kart. To relive my glory days and test my coding skills (in Circuit Python), I decided to recreate a retro style Mario Kart on an LED matrix that behaves just like a Wii. I used two Circuit Playground Bluefruits, one to program the game and the other to control it like a Wii remote. I used Bluetooth connection between the two of them to send/receive accelerometer values to control the "car" on the LED. I will go deeper into the code at that respective step, but that is the general overview and I hope you enjoy!
Supplies
Materials:
16x16 LED Matrix (I used this one on Amazon: BTF-LIGHTING WS2812B ECO RGB Alloy Wires 5050SMD Individual Addressable 16X16 256 Pixels LED Matrix Flexible FPCB Full Color Works with K-1000C,SP107E,etc Controllers Image Video Text Display DC5V)
Velcro tape
Alligator clips
2 Circuit Playground Bluefruits from Adafruit
1/8 inch wood (box to hold led)
Wood glue
2 battery packs and usb wires (to power CPBs)
PLA for 3D printing Wii remote
Heat shrink tubing for wires (prevents shorting)
Make Materials
Make all materials needed:
Tinker cad file for 3D printing wii remote
Maker case file then edit in Adobe Illustrator to customize box. Then lazer cut to hold LED.
Downloads
Add to Previous Materials
Velcro tape battery pack to wii remote and make sure CPB fits in top. Velcro tape LED matrix to box and leave the back panel open to be able to work with wiring.
Wire Up LED and CPB (Receiver)
Use DIN, 5V and GND wires from LED and connect to respective parts of CPB (I used alligator clips).
Sender Code
Now is the code. I will walk through what each line does in this code.
There are two files, the sender (remote) and receiver (game).
I will walk through sender here:
1. Import Required Libraries and Modules
board: Provides access to hardware pins.
time: Allows for time-related functions like delays.
digitalio: Provides digital I/O control for GPIO pins.
busio: Provides an interface for I2C communication.
Button: Debounces button inputs to avoid multiple triggers from a single press.
BLERadio: Manages the Bluetooth Low Energy (BLE) radio interface.
ProvideServicesAdvertisement: Advertises BLE services.
UARTService: Provides a UART (Universal Asynchronous Receiver-Transmitter) service over BLE for serial data communication.
RawTextPacket: Encodes and decodes packets of plain text to send over BLE.
adafruit_lis3dh: Provides functions for handling an LIS3DH accelerometer.
2. Set Up the Accelerometer with I2C Communication
Create an I2C interface: i2c connects the microcontroller to the accelerometer using I2C pins (SCL and SDA).
Set up an interrupt pin: int1 is configured to receive interrupts from the accelerometer, allowing the microcontroller to react to motion events.
Initialize the accelerometer: accelerometer is an object representing the LIS3DH accelerometer.
Set accelerometer range: The range is set to ±8G, which controls the sensitivity and range of acceleration the sensor can measure.
3) Set up BLE and Define Device Name
receiver_name: Stores the name of the BLE device we want to connect to.
ble: Initializes the Bluetooth Low Energy (BLE) interface.
uart_connection: Sets a variable to hold the BLE connection to the device, initialized as None.
4) Define Functions to Send Packets Over BLE
send_packet(uart_connection_name, packet):
Sends a packet over the established BLE UART connection and handles possible disconnections.
send_restart(uart_connection_name)
Sends a "RESTART" command over BLE.
5. Main Loop for BLE Connection and Accelerometer Data Transmission
BLE Scanning: If not connected, it scans for nearby BLE devices.
Connection: Connects to the device and stops scanning once connected.
6. Data Transmission Loop
Read Accelerometer Data: x, y, z values from accelerometer.acceleration represent acceleration on each axis.
Format Data: Combines x and y values with a comma and newline to make a readable format.
Send Data: Uses send_packet to transmit the data to the BLE device. If send_packet fails, it clears the uart_connection.
Sleep: Adds a small delay to control data rate.
Downloads
Receiver Code
Now I will walk through receiver code (much more complicated):
Step 1: Import Libraries
import board - Accesses the hardware pins of the board.
import neopixel - Allows control of NeoPixel (RGB) LEDs.
import digitalio - Used for digital I/O control, like enabling the speaker.
from adafruit_ble import BLERadio
from adafruit_ble.advertising.standard import ProvideServicesAdvertisement
from adafruit_ble.services.nordic import UARTService
from adafruit_bluefruit_connect.packet import Packet
from adafruit_bluefruit_connect.raw_text_packet import RawTextPacket - Everything adafruit_ble Imports modules to set up Bluetooth Low Energy (BLE) connections and services.
import time - Allows tracking of elapsed time for game events.
import random - Provides functions to randomize certain game events.
from audiomp3 import MP3Decoder
from audiopwmio import PWMAudioOut as AudioOut # For CPB & Pico (IF USING SOUND)
Step 2: Setup Bluetooth Connection
#CODE
ble = BLERadio()
uart = UARTService()
advertisement = ProvideServicesAdvertisement(uart)
advertisement.complete_name = "ACF-r"
ble.name = advertisement.complete_name
print("Running Receiver Code!")
#DESCRIPTION
Initializes the BLE module, creating a connection service and an advertisement that will be used to advertise the device.
Sets the BLE device name to "ACF-r" and starts the receiver code for BLE interactions.
Step 3: Setup NeoPixel and Game Variables
#CODE
pixel = neopixel.NeoPixel(board.A1, n=256, brightness=0.3, auto_write=False)
#DESCRIPTION
Initializes a 16x16 NeoPixel grid (256 LEDs total) on A1 with 0.3 brightness.
#CODE
car_position = [8, 15] # Starting position on a 16x16 grid
#DESCRIPTION
Sets the car's starting position in the center-bottom of the NeoPixel grid.
Step 4: Define Obstacle and Finish Line Properties
#CODE:
obstacle_speed = 0.5 # Speed for obstacles
last_obstacle_update = time.monotonic() # Track last update time
obstacle_interval = 2 # Time interval in seconds to add new obstacles
last_obstacle_spawn = time.monotonic()
#DESCRIPTION
Controls obstacle behavior, including speed, time tracking, and spawn intervals.
#CODE
obstacles = [{"x": random.randint(3, 12), "y": 0, "width": random.randint(1, 3), "original_x": random.randint(3, 12)}]
#DESCRIPTION
Initializes the obstacle list with a random obstacle position and size.
#CODE
finish_line_y = -1 # Start off-screen
finish_line_trigger_time = 15 # Time in seconds to trigger finish line
last_obstacle_hit_time = time.monotonic() # Last time an obstacle was hit
#DESCRIPTION
Defines the finish line properties and triggers, including its starting position and trigger time.
Step 5: Define Game Reset and Obstacle Reset Functions
#CODE
def reset_obstacle(obstacle):
obstacle["y"] = 0 # Reset to the top
obstacle["x"] = random.randint(3, 12) # Restrict x position within the "road" area
obstacle["original_x"] = obstacle["x"] # Set starting x position for mirroring
obstacle["width"] = random.randint(1, 3) # Random width between 1 and 3 pixels
#DESCRIPTION
Defines how obstacles reset to a random location and size at the top of the grid.
#CODE
def reset_game():
global car_position, obstacles, last_obstacle_hit_time, finish_line_y
car_position = [8, 15] # Reset car position
obstacles = [{"x": random.randint(3, 12), "y": 0, "width": random.randint(1, 3), "original_x": random.randint(3, 12)}]
last_obstacle_hit_time = time.monotonic() # Reset the timer
finish_line_y = -1 # Reset finish line off-screen
#DESCRIPTION
Resets the game state, including car position, obstacles, and timer.
Step 6: Main Loop - Advertising and BLE Connection Handling
#CODE
while True:
ble.start_advertising(advertisement) # Start advertising.
print(f"Advertising as: {advertisement.complete_name}")
was_connected = False
#DESCRIPTION
Advertises the BLE device and sets a connection flag.
Step 7: Handle Bluetooth Messages and Game Actions
#CODE
while not was_connected or ble.connected:
if ble.connected: # If BLE is connected...
was_connected = True
#DESCRIPTION
Continues the game loop while BLE is connected, allowing data reception.
#CODE
if uart.in_waiting:
try:
packet = Packet.from_stream(uart) # Create the packet object.
print(f"packet: {packet}")
except ValueError:
continue
#DESCRIPTION
Checks for incoming BLE data packets and prints them.
#CODE
if isinstance(packet, RawTextPacket): # If the packet is a RawTextPacket
message = packet.text.decode().strip()
print(f"Message Received: {message}")
#DESCRIPTION
Confirms the packet is raw text, then decodes and prints it.
#CODE
values = message.split(',')
x_accel = float(values[0])
print(f"Accelerometer values: X: {x_accel}")
#DESCRIPTION
Reads accelerometer values sent from the BLE-connected device.
#CODE
if x_accel < -2.5 and car_position[0] < 12: # Tilted left
car_position[0] += 1 # Move right
elif x_accel > 2.5 and car_position[0] > 3: # Tilted right
car_position[0] -= 1 # Move left
#DESCRIPTION
Moves the car left or right based on accelerometer input.
Step 8: Obstacle and Finish Line Update Logic
#CODE
current_time = time.monotonic()
if current_time - last_obstacle_update >= obstacle_speed:
#DESCRIPTION
Checks if it’s time to move the obstacles down by comparing elapsed time.
#CODE
if finish_line_y >= 0 and finish_line_y < 15:
finish_line_y += 1 # Move finish line down by 1 row
#DESCRIPTION
Moves the finish line down if it’s been triggered.
#CODE
for obstacle in obstacles:
if obstacle["y"] % 2 == 0:
obstacle["x"] = obstacle["original_x"]
else:
obstacle["x"] = 15 - obstacle["original_x"]
obstacle["y"] += 1 # Move down one row
#DESCRIPTION
Adjusts each obstacle’s position, mirroring them to simulate lane shifts.
#CODE
for i in range(obstacle["width"]):
if obstacle["x"] + i == car_position[0] and obstacle["y"] == car_position[1]:
print("Game Over!")
reset_game() # Automatically restart the game
break
# If the obstacle reaches the bottom, reset it to the top with a new random x position
if obstacle["y"] > 15:
reset_obstacle(obstacle)
last_obstacle_update = current_time
#DESCRIPTION
Checks for collisions between obstacles and the car.
Step 9: Add New Obstacles
#CODE
if current_time - last_obstacle_spawn >= obstacle_interval:
obstacles.append({
"x": random.randint(3, 12),
"y": 0,
"width": random.randint(1, 3),
"original_x": random.randint(3, 12)
})
last_obstacle_spawn = current_time
#DESCRIPTION
Spawns new obstacles periodically.
#CODE
# Check for finish line condition
if current_time - last_obstacle_hit_time >= finish_line_trigger_time and finish_line_y == -1:
finish_line_y = 0 # Start falling only once
print("Finish line started falling") # Debug statement
#DESCRIPTION
Makes the finish line start falling after 15 seconds of not hitting an obstacle
Step 10: Draw Car, Obstacles, and Finish Line on LED Matrix
#CODE
pixel.fill((0, 0, 0)) # Clear all pixels
for y in range(16):
pixel[y * 16 + 2] = (128, 128, 128) # Left road line in gray
pixel[y * 16 + 13] = (128, 128, 128) # Right road line in gray
car_index = car_position[1] * 16 + car_position[0]
pixel[car_index] = (0, 255, 0) # Car in green
#DESCRIPTION
Clears the screen, draws road lines, and places the car in green.
#CODE
for obstacle in obstacles:
for i in range(obstacle["width"]):
if 3 <= obstacle["x"] + i <= 12 and 0 <= obstacle["y"] < 16:
obstacle_index = obstacle["y"] * 16 + (obstacle["x"] + i)
pixel[obstacle_index] = (255, 0, 0) # Obstacles in red
#DESCRIPTION
Draws each obstacle as red pixels on the grid.
#CODE
if finish_line_y >= 0 and finish_line_y < 16:
for x in range(3, 13): # Draw the finish line across the road
pixel[int(finish_line_y) * 16 + x] = (255, 255, 0) # Finish line in yellow
#DESCRIPTION
Draws the finish line if it’s triggered, moving it down one row each cycle.
#CODE
pixel.show() # Update the LED matrix
#DESCRIPTION
Updates the NeoPixel display, refreshing the visual game state.
Downloads
Run Program
It is important that you start the receiver code before the sender and wait a few seconds before running the sender because of the delay in Bluetooth connection. The program should automatically start running once the receiver is connected to the sender, and the accelerometer values will be transmitted, and you can move the car.