Reputation: 355
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
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