Make a Build HAT game controller

By Richard Hayler and Marc Scott. Posted

In this project, you will use the Raspberry Pi Build HAT, a LEGO® Technic™ motor encoder and wheel, and the Python Turtle library to make a simple game controller that you can use to play Pong.

Pong is one of the earliest arcade video games, originally released in 1972 by Atari. It is a table tennis game featuring simple two-dimensional graphics. Players control paddles on each side of the screen, which they use to hit a ball back and forth.

You will learn how to read the degrees of rotation from LEGO Technic motors, how to draw and move Turtle graphics using LEGO Technic motors, and how to detect collisions between graphics using x and y coordinates.

You'll need...

Set up Build HAT

Before you begin, you’ll need to have set up your Raspberry Pi computer and attached a Build HAT. If you have a Maker Plate™, mount your Raspberry Pi to it using M2 bolts and nuts, making sure your Raspberry Pi is mounted on the side without the ‘edge’.

Mounting Raspberry Pi this way round enables easy access to the ports as well as the SD card slot. The Maker Plate will allow you to connect Raspberry Pi to the main structure of your dashboard more easily (this is an optional extra).

Line up the Build HAT with your Raspberry Pi, ensuring you can see the ‘This way up’ label. Make sure all the GPIO pins are covered by the HAT, and press down firmly.

You should now power your Raspberry Pi using the 7.5 V barrel jack on the Build HAT, which will allow you to use the motors.

Connect a motor to port A on the Build HAT

You will also need to install the buildhat Python library. Open a Terminal window on your Raspberry Pi and enter:

sudo pip3 install buildhat

Press ENTER and wait for the ‘installation completed’ message.

Motor encoders

Motor encoders can not only rotate: they can also accurately detect how many degrees they have been rotated.

The LEGO Spike™ motors all have encoders. If you look at the rotating disc part of the motor, you will see a mark shaped like a lollipop that can be lined up with the 0 mark on the white body of the motor itself. This is the encoder set to zero degrees, and any angular movement of the motor shaft can be measured relative to this point.

A motor encoder, also called a rotary or shaft encoder, is an electromechanical device that allows you to record the angular position or motion of the axle. It normally does this by converting the angular position to an analogue or digital output.

If a motor has an encoder, that means you can very accurately set the position of the axle. It also allows you to use the motor as an input device so that if something changes the position of the axle, this can be registered and used to trigger other actions in a computer program. Open Thonny from the Programming menu, click on the Shell area at the bottom, and enter:

from buildhat import Motor
motor_left = Motor('A')

Depending on how well you positioned the motor at the start, you should get a value close to ‘0’. Move the motor and type the second line again, and see how the value changes.

React to motor encoder movement

To use the LEGO Technic™ motors as a controller for a game, you’ll need to be able to constantly read their absolute positions.

In the main Thonny window above the Shell, you can use the commands you already know to find the absolute position of the motor. Then, in a while True: loop, you can print the value of the position.

Enter this code, save the file, and press Run.

from buildhat import Motor
motor_left = Motor('A')

while True:
    print(motor_left.get_aposition())

You should see that your program continually prints the position of the motor. If you rotate the motor, these values should change.

There is a better way of doing this, though. You only need to read the motor position if it is moved.

Delete the while True: loop from your program and create this simple function that prints the absolute position of the motor. You will also need to add another import line to use the pause() function.

from buildhat import Motor
from signal import pause
motor_left = Motor('A')

def moved_left(motor_speed, motor_pos, motor_apos):
    print(motor_apos)

Now set this function to run when the motor's encoder is moved:

from buildhat import Motor
from signal import pause
motor_left = Motor('A')

def moved_left(motor_speed, motor_pos, motor_apos):
    print(motor_apos)

motor_left.when_rotated = moved_left
pause()

Run your code and you should see the values printed out in the Shell change when the motor is moved.

Make your Pong screen

Open a new file in Thonny and add the following code to import the Turtle, time, and Build HAT libraries, and then set up a screen. Run the file and you should see a black window with the title ‘PONG’ open.

from turtle import Screen, Turtle
from time import sleep 
from buildhat import Motor   

game_area = Screen() #Create a screen   
game_area.title("PONG") #Give the screen a title   
game_area.bgcolor('black') #Set the background colour   
game_area.tracer(0) #Give smoother animations

The Turtle library also has a useful way of setting the co-ordinates for a screen area. Add this line to your program:

game_area.tracer(0)   
game_area.setworldcoordinates(-200, -170, 200, 170)

This creates a rectangular window 400 pixels wide and 340 high, with 0 being in the centre (see below).

The co‑ordinates for the game area in our version of Pong

Now, you need to update your game area, to see the paddle and ball. Add a game loop to the bottom of your code, and call the update() method.

while True:   
    game_area.update()

Run your code and you should see a black window appear. Next, you can make a ball by using a Turtle that is set to be a white circle. The ball should start in the middle of the screen, and shouldn’t draw a line when it moves. Above your while True: loop, add the following code:

ball = Turtle()   
ball.color('white')   
ball.shape('circle')   
ball.penup()   
ball.setpos(0,0)   

while True:

Run your code again. You should see a white ball appear in your window. Next, you can set up a paddle in the same way. It will be a green rectangle, and positioned on the far left of the screen.

paddle_left = Turtle()   
paddle_left.color('green')   
paddle_left.shape('square')   
paddle_left.shapesize(4, 1, 1)   
paddle_left.penup()   
paddle_left.setpos(-190, 0) 

Run your code and you should see your ball and paddle.

Move the ball

The ball is going to bounce around the screen, so two variables are needed to keep track of its speed in both the ‘x’ and ‘y’ directions. These numbers can be larger to make the game harder, or smaller to make the game easier.

ball.speed_x = 1   
ball.speed_y = 1  

You can check where a Turtle is by using turtle.xcor() and turtle.ycor() to find the ‘x’ and ‘y’ co-ordinates, respectively. So to make the ball move, you can combine the position and speed.

Add the lines below to your program:

while True:   
    game_area.update()   
    ball.setx(ball.xcor() + ball.speed_x)   
    ball.sety(ball.ycor() + ball.speed_y)

Run the program and see what happens! The ball should move diagonally upwards towards the top right corner of the game area… and then keep on going! If you want your game to be fast and challenging, you can increase the speed_x and speed_y values to make the ball move more quickly.

The ball should bounce off the top wall rather than disappear off the screen. To do this, the speed can be reversed, making the ball travel in the opposite direction, if its ‘y’ position is greater than 160.

Add the following code into your game loop and run it.

while True:   
    game_area.update()   
    ball.setx(ball.xcor() + ball.speed_x)   
    ball.sety(ball.ycor() + ball.speed_y)   
    if ball.ycor() > 160:   
        ball.speed_y *= -1  

Run your code again, and the ball should bounce off the top of the screen, but disappear off the right of the screen.

In the same way that the code checks the upper ‘y’ position of the ball, to make it bounce, it can check the right ‘x’ position and the lower ‘y’ position, in your game loop.

Add these checks on the ball’s position.

    if ball.ycor() > 160:   
        ball.speed_y *= -1   
    if ball.xcor() > 195:   
        ball.speed_x *= -1   
    if ball.ycor() < -160:   
        ball.speed_y *= -1 

The ball should now bounce around the screen, and fly off the left edge. Next, you will control your paddle to reflect the ball back from the left edge.

Control the paddle

The LEGO Spike motor is going to be used to control the position of the paddle, but you don’t want to be able to make full turns.

A simple way to limit the motion of the wheel is to add a LEGO element to prevent the wheel turning through a complete rotation.

Line up the encoder marks on your motor using the wheel, like before. Insert a peg or axle as close to level with the markers as possible.

Attach a peg to the rear of the wheel to prevent the wheel from spinning indefinitely

Add a line to create the motor_left object after the import line.

from buildhat import Motor

motor_left = Motor('A')

Now a new variable is needed to keep track of the location of the paddle. This will be called pos_left and set to 0.

ball.speed_x = 0.4   
ball.speed_y = 0.4   

pos_left = 0

Create a function for the paddle that will run when the motor encoder moves. Note that it uses a global variable so that it can change the value of the pos_left variable.

def moved_left(motor_speed, motor_rpos, motor_apos):   
    global pos_left   
    pos_left = motor_apos

Now add a single line that will use that function each time the motor is moved. It can be just before your while True: loop.

motor_left.when_rotated = moved_left

Then, add a line to the while True: loop to update the paddle object on the screen to the new position.

    if ball.ycor() < -160:   
        ball.speed_y *= -1   
    paddle_left.sety(pos_left)  

Run your code and then turn the wheel on your motor encoder. You should see your paddle moving up and down the screen.

In case there are errors, your code should currently look like pong.py.

Paddle collisions

The game is nearly complete – but first you need to add some extra collision detection that covers the ball hitting the paddle.

Within the while True: loop, check if the ball’s ‘x’ position is within the horizontal area covered by the paddle. Also use an and to check the ball’s ‘y’ position is in the vertical line in which the paddle moves.

paddle_left.sety(pos_left)   
if (ball.xcor() < -180 and ball.xcor() > -190) and (ball.ycor() < paddle_left.ycor() + 20 and ball.ycor() > paddle_left.ycor() - 20):
    ball.setx(-180)  
    ball.speed_x *= -1

Try the program out. You should be able to bounce the ball off your paddle and play a solo game of ‘squash’!

Now you have a way of preventing the ball from disappearing off-screen, it’s time to think about what happens if you fail to make a save. For now, let’s just reset the ball back to the start.

Add this code within the while True: loop:

        ball.speed_x *= -1   
    if ball.xcor() < -195: #Left   
        ball.hideturtle()   
        ball.goto(0,0)   
        ball.showturtle()

Once you’re happy with the various settings, it’s time to add in the second paddle. Using what you’ve created for the left-hand paddle as a starting point, add a second paddle on the right-hand side of the game area.

First of all, connect a second LEGO Technic motor to the Build HAT (port B) and set it up in the program.

motor_left = Motor('A')   
motor_right = Motor('B')

You can copy and paste your code for setting up your left paddle, and change the name and values for your right paddle. Create your right paddle.

paddle_left = Turtle()   
paddle_left.color('green')   
paddle_left.shape("square")   
paddle_left.shapesize(4,1,1)   
paddle_left.penup()   
paddle_left.setpos(-190,0)   

paddle_right = Turtle()   
paddle_right.color('blue')   
paddle_right.shape("square")   
paddle_right.shapesize(4,1,1)   
paddle_right.penup()   
paddle_right.setpos(190,0)  

Add a variable for the right paddle position, a function for the paddle, and the line to call the function when the right motor is moved.

pos_left = 0   
pos_right = 0   

def moved_left(motor_speed, motor_rpos, motor_apos):   
    global pos_left   
    pos_left = motor_apos    

def moved_right(motor_speed, motor_rpos, motor_apos):   
    global pos_right   
    pos_right = motor_apos  

motor_left.when_rotated = moved_left   
motor_right.when_rotated = moved_right 

Add a line to update the paddle on screen to the while True: loop:

    paddle_left.sety(pos_left)   
    paddle_right.sety(pos_right)

Currently, the ball will bounce off the right-hand wall. Modify the lines of your program that make that happen so that the ball is instead reset to the centre. Now add a similar condition for the right paddle as you did with the left, to handle collisions.

    if (ball.xcor() < -180 and ball.xcor() > -190) and (ball.ycor() < paddle_left.ycor() + 20 and ball.ycor() > paddle_left.ycor() - 20):   
        ball.setx(-180)   
        ball.speed_x *= -1   
    if (ball.xcor() > 180 and ball.xcor() < 190) and (ball.ycor() < paddle_right.ycor() + 20 and ball.ycor() > paddle_right.ycor() - 20):   
        ball.setx(180)   
        ball.speed_x *= -1 

You should now be able to enjoy a basic two-player game of Pong. Your code should currently look like two_player_basic.py.

Improve your project

There are a few additional features you can add to finish off your game. Keep track of the score by using two variables (one for each player) and update them whenever a round is lost. First of all, declare the new variables towards the top of the program and set the score to zero.

score_r = 0
score_l = 0

Whenever a ball is missed, increment the appropriate score variable by one. There are two conditional tests you’ll need to modify.

    if ball.xcor() > 195: #Right
        ball.hideturtle()
        ball.goto(0,0)
        ball.showturtle()
        score_r+=1
    if ball.xcor() < -195: #Left
        ball.hideturtle()
        ball.goto(0,0)
        ball.showturtle()
        score_l+=1

Now you need to display the score in the game area. You can use a fourth Turtle to do this. Add the following to your program after the creation of the paddle and ball Turtles, but before the while True: loop.

writer = Turtle()
writer.hideturtle()
writer.color('grey')
writer.penup()
style = ("Courier",30,'bold')
writer.setposition(0,150)
writer.write(f'{score_l} PONG {score_r}', font=style, align='center')

You can look at the documentation for the Turtle library to see what other options there are for how the text is displayed.

If you run your program now, the score and Pong legend should appear, but the scores themselves won’t get updated.

Find the two conditionals for each of the scoring situations – when the ball is missed by a paddle and disappears to the left or right – and update the score by rewriting the new value.

     writer.clear()
     writer.write(f'{score_l} PONG {score_r}', font=style, align='center')

Adding a buzzer

To include some simple sound effects, connect a buzzer to the GPIO pins on Raspberry Pi. Instead of using a breadboard, you could use jumper leads with female sockets at both ends and poke the legs of the buzzer into the socket. Then use some LEGO elements to mount the buzzer so that it doesn’t flop around and become disconnected during frantic gaming sessions.

Now add the gpiozero library to the list of imports at the start of your program:

from gpiozero import Buzzer

Then, make the buzzer available for the program to use, by setting which pin you have connected the positive (+) leg to. here, we used GPIO 17.

buzz = Buzzer(17)

If you didn’t use GPIO 17, change the value to reflect the pin your buzzer is connected to.

Now, whenever the paddle and ball make contact, you want the game to play a short tone.

Add this line to each action part of the collision detection if conditionals for the ball and paddle:

buzz.beep(0.1,0.1,background=True)

Then add a line to play a longer tone whenever the player misses the ball.

buzz.beep(0.5,0.5,background=True)

You can read more about the options available with buzzers in the GPIO Zero documentation.

Improve the Pong game

Here are some ideas to improve your game of Pong. You can add even more randomness to the speed and trajectory of the ball, and make the ball move faster as the game progresses.

Right now the game carries on forever – consider having a target score that a player must achieve in order to win and then start a new set of rounds; change the scoring method to count how many times the players return the ball to one another, and reset when someone misses. Or, introduce some haptic feedback, so that the motors turn a small amount when a point is lost.

At the moment it doesn’t matter what part of the paddle connects with the ball; it will always bounce off at the same angle as it hit. Modify the collision code so that the angle becomes more obtuse if the ball makes contact close to the end of the paddle.

Create more games that use the LEGO Technic motors as controllers.

How about a game in the style of Angry Birds, where two controllers are used to set the launch trajectory and the amount of force applied to the catapult?

From The MagPi store

Subscribe

Subscribe to the newsletter

Get every issue delivered directly to your inbox and keep up to date with the latest news, offers, events, and more.