Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Locked learning resources

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Moving and Rotating Your Objects

00:00 In the previous lesson, I went on a quick tangent to show you how frame rates interact with the animations on your screen. In this lesson, I’ll show you how to move and rotate your spaceship.

00:11 So far, you have a generic game object called, funnily enough, GameObject. There are three types of objects in the game: rocks, bullets, and ships.

00:21 Each of these is going to need a bit of code specific to it, so you’ll want to create objects more specific than GameObject. GameObject is still a good base class for the common code of all your sprites, but in this lesson, you’re going to add a child of GameObject that is the ship itself. To fly your ship, you’ll need the game to respond to keyboard events.

00:43 Once your new ship class is in place, you’ll add some code for controlling its direction. The movement model for the ship is a single rocket booster and the ability to spin. There is no linear deceleration. If you want to slow down, turn the ship around 180 degrees and fire the rocket in the other direction.

01:03 This makes flying the ship a bit challenging, but that’s part of the fun of the game. One quick thing to remember from the coordinate system: It’s a bit counterintuitive, but up is a negative number.

01:16 Remember that y gets bigger as you go down the screen, so if you want to go up, you have to subtract.

01:24 First off, let’s prepare for writing ship-specific code by creating a ship class. This is inside of models.py. I’ve started the new class called Spaceship that inherits from GameObject.

01:38 The .__init__() method here takes a position to create the ship at, then calls the parent .__init__() using the spaceship sprite image and an initial velocity of zero.

01:49 Let’s make the corresponding changes in game.py.

01:57 The new Spaceship class replaces the GameObject, both here in the import and here inside of the SpaceRocks.__init__() code.

02:06 Because I’m concentrating on the ship for now, I’ve also removed the rock from the the game. I’ll put it back in here later.

02:16 With the new ship class in place, you can now start to think about how it moves around. I’m now back inside of models.py with more changes for the spaceship.

02:26 As the ship moves and rotates, you’re going to need to do some vector math. To make the code clearer, I’ve created a constant here called DIRECTION_UP that represents the up direction on the screen.

02:38 This is to compensate for the “negative numbers are moving upwards” problem that I discussed earlier. Let me just scroll down.

02:49 Our good little ship needs to turn. To do that, you’ll have to rotate the sprite and track which direction it is pointing in. Just like when moving the little red dot, the rotation movement also has a speed.

03:01 This class constant represents the number of degrees to rotate each time you do a rotation movement.

03:08 A bit of math is needed to rotate the sprite correctly, so I’ve grouped that together inside of this .rotate() method. This first part figures out which direction to rotate. The default for the method is clockwise. To rotate clockwise, you’ll multiply the rotation by 1. To go counter-clockwise, you’ll multiply by -1.

03:31 The amount of rotation is the .ROTATION_SPEED times the sign, just calculated. And then the ship’s .direction vector needs to be updated by the rotation angle.

03:42 The .rotate_ip() method on the .direction vector does all the funky trig math you need to do to do the rotation. With the .rotate() method all set to go, you now just need to update the .draw() method to appropriately rotate the sprite based on the current direction vector. The Pygame rotozoom() method does both a rotation and scaling of a sprite at the same time. It takes three parameters: the sprite image being rotated, the angle of rotation, and a scale factor.

04:14 The angle here is calculated using the .angle_to() method on the .direction vector. This figures out the difference in degrees between the current direction and straight up. As you’re only interested in rotating and not scaling, the final parameter to rotozoom() is a scale factor of 1.0.

04:34 Remember that when you blit something, you’re always copying rectangles. Not only that, but it is always rectangles whose sides are parallel to the sides of the screen.

04:44 The newly-rotated sprite isn’t in this situation anymore. This makes the bounding box of the blit incorrect. If you just use the coordinates of the rotated sprite, you might chop part of the image off. Fortunately, the .get_size() method knows how to deal with this problem.

05:02 It returns a new bounding box that the rotated sprite fits inside of without it getting clipped.

05:09 You’ll also recall that the .position vector is based on the center of the game object. Previously, you just used the radius of the object to translate the ship’s local position into a blittable one. Now you do something similar, but use the recently calculated bounding box instead of the radius.

05:29 All that rotation and coordinate system translation math being done, you’re ready to actually blit the sprite onto the surface. Ships be drawn now.

05:43 Okay, I’m back inside of game.py. Let me scroll down to the input handling method. On line 29, I’m inserting some code that handles key presses.

05:54 You may recall from the zoomzoom script the use of the KEYDOWN event. Each time you press a key, you get one and only one KEYDOWN event.

06:03 In the case of rotating the ship, you want to be able to hold the key and have rotation continue. In this case, instead of using the KEYDOWN event, you use the pygame.key.get_pressed() function.

06:15 This returns a dict that has a key-value pair for every key Pygame supports. If the key is pressed at the time of the call to .get_pressed(), then the value in the dictionary will be True. Lines 30 through 31 are adding the ability to quit the game by pressing the Escape or q keys. Lines 32 and 33 call .ship.rotate() in the clockwise direction when the right arrow is being pressed. And lines 34 and 35 rotate the ship in the other direction when left is pressed. With all that in place, your little ship can spin. Let’s check that out.

06:59 Yay! Spinning! In both directions, even! If you find this a bit slow, you can always adjust the rotate speed constant inside of the Spaceship object.

07:12 Rotating is cool and all, but I want to fly. This ship needs a rocket booster. To keep it simple, let’s not animate any flames or anything, but it can definitely do with some motion. As I mentioned before, this ship will have a single rocket engine out the back and therefore can only accelerate in the direction it is pointing. To slow your ship down, you have to rotate and accelerate in the opposite direction.

07:38 Let’s write the code. I’m back inside models.py. Let me scroll down. Here’s the Spaceship object. And a new constant is born: this time, some acceleration to go with the rotation.

07:55 Just like with rotation, the acceleration math will be encapsulated in a method call. This math is simpler than rotation. All you have to do is multiply the direction in travel by the acceleration constant, making it go faster, and then add the new result to the velocity.

08:15 With the .accelerate() method in place, all that’s left to do is add a game control. I’m back inside of game.py. Let me move down to the ._handle_input() method.

08:25 I’ve added a check for the up arrow. If that key is pressed, the ship’s .accelerate() method is called and you go faster. Fly me to the moon, baby!

08:41 Uh oh, where did I go? Like our never-ending rock from lessons back, my ship is still trucking along, but trying to figure out the right rotation and acceleration to get it back on the screen could be a bit of a challenge without being able to see the ship.

08:56 As this is an Asteroids clone, let’s do what Asteroids did: wrap the screen.

09:03 You will recall that the motion of the ship is calculated inside of the .move() method of the base GameObject class in models.py.

09:12 If the ship is to stay in the boundaries of the screen, this is the place to make that happen. Line 20 is the old .move() line. Line 21 is the new change to the method that ensures the object’s always on the screen.

09:26 The wrap_position() function returns a new position based on the value passed in and the surface the GameObject is contained within.

09:34 If the GameObject is out of the surface’s bounds, wrap_position() will return a new position that is still in the bounds. Note that the .move() method now needs the surface being moved within for the call to work. The game.py script will need updating, but I’ll get back to that in a minute. You’ve seen how wrap_position() is used.

09:56 I’ve implemented it inside of the utils file. Don’t forget to import it, and now let me show you what the wrap_position() function itself looks like.

10:09 This is inside of utils.py, and line 15 is the beginning of the new wrap_position() function. The first part gets the x and y values out of the position vector passed in.

10:23 The second part finds the width and height of the surface being drawn upon. The last part returns a new Vector2 object, which contains modified x and y parameters.

10:35 The x value is modded against the width and the y value modded against the height. If the x of the object’s position exceeds the width, the mod operation will wrap it around. Same goes for the y and the height.

10:54 Now, inside of game.py, the ._game_logic() method needs to be updated. This reflects the change to the .move() method.

11:02 It now takes the screen as the surface parameter that forms the boundaries of the wrap_position() function. With this code in place, let’s go play with the ship.

11:22 And there it is. Your ship will now always be on the screen.

11:28 Your ship is all set to go! Now I want to rock. What? Okay, uh, you young ones go Google “Twisted Sister” and then insert your own dad joke here.

Avatar image for Rok Kužner

Rok Kužner on Dec. 30, 2021

Hello. Me again. I’m on Moving and Rotating Your Objects on 7:00 and i have an error. It seas:

Traceback (most recent call last): File “c:/Users/LENOVO/Documents/Programiranje/Asteroid-game/main.py”, line 5, in <module> space_rocks.main_loop() File “c:\Users\LENOVO\Documents\Programiranje\Asteroid-game\game.py”, line 26, in main_loop self._draw() File “c:\Users\LENOVO\Documents\Programiranje\Asteroid-game\game.py”, line 47, in _draw self.ship.draw(self.screen) File “c:\Users\LENOVO\Documents\Programiranje\Asteroid-game\models.py”, line 37, in draw angle = self.direction.angle_to(DIRECTION_UP) AttributeError: ‘Spaceship’ object has no attribute ‘direction’

This is my models.py:

from pygame.math import Vector2
from pygame.transform import rotozoom
from utils import load_sprite

DIRECTION_UP = Vector2(0, -1)

class GameObject:
    def __init__(self, position, sprite, velocity):
        self.position = Vector2(position)
        self.sprite = sprite
        self.radius = sprite.get_width() / 2
        self.velocity = Vector2(velocity)

    def draw(self, surface):
        position = self.position - Vector2(self.radius)
        surface.blit(self.sprite, position)

    def move(self):
        self.position = self.position + self.velocity

    def collides_with(self, other):
        distance = self.position.distance_to(other.position)
        return distance < self.radius + other.radius

class Spaceship(GameObject):

    def __init__(self, position):
        super().__init__(position, load_sprite('spaceship'), Vector2(0))

    def rotate(self, clockwise=True):
        sign = 1 if clockwise else -1
        angle = self.ROTATION_SPEED * sign

    def draw(self, surface):
        angle = self.direction.angle_to(DIRECTION_UP)
        rotated_surface = rotozoom(self.sprite, angle, 1.0)
        rotated_surface_size = Vector2(rotated_surface.get_size())

        blit_position = self.position - rotated_surface_size * 0.5
        surface.blit(rotated_surface, blit_position)


Avatar image for Rok Kužner

Rok Kužner on Dec. 30, 2021

I’m so sorry! I found the problem!

Become a Member to join the conversation.