Posts: 419
Threads: 34
Joined: May 2019
Dec-21-2020, 09:31 PM
In have a list of sparks that I iterate over. I like the effect well enough. I am seeking an efficient way of doing this so I can have the most sparks, and to add "wash" from the player/helicopter sprite that modifies the spark's wind variable. Have an area of effect under the helicopter, and if that spark's position is within it, a modification is made to the wind variable.
I drew a diagram, but the photo button only accepts links.
def animate_sparks(self):
for spark in self.sparks:
dirs = [-3, -3, -2, -1, 0, 1, 2, 3, 3]
x = random.choice(dirs)
y = random.choice(dirs)
spark[0] += self.wind + vec(x, y) Drawn, thus:
for spark in fire.sparks:
self.screen.set_at((int(spark[0][0]), int(spark[0][1])), (255, 255, 0)) The sparks are currently managed by the Fire sprite which spawns and animates them. Is it more efficient to animate them in the Game's draw method all at once?
I'll figure a better way to draw it once I figure out how I'm storing the spark's information.
Thank you all for any wisdom.
Posts: 5,151
Threads: 396
Joined: Sep 2016
im not sure which is more efficient, if one is even over the other. I usually try to keep logic out of the draw method for better organization, unless you are having issues with performance. You can usually test the speed of your program to the alternative to compare the two speeds. I would write up code to test these two variations and see what the performance difference is between them (if any).
To be honest i know mekire would have at one point been able to answer this question as he was more into maxing out pygame/python's ability. I think his particle experiment may help in structure as he was trying to maximize performance.
michael1789 likes this post
Recommended Tutorials:
Posts: 419
Threads: 34
Joined: May 2019
(Dec-25-2020, 12:06 AM)metulburr Wrote: im not sure which is more efficient, if one is even over the other. I usually try to keep logic out of the draw method for better organization, unless you are having issues with performance. You can usually test the speed of your program to the alternative to compare the two speeds. I would write up code to test these two variations and see what the performance difference is between them (if any).
To be honest i know mekire would have at one point been able to answer this question as he was more into maxing out pygame/python's ability. I think his particle experiment may help in structure as he was trying to maximize performance.
Thank-you, I will science it further. Thank-you for the link.
Posts: 544
Threads: 15
Joined: Oct 2016
It really depends on so many factors.
How many sparks are alive at one time ?
Here my rough example.
import pygame
import random
class State:
def __init__(self, engine):
self.engine = engine
def draw(self, surface):
pass
def event(self, event):
pass
def update(self, ticks, delta):
pass
class DisplayEngine:
def __init__(self, caption, width, height, flags=0):
pygame.display.set_caption(caption)
self.surface = pygame.display.set_mode((width, height), flags)
self.rect = self.surface.get_rect()
self.clock = pygame.time.Clock()
self.running = True
self.delta = 0
self.ticks = 0
self.fps = 60
self.state = State(self)
self.next_state = None
self.on_quit = self.quit
def quit(self):
self.running = False
def main_loop(self):
while self.running:
if self.next_state:
self.state = self.next_state
self.next_state = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.on_quit()
self.state.event(event)
self.ticks = pygame.time.get_ticks()
self.state.update(self.ticks, self.delta)
self.state.draw(self.surface)
pygame.display.flip()
self.delta = self.clock.tick(self.fps)
class Emmitter:
def __init__(self, position, color, choices, decay):
self.vector = pygame.Vector2(random.choice(choices), random.choice(choices))
self.position = pygame.Vector2(position)
self.rect = pygame.Rect(0, 0, 2, 2)
self.color = color
self.decay = decay
self.decay_color = 255 / self.decay * 0.8
self.color_vector = pygame.Vector3(self.color[:3])
def draw(self, surface):
self.rect.center = int(self.position.x), int(self.position.y)
surface.fill(self.color, self.rect)
def update(self, delta):
self.decay -= delta
self.position += self.vector * delta
self.color_vector = self.color_vector.elementwise() - (self.decay_color * delta)
x, y, z = self.color_vector
self.color_vector = pygame.Vector3(max(0, x), max(0, y), max(0, z))
r, g, b = tuple(map(int, self.color_vector))
self.color = pygame.Color(r, g, b)
class IntroState(State):
def __init__(self, engine):
State.__init__(self, engine)
self.background_color = pygame.Color("navy")
self.choices = [-0.03, -0.03, -0.02, -0.01, 0, 0.01, 0.02, 0.03, 0.03]
self.sparks = []
self.spark_color = pygame.Color("firebrick")
print(self.spark_color)
self.spark_tick = 200
self.spark_timer = self.spark_tick
self.spark_font = pygame.font.Font(None, 28)
self.spark_decay = 1200
self.spark_count = 0
self.mpos = None
self.update_spark_label()
self.fps_num = -1
self.fps_position = 5, 5
self.fps_tick = 100
self.fps_next = self.fps_tick
self.update_fps_label()
def draw(self, surface):
surface.fill(self.background_color)
for spark in self.sparks:
spark.draw(surface)
surface.blit(self.spark_label, self.spark_position)
surface.blit(self.fps_label, self.fps_position)
def event(self, event):
if event.type == pygame.MOUSEMOTION:
self.mpos = event.pos
def update(self, ticks, delta):
alive_sparks = []
for spark in self.sparks:
spark.update(delta)
if spark.decay > 0:
alive_sparks.append(spark)
self.sparks = alive_sparks
self.spark_timer -= delta
if self.spark_timer < 0:
self.spark_timer += self.spark_tick
if self.mpos:
self.sparks.append(Emmitter(self.mpos, self.spark_color, self.choices, self.spark_decay))
#print('Spark added', self.mpos)
if len(self.sparks) != self.spark_count:
self.spark_count = len(self.sparks)
self.update_spark_label()
if ticks > self.fps_next:
self.fps_next += self.fps_tick
self.update_fps_label()
def update_spark_label(self):
text = "{} sparks".format(len(self.sparks))
self.spark_label = self.spark_font.render(text, 1, pygame.Color('lawngreen'))
rect = self.spark_label.get_rect()
padding = 8
rect.topright = self.engine.rect.right - padding, padding
self.spark_position = rect.topleft
def update_fps_label(self):
text = "FPS: {}".format(int(self.engine.clock.get_fps()))
self.fps_label = self.spark_font.render(text, 1, pygame.Color("lawngreen"))
def main():
pygame.init()
engine = DisplayEngine("Emmitter test", 800, 600)
engine.state = IntroState(engine)
engine.main_loop()
if __name__ == '__main__':
main()
Posts: 544
Threads: 15
Joined: Oct 2016
I haven't gotten any lag with an emitter yet.
Here I clean up code. Still a little rough.
import pygame
import random
class State:
def __init__(self, engine):
self.engine = engine
def draw(self, surface):
pass
def event(self, event):
pass
def update(self, ticks, delta):
pass
class DisplayEngine:
def __init__(self, caption, width, height, flags=0):
pygame.display.set_caption(caption)
self.surface = pygame.display.set_mode((width, height), flags)
self.rect = self.surface.get_rect()
self.clock = pygame.time.Clock()
self.running = True
self.delta = 0
self.ticks = 0
self.fps = 60
self.state = State(self)
self.next_state = None
self.on_quit = self.quit
def quit(self):
self.running = False
def main_loop(self):
while self.running:
if self.next_state:
self.state = self.next_state
self.next_state = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.on_quit()
self.state.event(event)
self.ticks = pygame.time.get_ticks()
self.state.update(self.ticks, self.delta)
self.state.draw(self.surface)
pygame.display.flip()
self.delta = self.clock.tick(self.fps)
class Emitter:
def __init__(self, position, color, choices, size, decay):
self.vector = pygame.Vector2(random.choice(choices), random.choice(choices))
self.position = pygame.Vector2(position)
self.rect = pygame.Rect(0, 0, size, size)
self.color = color
self.decay = decay
self.decay_color = 255 / self.decay * 0.8
self.color_vector = pygame.Vector3(self.color[:3])
def draw(self, surface):
self.rect.center = int(self.position.x), int(self.position.y)
surface.fill(self.color, self.rect)
def update(self, delta):
self.decay -= delta
self.position += self.vector * delta
self.color_vector = self.color_vector.elementwise() - (self.decay_color * delta)
x, y, z = self.color_vector
self.color_vector = pygame.Vector3(max(0, x), max(0, y), max(0, z))
r, g, b = tuple(map(int, self.color_vector))
self.color = pygame.Color(r, g, b)
class Sparks:
def __init__(self, entity, colors, tick, choices, sizes, decays):
self.entity = entity
self.choices = choices
self.colors = colors
self.decays = decays
self.sizes = sizes
self.timer = tick
self.tick = tick
self.sparks = []
@property
def count(self):
return len(self.sparks)
def create(self):
position = self.entity.position
color = self.get_data(self.colors)
decay = self.get_data(self.decays)
size = self.get_data(self.sizes)
self.sparks.append(Emitter(position, color, self.choices, size, decay))
def draw(self, surface):
for spark in self.sparks:
spark.draw(surface)
def get_data(self, data):
if isinstance(data, (tuple, list)):
return random.choice(data)
return data
def update(self, ticks, delta):
alive_sparks = []
for spark in self.sparks:
spark.update(delta)
if spark.decay > 0:
alive_sparks.append(spark)
self.sparks = alive_sparks
if ticks > self.timer:
self.timer += self.tick
self.create()
# Dummy entity
class Entity:
def __init__(self, position):
self.position = position
class MouseEntity:
@property
def position(self):
return pygame.mouse.get_pos()
class LabelUpdater:
def __init__(self, font, text_format, color, callback, position, anchor, tick):
self.text_format = text_format
self.callback = callback
self.position = position
self.anchor = anchor
self.color = color
self.font = font
self.timer = tick
self.tick = tick
self.update_text(0)
def draw(self, surface):
surface.blit(self.image, self.rect)
def update(self, ticks):
if ticks > self.timer:
self.timer += self.tick
self.callback(self)
def update_text(self, data):
text = self.text_format.format(data)
self.image = self.font.render(text, 1, self.color)
self.rect = self.image.get_rect()
setattr(self.rect, self.anchor, self.position)
class IntroState(State):
def __init__(self, engine):
State.__init__(self, engine)
self.background_color = pygame.Color("navy")
choices = [-0.03, -0.03, -0.02, -0.01, 0, 0.01, 0.02, 0.03, 0.03]
color = pygame.Color('firebrick')
self.spark = Sparks(MouseEntity(), color, 60, choices, 2, 3000)
color = pygame.Color('dodgerblue')
self.center_spark = Sparks(Entity(self.engine.rect.center), color, 100, choices, 2, 1200)
font = pygame.font.Font(None, 28)
color = pygame.Color("lawngreen")
padding = 8
self.label_fps = LabelUpdater(font, "FPS: {}", color, self.update_fps, (padding, padding), "topleft", 100)
position = self.engine.rect.right - padding, padding
self.label_spark = LabelUpdater(font, "{} Sparks", color, self.update_sparks, position, "topright", 100)
def draw(self, surface):
surface.fill(self.background_color)
self.spark.draw(surface)
self.label_fps.draw(surface)
self.label_spark.draw(surface)
self.center_spark.draw(surface)
def update(self, ticks, delta):
self.spark.update(ticks, delta)
self.label_fps.update(ticks)
self.label_spark.update(ticks)
self.center_spark.update(ticks, delta)
def update_sparks(self, label):
label.update_text(self.spark.count + self.center_spark.count)
def update_fps(self, label):
label.update_text(int(self.engine.clock.get_fps()))
def main():
pygame.init()
engine = DisplayEngine("Emmitter test", 800, 600)
engine.state = IntroState(engine)
engine.main_loop()
if __name__ == '__main__':
main()
Posts: 419
Threads: 34
Joined: May 2019
HERE is a screen capture of a recent version of the effect in-game. I was messing with raycasting and placing the bullet-strike effect, so it looks broken because it is.
From the beginning the issue was performance. This lags, because it is in-game, but I have optimizations to implement which should allow several times more sparks. Current optimizations include sparks being point sources rather than rects or circles or images, sparks are actually in the background layer so they aren't .set_at() while everything else is drawn and the spark's color doesn't change. An earlier version had color transitions, and also use .get_alpha() to add spark_color to the pixel so they carried the bg color.
My major optimization is to replace the current system where the movement of the sparks is randomized each update, with one in which sparks grab the next move from a long preexisting tuple of moves. I think I accomplish the same game effect with fewer than 1/4 the current number of calculation. I will optimize the fire sprite similarly.
Posts: 544
Threads: 15
Joined: Oct 2016
I'm using pygame Vector2 math.
My math need more work. Added alot more sparks.
But I am still getting no lag. I using wind effect with mouse.
Do you get lag when you run my program ?
import pygame
import random
class State:
def __init__(self, engine):
self.engine = engine
def draw(self, surface):
pass
def event(self, event):
pass
def update(self, ticks, delta):
pass
class DisplayEngine:
def __init__(self, caption, width, height, flags=0):
pygame.display.set_caption(caption)
self.surface = pygame.display.set_mode((width, height), flags)
self.rect = self.surface.get_rect()
self.clock = pygame.time.Clock()
self.running = True
self.delta = 0
self.ticks = 0
self.fps = 60
self.state = State(self)
self.next_state = None
self.on_quit = self.quit
def quit(self):
self.running = False
def main_loop(self):
while self.running:
if self.next_state:
self.state = self.next_state
self.next_state = None
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.on_quit()
self.state.event(event)
self.ticks = pygame.time.get_ticks()
self.state.update(self.ticks, self.delta)
self.state.draw(self.surface)
pygame.display.flip()
self.delta = self.clock.tick(self.fps)
class Emitter:
def __init__(self, position, color, choices, size, decay):
self.vector = pygame.Vector2(random.choice(choices[0]), random.choice(choices[1]))
if self.vector.x != 0 and self.vector.y != 0:
self.vector.normalize_ip()
self.vector *= 0.03
self.position = pygame.Vector2(position)
self.rect = pygame.Rect(0, 0, size, size)
self.color = color
self.decay = decay
def draw(self, surface):
self.rect.center = int(self.position.x), int(self.position.y)
surface.fill(self.color, self.rect)
def update(self, delta, effects):
self.decay -= delta
self.position += self.vector * delta
for effect in effects:
self.position += effect.effect(self.position, delta)
class Sparks:
def __init__(self, entity, colors, tick, choices, sizes, decays):
self.entity = entity
self.choices = choices
self.colors = colors
self.decays = decays
self.sizes = sizes
self.timer = tick
self.tick = tick
self.sparks = []
@property
def count(self):
return len(self.sparks)
def create(self):
position = self.entity.position
color = self.get_data(self.colors)
decay = self.get_data(self.decays)
size = self.get_data(self.sizes)
self.sparks.append(Emitter(position, color, self.choices, size, decay))
def draw(self, surface):
for spark in self.sparks:
spark.draw(surface)
def get_data(self, data):
if isinstance(data, (tuple, list)):
return random.choice(data)
return data
def update(self, ticks, delta, effects):
alive_sparks = []
for spark in self.sparks:
spark.update(delta, effects)
if spark.decay > 0:
alive_sparks.append(spark)
self.sparks = alive_sparks
if ticks > self.timer:
self.timer += self.tick
self.create()
# Dummy entity
class Entity:
def __init__(self, position):
self.position = position
class MovementEffects:
def __init__(self, position, distance, strength):
self.position = pygame.Vector2(position)
self.distance = distance
self.strength = strength
def effect(self, position, delta):
#position = pygame.Vector2(position)
gap = (position - self.position).normalize()
distance = self.position.distance_to(position)
if distance < self.distance:
strength = self.strength / self.distance * (self.distance - distance)
return gap * strength * delta
return pygame.Vector2()
class LabelUpdater:
def __init__(self, font, text_format, color, callback, position, anchor, tick):
self.text_format = text_format
self.callback = callback
self.position = position
self.anchor = anchor
self.color = color
self.font = font
self.timer = tick
self.tick = tick
self.update_text(0)
def draw(self, surface):
surface.blit(self.image, self.rect)
def update(self, ticks):
if ticks > self.timer:
self.timer += self.tick
self.callback(self)
def update_text(self, data):
text = self.text_format.format(data)
self.image = self.font.render(text, 1, self.color)
self.rect = self.image.get_rect()
setattr(self.rect, self.anchor, self.position)
class IntroState(State):
def __init__(self, engine):
State.__init__(self, engine)
self.background_color = pygame.Color("navy")
choices = [x / 1000 for x in range(-30, 30)], [y / 1000 for y in range(-30, 0)]
color = pygame.Color('firebrick')
self.sparks = []
for x in range(20, engine.rect.width - 10, 30):
life = random.randint(1200, 1400)
emit = random.randint(80, 120)
size = 2
spark = Sparks(Entity((x, engine.rect.centery)), color, emit, choices, size, life)
self.sparks.append(spark)
font = pygame.font.Font(None, 28)
color = pygame.Color("lawngreen")
padding = 8
self.label_fps = LabelUpdater(font, "FPS: {}", color, self.update_fps, (padding, padding), "topleft", 100)
position = self.engine.rect.right - padding, padding
self.label_spark = LabelUpdater(font, "{} Sparks", color, self.update_sparks, position, "topright", 100)
self.wind_effect = MovementEffects((0, 0), 30, 10)
self.effects = [self.wind_effect]
def draw(self, surface):
surface.fill(self.background_color)
for spark in self.sparks:
spark.draw(surface)
self.label_fps.draw(surface)
self.label_spark.draw(surface)
def event(self, event):
if event.type == pygame.MOUSEMOTION:
self.wind_effect.position = pygame.Vector2(event.pos)
def update(self, ticks, delta):
for spark in self.sparks:
spark.update(ticks, delta, self.effects)
self.label_fps.update(ticks)
self.label_spark.update(ticks)
def update_sparks(self, label):
label.update_text(sum([spark.count for spark in self.sparks]))
def update_fps(self, label):
label.update_text(int(self.engine.clock.get_fps()))
def main():
pygame.init()
engine = DisplayEngine("Emmitter test", 800, 600)
engine.state = IntroState(engine)
engine.main_loop()
if __name__ == '__main__':
main()
Posts: 419
Threads: 34
Joined: May 2019
No lag at all in your program. I use Vector2 throughout the game. I didn't here because it's not required.
How does one measure cpu cycles used by an element anyway? Rendering text is costly. I commented out the FPS and count drawing and your program can run for x in range(20, engine.rect.width - 10, 2): and look fine to me.
The fewer cycles it uses, the more cycles I can use on more sparks, smoke, screen shake, parallax layers, etc. I think I need to make a program that will measure how long a piece of code takes to execute. I didn't use vectors because pygame.Vector2 is a class instantiation. I avoid doing that in the loop. Same with rendering text.
I've only coded Python, but it is known to be slow. My chopper game is an attempt to see what I can get out of pygame. I'm taking inspiration from the people who coded retro games. They had to use every cycle wisely. That is the same limitation python presents. Thus the decision for frame based movement. It's a frame-based animated pixel game anyhow.
I need to make my measuring program and test it, but I think for spark in set(self.sparks): might avoid double drawing, which is more and more a problem as the spark number increases.
Posts: 544
Threads: 15
Joined: Oct 2016
Using Vector2 math is low level math. Vector2 will cost less when moving any sprite.
Render text is costly. That why I render once every 100 ms. blit any image is fast. That why you save text to a surface. Loaded image must be converted to be fast.
Using set on a class objects won't work. All class object have separate id.
I can get pygame to move about 1000 sprites without any frame rate loss.
michael1789 likes this post
Posts: 419
Threads: 34
Joined: May 2019
(Jan-19-2021, 05:02 PM)Windspar Wrote: Using set on a class objects won't work. All class object have separate id.
My bad. My self.sparks list is just a list of points. I get them to the screen
(Jan-19-2021, 05:02 PM)Windspar Wrote: I can get pygame to move about 1000 sprites without any frame rate loss. Reputation +1
I'm looking to simulate 10000 things within an otherwise complicated, atmospheric game. What about prerendering sparks to transparent tiles?... I just had a light go on. The new plan is to prerender a spark effect onto transparent tiles then animate the tiles based on chopper location. I can layer them too.
This could have many game applications. An animated tile set seems like a worthy challenge. Now I have to make a flamethrower game! lol
|