You'll need...
Assortment of LEGO, including wheels (we used a selection from the LEGO Education SPIKE Prime kit)
Small breadboard, buzzer, and jumper leads (optional)
5 V power supply with a barrel jack (optional)
Code (optional)
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.
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).
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.
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?