如何实现粒子引擎

5 投票
1 回答
10866 浏览
提问于 2025-04-17 15:56

我做了一个烟雾粒子引擎,感觉挺不错的,跟我的游戏很搭。
现在我需要把它放进我的游戏里,但遇到了一些麻烦。我想知道怎么才能在我的游戏中使用这个粒子引擎。
我把烟雾的代码和我的游戏文件的代码都放在下面了。
我想把粒子效果和游戏文件分开,但又能在游戏中调用它。
最终,我还想在这个引擎里做更多的粒子效果,也能在游戏中使用。

有人能帮忙吗?可能需要稍微调整一下才能正常工作。

粒子代码:

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()

游戏代码:

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()

1 个回答

15

注意!这是一篇长文章!

粒子

首先,我们来看看你的 Smoke 类。它包含了一些 烟雾 的行为,但你的主循环也有一些。我们来解决这个问题,创建一个通用的 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)   

这个类的功能不多。它的通用性在于,所有的行为(简单的函数)都通过它的 __init__ 函数传入,粒子在 move 方法中应用这些函数。

粒子的行为

现在我们的粒子类很灵活,我们来想想粒子应该如何表现,才能让一堆粒子看起来像烟雾。

烟雾粒子应该向上升起,所以我们来写一个函数,让粒子向上移动:

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

现在我们有了一堆描述粒子行为的函数。它们都很小且自成一体。你可以创建无限数量的粒子,并随意组合它们,创造出不同的粒子。

烟雾

现在是时候真正创建一些粒子了:进入烟雾机!

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()

这到底是什么呢?简单来说,这是一个生成器,它不断发出新的粒子。每当有人想要获取下一个项目时,它就会调用内部的 create 函数。这个函数根据输入列表给 random.choice 返回 0 到 3 个粒子。换句话说,它有 70% 的概率返回 0 个粒子,10% 的概率返回 1、2 或 3 个粒子。

在下一行,我们定义粒子的行为。它只是一个函数的元组。注意每个函数调用都会返回它内部的函数。这样,这些函数就可以被参数化。

最后一步是随机给粒子分配一个颜色和大小。

你可以从命令行测试一下,看看它是如何工作的:

>>> 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>]

每次我们在生成器上调用 next 时,它都会返回最多 3 个粒子。

把一切结合起来

我们如何将烟雾机与游戏结合起来呢?让我们写一个类来处理这个问题:

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))

发射器可以保存一堆工厂函数(就像我们的 smoke_machine),每次更新时,它会将这些工厂的粒子添加到 self.particles 中,以便在屏幕上绘制它们。让我们详细看看一些函数:

如果我们用 add_factory 添加一个新的工厂,我们会调用这个 factory,并提前移动它的粒子 300 次(或其他 pre_fill 的值)。这样,一些粒子就已经存在了。

如果我们想绘制粒子,我们需要根据 Emitter 的位置来计算它们的位置。同时,我们需要使用游戏的相机来调整这个位置,因此我们接受一个函数作为参数,将我们的位置信息转换为正确的最终位置,以便我们的粒子在游戏中滚动。

现在我们不需要对游戏类做大改动就能使用 Emitter 了。我们只需在 GameScene 中创建一个新的列表,叫 self.emitter,然后创建一个 Emitter 并将 smoke_machine 工厂添加进去。

render 方法中,我们调用

for e in self.emitter:
    e.draw(screen, self.camera.apply)

update 方法中,我们调用

for e in self.emitter:
   e.update()

这样就完成了!

行动!

这里插入图片描述

完整代码:

game.py

因为字符限制省略了一些类 :-)

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()

particle.py

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))

总结

我们成功地将粒子整合进了游戏。我们通过编写小而独立的函数来描述不同的行为模式,并使用工厂函数将它们组合在一起。

在这个过程中,我们了解了 闭包生成器单一职责原则工厂模式策略模式

现在我们可以轻松地添加新的行为,比如改变颜色,或者创建新的粒子工厂,行为完全不同,但仍然可以使用我们现有的函数。

例如,尝试使用这个函数,让粒子生长:

def grow(amount):
    def _grow(particle):
        if random.randint(0,100) < particle.alive / 20:
            particle.size += amount
    return _grow

通过将 grow(0.5) 添加到 smoke_machine 的行为列表中

这里插入图片描述

小改动,效果显著。

真是美好的一天!

附言:你可以在 这个仓库 找到一个更快的版本(以及修复的bug)。它使用了 numpyitertoolspsycopygame.surfarray,并且为了大幅提升速度,避免使用 random 模块。

撰写回答