Reputation: 31
I am currently trying to make a game animation based off the game plant versus zombies. When the zombie moves across the screen and contacts the plant, the plant sprite gets saved in a list called plants_eaten_list. The zombie also stops moving to the left. After a 1 second delay (when the plants_eaten_event is triggered), the plant loses 10 health points. When the plant's health reaches 0, the plant sprite dies through the plant.kill() command.
The code for setting up the timer (pygame.time.set_timer(plants_eaten_event, 1000) needs to be inside the main while loop in order to constantly check for a plant within the plants_eaten_list. However, my problem is that for every refresh of the while loop, the timer for the plants_eaten_event gets reset back to 1 second, so the plants_eaten_event never occurs.
Is there any way I can structure the logic of this program to alleviate this issue?
I do not own copyright to any of these images, and the images are intended only for private use. The zombie image is downloaded from http://plantsvszombies.wikia.com/wiki/Zombie/Gallery?file=Regular_Zombie.png The plant image is downloaded from http://plantsvszombies.wikia.com/wiki/File:Peashooter.png
Here is my code:
import pygame
import random
BLACK = (0,0,0)
WHITE = (255,255,255)
RED = (255,0,0)
BLUE = (0,0,255)
GREEN = (0,255,0)
pygame.init()
borders_sound = pygame.mixer.Sound("bump.wav")
class Zombie(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("Regular_Zombie.png").convert()
self.image.set_colorkey(WHITE)
# Make our top-left corner the passed-in location.
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
# -- Attributes
# Set speed vector
self.change_x = -1
self.change_y = 0
def changespeed(self, x, y):
""" Change the speed of the player"""
self.change_x += x
self.change_y += y
def move(self):
""" Find a new position for the player"""
#self.change_x = -1
self.rect.x += self.change_x
#print(self.rect.x)
#times = 0
if self.rect.x < 0:
self.rect.x = 0
def eatplant(self,plant):
pygame.time.set_timer(plants_eaten_event,6000)
self.change_x = 0
plant.health = plant.health - 10
class Plant(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.image.load("Peashooter.png").convert()
self.image.set_colorkey(BLUE)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.alive = True
self.health = 60
screen_width = 700
screen_height = 400
screen = pygame.display.set_mode([screen_width, screen_height])
all_sprites_list = pygame.sprite.Group()
plants_list = pygame.sprite.Group()
zombies_list = pygame.sprite.Group()
zombie1 = Zombie(690, 15)
plant1 = Plant(300,15)
all_sprites_list.add(zombie1)
all_sprites_list.add(plant1)
plants_list.add(plant1)
zombies_list.add(zombie1)
done = False
clock = pygame.time.Clock()
plants_eaten_event = pygame.USEREVENT + 1
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# If plant.health = 0 at the end of the timer, the plant is eaten.
# If plant.health != 0 at the end of the timer, the entire event
# must happen again in the while loop until the health = 0
elif event.type == plants_eaten_event:
print("Zombie 1 ate plant 1")
zombie1.eatplant(plant)
# Set timer equal to 0 until another plant gets eaten or zombie continues eating current plant
pygame.time.set_timer(plants_eaten_event, 0)
if plant.health == 0:
plant.alive = False
plant.kill()
zombie1.change_x = -1
zombie1.move()
elif event.type == zombie_eaten_plant_event:
zombie1.change_x = -1
zombie1.move()
print("Zombie 1 is eating plant")
pygame.time.set_timer(zombie_eaten_plant_event, 0)
screen.fill(WHITE)
zombie1.move()
if times == 0 and zombie1.rect.x <= 0:
times = times + 1
borders_sound.play()
all_sprites_list.draw(screen)
all_sprites_list.update()
# I set the do kill command in spritecollide command to False because I
# needed to delay the plants that were eaten before they were removed. They
# will be removed after six seconds(symbolize zombies eating plants). Aftera
# a period of time, then I will create a loop that will loop through all plants
# in the plants_eaten_list and use .kill() . I will need to create a separate
# collision function that will stall the zombies until the plants are killed.
plants_eaten_list = pygame.sprite.spritecollide(zombie1,plants_list,False)
# I changed the delay to 6 seconds, and plant 1 still died instantly. The zombie
# stayed frozen in place for 6 seconds before teh 2nd plant was instantly destroyed
for plant in plants_eaten_list:
# If I remove this, plant1 is not killed, even though
# there is the same code within the plants_eaten_event function.
if plant1.health == 0:
plant1.kill()
# The while loop loops so fast that the timer doesnot have a chance to
# finish its countdown before it restarts again. Will I need some kind of a threading
# module to prevent the timers from restarting until they end?
pygame.time.set_timer(plants_eaten_event, 1000)
for plant_eaten in plants_eaten_list:
borders_sound.play()
clock.tick(60)
pygame.display.flip()
pygame.quit()
Upvotes: 1
Views: 835
Reputation: 20438
my problem is that for every refresh of the while loop, the timer for the plants_eaten_event gets reset back to 1 second, so the plants_eaten_event never occurs.
That happens because the rect of the zombie still collides with the rect of the plant in the next frame, spritecollide
returns the list with the colliding plant and the for plant in plants_eaten_list:
loop where you call set_timer
gets executed again each frame.
The first thing you need to do is to set the zombie.rect.left
position to plant.rect.right
after the first collision, so that the sprites don't collide anymore in the next frame:
for plant in plants_eaten_list:
zombie1.change_x = 0 # Stop the zombie.
# Move the rect so that it doesn't touch the plant's rect anymore.
zombie1.rect.left = plant.rect.right
if plant1.health <= 0:
plant1.kill()
pygame.time.set_timer(plants_eaten_event, 1000)
borders_sound.play()
However, once you start adding more zombies, the code will break because you use the zombie1
and the plant
variable (which depends on the for plant in plants_eaten_list:
loop) in the event loop, so it will work only for this zombie and the last collided plant. Unfortunately, it's not possible to send more information than the event type with pygame.time.set_timer
, otherwise you could attach the specific zombie and plant to the event in order to handle them in the event loop.
An alternative solution would be to give each zombie an own timer attribute (just a number) which you can update in their update
methods.
So when the zombie touches a plant, I start the timer by setting it to 1 (second) and then decrement it by the delta time (the time needed for the last frame) each frame. I also stop the zombie self.change_x = 0
, move the rect back self.rect.left = plant.rect.right
and store a reference to the plant because we need it to reduce its health when the timer is finished.
Once the timer is <= 0
the zombie starts to move again, takes a bite of the plant, the sprites touch again and the timer is started anew.
import pygame
WHITE = (255,255,255)
BLUE = (0,0,255)
GREEN = (0,255,0)
class Zombie(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((30, 50))
self.image.fill(BLUE)
self.rect = self.image.get_rect(topleft=(x, y))
self.change_x = -1
self.timer = 0 # Timer for the attacks.
self.plant = None # Currently attacked target.
def update(self, dt):
"""Update the zombie."""
if self.timer != 0:
self.timer -= dt
if self.timer <= 0: # Move if the timer is inactive.
self.change_x = -1
# Check if a plant is touching and eat it.
if self.plant is not None:
self.plant.health -= 10
print('Health', self.plant.health)
if self.plant.health <= 0:
print('Eaten')
self.plant.kill()
self.plant = None
self.timer = 0
self.rect.x += self.change_x
if self.rect.x < 0:
self.rect.x = 0
def eatplant(self, plants):
print('Play sound')
self.change_x = 0 # Stop the zombie.
self.timer = 1 # Start the timer.
for plant in plants:
self.plant = plant # Store a reference to the plant.
# Move the zombie back, so that the sprites
# don't collide anymore.
self.rect.left = plant.rect.right
class Plant(pygame.sprite.Sprite):
def __init__(self, x, y):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((30, 50))
self.image.fill(GREEN)
self.rect = self.image.get_rect(topleft=(x, y))
self.health = 30
pygame.init()
screen = pygame.display.set_mode([700, 400])
# No need to assign the sprites to variables.
plants = pygame.sprite.Group(Plant(500, 15), Plant(350, 115))
zombies = pygame.sprite.Group(Zombie(690, 15), Zombie(690, 115))
all_sprites = pygame.sprite.Group(plants, zombies)
clock = pygame.time.Clock()
dt = 0
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# Pass the delta time to the update methods of the sprites.
all_sprites.update(dt)
# groupcollide returns a dictionary of the collided zombies and plants.
plants_eaten = pygame.sprite.groupcollide(zombies, plants, False, False)
for zombie, collided_plants in plants_eaten.items():
zombie.eatplant(collided_plants)
screen.fill(WHITE)
all_sprites.draw(screen)
dt = clock.tick(60) / 1000 # Time in seconds since last tick.
pygame.display.flip()
pygame.quit()
Upvotes: 1