Reputation: 3
I need my game to be capable of handling hundreds of bullets on screen. While the game seems not to have large slowdowns with smaller amounts of bullets (under 50 or so), it nonetheless does have them, and the problem becomes even more pronounced when around 100-200 are on the screen at the same time. This raised a particular concern I had: I want enemies to fire massive amounts of bullets aswell, but if the player's projectiles already cause such slowdowns, then there is no way that that will be possible.
Before I begin showing the code in question:
I'm pretty fresh to programming and I am not very certain what exactly causes the issue, so I will try to provide as much relevant code as possible. I will provide more if none of the culprits seem to be the causes of the issue. I apologize in advance if this is not enough information.
The slowdown gets ridiculous mostly with a debug superweapon I'm using, though it is sometimes noticable even when not nearly as many bullets are spawned (28 or a bit more) when I have many apps opened. This is a very small game so far, however, so I don't exactly understand why.
def init_player_weapon_level(self):
# debug mode
if self.lev_weapon == -1:
self.set_weapon_values(100, 5, 100, True)
self.firing_pattern = [[self.bullet_images["blue"], 0, 0, 90, -24],
[self.bullet_images["blue"], 0, 0, 92, -24],
[self.bullet_images["blue"], 0, 0, 88, -24],
...]
...
The firing pattern list defined in this function is used below. It's very long. This list is replaced based on the player's weapon level, -1 being the highest. This function is called only when the player object is created or when it's manually initiated with a keypress.
def fire(self, now):
if self.check_firing_conditions(now):
self.last_shot = now
for o in self.firing_pattern:
bullet = PlayerBullet01(o[0], self.rect.centerx + o[1], self.rect.top + o[2], o[3], o[4])
all_sprites.add(bullet)
bullets.add(bullet)
self.generate_heat(now)
This is the function responsible for firing weapons. It creates as many bullet objects as there are lists of parameters in self.firing_pattern
, with an image [0], X [1] and Y [2] spawn offsets (which are then added or subtracted from the player's position to give an illusion that they're fired from the ship's cannons), angle [3] at which the bullet is fired (90 degrees is straight up) and speed [4] at which it traverses (it's negative in the firing patterns because the bullet moves upwards).
Since self.firing_pattern
has 35 lists of parameters in it (which I omitted as that takes up space), 35 bullets are created all at once, fired with a delay of 100 milliseconds between the shots if the player presses the fire key. This makes exactly 245 bullets appear on the screen before the player's weapon overheats, which causes the weapon to stop firing. It is a lot, but the slowdown seems to be caused when there's already some bullets on the screen.
class PlayerBullet01(pygame.sprite.DirtySprite):
def __init__(self, image, x, y, deg, speed):
pygame.sprite.DirtySprite.__init__(self)
self.image = image
self.rect = self.image.get_rect()
self.center = self.rect.center
self.position = pygame.math.Vector2(x, y)
self.speed_mod = 0.065
# deg - 90 because we don't want to rotate the image
# by the amount of degrees it's supposed to face,
# we want to rotate it by a much smaller amount
# we also want to rotate it only once
# since player bullets probably shouldn't follow
# any other movement patterns than going straight to the target
if deg != 0:
self.image = pygame.transform.rotate(self.image, -deg + 90)
self.rect = self.image.get_rect()
self.rect.center = self.center
# we only want the bullet to move in a particular direction at a particular speed
self.direction = pygame.math.Vector2(0, 0)
self.direction += self.move(deg, speed)
# spawn bullet at the center of the spaceship
self.rect.centerx = self.position.x
self.rect.bottom = self.position.y
def update(self):
self.dirty = 1
self.position.x += self.direction.x * self.speed_mod * x_clock.delay
self.position.y += self.direction.y * self.speed_mod * x_clock.delay
self.rect.center = (self.position.x, self.position.y)
# die if out of bounds
if self.rect.bottom < 0:
self.kill()
def move(self, degrees, offset):
rads = math.radians(degrees)
x = math.cos(rads) * offset
y = math.sin(rads) * offset
return x, y
This is the bullet class. I thought at first that the move
function being called during every update slowed down the game (which is in an earlier version of the code, not shown here), but nothing has actually changed.
hits_enemy = pygame.sprite.groupcollide(enemies, bullets, True, True)
for hit in hits_enemy:
create_generic_enemy(GFX_ENEMY)
Finally, this is the line that checks for collisions between the bullets and enemies (of which there are always only 10 on the screen). I disabled collisions for bullets and enemies and it still did not seem to have improved the performance of the game at all.
dirtyupdate = all_sprites.draw(screen)
pygame.display.update(dirtyupdate)
Oh, and this is how the game draws everything on screen in the main game loop.
Last possible culprit: I found a way on Stack Overflow to stop pygame from micro-stuttering. It's a very annoying issue that seemed to happen for no reason whatsoever. It requires using a custom clock. I introduced this code to my game from here: Simple Pygame animation stuttering
previous = time.time() * 1000
# this is so it's easy to get out of the game loop
# (you just set GAME_RUNNING to false)
GAME_RUNNING = True
while GAME_RUNNING:
# CLOCK
# a pretty advanced clock that stops pygame from stuttering like a -----
current = time.time() * 1000
elapsed = current - previous
previous = current
x_clock.delay = 1000.0 / MAX_FPS - elapsed
x_clock.delay = max(int(x_clock.delay), 0)
...
# delays the game by delay
# using pygame.time.wait() instead of pygame.time.delay()
# because pygame.time.delay() makes my system fan go -------
pygame.time.wait(x_clock.delay)
For the record, delay
from the x_clock
file is imported to class files, too. This is because it's required for these classes to adjust their movement to FPS drops. I don't believe this code is what causes the lag, but I remember that when I used pygame.time.delay() instead of wait(), my fan went nuts.
What causes this slowdown and how do I fix it? I have considered a few possibilities already (one of which may be Pygame not being able to handle this many projectiles), but I have already seen a game where a similar amount of bullets was consistently created on screen and it did not have any slowdowns whatsoever. I couldn't figure it out after looking at that game's code, either.
Upvotes: 0
Views: 339
Reputation: 652
I don't know if this is actually the source of the slowdown but the following might work:
use pygame.time.Clock()
as your timer. the clock.tick(framerate)
(or clock.tick_busy_loop(framerate)
which according to the documentation is more precise) will cap the maximal framerate. clock.get_time()
will get you the time in miliseconds between the last two call of clock.tick()
/ clock.tick_busy_loop()
.
Edit:
1 this is a more efficient __init__
method:
class PlayerBullet01(pygame.sprite.DirtySprite):
images = {}
for color in ['blue']: # and your other bullet colors
color_dict = {}
for angle in range(360):
img = pygame.transform.rotate(bullet_images[color], angle)
color_dict[angle] = img, img.get_rect()
images[color] = color_dict
def __init__(self, color, x, y, deg, speed):
pygame.sprite.DirtySprite.__init__(self)
self.image, self.rect = self.images[color][deg]
self.position = pygame.math.Vector2(x, y)
self.speed_mod = 0.065
# deg - 90 because we don't want to rotate the image
# by the amount of degrees it's supposed to face,
# we want to rotate it by a much smaller amount
# we also want to rotate it only once
# since player bullets probably shouldn't follow
# any other movement patterns than going straight to the target
# this code is now unnecessary
# we only want the bullet to move in a particular direction at a particular speed
self.direction = pygame.math.Vector2(0, 0)
self.direction += self.move(deg, speed)
# spawn bullet at the center of the spaceship
self.rect.centerx = self.position.x
self.rect.bottom = self.position.y
by preloading the rotated images it doesn't need to be done 35 times every time the player fires.
2 use the following script to get the runtime of your functions:
from time import time
def timed(name):
func_timer = FunctionTimer(name)
def decorator(func):
def wrapper(*args, **kwargs):
start_time = time()
return_value = func(*args, **kwargs)
func_timer.add_time((time() - start_time) * 1000)
return return_value
return wrapper
return decorator
class FunctionTimer:
all = []
old_time = time()
frame_time = 0
@classmethod
def get_frame_summary(cls):
new_time = time()
cls.frame_time = new_time - cls.old_time
cls.old_time = new_time
print(f'------------------------------\n'
f'FRAME TIME SUMMARY:\n'
f'total frame time: {cls.frame_time * 1000:.5}\n'
f'calls:\n'
f'name calls average min max total')
for t in sorted(cls.all, key=lambda x: x.avg_time, reverse=True):
if len(t.frame_list):
print(t)
for t in cls.all:
t.clear()
def __init__(self, name):
self.name = name
self.frame_list = []
self.frame_calls = 0
self.total = 0
self.min = 0
self.max = 0
self.avg = 0
self.all.append(self)
def __repr__(self):
return f'{self.name: <20} {self.frame_calls: <5} {self.avg_time: <10.5} ' \
f'{self.min_time: <10.5} {self.max_time: <10.5} {self.total_time: <10.5}'
def add_time(self, added_time):
self.frame_list.append(added_time)
self.frame_calls += 1
def clear(self):
self.frame_list = []
self.frame_calls = 0
@property
def min_time(self):
self.min = min(self.frame_list)
return self.min
@property
def max_time(self):
self.max = max(self.frame_list)
return self.max
@property
def avg_time(self):
try:
self.avg = sum(self.frame_list) / self.frame_calls
except ZeroDivisionError:
self.avg = 0
return self.avg
@property
def total_time(self):
self.total = sum(self.frame_list)
return self.total
usage:
@timed('name of the function')
def function(...):
...
put the timed decorator before any function you want to time.
call
FunctionTimer.get_frame_summary()
once in the main loop.
output:
------------------------------
FRAME TIME SUMMARY:
total frame time: [total time during this frame in ms]
calls:
name calls average min max total
[name of the function = parameter you passed into the decorator]
[amount of times this function was called]
[average runtime of the function, the output will be sorted by this value]
[minimal runtime]
[maximal runtime]
[total time spent in function (sum of all function call times)]
-- all functions that have been called will print this summary
sorted by their average runtime (descending) --
Upvotes: 0