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()
1 个解决方案
#1
14
Beware! Long post!
谨防!长帖!
The particle
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:
首先,让我们来看看你的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)
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.
这堂课没什么用。它是通用的,它将所有行为(简单函数)传递给它的__init__函数,并且粒子在移动方法中将这些函数应用于自身。
The behaviour of particles
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.
我们现在有一堆描述粒子行为的函数。所有这些都很小而且独立。您可以创建无限量的它们并根据需要组合它们以创建不同的粒子。
The smoke
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.
那到底是怎么回事?简单。它是一个不断发射新粒子的发生器。每当有人想要检索下一个项目时,它就会调用它的嵌套创建函数。该函数依次返回0到3个粒子,基于random.choice的输入列表。这是一个很好的方式,它说它返回0粒子,有70%的几率,1或2或3粒子,每粒粒子有10%的几率。
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.
看看每次我们在生成器上调用next时,它最多返回3个粒子。
Putting it all together
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:
发射器可以容纳一堆工厂函数(比如我们的smoke_machine),每次更新它时,它都会将这些工厂的粒子添加到它的self.particles中,这样它就可以将它们绘制到屏幕上。让我们详细看一些函数:
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.
如果我们使用add_factory添加一个新工厂,我们会调用该工厂并提前移动其粒子300(或任何pre_fill)。这样,一些粒子已经存在。
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.
我们现在不需要对游戏类进行大的改动就可以使用Emitter。我们只需在GameScene中创建一个名为self.emitter的新列表,然后在Emitter中添加smoke_machine工厂。
In the render
method we call
在我们调用的render方法中
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!
我们完成了!
Action !
complete listing:
game.py
omitted some classes because I hit the 30000 character limit :-)
省略了一些类因为我达到30000字符限制:-)
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))
Conclusion
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.
在这样做的同时,我们了解了闭包,发电机,SRP,工厂和战略模式。
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
通过将grow(0.5)添加到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.
P.S。:您可以在此存储库中找到更快的版本(和错误修正)。它使用numpy,itertools,psyco和pygame.surfarray并避开随机模块以提高速度。
#1
14
Beware! Long post!
谨防!长帖!
The particle
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:
首先,让我们来看看你的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)
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.
这堂课没什么用。它是通用的,它将所有行为(简单函数)传递给它的__init__函数,并且粒子在移动方法中将这些函数应用于自身。
The behaviour of particles
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.
我们现在有一堆描述粒子行为的函数。所有这些都很小而且独立。您可以创建无限量的它们并根据需要组合它们以创建不同的粒子。
The smoke
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.
那到底是怎么回事?简单。它是一个不断发射新粒子的发生器。每当有人想要检索下一个项目时,它就会调用它的嵌套创建函数。该函数依次返回0到3个粒子,基于random.choice的输入列表。这是一个很好的方式,它说它返回0粒子,有70%的几率,1或2或3粒子,每粒粒子有10%的几率。
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.
看看每次我们在生成器上调用next时,它最多返回3个粒子。
Putting it all together
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:
发射器可以容纳一堆工厂函数(比如我们的smoke_machine),每次更新它时,它都会将这些工厂的粒子添加到它的self.particles中,这样它就可以将它们绘制到屏幕上。让我们详细看一些函数:
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.
如果我们使用add_factory添加一个新工厂,我们会调用该工厂并提前移动其粒子300(或任何pre_fill)。这样,一些粒子已经存在。
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.
我们现在不需要对游戏类进行大的改动就可以使用Emitter。我们只需在GameScene中创建一个名为self.emitter的新列表,然后在Emitter中添加smoke_machine工厂。
In the render
method we call
在我们调用的render方法中
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!
我们完成了!
Action !
complete listing:
game.py
omitted some classes because I hit the 30000 character limit :-)
省略了一些类因为我达到30000字符限制:-)
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))
Conclusion
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.
在这样做的同时,我们了解了闭包,发电机,SRP,工厂和战略模式。
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
通过将grow(0.5)添加到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.
P.S。:您可以在此存储库中找到更快的版本(和错误修正)。它使用numpy,itertools,psyco和pygame.surfarray并避开随机模块以提高速度。