Source code for rubato.structure.gameobject.particles.system

"""A simple particle system."""
from __future__ import annotations
from enum import IntEnum, unique
from random import randint
from typing import Callable
import cython

from . import Particle
from .. import Component
from .... import Vector, Camera, Time, Math, Color, Draw, Surface


[docs]@unique class ParticleSystemMode(IntEnum): """The mode of the particle system.""" RANDOM = 0 """The particles are generated randomly.""" LOOP = 1 """Animate the generation around the shape.""" PINGPONG = 2 """Animate the generation in a pingpong fashion.""" BURST = 3 """Generate the particles in a burst."""
if not cython.compiled: from enum_tools import document_enum document_enum(ParticleSystemMode)
[docs]class ParticleSystem(Component): """ A simple particle system. Args: new_particle: The method to generate a new particle. Takes in an angle and should return a particle object. Defaults to `ParticleSystem.default_particle`. duration: The duration of the system in seconds (when to stop generating particles). Defaults to 5. loop: Whether the system should loop (start again at the end of its duration). Defaults to False. max_particles: The maximum number of particles in the system. Defaults to `Math.INF`. mode: The particle generation mode of the system. Defaults to `ParticleSystemMode.RANDOM`. spread: The gap between particles (in degrees). Defaults to 45. density: The density of the system. This is the number of particles generated per fixed update. Defaults to 1. local_space: Whether the particles should be in local space. running: Whether the system should start as soon as it becomes active. offset: The offset of the system. Defaults to (0, 0). rot_offset: The rotation offset of the system. Defaults to 0. z_index: The z-index of the system. Defaults to 0. """ def __init__( self, new_particle: Callable[[float], Particle] | None = None, duration: float = 5, loop: bool = False, max_particles: int = Math.INF, mode: ParticleSystemMode = ParticleSystemMode.RANDOM, spread: float = 5, density: int = 1, local_space: bool = False, running: bool = False, offset: Vector | tuple[float, float] = (0, 0), rot_offset: float = 0, z_index: int = 0, ): super().__init__(offset=offset, rot_offset=rot_offset, z_index=z_index) self.new_particle = new_particle or ParticleSystem.default_particle """The user-defined function that generates a particle.""" self.duration: float = duration """The duration of the system in seconds.""" self.loop: bool = loop """Whether the system should loop.""" self.max_particles: int = max_particles """The maximum number of particles in the system.""" self.mode: ParticleSystemMode = mode """The particle generation mode of the system.""" self.spread: float = spread """The gap between particles (in degrees).""" self.density: int = density """The density of the system. This is the number of particles generated per fixed update.""" self.local_space: bool = local_space """Whether the particles should be in local space.""" self.running: bool = running """Whether the system is allowed to generate particles.""" self.__particles: list[Particle] = [] self.__time: float = 0 self.__generated: int = 0 """ Number of particles generated this loop. (NOT EQUAL TO TOTAL NUMBER OF PARTICLES) """ self.__forward: bool = True """This controls the direction of the particle generation. (Only used in ParticleSystemMode.PINGPONG)"""
[docs] def num_particles(self): """ The number of particles in the system. """ return len(self.__particles)
[docs] def start(self): """Start the system (sets `running` to True).""" self.running = True
[docs] def stop(self): """Stop the system (sets `running` to False).""" self.running = False
[docs] def fixed_update(self): if self.running: self.generate_particles() self.__time += Time.fixed_delta if self.__time >= self.duration: if self.loop: self.__time = 0 self.__generated = 0 self.__forward = not self.__forward else: self.running = False i: int = 0 while i < len(self.__particles): particle = self.__particles[i] if particle.age >= particle.lifespan: self.__particles.pop(i) else: particle.age += Time.fixed_delta particle.movement(particle, Time.fixed_delta) i += 1
[docs] def draw(self, camera: Camera): for particle in self.__particles: if self.local_space: particle._system_z = self.true_z() particle._system_pos = self.true_pos().clone() particle._system_rotation = self.true_rotation() particle.surface.rotation = particle.rotation + particle._system_rotation particle.surface.scale = particle._original_scale * particle.scale Draw.queue_surface( particle.surface, particle._system_pos + particle.pos.rotate(particle._system_rotation), particle.z_index + particle._system_z, camera, )
[docs] def generate_particles(self): """ Generates particles. Called automatically by fixed_update. """ max_in_dur = round(360 / self.spread) * self.density for _ in range(self.density): if self.mode == ParticleSystemMode.BURST and self.__time == 0: while self.__generated < max_in_dur and len(self.__particles) < self.max_particles: self.gen_particle(self.__generated * self.spread) if len(self.__particles) < self.max_particles: if self.mode == ParticleSystemMode.RANDOM: self.gen_particle(randint(0, max_in_dur) * self.spread) elif self.__time >= self.duration / max_in_dur * self.__generated: if self.mode == ParticleSystemMode.LOOP: self.gen_particle(self.__generated * self.spread) elif self.mode == ParticleSystemMode.PINGPONG: if self.__forward: self.gen_particle(self.__generated * self.spread) else: self.gen_particle((max_in_dur - self.__generated) * self.spread)
def gen_particle(self, angle: float): part = self.new_particle(angle) if part is None: raise ValueError("new_particle must return a Particle.") if not self.local_space: part._system_rotation = self.true_rotation() part._system_pos = self.true_pos().clone() part._system_z = self.true_z() self.__particles.append(part) self.__generated += 1
[docs] def clear(self): """Clear the system.""" self.__particles.clear()
[docs] def clone(self) -> ParticleSystem: return ParticleSystem( self.new_particle, self.duration, self.loop, self.max_particles, self.mode, self.spread, self.density, self.local_space, self.running, self.offset.clone(), self.rot_offset, self.z_index, )
[docs] @staticmethod def default_particle(angle: float) -> Particle: """ The default particle generation function. This can be passed into the Particle System constructor. """ surf = Surface() surf.fill(Color.debug) return Particle(surf, velocity=Particle.circle_direction()(angle))
[docs] @staticmethod def particle_gen( surface: Surface, movement: Callable[[Particle, float], None] | None = None, pos_func: Callable[[float], Vector] | None = None, dir_func: Callable[[float], Vector] = Particle.circle_direction(), start_speed: float = 1, acceleration: Vector | tuple[float, float] = (0, 0), rotation: float = 0, rot_velocity: float = 0, rot_acceleration: float = 0, scale: Vector | tuple[float, float] = (1, 1), lifespan: float = 1, z_index: int = 0, age: float = 0, ) -> Callable[[float], Particle]: """ Generates a particle generation function for the Particle System constructor. Args: surface: The surface to use for the particle. movement: The movement function. Defaults to `Particle.default_movement`. pos_func: The function used to determine each starting position. Must take in an angle relative to the system. Defaults to `lambda _: Vector(0, 0)`. dir_func: The function used to determine each starting direction. Must take in an angle relative to the system. Defaults to `Particle.circle_direction()`. start_speed: The starting speed. Defaults to 1. acceleration: The starting acceleration. Defaults to (0, 0). rotation: The starting rotation. Defaults to 0. rot_velocity: The starting rotational velocity. Defaults to 0. rot_acceleration: The starting rotational acceleration. Defaults to 0. scale: The starting scale. Defaults to (1, 1). lifespan: The lifespan of each particle. Defaults to 1. z_index: The z-index of each particle. Defaults to 0. age: The starting age of each particle. Defaults to 0. Returns: A particle generation function. """ acc = Vector.create(acceleration) sca = Vector.create(scale) def gen(angle: float) -> Particle: return Particle( surface.clone(), movement or Particle.default_movement, pos_func(angle) if pos_func else Vector(0, 0), dir_func(angle) * start_speed, acc.clone(), rotation, rot_velocity, rot_acceleration, sca.clone(), lifespan, z_index, age, ) return gen