Learn to develop games on UNIHIKER

1 1034 Medium

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.

HARDWARE LIST
1 UNIHIKER
STEP 1
Develop the game template

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.

CODE
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.

STEP 2
Add few classes
CODE
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.

CODE
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.

CODE
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.

CODE
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.

CODE
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.

STEP 3
Add all classes into template
CODE
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.

STEP 4
Create 3 screens
CODE
# 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).

CODE
# 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.

CODE
# 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.

CODE
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.

STEP 5
Images

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.

projectImage
projectImage
projectImage
projectImage
projectImage
projectImage
projectImage
STEP 6
Upload

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.

License
All Rights
Reserved
licensBg
1