Python Forum
[PyGame] Chopper wash effect efficiency questions.
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[PyGame] Chopper wash effect efficiency questions.
#1
Music 
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.
Reply
#2
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
Reply
#3
(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.
snippsat likes this post
Reply
#4
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()
Reply
#5
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()
Reply
#6
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.
Reply
#7
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()
Reply
#8
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.
Reply
#9
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
Reply
#10
(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
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyGame] sound effect delay and program laggy xBlackHeartx 26 3,978 Oct-09-2019, 11:36 AM
Last Post: metulburr

Forum Jump:

User Panel Messages

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