compose/remote/remote-creation/doc/guides/PARTICLE_SYSTEM_GUIDE.md
The RemoteCompose particle system allows for efficient, high-performance animations of many similar objects (particles) using procedural creation APIs. It minimizes communication by running the particle evolution logic directly on the player side.
The system consists of two main operations:
ParticlesCreate: Initializes a set of particles with starting values for a fixed set of variables.ParticlesLoop: Iterates over the particles, updates their variables using mathematical equations (RPN-based), and optionally restarts them based on a condition.Use createParticles to define the variables each particle will track and their initial state.
val variables: Array<RFloat> = Array(4) { RFloat(this, 0f) }
val psId = createParticles(
variables,
arrayOf(
cx, // initial x
cy, // initial y
rand() - 0.5f, // initial dx
rand() - 0.5f // initial dy
),
pCount = 100
)
val (px, py, dx, dy) = variables
variables: An array of RFloat objects representing the state of a single particle during the loop.initialValues: An array of values or expressions used to initialize each particle. You can use index() to vary initial values per particle.pCount: The total number of particles in the system.Use particlesLoops to define how particles evolve over time and how they are rendered.
val dt = deltaTime() // Time since last frame
particlesLoops(
psId,
restartCondition = null, // Optional expression, restarts particle if > 0
updateEquations = arrayOf(
px + dx * dt, // New px
py + dy * dt, // New py
dx, // dx remains same
dy + 9.8f * dt // Add gravity to dy
)
) {
// Inside this block, 'variables' (px, py, etc.) hold the current particle's state
drawCircle(px.toFloat(), py.toFloat(), 5f)
}
restartCondition: An expression that, if it evaluates to > 0, triggers a call to the initialization equations for that specific particle.updateEquations: New values for each of the variables defined in createParticles.drawingBlock: Operations in this block are executed for each particle in every frame.Particles are often used inside an impulse block to manage their lifetime and ensure they only run when triggered.
val eventTime = ContinuousSec().toFloat()
impulse(durationMs = 2000f, eventTime) {
// System setup
impulseProcess() {
// particlesLoops and drawing
}
}
rand(): Returns a random float between 0 and 1 on the player side. Essential for varying particle behavior.index(): Returns the index (0..pCount-1) of the particle currently being initialized or updated.particlesComparisonThe particlesComparison operation (implemented by ParticlesCompare.java) allows for conditional logic and inter-particle interactions. It can operate in two modes:
Used to check a condition for each particle independently (e.g., hitting a wall).
particlesComparison(
id = psId,
min = -1f, max = -1f, // Process all particles
condition = px + ballRad - w, // Evaluates per particle
then = arrayOf(
dx * -elastic, // Update dx (bounce)
dy,
w - ballRad - 1f, // Snap back inside x
py
)
) {
// Optional: Draw something or trigger haptics when condition is met
performHaptic(2)
}
If you provide both a then and an else (or two result blocks), the system can perform $O(N^2)$ comparisons between pairs of particles. This is useful for collisions between particles or gravity-like effects.
restartCondition: Instead of creating new systems, use the restart condition in particlesLoops to recycle particles (e.g., resetting a spark to the origin).