1"""Particle simulation"""
2import sys
3import random
4import sdl2
5import sdl2.ext
6import sdl2.ext.particles
7
8# Create a resource, so we have easy access to the example images.
9RESOURCES = sdl2.ext.Resources(__file__, "resources")
10
11
12# The Particle class offered by sdl2.ext.particles only contains the life
13# time information of the particle, which will be decreased by one each
14# time the particle engine processes it, as well as a x- and
15# y-coordinate. This is not enough for us, since we want them to have a
16# velocity as well to make moving them around easier. Also, each
17# particle can look different for us, so we also store some information
18# about the image to display on rendering in ptype.
19#
20# If particles run out of life, we want to remove them, since we do not
21# want to flood our world with unused entities. Thus, we store a
22# reference to the entity, the particle belongs to, too. This allows use
23# to remove them easily later on.
24class CParticle(sdl2.ext.particles.Particle):
25    def __init__(self, entity, x, y, vx, vy, ptype, life):
26        super(CParticle, self).__init__(x, y, life)
27        self.entity = entity
28        self.type = ptype
29        self.vx = vx
30        self.vy = vy
31
32
33# A simple Entity class, that contains the particle information. This
34# represents our living particle object.
35class EParticle(sdl2.ext.Entity):
36    def __init__(self, world, x, y, vx, vy, ptype, life):
37        self.cparticle = CParticle(self, x, y, vx, vy, ptype, life)
38
39
40# A callback function for creating new particles. It is needed by the
41# ParticleEngine and the requirements are explained below.
42def createparticles(world, deadones, count=None):
43    if deadones is not None:
44        count = len(deadones)
45    # Create a replacement for each particle that died. The particle
46    # will be created at the current mouse cursor position (explained
47    # below) with a random velocity, life time, and image to be
48    # displayed.
49    for c in range(count):
50        x = world.mousex
51        y = world.mousey
52        vx = random.random() * 3 - 1
53        vy = random.random() * 3 - 1
54        life = random.randint(20, 100)
55        ptype = random.randint(0, 2)  # 0-2 denote the image to be used
56        # We do not need to assign the particle to a variable, since it
57        # will be added to the World and we do not need to do perform
58        # any post-creation operations.
59        EParticle(world, x, y, vx, vy, ptype, life)
60
61
62# A callback function for updating particles. It is needed by the
63# ParticleEngine and the requirements are explained below.
64def updateparticles(world, particles):
65    # For each existing, living particle, move it to a new location,
66    # based on its velocity.
67    for p in particles:
68        p.x += p.vx
69        p.y += p.vy
70
71
72# A callback function for deleting particles. It is needed by the
73# ParticleEngine and the requirements are explained below.
74def deleteparticles(world, deadones):
75    # As written in the comment for the CParticle class, we will use the
76    # stored entity reference of the dead particle components to delete
77    # the dead particles from the world.
78    world.delete_entities(p.entity for p in deadones)
79
80
81# Create a simple rendering system for particles. This is somewhat
82# similar to the TextureSprinteRenderSystem from sdl2.ext. Since we operate on
83# particles rather than sprites, we need to provide our own rendering logic.
84class ParticleRenderSystem(sdl2.ext.System):
85    def __init__(self, renderer, images):
86        # Create a new particle renderer. The surface argument will be
87        # the targets surface to do the rendering on. images is a set of
88        # images to be used for rendering the particles.
89        super(ParticleRenderSystem, self).__init__()
90        # Define, what component instances are processed by the
91        # ParticleRenderer.
92        self.componenttypes = (CParticle,)
93        self.renderer = renderer
94        self.images = images
95
96    def process(self, world, components):
97        # Processing code that will render all existing CParticle
98        # components that currently exist in the world. We have a 1:1
99        # mapping between the created particle entities and associated
100        # particle components; that said, we render all created
101        # particles here.
102
103        # We deal with quite a set of items, so we create some shortcuts
104        # to save Python the time to look things up.
105        #
106        # The SDL_Rect is used for the blit operation below and is used
107        # as destination position for rendering the particle.
108        r = sdl2.SDL_Rect()
109
110        # The SDL2 blit function to use. This will take an image
111        # (SDL_Texture) as source and copies it on the target.
112        dorender = sdl2.SDL_RenderCopy
113
114        # And some more shortcuts.
115        sdlrenderer = self.renderer.sdlrenderer
116        images = self.images
117        # Before rendering all particles, make sure the old ones are
118        # removed from the window by filling it with a black color.
119        self.renderer.clear(0x0)
120
121        # Render all particles.
122        for particle in components:
123            # Set the correct destination position for the particle
124            r.x = int(particle.x)
125            r.y = int(particle.y)
126
127            # Select the correct image for the particle.
128            img = images[particle.type]
129            r.w, r.h = img.size
130            # Render (or blit) the particle by using the designated image.
131            dorender(sdlrenderer, img.texture, None, r)
132        self.renderer.present()
133
134
135def run():
136    # Create the environment, in which our particles will exist.
137    world = sdl2.ext.World()
138
139    # Set up the globally available information about the current mouse
140    # position. We use that information to determine the emitter
141    # location for new particles.
142    world.mousex = 400
143    world.mousey = 300
144
145    # Create the particle engine. It is just a simple System that uses
146    # callback functions to update a set of components.
147    engine = sdl2.ext.particles.ParticleEngine()
148
149    # Bind the callback functions to the particle engine. The engine
150    # does the following on processing:
151    # 1) reduce the life time of each particle by one
152    # 2) create a list of particles, which's life time is 0 or below.
153    # 3) call createfunc() with the world passed to process() and
154    #    the list of dead particles
155    # 4) call updatefunc() with the world passed to process() and the
156    #    set of particles, which still are alive.
157    # 5) call deletefunc() with the world passed to process() and the
158    #    list of dead particles. deletefunc() is respsonible for
159    #    removing the dead particles from the world.
160    engine.createfunc = createparticles
161    engine.updatefunc = updateparticles
162    engine.deletefunc = deleteparticles
163    world.add_system(engine)
164
165    # We create all particles at once before starting the processing.
166    # We also could create them in chunks to have a visually more
167    # appealing effect, but let's keep it simple.
168    createparticles(world, None, 300)
169
170    # Initialize the video subsystem, create a window and make it visible.
171    sdl2.ext.init()
172    window = sdl2.ext.Window("Particles", size=(800, 600))
173    window.show()
174
175    # Create a hardware-accelerated sprite factory. The sprite factory requires
176    # a rendering context, which enables it to create the underlying textures
177    # that serve as the visual parts for the sprites.
178    renderer = sdl2.ext.Renderer(window)
179    factory = sdl2.ext.SpriteFactory(sdl2.ext.TEXTURE, renderer=renderer)
180
181    # Create a set of images to be used as particles on rendering. The
182    # images are used by the ParticleRenderer created below.
183    images = (factory.from_image(RESOURCES.get_path("circle.png")),
184              factory.from_image(RESOURCES.get_path("square.png")),
185              factory.from_image(RESOURCES.get_path("star.png"))
186              )
187
188    # Center the mouse on the window. We use the SDL2 functions directly
189    # here. Since the SDL2 functions do not know anything about the
190    # sdl2.ext.Window class, we have to pass the window's SDL_Window to it.
191    sdl2.SDL_WarpMouseInWindow(window.window, world.mousex, world.mousey)
192
193    # Hide the mouse cursor, so it does not show up - just show the
194    # particles.
195    sdl2.SDL_ShowCursor(0)
196
197    # Create the rendering system for the particles. This is somewhat
198    # similar to the SoftSpriteRenderSystem, but since we only operate with
199    # hundreds of particles (and not sprites with all their overhead),
200    # we need an own rendering system.
201    particlerenderer = ParticleRenderSystem(renderer, images)
202    world.add_system(particlerenderer)
203
204    # The almighty event loop. You already know several parts of it.
205    running = True
206    while running:
207        for event in sdl2.ext.get_events():
208            if event.type == sdl2.SDL_QUIT:
209                running = False
210                break
211
212            if event.type == sdl2.SDL_MOUSEMOTION:
213                # Take care of the mouse motions here. Every time the
214                # mouse is moved, we will make that information globally
215                # available to our application environment by updating
216                # the world attributes created earlier.
217                world.mousex = event.motion.x
218                world.mousey = event.motion.y
219                # We updated the mouse coordinates once, ditch all the
220                # other ones. Since world.process() might take several
221                # milliseconds, new motion events can occur on the event
222                # queue (10ths to 100ths!), and we do not want to handle
223                # each of them. For this example, it is enough to handle
224                # one per update cycle.
225                sdl2.SDL_FlushEvent(sdl2.SDL_MOUSEMOTION)
226                break
227        world.process()
228        sdl2.SDL_Delay(1)
229
230    sdl2.ext.quit()
231    return 0
232
233if __name__ == "__main__":
234    sys.exit(run())
235