Cool_Cornflakes
Cool_Cornflakes

Reputation: 355

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

I am trying to make explosion with particles in pygame but it is very very slow. One of the slowest parts of the code is the part that creates surfaces with required color and transparency. I am posting the whole code here for clarity, but the function is called makeSurface. It's in both SmokeParticle and Spark 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()

Upvotes: 2

Views: 209

Answers (1)

Rabbid76
Rabbid76

Reputation: 210909

An obvious improvement is not to create a Surface for each particle every time it moves. Create the Surface only once and fill it with the new color when the color changes

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)

This still won't be enough performance improvement, but it will be better.

Upvotes: 1

Related Questions