So I have made a particle engine for smoke that I'm pretty happy with I think it fits my game pretty well.
I now need to implement it into my game and I'm having a little bit of trouble. I was wondering if someone could explain how I would go about using my particle engine in my game.
I added the both my codes(for smoke and my game file) below.
I want to keep the particles separate from the game files but call it in my game.
Eventually, I want to make more particle effects in the engine that I could also call.
Can someone help? It might need a little bit of tweaking to work.
PARTICLE CODE:
import pygame,random
from pygame.locals import *
xmax = 1000 #width of window
ymax = 600 #height of window
class Smoke():
def __init__(self, startx, starty, col):
self.x = startx
self.y = random.randint(0, starty)
self.col = col
self.sx = startx
self.sy = starty
def move(self):
if self.y < 0:
self.x = self.sx
self.y = self.sy
else:
self.y -= 1
self.x += random.randint(-1, 2)
def main():
pygame.init()
screen = pygame.display.set_mode((xmax,ymax))
black = (0,0,0)
grey = (145,145,145)
light_grey = (192,192,192)
dark_grey = (183, 183, 183)
clock = pygame.time.Clock()
particles = []
for part in range(600):
if part % 2 > 0: col = grey
#elif part % 5 > 0: col = dark_grey
elif part % 3 > 0: col = dark_grey
else: col = light_grey
particles.append( Smoke(0, 500, col) )
exitflag = False
while not exitflag:
for event in pygame.event.get():
if event.type == QUIT:
exitflag = True
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
exitflag = True
screen.fill(black)
for p in particles:
p.move()
pygame.draw.circle(screen, p.col, (p.x, p.y), 15)
pygame.display.flip()
clock.tick(80)
pygame.quit()
if __name__ == "__main__":
main()
GAME CODE
import pygame
from pygame import *
WIN_WIDTH = 1120 - 320
WIN_HEIGHT = 960 - 320
HALF_WIDTH = int(WIN_WIDTH / 2)
HALF_HEIGHT = int(WIN_HEIGHT / 2)
DISPLAY = (WIN_WIDTH, WIN_HEIGHT)
DEPTH = 0
FLAGS = 0
CAMERA_SLACK = 30
levels = {0: {'level': [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" E ",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPP PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPP P",
" PPPP P",
" PPPP PPPPPPP",
" PPPPPPPPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
"PPPPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPPP PPPP PPPPPPP",
"PPP PPPP",
"PPP PPPP",
"PPP PPPP",
"PPP PPPPPPPPPPPPPPPPPP",
"PPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",],
'enemies': [(9, 38)]},
1: {'level': [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" E ",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPP PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPP P",
" PPPP P",
" PPPP PPPPPPP",
" PPPPPPPPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
"PPPPP PPPP PPPPPPP",
"PPP PPPPPPPPPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPPPPPP PPPP PPPPPPP",
"PPP PPPP",
"PPP PPPP",
"PPP PPPPP PPPP",
"PPP P PPPPPPPPPPPPPPPPPP",
"PPP P PPPPPPPPPPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",],
'enemies': [(9, 38), (18, 38), (15, 15)]}}
class Scene(object):
def __init__(self):
pass
def render(self, screen):
raise NotImplementedError
def update(self):
raise NotImplementedError
def handle_events(self, events):
raise NotImplementedError
class GameScene(Scene):
def __init__(self, levelno):
super(GameScene, self).__init__()
self.bg = Surface((32,32))
self.bg.convert()
self.bg.fill(Color("#0094FF"))
up = left = right = False
self.entities = pygame.sprite.Group()
self.player = Player(5, 40)
self.player.scene = self
self.platforms = []
self.levelno = levelno
levelinfo = levels[levelno]
self.enemies = [Enemy(*pos) for pos in levelinfo['enemies']]
level = levelinfo['level']
total_level_width = len(level[0]) * 32
total_level_height = len(level) * 32
# build the level
x = 0
y = 0
for row in level:
for col in row:
if col == "P":
p = Platform(x, y)
self.platforms.append(p)
self.entities.add(p)
if col == "E":
e = ExitBlock(x, y)
self.platforms.append(e)
self.entities.add(e)
x += 32
y += 32
x = 0
self.camera = Camera(complex_camera, total_level_width, total_level_height)
self.entities.add(self.player)
for e in self.enemies:
self.entities.add(e)
def render(self, screen):
for y in range(20):
for x in range(25):
screen.blit(self.bg, (x * 32, y * 32))
for e in self.entities:
screen.blit(e.image, self.camera.apply(e))
def update(self):
pressed = pygame.key.get_pressed()
up, left, right = [pressed[key] for key in (K_UP, K_LEFT, K_RIGHT)]
self.player.update(up, left, right, self.platforms)
for e in self.enemies:
e.update(self.platforms)
self.camera.update(self.player)
def exit(self):
if self.levelno+1 in levels:
self.manager.go_to(GameScene(self.levelno+1))
else:
self.manager.go_to(CustomScene("You win!"))
def die(self):
self.manager.go_to(CustomScene("You lose!"))
def handle_events(self, events):
for e in events:
if e.type == KEYDOWN and e.key == K_ESCAPE:
self.manager.go_to(TitleScene())
class CustomScene(object):
def __init__(self, text):
self.text = text
super(CustomScene, self).__init__()
self.font = pygame.font.SysFont('Arial', 56)
def render(self, screen):
# ugly!
screen.fill((0, 200, 0))
text1 = self.font.render(self.text, True, (255, 255, 255))
screen.blit(text1, (200, 50))
def update(self):
pass
def handle_events(self, events):
for e in events:
if e.type == KEYDOWN:
self.manager.go_to(TitleScene())
class TitleScene(object):
def __init__(self):
super(TitleScene, self).__init__()
self.font = pygame.font.SysFont('Arial', 56)
self.sfont = pygame.font.SysFont('Arial', 32)
def render(self, screen):
# ugly!
screen.fill((0, 200, 0))
text1 = self.font.render('Crazy Game', True, (255, 255, 255))
text2 = self.sfont.render('> press space to start <', True, (255, 255, 255))
screen.blit(text1, (200, 50))
screen.blit(text2, (200, 350))
def update(self):
pass
def handle_events(self, events):
for e in events:
if e.type == KEYDOWN and e.key == K_SPACE:
self.manager.go_to(GameScene(0))
class SceneMananger(object):
def __init__(self):
self.go_to(TitleScene())
def go_to(self, scene):
self.scene = scene
self.scene.manager = self
def main():
pygame.init()
screen = pygame.display.set_mode(DISPLAY, FLAGS, DEPTH)
pygame.display.set_caption("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
timer = pygame.time.Clock()
running = True
manager = SceneMananger()
while running:
timer.tick(60)
if pygame.event.get(QUIT):
running = False
return
manager.scene.handle_events(pygame.event.get())
manager.scene.update()
manager.scene.render(screen)
pygame.display.flip()
class Camera(object):
def __init__(self, camera_func, width, height):
self.camera_func = camera_func
self.state = Rect(0, 0, width, height)
def apply(self, target):
try:
return target.rect.move(self.state.topleft)
except AttributeError:
return map(sum, zip(target, self.state.topleft))
def update(self, target):
self.state = self.camera_func(self.state, target.rect)
def complex_camera(camera, target_rect):
l, t, _, _ = target_rect
_, _, w, h = camera
l, t, _, _ = -l + HALF_WIDTH, -t +HALF_HEIGHT, w, h
l = min(0, l) # stop scrolling left
l = max(-(camera.width - WIN_WIDTH), l) # stop scrolling right
t = max(-(camera.height-WIN_HEIGHT), t) # stop scrolling bottom
return Rect(l, t, w, h)
class Entity(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
class Player(Entity):
def __init__(self, x, y):
Entity.__init__(self)
self.xvel = 0
self.yvel = 0
self.onGround = False
self.image = Surface((32,32))
self.image.fill(Color("#0000FF"))
self.image.convert()
self.rect = Rect(x*32, y*32, 32, 32)
def update(self, up, left, right, platforms):
if self.rect.top > 1440 or self.rect.top < 0:
self.scene.die()
if self.rect.left > 1408 or self.rect.right < 0:
self.scene.die()
if up:
if self.onGround:
self.yvel = 0
self.yvel -= 10 # only jump if on the ground
if left:
self.xvel = -10
if right:
self.xvel = 10
if not self.onGround:
self.yvel += 0.3 # only accelerate with gravity if in the air
if self.yvel > 80: self.yvel = 80 # max falling speed
if not(left or right):
self.xvel = 0
self.rect.left += self.xvel # increment in x direction
if self.collide(self.xvel, 0, platforms): # do x-axis collisions
self.rect.top += self.yvel # increment in y direction
self.onGround = False; # assuming we're in the air
self.collide(0, self.yvel, platforms) # do y-axis collisions
def collide(self, xvel, yvel, platforms):
for p in platforms:
if pygame.sprite.collide_rect(self, p):
if isinstance(p, ExitBlock):
self.scene.exit()
return False
if xvel > 0: self.rect.right = p.rect.left
if xvel < 0: self.rect.left = p.rect.right
if yvel > 0:
self.rect.bottom = p.rect.top
self.onGround = True
if yvel < 0:
self.rect.top = p.rect.bottom
return True
class Enemy(Entity):
def __init__(self, x, y):
Entity.__init__(self)
self.yVel = 0
self.xVel = 2 # start moving immediately
self.image = Surface((32,32))
self.image.fill(Color("#00FF00"))
self.image.convert()
self.rect = Rect(x*32, y*32, 32, 32)
self.onGround = False
def update(self, platforms):
if not self.onGround:
self.yVel += 0.3
# no need for right_dis to be a member of the class,
# since we know we are moving right if self.xVel > 0
right_dis = self.xVel > 0
# create a point at our left (or right) feet
# to check if we reached the end of the platform
m = (1, 1) if right_dis else (-1, 1)
p = self.rect.bottomright if right_dis else self.rect.bottomleft
fp = map(sum, zip(m, p))
# if there's no platform in front of us, change the direction
collide = any(p for p in platforms if p.rect.collidepoint(fp))
if not collide:
self.xVel *= -1
self.rect.left += self.xVel # increment in x direction
self.collide(self.xVel, 0, platforms) # do x-axis collisions
self.rect.top += self.yVel # increment in y direction
self.onGround = False; # assuming we're in the air
self.collide(0, self.yVel, platforms) # do y-axis collisions
def collide(self, xVel, yVel, platforms):
for p in platforms:
if pygame.sprite.collide_rect(self, p):
if xVel > 0:
self.rect.right = p.rect.left
self.xVel *= -1 # hit wall, so change direction
if xVel < 0:
self.rect.left = p.rect.right
self.xVel *= -1 # hit wall, so change direction
if yVel > 0:
self.rect.bottom = p.rect.top
self.onGround = True
if yVel < 0:
self.rect.top = p.rect.bottom
class Platform(Entity):
def __init__(self, x, y):
Entity.__init__(self)
#self.image = Surface([32, 32], pygame.SRCALPHA, 32) #makes blocks invisible for much better artwork
self.image = Surface((32,32)) #makes blocks visible for building levels
self.image.convert()
self.rect = Rect(x, y, 32, 32)
def update(self):
pass
class ExitBlock(Platform):
def __init__(self, x, y):
Platform.__init__(self, x, y)
self.image = Surface((32,32)) #makes blocks visible for building levels
self.image.convert()
self.rect = Rect(x, y, 32, 32)
if __name__ == "__main__":
main()
Beware! Long post!
First, let's take a look at your Smoke
class. It contains some of the smoke behaviour, but also your main loop does. Let's fix that by creating a generic Particle
class that does nothing but represent a particle:
class Particle():
def __init__(self, col, size, *strategies):
self.x, self.y = 0, 0
self.col = col
self.alive = 0
self.strategies = strategies
self.size = size
def kill(self):
self.alive = -1 # alive -1 means dead
def move(self):
for s in self.strategies:
s(self)
This class does not do much. It is generic in a way that all it's behaviour (simple functions) is passed to its __init__
function, and the particle applies these functions to itself in the move
method.
Now that our particle class is flexible, let's think about how particles should behave so that a bunch of it look like smoke.
A smoke particle should ascend, so let's create a function that moves the particle upwards:
def ascending(speed):
def _ascending(particle):
particle.y -= speed
return _ascending
A smoke particle should die at some point, so let's write a function that kills at a certain condition:
def kill_at(max_x, max_y):
def _kill_at(particle):
if particle.x < -max_x or particle.x > max_x or particle.y < -max_y or particle.y > max_y:
particle.kill()
return _kill_at
We need to keep track how long a particle is alive (comes in handy later), so we need a function that let a particle age:
def age(amount):
def _age(particle):
particle.alive += amount
return _age
When the smoke ascends, it should not do it in a straight line (how boring!), but it should fan out:
def fan_out(modifier):
def _fan_out(particle):
d = particle.alive / modifier
d += 1
particle.x += random.randint(-d, d)
return _fan_out
Nice! Now our smoke can fan out, but it's still a little boring, so let's write a function that simulates a little wind:
def wind(direction, strength):
def _wind(particle):
if random.randint(0,100) < strength:
particle.x += direction
return _wind
We have now a bunch of functions that describe the bahaviour of a particle. All of them are small and self-contained. You can create an infinity amount of them and combine them as you like to create different particles.
Time to create some particles actually: Enter the smoke machine!
def smoke_machine():
colors = {0: grey,
1: dark_grey,
2: light_grey}
def create():
for _ in xrange(random.choice([0,0,0,0,0,0,0,1,2,3])):
behaviour = age(1), ascending(1), fan_out(400), wind(1, 15), kill_at(1000, 1000)
p = Particle(colors[random.randint(0, 2)], random.randint(10, 15), *behaviour)
yield p
while True:
yield create()
So what the hell is this? Easy. It's a generator that constantly emits new particles. Every time someone wants to retrieve the next item out if it, it calls its nested create
function. That function in turn returns 0 to 3 particles, based on the input list to random.choice
. It's a nice way to say that it returns 0 particles with a 70% chance, and 1, 2 or 3 particles with a 10% chance each.
In the next line we define the behaviour of the particle. It's just a tuple of functions. Note how each function call returns its nested function in return. This way these functions are parameterized.
The last step is to randomly assign a color and a size to the particle.
You can test it from the command line to see how it works:
>>> s=smoke_machine()
>>> list(next(s))
[]
>>> list(next(s))
[<particle.Particle instance at 0x02AD94B8>, <particle.Particle instance at 0x02
AD9030>]
>>> list(next(s))
[]
>>> list(next(s))
[]
>>> list(next(s))
[<particle.Particle instance at 0x02AD9030>]
>>> list(next(s))
[<particle.Particle instance at 0x02AD9418>, <particle.Particle instance at 0x02
AD93C8>]
>>> list(next(s))
[<particle.Particle instance at 0x02AD9030>]
See how every time we call next on the generator, it returns up to 3 particles.
How do we combine our smoke machine with our game? Let's write a class that handles it for us:
class Emitter(object):
def __init__(self, pos=(0, 0)):
self.particles = []
self.pos = pos
self.factories = []
def add_factory(self, factory, pre_fill=300):
self.factories.append(factory)
tmp = []
for _ in xrange(pre_fill):
n = next(factory)
tmp.extend(n)
for p in tmp:
p.move()
self.particles.extend(tmp)
def update(self):
for f in self.factories:
self.particles.extend(next(f))
for p in self.particles[:]:
p.move()
if p.alive == -1:
self.particles.remove(p)
def draw(self, screen, position_translater_func):
for p in self.particles:
target_pos = position_translater_func(map(sum, zip((p.x, p.y), self.pos)))
pygame.draw.circle(screen, p.col, target_pos, int(p.size))
The emitter can hold a bunch of factory functions (like our smoke_machine
), can every time it's updated, it adds the particles of theses factories to its self.particles
so it can draw them to the screen. Let's look at some functions in detail:
If we add a new factory with add_factory
, we call that factory
and move its particles 300 (or whatever pre_fill
is) times in advance. This way, some particles are already there.
If we want to draw the particles, we have to calculate thier positions using the position of the Emitter
. Also, we need to adjust this position using the camera of the game, so we just accept a function as parameter that translates our position to the correct final position so that our particles scroll accordingly within the game.
We don't need big changes to the game class to use the Emitter
now. We just create a new list in the GameScene
called self.emitter
, and an Emitter
and add the smoke_machine
factory to it.
In the render
method we call
for e in self.emitter:
e.draw(screen, self.camera.apply)
and in the update
method we call
for e in self.emitter:
e.update()
and we are done!
omitted some classes because I hit the 30000 character limit :-)
import pygame
from pygame import *
from particle import Emitter, smoke_machine
WIN_WIDTH = 1120 - 320
WIN_HEIGHT = 960 - 320
HALF_WIDTH = int(WIN_WIDTH / 2)
HALF_HEIGHT = int(WIN_HEIGHT / 2)
DISPLAY = (WIN_WIDTH, WIN_HEIGHT)
DEPTH = 0
FLAGS = 0
CAMERA_SLACK = 30
levels = {0: {'level': [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" E ",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPP PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPP P",
" PPPP P",
" PPPP PPPPPPP",
" PPPPPPPPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
"PPPPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPPP PPPP PPPPPPP",
"PPP PPPP",
"PPP PPPP",
"PPP PPPP",
"PPP PPPPPPPPPPPPPPPPPP",
"PPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP S PPPPPPPPPPPPPPPPPP",],
'enemies': [(9, 38)]},
1: {'level': [
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" ",
" E ",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPPP PPPPPPPPPPPPPPPP",
" PPPPPPPPPPPPPPPP",
" PPPP P",
" PPPP P",
" PPPP PPPPPPP",
" PPPPPPPPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
" PPPP PPPPPPP",
"PPPPP PPPP PPPPPPP",
"PPP PPPPPPPPPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPP PPPPPPP",
"PPP PPPPPPPP PPPP PPPPPPP",
"PPP PPPP",
"PPP PPPP",
"PPP PPPPP PPPP",
"PPP P PPPPPPPPPPPPPPPPPP",
"PPP P PPPPPPPPPPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",
"PPPPPPPPPPPPPPP PPPPPPPPPPPPPPPPPP",],
'enemies': [(9, 38), (18, 38), (15, 15)]}}
...
class GameScene(Scene):
def __init__(self, levelno):
super(GameScene, self).__init__()
self.bg = Surface((32,32))
self.bg.convert()
self.bg.fill(Color("#0094FF"))
up = left = right = False
self.entities = pygame.sprite.Group()
self.player = Player(5, 40)
self.player.scene = self
self.platforms = []
self.levelno = levelno
levelinfo = levels[levelno]
self.enemies = [Enemy(*pos) for pos in levelinfo['enemies']]
level = levelinfo['level']
total_level_width = len(level[0]) * 32
total_level_height = len(level) * 32
self.emitter = []
# build the level
x = 0
y = 0
for row in level:
for col in row:
if col == "P":
p = Platform(x, y)
self.platforms.append(p)
self.entities.add(p)
if col == "E":
e = ExitBlock(x, y)
self.platforms.append(e)
self.entities.add(e)
if col == "S":
e = Emitter((x, total_level_height))
e.add_factory(smoke_machine())
self.emitter.append(e)
x += 32
y += 32
x = 0
self.camera = Camera(complex_camera, total_level_width, total_level_height)
self.entities.add(self.player)
for e in self.enemies:
self.entities.add(e)
def render(self, screen):
for y in range(20):
for x in range(25):
screen.blit(self.bg, (x * 32, y * 32))
for e in self.emitter:
e.draw(screen, self.camera.apply)
for e in self.entities:
screen.blit(e.image, self.camera.apply(e))
def update(self):
for e in self.emitter:
e.update()
pressed = pygame.key.get_pressed()
up, left, right = [pressed[key] for key in (K_UP, K_LEFT, K_RIGHT)]
self.player.update(up, left, right, self.platforms)
for e in self.enemies:
e.update(self.platforms)
self.camera.update(self.player)
def exit(self):
if self.levelno+1 in levels:
self.manager.go_to(GameScene(self.levelno+1))
else:
self.manager.go_to(CustomScene("You win!"))
def die(self):
self.manager.go_to(CustomScene("You lose!"))
def handle_events(self, events):
for e in events:
if e.type == KEYDOWN and e.key == K_ESCAPE:
self.manager.go_to(TitleScene())
...
if __name__ == "__main__":
main()
import pygame,random
def ascending(speed):
def _ascending(particle):
particle.y -= speed
return _ascending
def kill_at(max_x, max_y):
def _kill_at(particle):
if particle.x < -max_x or particle.x > max_x or particle.y < -max_y or particle.y > max_y:
particle.kill()
return _kill_at
def age(amount):
def _age(particle):
particle.alive += amount
return _age
def fan_out(modifier):
def _fan_out(particle):
d = particle.alive / modifier
d += 1
particle.x += random.randint(-d, d)
return _fan_out
def wind(direction, strength):
def _wind(particle):
if random.randint(0,100) < strength:
particle.x += direction
return _wind
class Particle():
def __init__(self, col, size, *strategies):
self.x, self.y = 0, 0
self.col = col
self.alive = 0
self.strategies = strategies
self.size = size
def kill(self):
self.alive = -1 # alive -1 means dead
def move(self):
for s in self.strategies:
s(self)
black = (0,0,0)
grey = (145,145,145)
light_grey = (192,192,192)
dark_grey = (183, 183, 183)
def smoke_machine():
colors = {0: grey,
1: dark_grey,
2: light_grey}
def create():
for _ in xrange(random.choice([0,0,0,0,0,0,0,1,2,3])):
behaviour = ascending(1), kill_at(1000, 1000), fan_out(400), wind(1, 15), age(1)
p = Particle(colors[random.randint(0, 2)], random.randint(10, 15), *behaviour)
yield p
while True:
yield create()
class Emitter(object):
def __init__(self, pos=(0, 0)):
self.particles = []
self.pos = pos
self.factories = []
def add_factory(self, factory, pre_fill=300):
self.factories.append(factory)
tmp = []
for _ in xrange(pre_fill):
n = next(factory)
tmp.extend(n)
for p in tmp:
p.move()
self.particles.extend(tmp)
def update(self):
for f in self.factories:
self.particles.extend(next(f))
for p in self.particles[:]:
p.move()
if p.alive == -1:
self.particles.remove(p)
def draw(self, screen, position_translater_func):
for p in self.particles:
target_pos = position_translater_func(map(sum, zip((p.x, p.y), self.pos)))
pygame.draw.circle(screen, p.col, target_pos, int(p.size))
We have successfully integrated the particles in the game. We have done this by writting small, self-contained functions that describe different behavioral patterns, and composed them by using a factory function.
And while doing this, we learned about closures, generators, the SRP, the factory- and the strategy pattern.
We could now easily add new behaviours, e.g. changing colors, or create new particle factories that behave totally different, but use our exiting functions.
For example, try using this function that will let the particles grow
def grow(amount):
def _grow(particle):
if random.randint(0,100) < particle.alive / 20:
particle.size += amount
return _grow
by adding grow(0.5)
to the list of behaviours in the smoke_machine
Small change, impressive effect.
What a nice day!
P.S.: You can find a faster version (and bugfixes) at this repository. It uses numpy
, itertools
, psyco
and pygame.surfarray
and eschews the random
module for massive speed improvement.