Python Forum
[PyGame] Adding player effects (part 4) - Printable Version

+- Python Forum (https://python-forum.io)
+-- Forum: General (https://python-forum.io/forum-1.html)
+--- Forum: Tutorials (https://python-forum.io/forum-4.html)
+---- Forum: Game Tutorials (https://python-forum.io/forum-33.html)
+---- Thread: [PyGame] Adding player effects (part 4) (/thread-405.html)



Adding player effects (part 4) - metulburr - Oct-09-2016

Back to part 3
https://python-forum.io/Thread-PyGame-Basic-event-handling-part-3


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.

import 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).


Part 5
https://python-forum.io/Thread-PyGame-Basic-animation-part-5