Animation
This lesson will teach you how to animate turtle graphics to move on the screen.
Version Control
The Object Oriented Programming lesson must be completed prior to this lesson. With the program you created in that lesson open, save the code. Then go to File > Save As.. to save it again with a name of spaceGame_v2.py.Now you have two files with the same code. Version 1 (spaceGame_v1.py) serves as a backup. If you make an error in spaceGame_v2.py that you are unable to fix, you can always go back to your first version instead of starting the entire project over from scratch.
This approach will be taken with all of the Space Game lessons.
A Second Player
Now that we have switched to an OOP model, adding a second player should be pretty easy. Create a new Player and draw the player's ship with just two lines of code.p1 = Player("blue") p1.drawShip() p2 = Player("red") p2.drawShip()
Run the program and you should see a red space ship appear!
However, what happened to the first player's blue space ship? You may see it for a moment right when you run the program before the red space ship is drawn right on top of it.
The blue ship is still there, it is just hidden behind the red ship. The solution is to set them to two different positions when they are initialized.
p1 = Player("blue", (-200, -200)) p1.drawShip() p2 = Player("red", (200, 200)) p2.drawShip()
Here, we are passing another argument that consists of two values within parenthesis. This collection of values listed with parenthesis is called a tuple and will be discussed in more detail later in the course. For now, understand that we are simply passing two values as a single parameter.
The two values represent the horizontal position (x) and the vertical position (y) for where the ship should be initally placed. The first player's ship will be placed in the bottom-left of the game screen: 200 pixels to the left of center and 200 pixels below center. The second player's ship will be placed at the top-right of the game screen: 200 pixels to the right of center and 200 pixels above center.
You might not always want the ship's initial heading to be the same either. Pass an additional argument to initialize the first player's heading to be north and the second player's heading to be south.
p1 = Player("blue", (-200, -200), 90) p1.drawShip() p2 = Player("red", (200, 200), 270) p2.drawShip()
You can see from the description of the setheading function within the Turtle API that the heading of a Turtle is defined in degrees and that in the standard (default) mode, 0 corresponds to east, 90 to north, 180 to west, and 270 to south.
You have added more arguments to the call to the Player constructor, so the parameter list for the __init__ function must be updated accordingly.
class Player: def __init__(self, color, position, heading):
Then use the initial values to set the position and heading of the player's turtle.
class Player: def __init__(self, color, position, heading): self.color = color self.turtle = turtle.Turtle() self.turtle.ht() self.turtle.speed(0) self.turtle.setpos(position) self.turtle.setheading(heading)
Play the game again and you should see the initial position and heading of each player based on where their ship is drawn.
Gameloop
To move the ships you will need to repeatedly move the turtles and redraw the ships. A gameloop can be created for this.First, import the time module that is part of the core python language.
import turtle import time
After the Player class, create a gameloop function that will contain the code that needs to be repeated to create the allusion of motion.
The turtle's clear method is used to clear the drawing created by the turtle. Because there are two turtle's, the clear method of each should be called. Then, move each turtle forward by one pixel and redraw the ships.
def gameloop(): p1.turtle.clear() p2.turtle.clear() p1.turtle.fd(1) p2.turtle.fd(1) p1.drawShip() p2.drawShip()
This function is not part of the Player class so should not be indented.
Next, make a call to the gameloop function. Because the function includes commands for drawing the ships, delete the previous calls to the drawShip methods.
screen.bgcolor("black") p1 = Player("blue", (-200, -200), 90) p2 = Player("red", (200, 200), 270) gameloop()
If you run the program now, you should not see any difference, though the ships are actually being drawn one pixel ahead of where they were previously being drawn.
Now, towards the end of the gameloop, call the Screen object's ontimer method so that a call to the gameloop function will occur again after a 20 milisecond delay.
p1.drawShip() p2.drawShip() screen.ontimer(gameloop, 20)
Having a function call itself is called recursion, and in this case the recursive function results in an infinite loop. Each new drawing of the scene is called a frame, and the number of times the function executes per second is called the framerate.
What is the framerate for this gameloop? Because there are 1000 milliseconds in a second, calling the gameloop every 20 milliseconds will result in a framerate of 50 fps (frames per second).
You could change the framerate to speed up or slow down the game. However, this is not a good idea. A framerate that is too slow will not be sufficient for creating the illusion of motion. A framerate that is too high might cause the program to crash or behave strangely if the code in the loop cannot finish executing before it begins again. Also, changing the framerate would effect the speed of everything in the scene instead.
We will learn another strategy for changing the speed of the ships later in the lesson that does not effect the framerate or other animated objects in the scene.
If you run the program now, you will see the ships move across the screen very slowly and with a choppy animation.
This is because the screen is being refreshed after every single turtle command that draws to the screen. Looking at the number of fd and bk commands in the drawShip method, you can understand how that would cause far too many screen refreshes for a game running at 50 fps.
To fix this, call the tracer method to disable the automatic screen refreshes.
p1 = Player("blue", (-200, -200), 90) p2 = Player("red", (200, 200), 270) turtle.tracer(0, 0) # disable automatic screen refreshing gameloop()
As stated in the API, the first parameter determines how many of the regular screen updates are actually performed. If the value were set to 5, every 5th screen update would be performed. With a value of 0, no screen updates will automatically occur.
The second parameter of the tracer method is used to set the delay; the approximate time interval between two consecutive screen updates. Setting this value to zero ensures no delay.
Because the automatic screen refreshing is now disabled, you must manually refresh the screen at the appropriate time. For a game, it makes sense to refresh the screen at the end of each gameloop.
def gameloop(): p1.turtle.clear() p2.turtle.clear() p1.turtle.fd(1) p2.turtle.fd(1) p1.drawShip() p2.drawShip() turtle.update() # refresh screen screen.ontimer(gameloop, 20)
Play the game again and you should see the ships moving much more smoothly across the screen.
Speed
The ships are moving pretty slow right now. Let's speed them up.You may have already realized how this might be done without changing the framerate for the gameloop. You could simply change the amount of pixels the ship moves each frame.
p1.turtle.fd(10) p2.turtle.fd(10)
Moving 10 pixels instead of 1 pixel each frame will cause the ships to move 10 times faster. Go ahead and run the program to see how that looks.
However, in our Space Game, we want the player to be able to contol the speed of their ship. If the speed is going to change, we will need to create a variable.
Since each player will have their own unique speed, add speed as a property of the Player class.
class Player: def __init__(self, color, position, heading): self.speed = 10 self.color = color
Then, use that property to specify how far forward each ship should be moved each frame of the gameloop.
p1.turtle.fd(p1.speed) p2.turtle.fd(p2.speed)
Run the program to make sure the ships are both moving at the initials speed you set for them.
Once everything is working, set the initial value for the speed of new Player objects to zero. After all, it will be up to the player to increase their own speed.
class Player: def __init__(self, color, position, heading): self.speed = 0 self.color = color
Player Control - Windows
In a previous lesson you learned how to get player input through the console. For the Space Game, you need to detect every time the player presses and releases a key. The Screen class of the turtle module provides methods that support this.If you are using a Mac, the below way of implementing key pressing and releasing will not work. Go to the next section,
Player Control - Macfor instructions on an alternative way to put together player controls.
First, create your own custom functions that will execute when the player presses down or releases the W key.
def wPressed(): print("W pressed") def wReleased(): print("W released") screen.bgcolor("black")
Use the listen method of the screen object to make the screen listen for keyboard and mouse inputs. Then use the onkeypress and onkeyrelease methods to detect when the W key is pressed. As the first argument, specify the function that should be called when the keypress occurs.
turtle.tracer(0, 0) # disable automatic screen refreshing screen.onkeypress(wPressed, "w") screen.onkeyrelease(wReleased, "w") screen.listen() gameloop()
The onkeypress and onkeyrelease methods are examples of event listeners. The wPressed and wReleased functions we created to handle the events are examples of event handlers.
Run the program to test it out.
Notice that when you hold the W key down for too long, the onkeypress event begins triggering on its own. This is not because your program is not working, but instead because your operating system is set up to do this.
The same behavior can be seen when typing a characters into text document or typing code in IDLE. Type a character into IDLE and do not release the key; you will see after a brief pause that the letter automatically begins repeating.
This behavior will not be a problem for our moving functionality.
You could increase the first player's speed immediately each time the W key is pressed.
def wPressed(): p1.speed += 1
However, you do not want the player to have to press the key over and over to continue accelerating. You also do not want to rely on the operating systems feature of repeating an input when a key is pressed down. This is because that feature requires a short delay before being activated, you do not have control over how fast it repeats, and it is interrupted by the pressing of other keys.
Instead, create a new property for the player to keep track of whether or not the player is accelerating. Set the property's initial value to False so the player is required to press W after the game begins to being moving.
class Player: def __init__(self, color, position, heading): self.isAccelerating = False self.speed = 0
Then, use the functions you created to change the isAccelerating property of only the first player when the W key is pressed and released.
def wPressed(): p1.isAccelerating = True def wReleased(): p1.isAccelerating = False
Think of the W key as the gas pedal of your car. When it is pressed, the car is accelerating and when it is release the car is not accelerating. In fact, because of friction the car will decelerate when not accelerating.
Now, in the gameloop, you could increase the speed for each player who is accelerating prior to moving them forward.
if p1.isAccelerating p1.speed += 1 p1.turtle.fd(p1.speed) if p2.isAccelerating p2.speed += 1 p2.turtle.fd(p2.speed)
While this is what we are trying to achieve, the code in the gameloop related to moving the player is getting a bit long and each step of the movement code has to be written for each player seperately.
Instead of adding this code into the gameloop, create a move method in the Player class to take care of all the player movement.
Then, delete the calls to the fd method from the gameloop and replace them with calls to each player's move method.
# RESET TURTLE t.pu() t.setpos(startPos) t.setheading(startHeading) def move(self): if self.isAccelerating: self.speed += 1 self.turtle.fd(self.speed) def gameloop(): p1.turtle.clear() p2.turtle.clear() p1.move() p2.move() p1.drawShip() p2.drawShip()
When you play the game, you should be able to press W to move the first player's ship and hold down W to constantly accelerate.
Just a couple of tweaks to make now. First, the speed of the ship should have some limit. Also, the ship should have some way of slowing down.
To limit the player's speed, add a property to the Player class that will be shared by all Player objects.
class Player: MAX_SPEED = 10 def __init__(self, color, position, heading): self.isAccelerating = False self.speed = 0
Capitalizing all letters of a variable is the convention in python and many other languages to indicate that it is a constant; that is a variable whose value does not change. In python, there is not a way to enforce the prevention of changes to the variable because python does not actually support the concept of constants. However, using all caps still helps programmers working on the project remember not to change the values of these variables.
Add a conditional statement to the move method to implement this speed limitation. After increasing the speed, do a check to see if the speed has become greater than the maximum value allowed. If so, set the speed back to that maximum value.
def move(self): if self.isAccelerating: self.speed += 1 if self.speed > Player.MAX_SPEED: self.speed = Player.MAX_SPEED self.turtle.fd(self.speed)
Run the program now and hold down W to see that the ship continues to accelerate only until it reaches it's maximum speed.
You could set up another event listener to allow the player to press a key to slow the ship down. Another option is to just slow the ship down anytime the player is not accelerating.
Space purists may balk at the idea of the ship slowing down on its own when their is no friction. However, this is a game so we can do whatever we want!
Be sure to include code to ensure the speed never drops below zero.
def move(self): if self.isAccelerating: self.speed += 1 else: self.speed -= 1 if self.speed > Player.MAX_SPEED: self.speed = Player.MAX_SPEED elif self.speed < 0: self.speed = 0 self.turtle.fd(self.speed)
Your ship should now accelerate and decelerate based on whether or not the W key is being pressed down. The ship does not need to decelerate at the same rate that it accelerates. Modify the amount of acceleration and deceleration to your liking, playing the game to test it out. It is ok to use decimal values.
def move(self): if self.isAccelerating: self.speed += 0.5 else: self.speed -= 0.2
Now that the functionality for movement is complete, add event listeners and event handlers for when the Up Arrow is pressed and released. This player control will be used to move the second player's ship.
def wPressed(): p1.isAccelerating = True def wReleased(): p1.isAccelerating = False def upArrowPressed(): p2.isAccelerating = True def upArrowReleased(): p2.isAccelerating = False screen.bgcolor("black") p1 = Player("blue", (-200, -200), 90) p2 = Player("red", (200, 200), 270) turtle.tracer(0, 0) # disable automatic screen refreshing screen.onkeypress(wPressed, "w") screen.onkeyrelease(wReleased, "w") screen.onkeypress(upArrowPressed, "Up") screen.onkeyrelease(upArrowReleased, "Up") screen.listen() gameloop()
Now, player 1 and player 2 should be able to accelerate and decelerate!
Player Control - Mac
Because Mac computers don't register events the same way as Windows computers, you will need to use a different method for player control. Macs will register the key down input correctly, but not the key up input.If you are using a Windows computer, you can skip to the next section:
Player Rotation - Windows.
First, create a custom function that will execute when the player presses down the W key.
def wPressed(): print("W pressed") screen.bgcolor("black")
Use the listen method of the screen object to make the screen listen for keyboard and mouse inputs. Then use the onkeypress method to detect when the W key is pressed. As the first argument, specify the function that should be called when the keypress occurs.
turtle.tracer(0, 0) # disable automatic screen refreshing screen.onkeypress(wPressed, "w") screen.listen() gameloop()
The onkeypress method is an example of an event listener. The wPressed function we created to handle the event is an example of an event handler.
Run the program to test it out.
Notice that when you hold the W key down for too long, the onkeypress event begins triggering on its own. This is not because your program is not working, but instead because your operating system is set up to do this.
The same behavior can be seen when typing a characters into text document or typing code in IDLE. Type a character into IDLE and do not release the key; you will see after a brief pause that the letter automatically begins repeating.
This behavior will not be a problem for our moving functionality.
You could increase the first player's speed immediately each time the W key is pressed.
def wPressed(): p1.speed += 1
However, you do not want the player to have to press the key over and over to continue accelerating. You also do not want to rely on the operating systems feature of repeating an input when a key is pressed down. This is because that feature requires a short delay before being activated, you do not have control over how fast it repeats, and it is interrupted by the pressing of other keys.
Instead, create a new property for the player to keep track of whether or not the player is accelerating. Set the property's initial value to False so the player is required to press W after the game begins to being moving.
class Player: def __init__(self, color, position, heading): self.isAccelerating = False self.speed = 0
Then, use the functions you created to change the isAccelerating property of only the first player when the W key is pressed. This code will change the value to its opposite value on every key press, so players can press the key once to accelerate, and press the key again to stop accelerating.
There are other ways you could implement this, such as making the
stop acceleratingkey a separate key.
def wPressed(): p1.isAccelerating = not p1.isAccelerating
Now, in the gameloop, you could increase the speed for each player who is accelerating prior to moving them forward.
if p1.isAccelerating p1.speed += 1 p1.turtle.fd(p1.speed) if p2.isAccelerating p2.speed += 1 p2.turtle.fd(p2.speed)
While this is what we are trying to achieve, the code in the gameloop related to moving the player is getting a bit long and each step of the movement code has to be written for each player separately.
Instead of adding this code into the gameloop, create a move method in the Player class to take care of all the player movement.
This code will cause the ship to speed up.
An else case will slow the ship down if it is not currently accelerating, as long as it is moving.
Then, delete the calls to the fd method from the gameloop and replace them with calls to each player's move method.
# RESET TURTLE t.pu() t.setpos(startPos) t.setheading(startHeading) def move(self): if self.isAccelerating: self.speed += 1 else: if self.speed > 0: self.speed -= 1 self.turtle.fd(self.speed) def gameloop(): p1.turtle.clear() p2.turtle.clear() p1.move() p2.move() p1.drawShip() p2.drawShip()
When you play the game, you should be able to press W to move the first player's ship and press W again to slow the ship down.
Just a couple of tweaks to make now. First, the speed of the ship should have some limit.
To limit the player's speed, add a property to the Player class that will be shared by all Player objects.
class Player: MAX_SPEED = 10 def __init__(self, color, position, heading): self.isAccelerating = False self.speed = 0
Capitalizing all letters of a variable is the convention in python and many other languages to indicate that it is a constant; that is a variable whose value does not change. In python, there is not a way to enforce the prevention of changes to the variable because python does not actually support the concept of constants. However, using all caps still helps programmers working on the project remember not to change the values of these variables.
Add a conditional statement to the move method to implement this speed limitation. After increasing the speed, do a check to see if the speed has become greater than the maximum value allowed. If so, set the speed back to that maximum value.
def move(self): if self.isAccelerating: self.speed += 1 if self.speed > Player.MAX_SPEED: self.speed = Player.MAX_SPEED else: if self.speed > 0: self.speed -= 1 self.turtle.fd(self.speed)
Run the program now and press W to see that the ship continues to accelerate only until it reaches it's maximum speed.
You could also set up another event listener to allow the player to press a key to slow the ship down.
Space purists may balk at the idea of the ship slowing down on its own when there is no friction. However, this is a game so we can do whatever we want!
The ship does not need to decelerate at the same rate that it accelerates. Modify the amount of acceleration and deceleration to your liking, playing the game to test it out. It is ok to use decimal values.
Now that the functionality for movement is complete, add event listeners and event handlers for when the Up Arrow is pressed. This player control will be used to move the second player's ship.
def wPressed(): p1.isAccelerating = True def upArrowPressed(): p2.isAccelerating = True screen.bgcolor("black") p1 = Player("blue", (-200, -200), 90) p2 = Player("red", (200, 200), 270) turtle.tracer(0, 0) # disable automatic screen refreshing screen.onkeypress(wPressed, "w") screen.onkeypress(upArrowPressed, "Up") screen.listen() gameloop()
Now, player 1 and player 2 should be able to accelerate and decelerate!
Player Rotation - Windows
Now that the players can move forward, the next step is to allow them to change direction. This can be done simply by changing the heading of a player's turtle when the appropriate keys are being held down.First, create a constant that defines the number of degrees the ship will rotate when it is turning. Also, add the properties for the Player class to track whether or not the player is turning.
class Player: MAX_SPEED = 10 ROTATION_SPEED = 5 def __init__(self, color, position, heading): self.isAccelerating = False self.isTurningLeft = False self.isTurningRight = False self.speed = 0
Next, update the move method to rotate the player when needed. Add comments to make it clear which code is used to rotate, accelerate, and move forward.
def move(self): # Rotate if self.isTurningLeft: self.turtle.left(Player.ROTATION_SPEED) if self.isTurningRight: self.turtle.right(Player.ROTATION_SPEED) # Accelerate if self.isAccelerating: self.speed += 0.5 else: self.speed -= 0.2 if self.speed > Player.MAX_SPEED: self.speed = Player.MAX_SPEED elif self.speed < 0: self.speed = 0 # Move Forward self.turtle.fd(self.speed)
Next, group the event handlers together with a comment to describe the groupings. Then add new event handlers for the keys that will allow the players to turn left and right. Use A and D for the first player's rotation controls. Use the left and right arrow keys for the second player's rotation controls.
In this case, it may improve readability to entirely define each function in a single line.
# Player 1 Event Handlers def wPressed(): p1.isAccelerating = True def wReleased(): p1.isAccelerating = False def aPressed(): p1.isTurningLeft = True def aReleased(): p1.isTurningLeft = False def dPressed(): p1.isTurningRight = True def dReleased(): p1.isTurningRight = False # Player 2 Event Handlers def upArrowPressed(): p2.isAccelerating = True def upArrowReleased(): p2.isAccelerating = False def leftArrowPressed(): p2.isTurningLeft = True def leftArrowReleased(): p2.isTurningLeft = False def rightArrowPressed(): p2.isTurningRight = True def rightArrowReleased(): p2.isTurningRight = False screen.bgcolor("black")
Finally, setup the event listeners to detect when the appropriate keys are pressed and released, again grouping them for each player under meaningful comments.
turtle.tracer(0, 0) # disable automatic screen refreshing # Player 1 Event Listeners screen.onkeypress(wPressed, "w") screen.onkeyrelease(wReleased, "w") screen.onkeypress(aPressed, "a") screen.onkeyrelease(aReleased, "a") screen.onkeypress(dPressed, "d") screen.onkeyrelease(dReleased, "d") # Player 2 Event Listeners screen.onkeypress(upArrowPressed, "Up") screen.onkeyrelease(upArrowReleased, "Up") screen.onkeypress(leftArrowPressed, "Left") screen.onkeyrelease(leftArrowReleased, "Left") screen.onkeypress(rightArrowPressed, "Right") screen.onkeyrelease(rightArrowReleased, "Right") screen.listen() gameloop()
Both players should now be able to fly anywhere within the game using just their left hand. The ring finger is used to turn left, the middle finger is used to accelerate, and the index finger is used to turn right.
Players will need their right hand to fire at their opponent. Player 1 will use the Spacebar while Player 2 will use the mouse.
The next lesson will go over how to fire bullets and how to use lists to manage all the bullets on the screen.
Player Rotation - Mac
Now that the players can move forward, the next step is to allow them to change direction. This can be done simply by changing the heading of a player's turtle when the appropriate key is toggled.First, create a constant that defines the number of degrees the ship will rotate when it is turning. Also, add the properties for the Player class to track whether or not the player is turning.
class Player: MAX_SPEED = 10 ROTATION_SPEED = 5 def __init__(self, color, position, heading): self.isAccelerating = False self.isTurningLeft = False self.isTurningRight = False self.speed = 0
Next, update the move method to rotate the player when needed. Add comments to make it clear which code is used to rotate, accelerate, and move forward.
def move(self): # Rotate if self.isTurningLeft: self.turtle.left(Player.ROTATION_SPEED) if self.isTurningRight: self.turtle.right(Player.ROTATION_SPEED) # Accelerate if self.isAccelerating: self.speed += 1 if self.speed > Player.MAX_SPEED: self.speed = Player.MAX_SPEED else: if self.speed > 0: self.speed -= 1 # Move Forward self.turtle.fd(self.speed)
Next, group the event handlers together with a comment to describe the groupings. Then add new event handlers for the keys that will allow the players to turn left and right. Use A and D for the first player's rotation controls. Use the left and right arrow keys for the second player's rotation controls.
You also need to make turning the opposite direction false, in case the player tries to turn one direction when they are already going the other direction.
# Player 1 Event Handlers def wPressed(): p1.isAccelerating = not p1.isAccelerating def aPressed(): p1.isTurningLeft = not p1.isTurningLeft p1.isTurningRight = False def dPressed(): p1.isTurningRight = not p1.isTurningRight p1.isTurningLeft = False # Player 2 Event Handlers def upArrowPressed(): p2.isAccelerating = not p2.isAccelerating def leftArrowPressed(): p2.isTurningLeft = not p2.isTurningLeft p2.isTurningRight = False def rightArrowPressed(): p2.isTurningRight = not p2.isTurningRight p2.isTurningLeft = False screen.bgcolor("black")
Finally, setup the event listeners to detect when the appropriate keys are pressed and released, again grouping them for each player under meaningful comments.
turtle.tracer(0, 0) # disable automatic screen refreshing # Player 1 Event Listeners screen.onkeypress(wPressed, "w") screen.onkeypress(aPressed, "a") screen.onkeypress(dPressed, "d") # Player 2 Event Listeners screen.onkeypress(upArrowPressed, "Up") screen.onkeypress(leftArrowPressed, "Left") screen.onkeypress(rightArrowPressed, "Right") screen.listen() gameloop()
Both players should now be able to fly anywhere within the game using just their left hand. The ring finger is used to turn left, the middle finger is used to accelerate, and the index finger is used to turn right.
Players will need their right hand to fire at their opponent. Player 1 will use the Spacebar while Player 2 will use the mouse.
The next lesson will go over how to fire bullets and how to use lists to manage all the bullets on the screen.