Core classes/modules:

Advanced:

Experimental/Broken:

Handling basic events and movement

The examples in this tutorial can be found here.

Here we are going to use the latest code from the image loading tutorial of the spaceship.

import pygame as pg
 
pg.init()
 
class Player:
    def __init__(self, screen_rect):
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        self.rect = self.image.get_rect(center=screen_rect.center)
        self.speed = 5
        
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_LEFT:
                self.rect.x -= self.speed
            elif event.key == pg.K_RIGHT:
                self.rect.x += self.speed
         
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
done = False
while not done:
    screen.fill((0,0,0))
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    player.draw(screen)
    pg.display.update()

First we added the get_event method for the player. This gets executed every frame for every event. It first checks for a keydown event type. Then it checks for either a left or right arrow key press. If it detects that it then modifies the image rect accordingly. It decreasing the rect x value for left, and increasing for right. This is because the top left of your window is (0,0), while the bottom right of the window is our defined windows size (800,600). As is we are subtracting a positive value to make it go left. But you could also add a negative number instead.
        self.speed = 5
        
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_LEFT:
                self.rect.x -= self.speed
            elif event.key == pg.K_RIGHT:
                self.rect.x += self.speed
And then we executed that method within the main event loop.
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)

This is perfect for things like shooting lasers from the ship as you would want only one shot per key press (for example). Not so good though for constant movement.

However lets say you want to hold the arrow keys down to constantly move the ship back and forth. The best way to do this is to use pygame.key.get_pressed(). This is going to force us to add a few more things. First we need to get rid of Player.get_event method. We do not need to catch each key press, we only need to check if that key is down or not.

import pygame as pg
 
pg.init()
 
class Player:
    def __init__(self, screen_rect):
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        self.rect = self.image.get_rect(center=screen_rect.center)
        self.speed = 5
        
    def update(self, keys):
        if keys[pg.K_LEFT]:
            self.rect.x -= self.speed
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.speed
         
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    screen.fill((0,0,0))
    keys = pg.key.get_pressed()
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.update(keys)
    player.draw(screen)
    pg.display.update()
    clock.tick(60)

So we ditched the get_event method in Player class as well as its call in the main event loop. We replaced that method with an update method. And we replaced the call to the new update method. However this method is called every frame, unlike the get_event method that was called in the main event loop. This method is meant to be called every frame regardless of key press. pygame.key.get_pressed() gives us a list of pressed keys, which in turn gets sent to the Player.update method. This method then checks those keys for your movement key and moves the ship the same way we did with the event movements. Because this is called every frame, it will move every frame (if you have a key pressed).

The next thing we had to add was a clock. Without this clock the main game loop would run at different speeds depending on the speed of each computer. This clock slows the speed down to 60 frames per second. We do this by creating a clock object from pygame.time.Clock() and calling its method tick(60) in the main game loop.

Now you should be able to move the ship to the left and right by just holding down the left or right arrow key.

Now lets lock the player to the screen so it cannot move out of the screen. With pygame.Rects this is a simple task. This requires only 2 more line sto be added.
import pygame as pg
 
pg.init()
 
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        self.rect = self.image.get_rect(center=screen_rect.center)
        self.speed = 5
        
    def update(self, keys):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.speed
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.speed
         
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    screen.fill((0,0,0))
    keys = pg.key.get_pressed()
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    player.update(keys)
    player.draw(screen)
    pg.display.update()
    clock.tick(60)

In the Player dunder init method (__init__), we add an attribute for screen rect. This is just so we can call it from another method. The next line actually restricts the players rect from moving outside of the screen rect. We add this line to the update method. rect.clamp_ip() clamps the rect to not allow it to move outside of the rect in its arguments. Because the screen rect is the arg, it restricts the player from moving outside of the screen.

Now lets switch the player to move using delta time. This part is not required. This is here for just completness. Some people move their objects via per pixels, and some via delta time. The choice is yours.

import pygame as pg
 
pg.init()
 
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        self.rect = self.image.get_rect(center=screen_rect.center)
        self.dx = 100
        
    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
         
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time)
    player.draw(screen)
    pg.display.update()

    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time)

Here we got the return of clock.tick(60)/1000 to get per seconds for delta time. We then pass that to the players update method.

    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt

But now we use delta time to move the rect instead. We multiply delta time by our movement change in x.


Now we are going to make things a little more complex. We are going to add the ability to make the ship shoot a laser. Unlike the movement of the ship though, we want it to occur on keypress, and not constantly. So we are going to add a get_event method to the player class again. We are also going to make the lasers itself another object, thus it needs its own class. Normally the laser class would be its own module, the player class its own module, the control class its own module, and would all be imported into a "game" class (so to speak)...that basically keeps track of whats in the environment of the game. However to make things easier in a tutorial, all classes are going to be meshed in one file (at least for this one).

import pygame as pg
 
pg.init()

class Laser:
    def __init__(self, loc):
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
        
    def update(self):
        self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)
 
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.lasers = []
        
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                self.lasers.append(Laser(self.rect.center))
        
    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        for laser in self.lasers:
            laser.update()
         
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time)
    player.draw(screen)
    pg.display.update()

So lets start with the Laser class. This class is similar to the player class when we first started that. It has an update method to update every frame. It has a render method (draw), that draws the image to the rect position. Instead of loading an image though, it creates a rectangle surface narrowed to look similar to a laser. **You should substitute your image loads for an arbitrary pygame.Surface like this when posting for help. And remove the image load. This makes it so the people on the other end do not require your esources (images).** We fill the surface with a color and then set the lasers rect position. To keep things as simple as possible I did not use delta time for movement.

class Laser:
    def __init__(self, loc):
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
        
    def update(self):
        self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)

Now in the player class we did some cleanup as well as adding the lasers. We centered the ship lower on the screen, as opposed to smack dab in the center. We added a laser list attribute to house the active lasers in. We added a get_event to check for keypresses to add a new laser into the laser list. And we added the laser updates and draw method executions in the players same methods. You can avoid these loops and save yourself a line per loop by using sprite groups, but that is for another tutorial. And that is up to you whether you want to use loops or use sprite groups. I positioned the laser render methods loop before the drawing of the ship. This creates the effect that the laser shoots from underneath the ship. Otherwise the laser is seen directly over top of the ship when it is created.

class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.lasers = []
        
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                self.lasers.append(Laser(self.rect.center))
        
    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        for laser in self.lasers:
            laser.update()
         
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)

The only other thing that was added was the execution of the new player.get_event method.

    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)

Now i am going to show you a variation on the lasers. In this example I housed the laser list in the player class. Some people would prefer to keep the lasers outside of the player class. This is possible too. In this way you could make the laser list a class varaible instead. Take out all the laser updates, draw method, and even checks from your player class and put them into your main game loop instead.

import pygame as pg
 
pg.init()

class Laser:
    lasers = []
    def __init__(self, loc):
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
        
    def update(self):
        self.rect.y -= self.speed
    
    def render(self, surf):
        surf.blit(self.image, self.rect)
 
class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        
    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                return True
        
    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
         
    def draw(self, surf):
        surf.blit(self.transformed_image, self.rect)
 
screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get(): 
        if event.type == pg.QUIT:
            done = True
        add_laser = player.get_event(event)
        if add_laser:
            Laser.lasers.append(Laser(player.rect.center))
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time)
    for laser in Laser.lasers:
        laser.update()
        laser.render(screen)
    player.draw(screen)
    pg.display.update()

As is our lasers are unlimited. You can essentially make a constant beam by repeatedly pressing the spacebar. This is not how we want it. So we are going to limit the lasers.

import pygame as pg

pg.init()

class Laser:
    limit = 3
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5

    def update(self):
        self.rect.y -= self.speed
        
        if self.rect.bottom < self.screen_rect.top:
            return True

    def render(self, surf):
        surf.blit(self.image, self.rect)

class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.lasers = []

    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if len(self.lasers) < Laser.limit:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))

    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        for laser in self.lasers[:]:
            rm = laser.update()
            if rm:
                self.lasers.remove(laser)

    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)

screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time)
    player.draw(screen)
    pg.display.update()

Let's start with the Laser class.

class Laser:
    limit = 3
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
 
    def update(self):
        self.rect.y -= self.speed
         
        if self.rect.bottom < self.screen_rect.top:
            return True
 
    def render(self, surf):
        surf.blit(self.image, self.rect)

In this code snippet, we passed screen_rect to out laser class. This is so we can tell the laser the location to remove itself from the list. In this case, when the laser goes off screen. We add a class variable limit to indiicate the max number of lasers allowed on screen for the player. In the update method we added the condition on which to remove itself from the laser list. Now because the laser itself is a single object, you cannot remove the laser from the list within the class. We can however remove it from the list from where laser update method is being called from. So we are going to return a flag to indiicate whether to remove this object or not from Laser.update (in this case it is just True).

    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if len(self.lasers) < Laser.limit:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))

Now in the Player class get event method we add the condition to which it only adds a new laser if the limit is not reached.

    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        for laser in self.lasers[:]:
            rm = laser.update()
            if rm:
                self.lasers.remove(laser)

In Player update method we remove the laser if Laser.update returns True. We add a [:] to self.lasers to loop a copy of the list instead of the actual list. This is solely so we can remove an object of the actual list based on the loop of the copy. The rule of thumb is to never remove from a list that you are looping, and this is the bypass...to remove from a copy of the list. You can bypass the rm variable and just do if laser.update: for the removal condition, but this states what is going on and is easier to read.

After these additions your lasers should be limited to 3 to the screen.

Now we are going to modify the code to instead of limit the number of lasers, to add a time delay between each laser. So we are going to revert the code to before the adding the laser limit.

mport pygame as pg

pg.init()

class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.Surface((5,40)).convert()
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5

    def update(self):
        self.rect.y -= self.speed

    def render(self, surf):
        surf.blit(self.image, self.rect)

class Player:
    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False

    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False

    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True

    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)

screen = pg.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
clock = pg.time.Clock()
done = False
while not done:
    keys = pg.key.get_pressed()
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    delta_time = clock.tick(60)/1000.0
    player.update(keys, delta_time)
    player.draw(screen)
    pg.display.update()

So we removed all the code from Laser class, such as the class variable limit as well as in the update method the condition to check if within the screen.

    def __init__(self, screen_rect):
        self.screen_rect = screen_rect
        self.image = pg.image.load('spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.transformed_image = pg.transform.rotate(self.image, 180)
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False

In the Player dunder init method we added 3 attributes. timer, laser_delay, and add_laser.

    def get_event(self, event):
        if event.type == pg.KEYDOWN:
            if event.key == pg.K_SPACE:
                if self.add_laser:
                    self.lasers.append(Laser(self.rect.center, self.screen_rect))
                    self.add_laser = False

In our Player get_event method we changed the condition to add a laser. If add_lasers is True then add a laser, and reset it to False, otherwise do not do anything.

    def update(self, keys, dt):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pg.K_LEFT]:
            self.rect.x -= self.dx * dt
        elif keys[pg.K_RIGHT]:
            self.rect.x += self.dx * dt
        for laser in self.lasers:
            laser.update()
        if pg.time.get_ticks()-self.timer > self.laser_delay:
            self.timer = pg.time.get_ticks()
            self.add_laser = True

In the Player update method we added a condition to changed our add_laser attribute. This whole condition basically is just a timer to do something every half a second. Half a second indicated by the value of laser_delay which is in milliseconds. You can modify this value to get a different delay in the lasers. pygame.time.get_ticks() gets number of ticks since game started. If the timer is greater than our laser delay, then execute the code. In that code we reset the timer to the current number of ticks since the start of the game. Everything after this code within this if condition gets executed every 500 milliseconds (half second). In this case add_laser can only be set to True twice a second. This puts a delay on the laser. You can increase this delay by incrementing the laser_delay value. The longer the delay, the longer timeframe between the allowed time to add a new laser in. So even if you tapped the spacebar at a 100 times a second, you are only going to get a max of 2 per second (at 500 ms).