在 pygame 中有没有办法优化具有透明度的表面?

Is there a way to optimize surfaces with transparency in pygame?

我正在尝试用 pygame 中的粒子进行爆炸,但速度非常非常慢。代码中最慢的部分之一是创建具有所需颜色和透明度的表面的部分。为了清楚起见,我在这里发布了整个代码,但该函数称为 makeSurface。它在 SmokeParticleSpark class.

import pygame
import math
import random

##draw functions##

def drawSpark(window, spark):
    if not spark.dead(): 
        window.blit(spark.surface, spark.position)

def drawParticle(window, particle):
    window.blit(particle.surface, particle.position)

def drawCluster(window, cluster):
    for particle in cluster.particles:
        drawParticle(window, particle)

def drawSmoke(window, smoke):
    for cluster in smoke.clusters:
        drawCluster(window, cluster)

def drawExplosions(window, em):
    for s in em.smoke:
        drawSmoke(window, s)
    for spark in em.sparks:
        drawSpark(window, spark)
    
##other functions##

#thank you rabbid76
def randomDistBiasedMiddle(_min, _max):
    r = lambda : random.uniform(-1, 1)
    r1, r2 = r(), r()
    bias = lambda _r:  _r**3 * (_max - _min) / 2 + (_min + _max) / 2
    return (bias(r1), bias(r2))

##########################################

class Spark:
    def __init__(self, travel, position, rotation, scale):
        self.position = list(position)
        self.rotation = rotation
        self.goTowards = (math.sin(rotation), math.cos(rotation))
        self.scale = scale
        self.speed = random.randint(70, 130) * scale
        self.maxTravel = travel * self.scale * 2
        self.maxAlpha = 150
        self.minAlpha = 0
        self.randomColor = random.choice((
                (188, 98, 5),
                (255, 215, 0),
                (255, 127, 80),
                (255, 140, 0)))
        self.travelled = 0
        self.makeSurface()

    def shoot(self):
        if self.travelled < self.maxTravel:
            self.travelled += 5 #could use actual distance but performance :( . 5 seems to be a good fit tho
            self.position[0] += self.goTowards[0] * (3/self.travelled) * self.speed
            self.position[1] += self.goTowards[1] * (3/self.travelled) * self.speed

    def makeSurface(self):
        self.alpha = ((1 - (self.travelled / self.maxTravel)) * (self.maxAlpha - self.minAlpha)) + self.minAlpha
        self.color = (*self.randomColor, self.alpha)
        if self.alpha > 0:
            self.surface = pygame.Surface((3, 15), pygame.SRCALPHA)
            self.surface.fill(self.color)
            self.surface = pygame.transform.rotate(self.surface, math.degrees(self.rotation))

    def dead(self):
        return self.alpha < 1

class SmokeParticle:
    def __init__(self, position, distFromcentre, explosionRange, centre):
        self.position = list(position)
        self.distFromCentre = distFromcentre #distance from centre of explosion, not the cluster
        self.possibleMoves = (
            (-1, -1), (0, -1), (1, -1), (1, 0),
            (1, 1), (0, 1), (-1, 1), (-1, 0)
            )

        self.blowSpeed = 2
        self.blowDir = (self.position[0] - centre[0], self.position[1] - centre[1])
        self.blowDir = (self.blowDir[0] / distFromcentre, self.blowDir[1] / distFromcentre)
        self.blowDistance = math.hypot(*self.blowDir)
        
        self.explosionRange = explosionRange
        self.maxAlpha = 200
        self.minAlpha = 80
        self.cf = (255, 140, 0)#smoke color farthest from the explosion centre
        self.cn = (20, 20, 20)#smoke color closest to the explosion centre
        self.makeSurface()

    def makeSurface(self):
        self.alpha = ((1 - (self.distFromCentre / self.explosionRange)) * (self.maxAlpha - self.minAlpha)) + self.minAlpha
        findColor = lambda c : max(0, min(((1 - (self.distFromCentre / self.explosionRange)) * (self.cf[c] - self.cn[c])) + self.cn[c], 255))
        self.color = (findColor(0), findColor(1), findColor(2), self.alpha)
        self.surface = pygame.Surface((8, 8), pygame.SRCALPHA)
        if self.alpha > 0:
            self.surface.fill(self.color)
        
    def blowAway(self, dt):
        move = random.choice(self.possibleMoves)
        self.position[0] += move[0]
        self.position[1] += move[1]
        self.position[0] += self.blowDir[0] * self.blowSpeed 
        self.position[1] += self.blowDir[1] * self.blowSpeed 
        self.distFromCentre += self.blowDistance * self.blowSpeed 

    def dead(self):
        return self.distFromCentre > self.explosionRange
        
class SmokeCluster:
    def __init__(self, position, explosionRange, distFromCentre, centre):
        self.particles = []
        self.explosionRange = explosionRange
        self.radius = 5
        self.nParticels = 10
        self.position = position
        self.distFromCentre = distFromCentre
        
        for i in range(self.nParticels):
            randomPos = randomDistBiasedMiddle(-self.radius, self.radius)
            pos = (self.position[0] + randomPos[0], self.position[1] + randomPos[1])
            self.particles.append(SmokeParticle(pos, self.distFromCentre, self.explosionRange, centre))

    def do(self, dt):
        for particle in self.particles:
            particle.blowAway(dt)
            particle.makeSurface()

    def removeDeadParticles(self):
        self.particles = [particle for particle in self.particles if not particle.dead()]

    def dead(self):
        return not self.particles

class SmokeMass:
    def __init__(self, explosionRange, mousePos):
        self.clusters = []
        self.explosionRange = explosionRange 
        self.nClusters = int(200 * (explosionRange * 0.1))
        for i in range(self.nClusters):
            randomPos = randomDistBiasedMiddle(-self.explosionRange, self.explosionRange)
            clusterPos = (randomPos[0] + mousePos[0], randomPos[1] + mousePos[1])
            self.clusters.append(SmokeCluster(clusterPos, self.explosionRange, math.dist(clusterPos, mousePos), mousePos))
            
    def do(self, dt):
        for cluster in self.clusters:
             cluster.do(dt)

    def removeDeadClusters(self):
        for cluster in self.clusters:
            cluster.removeDeadParticles()
        self.clusters = [cluster for cluster in self.clusters if not cluster.dead()]
        
    
class ExplosionManager:
    def __init__(self, scale):
        self.sparks = []
        self.smoke = []
        self.scale = scale
        self.cloudPatches = []
        self.maxSpread = 100
        self.removeSparkles = pygame.USEREVENT + 0
        pygame.time.set_timer(self.removeSparkles, 2500)
        
    def generateSparks(self, mousePos):
        for i in range(int(self.scale * 20)):
            rotation = random.uniform(0, 2 * math.pi)
            self.sparks.append(Spark(self.maxSpread, mousePos, rotation, self.scale))

    def generateSmoke(self, mousePos):
        self.smoke.append(SmokeMass(200 * self.scale, mousePos))

    def triggered(self, events):
        for event in events:
            if event.type == pygame.MOUSEBUTTONDOWN:
                return True

    def explode(self, dt):
        for spark in self.sparks:
            spark.shoot()
            spark.makeSurface()

        for s in self.smoke:
            s.do(dt)

    def removeDeadSparks(self, events):
        for event in events:
            if event.type == self.removeSparkles:
                self.sparks = [spark for spark in self.sparks if not spark.dead()]

    def removeDeadSmoke(self):
        for s in self.smoke:
            s.removeDeadClusters()
        

pygame.init()
winSize = (600, 600)
window = pygame.display.set_mode(winSize)
clock = pygame.time.Clock()
fps = 100

explosionScale = 1
explosionScale = min(max(explosionScale, 0), 1)#because i never know what I will do
em = ExplosionManager(explosionScale)


while True:
    events = pygame.event.get()
    mousePos = pygame.mouse.get_pos()
    mousePressed = pygame.mouse.get_pressed()
    dt = clock.tick(fps) * 0.001
    pygame.display.set_caption(f"FPS: {clock.get_fps()}")
    gen = False
    for event in events:
        if event.type == pygame.QUIT:
            pygame.quit()
            raise SystemExit

    if em.triggered(events):
        em.generateSparks(mousePos)
        em.generateSmoke(mousePos)

    em.explode(dt)
    em.removeDeadSparks(events)
    em.removeDeadSmoke()

    window.fill((40, 50, 50))
    drawExplosions(window, em)
    pygame.display.flip()

一个明显的改进是不在每次移动时为每个粒子创建一个表面。仅创建一次 Surface 并在颜色变化时用新颜色填充它

class SmokeParticle:
     def __init__(self, position, distFromcentre, explosionRange, centre):
        # [...]

        self.surface = pygame.Surface((8, 8), pygame.SRCALPHA)
        self.color = None
        self.makeSurface()

    def makeSurface(self):
        self.alpha = ((1 - (self.distFromCentre / self.explosionRange)) * (self.maxAlpha - self.minAlpha)) + self.minAlpha
        findColor = lambda c : max(0, min(((1 - (self.distFromCentre / self.explosionRange)) * (self.cf[c] - self.cn[c])) + self.cn[c], 255))
        color = (findColor(0), findColor(1), findColor(2), self.alpha)
        if self.alpha > 0 and self.color != color:
            self.color = color
            self.surface.fill(self.color)

这仍然不足以提高性能,但会更好。