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