Python Forum
[PyGame] explosion-animation
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] explosion-animation
#1
Dear forum members,
I have worked through the pygame tutorial by metulburr (without
state maschines) and now wanted to add a nice explosion to the laser (player)-enemy-collision.
First: I use the code of the tutorial and for the explosion the class of the thread of Russ_CW.
Problem: I try frame-based and time-based animation.
The program runs perfectly when sprite groups are used.
However, my goal was to get by without sprite groups:
In this case, the animation - both time-based and frame-based - runs so fast that it appears as a single frame.
What am I doing wrong and where is my thinking error?
I would be grateful for help and suggestions

code:
import pygame
import math

pygame.init()

screen = pygame.display.set_mode((800,600))
screen_rect = screen.get_rect()

BLACK = (0, 0, 0)
FPS  = 60
   
def enemy_image_load():
    image = pygame.image.load('data/enemy.png').convert()
    image.set_colorkey((255,0,255))
    transformed_image = pygame.transform.rotate(image, 180)
    orig_image = pygame.transform.scale(transformed_image, (40,80))
    mask = pygame.mask.from_surface(orig_image)
    return (orig_image, mask)
    

class Enemy:
    def __init__(self, images, screen_rect):
        self.screen_rect = screen_rect
        self.image = images[0]
        self.mask = images[1]
        start_buffer = 0
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.distance_above_player = 100 
        self.speed = 2
        self.bullet_color = (255,0,0)
        self.is_hit = False
        self.range_to_fire = False
        self.timer = 0.0
        self.bullets = [ ]
        self.dead = False
           
    def pos_towards_player(self, player_rect):
        c = math.sqrt((player_rect.x - self.rect.x) ** 2 + (player_rect.y - self.distance_above_player  - self.rect.y) ** 2)
        try:
            x = (player_rect.x - self.rect.x) / c
            y = ((player_rect.y - self.distance_above_player)  - self.rect.y) / c
        except ZeroDivisionError: 
            return False
        return (x,y)
           
    def update(self, dt, player):
        new_pos = self.pos_towards_player(player.rect)
        if new_pos: #if not ZeroDivisonError
            self.rect.x, self.rect.y = (self.rect.x + new_pos[0] * self.speed, self.rect.y + new_pos[1] * self.speed)
           
        self.check_attack_ability(player)
        if self.range_to_fire:  
            if pygame.time.get_ticks() - self.timer > 1500.0:
                self.timer = pygame.time.get_ticks()
                self.bullets.append(Laser(self.rect.center, self.bullet_color))
                   
        self.update_bullets(player)
                   
    def draw(self, surf):
        if self.bullets:
            for bullet in self.bullets:
                surf.blit(bullet.image, bullet.rect)
        surf.blit(self.image, self.rect)
           
    def check_attack_ability(self, player):
        #if player is lower than enemy
        if player.rect.y >= self.rect.y: 
            try:
                offset_x =  self.rect.x - player.rect.x
                offset_y =  self.rect.y - player.rect.y
                d = int(math.degrees(math.atan(offset_x / offset_y)))
            except ZeroDivisionError: #player is above enemy
                return
            #if player is within 15 degrees lower of enemy
            if math.fabs(d) <= 15: 
                self.range_to_fire = True
            else:
                self.range_to_fire = False
                   
    def update_bullets(self, player):
        if self.bullets:
            for obj in self.bullets[:]:
                obj.update('down')
                #check collision
                if obj.rect.colliderect(player.rect):
                    offset_x =  obj.rect.x - player.rect.x 
                    offset_y =  obj.rect.y - player.rect.y
                    if player.mask.overlap(obj.mask, (offset_x, offset_y)):
                        player.take_damage(1)
                        self.bullets.remove(obj)
           
class Laser:
    def __init__(self, loc, screen_rect):
        self.screen_rect = screen_rect
        self.image = pygame.Surface((5,40)).convert_alpha()
        #self.image.set_colorkey((255,0,255))
        self.mask = pygame.mask.from_surface(self.image)
        self.image.fill((255,255,0))
        self.rect = self.image.get_rect(center=loc)
        self.speed = 5
     
    def update(self,direction='up'):
        if direction == 'down':
            self.rect.y += self.speed
        else:
            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 = pygame.image.load('data/spaceship.png').convert()
        self.image.set_colorkey((255,0,255))
        self.mask = pygame.mask.from_surface(self.image)
        self.transformed_image = pygame.transform.rotate(self.image, 180)
        self.transformed_image = pygame.transform.scale(self.transformed_image, (63,108))
        start_buffer = 300
        self.rect = self.image.get_rect(
            center=(screen_rect.centerx, screen_rect.centery + start_buffer)
        )
        self.dx = 300
        self.dy = 300
        self.lasers = []
        self.timer = 0.0
        self.laser_delay = 500
        self.add_laser = False
        self.damage = 10
         
    def take_damage(self, value):
        self.damage -= value
     
    def get_event(self, event):
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.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, enemies):
        self.rect.clamp_ip(self.screen_rect)
        if keys[pygame.K_LEFT]:
            self.rect.x -= self.dx * dt
        if keys[pygame.K_RIGHT]:
            self.rect.x += self.dx * dt
        if keys[pygame.K_UP]:
            self.rect.y -= self.dy * dt
        if keys[pygame.K_DOWN]:
            self.rect.y += self.dy * dt
        if pygame.time.get_ticks() - self.timer > self.laser_delay:
            self.timer = pygame.time.get_ticks()
            self.add_laser = True
        self.check_laser_collision(enemies)
         
    def check_laser_collision(self, enemies):
        for laser in self.lasers[:]:
            laser.update()
            for e in enemies:
                if laser.rect.colliderect(e.rect):
                    offset_x =  laser.rect.x - e.rect.x 
                    offset_y =  laser.rect.y - e.rect.y
                    if e.mask.overlap(laser.mask, (offset_x, offset_y)):
                        # alternativ-Code without spritegroup
                        # time_based
                        #expl = Explosion(e.rect.centerx, e.rect.centery)
                        #expl.update(delta_time)
                        #expl.draw()
                        # frame_based
                        #expl = Explosion(e.rect.centerx, e.rect.centery)
                        #expl.update()
                        #expl.draw()
                        
                        # with Spritegroup
                        expl = Explosion(e.rect.centerx, e.rect.centery)
                        explosion_group.add(expl)
                        
                        e.dead = True
                        self.lasers.remove(laser)
             
    def draw(self, surf):
        for laser in self.lasers:
            laser.render(surf)
        surf.blit(self.transformed_image, self.rect)
        
        
class Explosion(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.images = []
        for num in range(1, 6):
            img = pygame.image.load(f"img/exp{num}.png")
            img = pygame.transform.scale(img, (120, 120))
            self.images.append(img)
        
        self.timer = None
        self.index = 0
        self.image = self.images[self.index]
        self.rect = self.image.get_rect()
        self.rect.center = [x, y]
        
        self.animation_time =  .05
        self.current_time = 0
        
        self.animation_frames = 6
        self.current_frame = 0

    #def update(self):
        #  frame_based 
        #explosion_speed = 4 
        #self.current_frame += 1
        #if self.current_frame >= explosion_speed and self.index < len(self.images) - 1:
        #    self.current_frame = 0
        #    self.index += 1
        #    self.image = self.images[self.index]
        #if self.index >= len(self.images) - 1 and self.current_frame >= explosion_speed:
        #    self.kill()
    
    def update(self, delta_time):
        
        # time_based
        self.current_time += delta_time
        if self.current_time >= self.animation_time and self.index < len(self.images)-1:
            self.current_time = 0
            self.index += 1
            self.image = self.images[self.index]

        if self.index >= len(self.images)-1 and self.current_time >= self.animation_time:
            self.kill()
     def draw(self):
        screen.blit(self.image, self.rect)

screen = pygame.display.set_mode((800,600))
screen_rect = screen.get_rect()
player = Player(screen_rect)
ENEMY_IMAGE = enemy_image_load()
enemies = []
enemies.append(Enemy(ENEMY_IMAGE, screen_rect))
explosion_group = pygame.sprite.Group()

clock = pygame.time.Clock()
done = False

while not done:
    #clock.tick(FPS)
    keys = pygame.key.get_pressed()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        player.get_event(event)
    screen.fill((0,0,0))
    
    delta_time = clock.tick(60)/1000.0
    explosion_group.draw(screen)
    # frame_based
    #explosion_group.update()
    # time_based
    explosion_group.update(delta_time)
     player.update(keys, delta_time, enemies)
    for e in enemies[:]:
        e.update(delta_time, player)
        if e.dead:
            enemies.remove(e) # remove: Listenmethode
        e.draw(screen)
    player.draw(screen)
    pygame.display.update()
Reply
#2
Your code works as expected on my system and the timing on the explosion seems perfect.
Reply
#3
That's what I say, but if you use the code that doesn't need a sprite group - comment code under Player.check_laser_collision off - the programme behaves as described.
code:
def check_laser_collision(self, enemies):
        for laser in self.lasers[:]:
            laser.update()
            for e in enemies:
                if laser.rect.colliderect(e.rect):
                    offset_x =  laser.rect.x - e.rect.x 
                    offset_y =  laser.rect.y - e.rect.y
                    if e.mask.overlap(laser.mask, (offset_x, offset_y)):
                        # alternativ-Code without Spritegroup
                        # time_based
                        expl = Explosion(e.rect.centerx, e.rect.centery)
                        expl.update(delta_time)
                        expl.draw()
                        # frame_based
                        #expl = Explosion(e.rect.centerx, e.rect.centery)
                        #expl.update()
                        #expl.draw()
                        
                        # with Spritegroup
                        #expl = Explosion(e.rect.centerx, e.rect.centery)
                        #explosion_group.add(expl)
                        
                        e.dead = True
                        self.lasers.remove(laser)
Reply
#4
Its been an a couple years since i have programmed at all. So take my current advice with a grain of salt as it could be off.

If i remember correctly...Sprite groups "basically" just automate the update and draw methods. So if you wanted to remove it as a Sprite Group you would first get rid of the explosion class from inheriting from pygame.sprite.Sprite. Then run update and draw methods yourself.

An explosion is just running through a list of explosion images slowing them down for you to see. This example switches a number every second. You can utilize that same concept to flip through a list of explosion images. But make them faster that 1 second (1000 milliseconds would be more like 100 milliseconds, etc).

    def update(self):
        if pygame.time.get_ticks()-self.timer > self.delay:
            self.timer = pygame.time.get_ticks()
            #flip_to_next_image()
where flip_to_next_image() would be an Explosion method flipping to next image and detecting if at the end of the list to ensure it doesnt fail. timer is initialized to 0, and delay is 100 for 100 milliseconds. Which would execute flip_to_next_image() every 10th of a second.

Sorry i couldnt help any more, i just havent been programming during the pandemic as i have been busy doing other things.
Recommended Tutorials:
Reply
#5
(Dec-01-2021, 01:10 AM)metulburr Wrote: Its been an a couple years since i have programmed at all. So take my current advice with a grain of salt as it could be off.

If i remember correctly...Sprite groups "basically" just automate the update and draw methods. So if you wanted to remove it as a Sprite Group you would first get rid of the explosion class from inheriting from pygame.sprite.Sprite. Then run update and draw methods yourself.

An explosion is just running through a list of explosion images slowing them down for you to see. This example switches a number every second. You can utilize that same concept to flip through a list of explosion images. But make them faster that 1 second (1000 milliseconds would be more like 100 milliseconds, etc).

    def update(self):
        if pygame.time.get_ticks()-self.timer > self.delay:
            self.timer = pygame.time.get_ticks()
            #flip_to_next_image()
where flip_to_next_image() would be an Explosion method flipping to next image and detecting if at the end of the list to ensure it doesnt fail. timer is initialized to 0, and delay is 100 for 100 milliseconds. Which would execute flip_to_next_image() every 10th of a second.

Sorry i couldnt help any more, i just havent been programming during the pandemic as i have been busy doing other things.


Dear metulburr, thank you very much for your answer and your suggestions, I will try them out.
But most of all I want to thank you for your pygame tutorial. I think it's one of the clearest pygame tutorials I know (and therefore also one of the best). Especially when it comes to getting rid of redundant code and organising code.
Greetings hespa
metulburr likes this post
Reply
#6
Thanks. I created those tutorials because I saw a lot of pygame tutorials were providing bad coding habits (multiple game loops, not using classes and file structure to organize, etc.) I wish I created more while I was in the zone. But I am glad that people get use out of them.

If you like my style you would like whom I mentored from. He had his examples on Github. They are not tutorials so they don't explain much. But they go into more than my tutorials do if you can figure it out from code alone. Such as RPG, or platforming, etc.
https://github.com/Mekire

In which the same style also turned into a collaboration effort for a game.
https://github.com/iminurnamez/pyroller

Some extra things to look at.
Recommended Tutorials:
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGame] Sprite Animation to create an explosion Russ_CW 2 3,954 Oct-23-2020, 04:57 PM
Last Post: Russ_CW

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020