Since Unihiker already comes with everything you need for games, here is a small tutorial. You will learn how to create a game with PyGame, use sensors (accelerometer), make inputs (touch screen and buttons) and use the buzzer for output signals. It doesn't matter where (on which platform/OS) and with what (Editor/IDE) you develop the code.
Create a folder for your project on your local device (PC/Notebook). All other necessary files are then created and stored in it. You should keep the name of the folder simple (without spaces or special characters) and remember it. For example give the folder name "DirtyRoadDriver".
Now switch to this folder and create a Python file. Again, without spaces or special characters. For example give the filename "dirty_road_driver.py".
Create the following content in it.
from random import randint, choice
from sys import exit
from time import sleep
import pygame
from pinpong.board import Board, Pin, Tone
from pinpong.extension.unihiker import accelerometer, button_a
BACKGROUND = (0, 255, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
FRAMERATE = 30
if __name__ == '__main__':
# initialize PinBoard
Board().begin()
# initialize PyGame
pygame.init()
pygame.display.set_caption('Car Driver')
pygame.event.set_allowed([pygame.MOUSEBUTTONUP])
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
while True:
# exit game and stop program
if button_a.is_pressed():
break
# render output
pygame.display.flip()
clock.tick(FRAMERATE)
# stop application
pygame.quit()
exit()
You have created already a running template for your game! In lines 1 to 7, all necessary Python modules and packages are imported (including those that are currently not required).
Lines 10 through 15 create Python constants with values. These do not change (in contrast to variables). This should prevent magic values in the later code, simplify any configurations and simplify maintenance.
Since this game is to be executed directly (code will not imported as a module later), a first condition is created in line 18.
In lines 19 to 27, PinPong and PyGame are initialized and configured. Line 29 contains an infinite loop, the entire game will later be executed in it. Pressing the A button exits this loop.
The last two lines 39 and 40 terminate the program and release all necessary system resources again.
class Road:
def __init__(self, image):
"""
Road constructor
:param image: image/path for road as str
"""
self._img = pygame.image.load(image).convert()
self._pos_x = int((SCREEN_WIDTH - self._img.get_width()) / 2)
self._pos_y = 0
def draw_road(self, interface, speed):
"""
draw road animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
interface.blit(self._img, (self._pos_x, self._pos_y))
interface.blit(self._img, (self._pos_x, self._pos_y - SCREEN_HEIGHT))
if self._pos_y == SCREEN_HEIGHT:
interface.blit(self._img, (self._pos_x, self._pos_y + SCREEN_HEIGHT))
self._pos_y = 0
self._pos_y += speed
The “Road” class contains a constructor that we pass parameters and a method that animates the road on the screen. The road (an image) should later be animated infinitely (with a specific speed) from top to bottom.
class Car:
def __init__(self, image):
"""
Car constructor
:param image: image/path for car as str
"""
self._img = pygame.image.load(image).convert_alpha()
self._pos_x = int(SCREEN_WIDTH / 2) + 25
self._pos_y = SCREEN_HEIGHT - self._img.get_height() - 10
self.rect = self._img.get_rect(topleft=(self._pos_x, self._pos_y))
@staticmethod
def _get_direction():
"""
get direction from accelerometer
:return: str
"""
y_accel = accelerometer.get_y()
value = int(y_accel * 100)
# depending to delivered hardware setup
# x_accel = accelerometer.get_x()
# value = int(x_accel * 100)
if value < -10:
return 'right'
elif value > 10:
return 'left'
else:
return 'middle'
def draw_car(self, interface, speed):
"""
draw car animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
direction = Car._get_direction()
if direction == 'right' and self.rect.x < 190:
self.rect.x += speed
if direction == 'left' and self.rect.x > 5:
self.rect.x -= speed
interface.blit(self._img, self.rect)
The “Car” class also contains a constructor, a static method (to query the accelerometer), and a method to place the object (on the screen). The object can later move horizontally depending on the tilt of the device.
class Obstacle:
def __init__(self, image, damage):
"""
Obstacle constructor
:param image: image/path for obstacle as str
:param damage: the damage produced by obstacle as int
"""
self.damage = damage
self._img = pygame.image.load(image).convert_alpha()
self._pos_x = None
self._pos_y = None
self.rect = None
self.active = None
self.reset_obstacle()
def draw_obstacle(self, interface, speed):
"""
draw obstacle animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
if self.rect.y < SCREEN_HEIGHT + self.rect.h + 10 and self.active:
self.rect.y += speed
interface.blit(self._img, self.rect)
def reset_obstacle(self):
"""
set/reset all values for obstacle
:return: None
"""
self._pos_x = randint(int(self._img.get_width()), (SCREEN_WIDTH - int(self._img.get_width())))
self._pos_y = -100
self.rect = self._img.get_rect(topleft=(self._pos_x, self._pos_y))
self.active = False
The "Obstacle" class also contains a constructor and 2 methods. One for placement and one for reset. The last one ensures that all obstacles start at the top again.
class Buzzer:
def __init__(self, pin):
"""
Buzzer constructor
:param pin: pin object for buzzer
"""
self._tone = Tone(Pin(pin))
self._tone.freq(800)
def play_tone(self, duration):
"""
play buzzer sound
:param duration: int/float for buzzer duration
:return: None
"""
self._tone.on()
sleep(duration)
self._tone.off()
The "Buzzer" class later generates the sounds in the event of a collision.
class Display:
def __init__(self, intro_img, outro_img):
"""
Display constructor
:param intro_img: image/path for intro display as str
:param outro_img: image/path for outro display as str
"""
self.intro_img = pygame.image.load(intro_img).convert()
self.outro_img = pygame.image.load(outro_img).convert()
self.intro_screen = True
self.game_screen = False
self.outro_screen = False
self.font = pygame.font.SysFont("Arial", 18)
self.damage = 0
self.crashes = 0
def show_display(self, interface):
"""
show information on display
:param interface: pygame display object
:return: None
"""
pygame.draw.rect(interface, WHITE, pygame.Rect(0, 0, SCREEN_WIDTH, 25))
info_text = f'Fails: {self.crashes} Damage: {self.damage}%'
text_surface = self.font.render(info_text, True, BLACK)
interface.blit(text_surface, (5, 2))
The last class "Display" should inform the user (player) about the condition of the car.
from random import randint, choice
from sys import exit
from time import sleep
import pygame
from pinpong.board import Board, Pin, Tone
from pinpong.extension.unihiker import accelerometer, button_a
BACKGROUND = (0, 255, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
FRAMERATE = 30
class Road:
def __init__(self, image):
"""
Road constructor
:param image: image/path for road as str
"""
self._img = pygame.image.load(image).convert()
self._pos_x = int((SCREEN_WIDTH - self._img.get_width()) / 2)
self._pos_y = 0
def draw_road(self, interface, speed):
"""
draw road animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
interface.blit(self._img, (self._pos_x, self._pos_y))
interface.blit(self._img, (self._pos_x, self._pos_y - SCREEN_HEIGHT))
if self._pos_y == SCREEN_HEIGHT:
interface.blit(self._img, (self._pos_x, self._pos_y + SCREEN_HEIGHT))
self._pos_y = 0
self._pos_y += speed
class Car:
def __init__(self, image):
"""
Car constructor
:param image: image/path for car as str
"""
self._img = pygame.image.load(image).convert_alpha()
self._pos_x = int(SCREEN_WIDTH / 2) + 25
self._pos_y = SCREEN_HEIGHT - self._img.get_height() - 10
self.rect = self._img.get_rect(topleft=(self._pos_x, self._pos_y))
@staticmethod
def _get_direction():
"""
get direction from accelerometer
:return: str
"""
y_accel = accelerometer.get_y()
value = int(y_accel * 100)
# depending to delivered hardware setup
# x_accel = accelerometer.get_x()
# value = int(x_accel * 100)
if value < -10:
return 'right'
elif value > 10:
return 'left'
else:
return 'middle'
def draw_car(self, interface, speed):
"""
draw car animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
direction = Car._get_direction()
if direction == 'right' and self.rect.x < 190:
self.rect.x += speed
if direction == 'left' and self.rect.x > 5:
self.rect.x -= speed
interface.blit(self._img, self.rect)
class Obstacle:
def __init__(self, image, damage):
"""
Obstacle constructor
:param image: image/path for obstacle as str
:param damage: the damage produced by obstacle as int
"""
self.damage = damage
self._img = pygame.image.load(image).convert_alpha()
self._pos_x = None
self._pos_y = None
self.rect = None
self.active = None
self.reset_obstacle()
def draw_obstacle(self, interface, speed):
"""
draw obstacle animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
if self.rect.y < SCREEN_HEIGHT + self.rect.h + 10 and self.active:
self.rect.y += speed
interface.blit(self._img, self.rect)
def reset_obstacle(self):
"""
set/reset all values for obstacle
:return: None
"""
self._pos_x = randint(int(self._img.get_width()), (SCREEN_WIDTH - int(self._img.get_width())))
self._pos_y = -100
self.rect = self._img.get_rect(topleft=(self._pos_x, self._pos_y))
self.active = False
class Buzzer:
def __init__(self, pin):
"""
Buzzer constructor
:param pin: pin object for buzzer
"""
self._tone = Tone(Pin(pin))
self._tone.freq(800)
def play_tone(self, duration):
"""
play buzzer sound
:param duration: int/float for buzzer duration
:return: None
"""
self._tone.on()
sleep(duration)
self._tone.off()
class Display:
def __init__(self, intro_img, outro_img):
"""
Display constructor
:param intro_img: image/path for intro display as str
:param outro_img: image/path for outro display as str
"""
self.intro_img = pygame.image.load(intro_img).convert()
self.outro_img = pygame.image.load(outro_img).convert()
self.intro_screen = True
self.game_screen = False
self.outro_screen = False
self.font = pygame.font.SysFont("Arial", 18)
self.damage = 0
self.crashes = 0
def show_display(self, interface):
"""
show information on display
:param interface: pygame display object
:return: None
"""
pygame.draw.rect(interface, WHITE, pygame.Rect(0, 0, SCREEN_WIDTH, 25))
info_text = f'Fails: {self.crashes} Damage: {self.damage}%'
text_surface = self.font.render(info_text, True, BLACK)
interface.blit(text_surface, (5, 2))
if __name__ == '__main__':
# initialize PinBoard
Board().begin()
# initialize PyGame
pygame.init()
pygame.display.set_caption('Car Driver')
pygame.event.set_allowed([pygame.MOUSEBUTTONUP])
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
# create variables
game_speed = 5
sound_duration = .05
btn = (75, 275, 105, 20)
# create game objects
road = Road(image='road.jpg')
car = Car(image='car.png')
obstacles = [Obstacle(image='oil.png', damage=5),
Obstacle(image='pothole.png', damage=10),
Obstacle(image='stone.png', damage=15)]
sound = Buzzer(pin=Pin.P26)
info = Display(intro_img='intro.jpg', outro_img='outro.jpg')
while True:
# exit game and stop program
if button_a.is_pressed():
break
# game logic will follow here
# render output
pygame.display.flip()
clock.tick(FRAMERATE)
# stop application
pygame.quit()
exit()
You're almost there!
In lines 193 to 196 we have created variables. These could be changed later (depending on the level or use). All objects are created in lines 198 to 205 (with the necessary parameters). Since you may want to use different obstacles, they are created in a list (lines 201 to 203). So you can even add more later.
# intro screen
if info.intro_screen:
# catch touch to start the game
event = pygame.event.wait()
if event.type == pygame.MOUSEBUTTONUP:
mouse_pos = event.__dict__['pos']
if btn[0] < mouse_pos[0] < (btn[0] + btn[2]) and btn[1] < mouse_pos[1] < (btn[1] + btn[3]):
info.intro_screen = False
info.game_screen = True
# show introduction image
screen.blit(info.intro_img, (0, 0))
pygame.draw.rect(screen, WHITE, btn)
text = info.font.render('Start Game', True, BLACK)
screen.blit(text, btn)
The first screen (intro screen) should be displayed to the user so that player can start the game (via touch screen).
# game screen
if info.game_screen:
# game exit condition
if info.damage >= 100:
info.game_screen = False
info.outro_screen = True
# set background color on screen
screen.fill(BACKGROUND)
# draw road on screen
road.draw_road(interface=screen, speed=game_speed)
# draw car on screen
car.draw_car(interface=screen, speed=game_speed)
# select random obstacle from list
obstacle_active_list = []
for item in obstacles:
obstacle_active_list.append(item.active)
if not any(obstacle_active_list):
active_obstacle = choice(obstacles)
if not active_obstacle.active:
active_obstacle.active = True
# draw active obstacle on screen
for item in obstacles:
if item.active:
item.draw_obstacle(interface=screen, speed=game_speed)
if item.active and item.rect.y > SCREEN_HEIGHT + item.rect.h:
item.reset_obstacle()
# verify collision for active obstacle and car
for item in obstacles:
if item.active:
collide = pygame.Rect.colliderect(car.rect, item.rect)
if collide:
item.reset_obstacle()
sound.play_tone(duration=sound_duration)
info.crashes += 1
info.damage += item.damage
# draw information on screen
info.show_display(interface=screen)
The second screen (game screen) shows the running game. The logic of the game is also located here.
# outro screen
if info.outro_screen:
screen.blit(info.outro_img, (0, 0))
text_outro = info.font.render('Press button A to exit', True, WHITE)
screen.blit(text_outro, (10, 295))
The third screen (Outro screen) is shown to the player when the game is lost.
from random import randint, choice
from sys import exit
from time import sleep
import pygame
from pinpong.board import Board, Pin, Tone
from pinpong.extension.unihiker import accelerometer, button_a
BACKGROUND = (0, 255, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
FRAMERATE = 30
class Road:
def __init__(self, image):
"""
Road constructor
:param image: image/path for road as str
"""
self._img = pygame.image.load(image).convert()
self._pos_x = int((SCREEN_WIDTH - self._img.get_width()) / 2)
self._pos_y = 0
def draw_road(self, interface, speed):
"""
draw road animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
interface.blit(self._img, (self._pos_x, self._pos_y))
interface.blit(self._img, (self._pos_x, self._pos_y - SCREEN_HEIGHT))
if self._pos_y == SCREEN_HEIGHT:
interface.blit(self._img, (self._pos_x, self._pos_y + SCREEN_HEIGHT))
self._pos_y = 0
self._pos_y += speed
class Car:
def __init__(self, image):
"""
Car constructor
:param image: image/path for car as str
"""
self._img = pygame.image.load(image).convert_alpha()
self._pos_x = int(SCREEN_WIDTH / 2) + 25
self._pos_y = SCREEN_HEIGHT - self._img.get_height() - 10
self.rect = self._img.get_rect(topleft=(self._pos_x, self._pos_y))
@staticmethod
def _get_direction():
"""
get direction from accelerometer
:return: str
"""
y_accel = accelerometer.get_y()
value = int(y_accel * 100)
# depending to delivered hardware setup
# x_accel = accelerometer.get_x()
# value = int(x_accel * 100)
if value < -10:
return 'right'
elif value > 10:
return 'left'
else:
return 'middle'
def draw_car(self, interface, speed):
"""
draw car animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
direction = Car._get_direction()
if direction == 'right' and self.rect.x < 190:
self.rect.x += speed
if direction == 'left' and self.rect.x > 5:
self.rect.x -= speed
interface.blit(self._img, self.rect)
class Obstacle:
def __init__(self, image, damage):
"""
Obstacle constructor
:param image: image/path for obstacle as str
:param damage: the damage produced by obstacle as int
"""
self.damage = damage
self._img = pygame.image.load(image).convert_alpha()
self._pos_x = None
self._pos_y = None
self.rect = None
self.active = None
self.reset_obstacle()
def draw_obstacle(self, interface, speed):
"""
draw obstacle animated on display
:param interface: pygame display object
:param speed: speed as integer
:return: None
"""
if self.rect.y < SCREEN_HEIGHT + self.rect.h + 10 and self.active:
self.rect.y += speed
interface.blit(self._img, self.rect)
def reset_obstacle(self):
"""
set/reset all values for obstacle
:return: None
"""
self._pos_x = randint(int(self._img.get_width()), (SCREEN_WIDTH - int(self._img.get_width())))
self._pos_y = -100
self.rect = self._img.get_rect(topleft=(self._pos_x, self._pos_y))
self.active = False
class Buzzer:
def __init__(self, pin):
"""
Buzzer constructor
:param pin: pin object for buzzer
"""
self._tone = Tone(Pin(pin))
self._tone.freq(800)
def play_tone(self, duration):
"""
play buzzer sound
:param duration: int/float for buzzer duration
:return: None
"""
self._tone.on()
sleep(duration)
self._tone.off()
class Display:
def __init__(self, intro_img, outro_img):
"""
Display constructor
:param intro_img: image/path for intro display as str
:param outro_img: image/path for outro display as str
"""
self.intro_img = pygame.image.load(intro_img).convert()
self.outro_img = pygame.image.load(outro_img).convert()
self.intro_screen = True
self.game_screen = False
self.outro_screen = False
self.font = pygame.font.SysFont("Arial", 18)
self.damage = 0
self.crashes = 0
def show_display(self, interface):
"""
show information on display
:param interface: pygame display object
:return: None
"""
pygame.draw.rect(interface, WHITE, pygame.Rect(0, 0, SCREEN_WIDTH, 25))
info_text = f'Fails: {self.crashes} Damage: {self.damage}%'
text_surface = self.font.render(info_text, True, BLACK)
interface.blit(text_surface, (5, 2))
if __name__ == '__main__':
# initialize PinBoard
Board().begin()
# initialize PyGame
pygame.init()
pygame.display.set_caption('Car Driver')
pygame.event.set_allowed([pygame.MOUSEBUTTONUP])
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
# create variables
game_speed = 5
sound_duration = .05
btn = (75, 275, 105, 20)
# create game objects
road = Road(image='road.jpg')
car = Car(image='car.png')
obstacles = [Obstacle(image='oil.png', damage=5),
Obstacle(image='pothole.png', damage=10),
Obstacle(image='stone.png', damage=15)]
sound = Buzzer(pin=Pin.P26)
info = Display(intro_img='intro.jpg', outro_img='outro.jpg')
while True:
# exit game and stop program
if button_a.is_pressed():
break
# intro screen
if info.intro_screen:
# catch touch to start the game
event = pygame.event.wait()
if event.type == pygame.MOUSEBUTTONUP:
mouse_pos = event.__dict__['pos']
if btn[0] < mouse_pos[0] < (btn[0] + btn[2]) and btn[1] < mouse_pos[1] < (btn[1] + btn[3]):
info.intro_screen = False
info.game_screen = True
# show introduction image
screen.blit(info.intro_img, (0, 0))
pygame.draw.rect(screen, WHITE, btn)
text = info.font.render('Start Game', True, BLACK)
screen.blit(text, btn)
# game screen
if info.game_screen:
# game exit condition
if info.damage >= 100:
info.game_screen = False
info.outro_screen = True
# set background color on screen
screen.fill(BACKGROUND)
# draw road on screen
road.draw_road(interface=screen, speed=game_speed)
# draw car on screen
car.draw_car(interface=screen, speed=game_speed)
# select random obstacle from list
obstacle_active_list = []
for item in obstacles:
obstacle_active_list.append(item.active)
if not any(obstacle_active_list):
active_obstacle = choice(obstacles)
if not active_obstacle.active:
active_obstacle.active = True
# draw active obstacle on screen
for item in obstacles:
if item.active:
item.draw_obstacle(interface=screen, speed=game_speed)
if item.active and item.rect.y > SCREEN_HEIGHT + item.rect.h:
item.reset_obstacle()
# verify collision for active obstacle and car
for item in obstacles:
if item.active:
collide = pygame.Rect.colliderect(car.rect, item.rect)
if collide:
item.reset_obstacle()
sound.play_tone(duration=sound_duration)
info.crashes += 1
info.damage += item.damage
# draw information on screen
info.show_display(interface=screen)
# outro screen
if info.outro_screen:
screen.blit(info.outro_img, (0, 0))
text_outro = info.font.render('Press button A to exit', True, WHITE)
screen.blit(text_outro, (10, 295))
# render output
pygame.display.flip()
clock.tick(FRAMERATE)
# stop application
pygame.quit()
exit()
Complete! The whole game is developed. Take a closer look at the code and try to understand everything better. With a few changes and additional features, you can add levels (e.g. with higher speeds) or display more graphic details.
Here are all the images you can use. These are separate and not as a sprite. These must be saved in the same folder as the Python script.
You can use different options to upload the folder (including all files). I can't dictate which option you want to use! Personally I would recommend SCP (on MacOS/Linux) or SMB (on Windows).
SCP example:
$ scp -r /path/to/local-source root@10.1.2.3:/root/DirtyRoadDriver
Password: dfrobot
Note
This is my first tutorial here and I hope you like it. I would be very happy about a rating or any feedback.