Also in this tutorial I will show you how to develop a game for UNIHIKER with PyGame. But this time we can do without pictures! The development of this game will be very simple, using the accelerometer and a button to cancel/exit the game.
from sys import exit
from time import sleep
from random import choice
import pygame
from pinpong.board import Board, Pin, Tone
from pinpong.extension.unihiker import button_a, accelerometer
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
SOUND_DURATION = .05
COLLISION_TOLERANCE = 5
COLORS = ['black', 'white', 'blue', 'green', 'red']
FPS = 50
if __name__ == '__main__':
# initialize board
Board().begin()
# initialize and configure pygame
pygame.init()
pygame.display.set_caption('BrickBreaker')
pygame.event.set_allowed(None)
pygame.mouse.set_visible(False)
# create screen surface, clock object and font
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
while True:
# exit game loop conditions
if button_a.is_pressed():
break
# set background color
screen.fill(pygame.Color(COLORS[0]))
# render game
pygame.display.flip()
clock.tick(FPS)
# stop application
pygame.quit()
exit()
At the beginning a very simple template is created. This template can even already be executed. However, only a black screen is displayed, which is ended by pressing the A button.
In the first lines all necessary Python modules/packages are imported. In addition, constants are created and given a fixed value for them.
The first condition "if __name__ == '__main__':" is met as soon as you run the program directly. So you cannot easily call the program from other Python programs.
If the condition is met, PinBoard and PyGame are initialized. PyGame is then configured a bit. The actual game is inside the "while" loop. If button A is pressed, the loop will be broken and the game/program will end.
class Striker(pygame.sprite.Sprite):
def __init__(self):
"""
Striker constructor
"""
super().__init__()
self._width = 40
self._height = 5
self._pos_x = SCREEN_WIDTH / 2
self._pos_y = SCREEN_HEIGHT - self._height - 5
self._speed = 10
self.image = pygame.Surface([self._width, self._height])
self.image.fill(COLORS[1])
self.rect = self.image.get_rect()
self.rect.center = [self._pos_x, self._pos_y]
@staticmethod
def _get_direction() -> str:
"""
get accelerometer direction as string
:return: str
"""
y_accel = int(accelerometer.get_y() * 100)
if y_accel < -10:
return 'right'
elif y_accel > 10:
return 'left'
else:
return 'middle'
def update(self) -> None:
"""
set striker position
:return: None
"""
direction = Striker._get_direction()
if direction == 'left' and self._pos_x > (self._width / 2):
self._pos_x -= self._speed
if direction == 'right' and self._pos_x < (SCREEN_WIDTH - self._width / 2):
self._pos_x += self._speed
self.rect.center = [self._pos_x, self._pos_y]
With the class "Striker" you create the paddle that should stop the ball on the lower screen and smash it back. The paddle will move horizontally and is controlled by the user via an accelerometer sensor.
The accelerometer values are read out again and again in the static method. The update method then changes the paddle position.
class Ball(pygame.sprite.Sprite):
def __init__(self):
"""
Ball constructor
"""
super().__init__()
self._pos_x = None
self._pos_y = None
self.speed_x = None
self.speed_y = None
self.reset()
self.image = pygame.Surface([10, 10])
self.image.fill(COLORS[0])
self.image.set_colorkey(COLORS[0])
pygame.draw.circle(self.image, COLORS[1], (5, 5), 5)
self.rect = self.image.get_rect()
self.rect.center = [self._pos_x, self._pos_y]
def reset(self) -> None:
"""
reset ball x, y-position and x, y-speed
:return: None
"""
self._pos_x = SCREEN_WIDTH / 2
self._pos_y = SCREEN_HEIGHT / 2
self.speed_x = choice([-3, 3])
self.speed_y = choice([-3, 3])
def update(self) -> None:
"""
set ball position
:return: None
"""
self._pos_x += self.speed_x
self._pos_y += self.speed_y
self.rect.center = [self._pos_x, self._pos_y]
The class “Ball” is used to create the ball and to update the position on the screen.
The method reset will help to setup on start and if ball get lost, to have a start position.
class Brick(pygame.sprite.Sprite):
def __init__(self, x: int, y: int, w: int, h: int, c: tuple):
"""
Brick constructor
:param x: x-position on screen
:param y: y-position on screen
:param w: width in px
:param h: height in px
:param c: color
"""
super().__init__()
self._width = w
self._height = h
self._pos_x = x
self._pos_y = y
self.image = pygame.Surface([self._width, self._height])
self.image.fill(c)
self.rect = self.image.get_rect()
self.rect.center = [self._pos_x, self._pos_y]
So that you collect points and the ball doesn't just fly around (as in pong), the game still needs small stones which are destroyed by the ball.
A separate object of this class is created for each stone that we want to create.
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) -> None:
"""
play buzzer sound
:return: None
"""
self._tone.on()
sleep(SOUND_DURATION)
self._tone.off()
It gets boring without sound! Since UNIHIKER has already installed a buzzer, we use it too!
def generate_bricks(group, x: int) -> None:
"""
generate all blocks
:param group: sprite group object
:param x: start x-position
:return: None
"""
start_y_values = [55, 75, 95]
colors = [COLOR_BLUE, COLOR_GREEN, COLOR_RED]
for start_y, color in zip(start_y_values, colors):
start_x = x
for _ in range(7):
brick = Brick(x=start_x, y=start_y, w=20, h=10, c=color)
group.add(brick)
start_x += 30
In the game there should be several small stones. This function will save us a lot of work (code) when creating these bricks.
if __name__ == '__main__':
# initialize board
Board().begin()
# initialize and configure pygame
pygame.init()
pygame.display.set_caption('BrickBreaker')
pygame.event.set_allowed(None)
pygame.mouse.set_visible(False)
# create screen surface, clock object and font
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 15)
# variables
score = 0
lives = 3
# create striker object
striker = Striker()
striker_group = pygame.sprite.Group()
striker_group.add(striker)
# create ball object
ball = Ball()
ball_group = pygame.sprite.Group()
ball_group.add(ball)
# create brick objects
bricks_group = pygame.sprite.Group()
generate_bricks(bricks_group, x=30)
# buzzer sound
sound = Buzzer(pin=Pin.P26)
while True:
# exit game loop conditions
if button_a.is_pressed():
break
if lives == 0:
break
# set background color
screen.fill(pygame.Color(COLORS[0]))
# draw striker
striker_group.update()
striker_group.draw(screen)
# draw ball
if ball.rect.left <= 0 or ball.rect.right >= SCREEN_WIDTH:
ball.speed_x *= -1
if ball.rect.top <= 25:
ball.speed_y *= -1
if pygame.sprite.collide_rect(striker, ball):
if ball.rect.bottom - striker.rect.top < COLLISION_TOLERANCE and ball.speed_y > 0:
ball.speed_y *= -1
sound.play_tone()
if ball.rect.top > SCREEN_HEIGHT:
lives -= 1
ball.reset()
ball_group.update()
ball_group.draw(screen)
# draw bricks
if not len(bricks_group):
generate_bricks(bricks_group, x=30)
for block in bricks_group:
if pygame.sprite.collide_rect(ball, block):
score += 1
sound.play_tone()
if abs(ball.rect.top - block.rect.bottom) < COLLISION_TOLERANCE and ball.speed_y < 0:
ball.speed_y *= -1
if abs(ball.rect.bottom - block.rect.top) < COLLISION_TOLERANCE and ball.speed_y > 0:
ball.speed_y *= -1
if abs(ball.rect.right - block.rect.left) < COLLISION_TOLERANCE and ball.speed_x > 0:
ball.speed_x *= -1
if abs(ball.rect.left - block.rect.right) < COLLISION_TOLERANCE and ball.speed_x < 0:
ball.speed_x *= -1
bricks_group.remove(block)
bricks_group.draw(screen)
# user information
info_text = f'Lives: {lives} - Score: {score}'
screen_text = font.render(info_text, True, COLORS[1])
screen.blit(screen_text, (5, 5))
pygame.draw.line(screen, COLORS[1], (5, 25), (SCREEN_WIDTH - 5, 25), 1)
# render game
pygame.display.flip()
clock.tick(FPS)
Let's expand the template. Add a few variables (score, lives) as well as all objects (Font, Striker, Ball, Bricks, Sound) before the loop starts.
Inside the loop:
The ball bounces off the walls on the top, right, and left. If the ball leaves the lower play area, one life is deducted. In addition, the loop is also interrupted when the lives are no longer greater than 0.
If the ball touches the paddle, it is reflected back. If the ball touches a stone, it is destroyed and a point is awarded. When all stones have been destroyed, they are regenerated.
The information about the lives and the points are displayed to the player. As soon as the ball touches the paddle or a stone, a tone is also emitted.
from sys import exit
from time import sleep
from random import choice
import pygame
from pinpong.board import Board, Pin, Tone
from pinpong.extension.unihiker import button_a, accelerometer
SCREEN_WIDTH = 240
SCREEN_HEIGHT = 320
SOUND_DURATION = .05
COLLISION_TOLERANCE = 5
COLORS = ['black', 'white', 'blue', 'green', 'red']
FPS = 50
class Striker(pygame.sprite.Sprite):
def __init__(self):
"""
Striker constructor
"""
super().__init__()
self._width = 40
self._height = 5
self._pos_x = SCREEN_WIDTH / 2
self._pos_y = SCREEN_HEIGHT - self._height - 5
self._speed = 10
self.image = pygame.Surface([self._width, self._height])
self.image.fill(COLORS[1])
self.rect = self.image.get_rect()
self.rect.center = [self._pos_x, self._pos_y]
@staticmethod
def _get_direction() -> str:
"""
get accelerometer direction as string
:return: str
"""
y_accel = int(accelerometer.get_y() * 100)
if y_accel < -10:
return 'right'
elif y_accel > 10:
return 'left'
else:
return 'middle'
def update(self) -> None:
"""
set striker position
:return: None
"""
direction = Striker._get_direction()
if direction == 'left' and self._pos_x > (self._width / 2):
self._pos_x -= self._speed
if direction == 'right' and self._pos_x < (SCREEN_WIDTH - self._width / 2):
self._pos_x += self._speed
self.rect.center = [self._pos_x, self._pos_y]
class Ball(pygame.sprite.Sprite):
def __init__(self):
"""
Ball constructor
"""
super().__init__()
self._pos_x = None
self._pos_y = None
self.speed_x = None
self.speed_y = None
self.reset()
self.image = pygame.Surface([10, 10])
self.image.fill(COLORS[0])
self.image.set_colorkey(COLORS[0])
pygame.draw.circle(self.image, COLORS[1], (5, 5), 5)
self.rect = self.image.get_rect()
self.rect.center = [self._pos_x, self._pos_y]
def reset(self) -> None:
"""
reset ball x, y-position and x, y-speed
:return: None
"""
self._pos_x = SCREEN_WIDTH / 2
self._pos_y = SCREEN_HEIGHT / 2
self.speed_x = choice([-3, 3])
self.speed_y = choice([-3, 3])
def update(self) -> None:
"""
set ball position
:return: None
"""
self._pos_x += self.speed_x
self._pos_y += self.speed_y
self.rect.center = [self._pos_x, self._pos_y]
class Brick(pygame.sprite.Sprite):
def __init__(self, x: int, y: int, w: int, h: int, c: tuple):
"""
Brick constructor
:param x: x-position on screen
:param y: y-position on screen
:param w: width in px
:param h: height in px
:param c: color
"""
super().__init__()
self._width = w
self._height = h
self._pos_x = x
self._pos_y = y
self.image = pygame.Surface([self._width, self._height])
self.image.fill(c)
self.rect = self.image.get_rect()
self.rect.center = [self._pos_x, self._pos_y]
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) -> None:
"""
play buzzer sound
:return: None
"""
self._tone.on()
sleep(SOUND_DURATION)
self._tone.off()
def generate_bricks(group, x: int) -> None:
"""
generate all blocks
:param group: sprite group object
:param x: start x-position
:return: None
"""
start_y_values = [55, 75, 95]
colors = [COLORS[2], COLORS[3], COLORS[4]]
for start_y, color in zip(start_y_values, colors):
start_x = x
for _ in range(7):
brick = Brick(x=start_x, y=start_y, w=20, h=10, c=color)
group.add(brick)
start_x += 30
if __name__ == '__main__':
# initialize board
Board().begin()
# initialize and configure pygame
pygame.init()
pygame.display.set_caption('BrickBreaker')
pygame.event.set_allowed(None)
pygame.mouse.set_visible(False)
# create screen surface, clock object and font
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial", 15)
# variables
score = 0
lives = 3
# create striker object
striker = Striker()
striker_group = pygame.sprite.Group()
striker_group.add(striker)
# create ball object
ball = Ball()
ball_group = pygame.sprite.Group()
ball_group.add(ball)
# create brick objects
bricks_group = pygame.sprite.Group()
generate_bricks(bricks_group, x=30)
# buzzer sound
sound = Buzzer(pin=Pin.P26)
while True:
# exit game loop conditions
if button_a.is_pressed():
break
if lives == 0:
break
# set background color
screen.fill(pygame.Color(COLORS[0]))
# draw striker
striker_group.update()
striker_group.draw(screen)
# draw ball
if ball.rect.left <= 0 or ball.rect.right >= SCREEN_WIDTH:
ball.speed_x *= -1
if ball.rect.top <= 25:
ball.speed_y *= -1
if pygame.sprite.collide_rect(striker, ball):
if ball.rect.bottom - striker.rect.top < COLLISION_TOLERANCE and ball.speed_y > 0:
ball.speed_y *= -1
sound.play_tone()
if ball.rect.top > SCREEN_HEIGHT:
lives -= 1
ball.reset()
ball_group.update()
ball_group.draw(screen)
# draw bricks
if not len(bricks_group):
generate_bricks(bricks_group, x=30)
for block in bricks_group:
if pygame.sprite.collide_rect(ball, block):
score += 1
sound.play_tone()
if abs(ball.rect.top - block.rect.bottom) < COLLISION_TOLERANCE and ball.speed_y < 0:
ball.speed_y *= -1
if abs(ball.rect.bottom - block.rect.top) < COLLISION_TOLERANCE and ball.speed_y > 0:
ball.speed_y *= -1
if abs(ball.rect.right - block.rect.left) < COLLISION_TOLERANCE and ball.speed_x > 0:
ball.speed_x *= -1
if abs(ball.rect.left - block.rect.right) < COLLISION_TOLERANCE and ball.speed_x < 0:
ball.speed_x *= -1
bricks_group.remove(block)
bricks_group.draw(screen)
# user information
info_text = f'Lives: {lives} - Score: {score}'
screen_text = font.render(info_text, True, COLORS[1])
screen.blit(screen_text, (5, 5))
pygame.draw.line(screen, COLORS[1], (5, 25), (SCREEN_WIDTH - 5, 25), 1)
# render game
pygame.display.flip()
clock.tick(FPS)
# stop application
pygame.quit()
exit()
That's already the code (with only about 275 lines) for the game! Simply upload to the UNIHIKER, select in the menu and start playing.