reckfellENGINE

2D particles: CPU, GPU and effect lifecycle

A complete example covering emission control, restart and simulation space.

On this page

Choose a system

SystemMotion configurationConsiderations
CpuParticles2DProperties on the entityCPU simulation; display still goes through the renderer
GpuParticles2DProcessMaterialGPU simulation; advanced effects depend on the backend

Both systems create visual particles rather than an entity for every point. Individual particles here have no C# callback, physics body or network ID. Gameplay projectiles with independent logic need a different approach.

The example's basic GPU particles work in Compatibility/OpenGL. This does not imply trails or sub-emitter support in that backend; those features have separate restrictions. Equal parameters and seeds do not guarantee matching CPU/GPU pixels. Choose according to required effects and measurements on target hardware, not a claim that GPU is always faster.

Run the complete example

  1. Create a C# project with Reckfell.NET.Sdk 1.0.0, net10.0 and Compatibility rendering.
  2. Save the two files below and run dotnet build.
  3. Create a level with an Entity2D root, attach Particle2DExample.cs, save particles.rscene and run it.
  4. The fountain gradually fills. ui_accept restarts it; ui_cancel stops new emission. Default bindings include Enter/Space and Escape; focus the game window.

UseGpu=false selects CPU and true selects GPU. OneShotBurst=true creates a single burst; false emits continuously. LocalSpace selects simulation space. Count accepts 1 through 2048; the upper bound protects this example and is not an engine limit. The root script reads these options at _Ready, so set them before running. Access its Cpu or Gpu to change the created system afterward.

The example creates its emitter and 8×8 texture, sets a logical 320×240 game window during execution and needs no third-party assets.

Particle2DPreset.cs
C#
using System;
using Reckfell;

public static class Particle2DPreset
{
    private static void Validate(int count)
    {
        if (count < 1 || count > 2048)
            throw new ArgumentOutOfRangeException(nameof(count));
    }

    public static ParticleProcessMaterial Motion() => new()
    {
        ParticleFlagDisableZ = true,
        Direction = new Vector3(0, -1, 0),
        Spread = 30,
        InitialVelocityMin = 60,
        InitialVelocityMax = 100,
        Gravity = new Vector3(0, 90, 0),
        Color = new Color(0.55f, 0.72f, 1)
    };

    public static GpuParticles2D Gpu(int count, bool oneShot, bool local)
    {
        Validate(count);
        return new GpuParticles2D
        {
            Emitting = false,
            Amount = count,
            Lifetime = 2,
            OneShot = oneShot,
            Explosiveness = oneShot ? 1 : 0,
            LocalCoords = local,
            UseFixedSeed = true,
            Seed = 42,
            FixedFps = 60,
            ProcessMaterial = Motion(),
            Texture = Texture(),
            VisibilityRect = new Rect2(-140, -120, 280, 320)
        };
    }

    public static CpuParticles2D Cpu(int count, bool oneShot, bool local)
    {
        Validate(count);
        return new CpuParticles2D
        {
            Emitting = false,
            Amount = count,
            Lifetime = 2,
            OneShot = oneShot,
            Explosiveness = oneShot ? 1 : 0,
            LocalCoords = local,
            UseFixedSeed = true,
            Seed = 42,
            FixedFps = 60,
            Direction = Vector2.Up,
            Spread = 30,
            InitialVelocityMin = 60,
            InitialVelocityMax = 100,
            Gravity = new Vector2(0, 90),
            Color = new Color(0.55f, 0.72f, 1),
            Texture = Texture()
        };
    }

    private static ImageTexture Texture()
    {
        using var image = Image.CreateEmpty(8, 8, false, Image.Format.Rgba8);
        image.Fill(Colors.Transparent);
        for (int y = 0; y < 8; y++)
        for (int x = 0; x < 8; x++)
            if (new Vector2(x - 3.5f, y - 3.5f).LengthSquared() <= 12)
                image.SetPixel(x, y, Colors.White);
        return ImageTexture.CreateFromImage(image);
    }
}
Particle2DExample.cs
C#
using Reckfell;

public partial class Particle2DExample : Entity2D
{
    [Export] public bool UseGpu { get; set; } = true;
    [Export] public bool OneShotBurst { get; set; }
    [Export] public bool LocalSpace { get; set; } = true;
    [Export] public int Count { get; set; } = 128;
    public GpuParticles2D? Gpu { get; private set; }
    public CpuParticles2D? Cpu { get; private set; }

    public override void _Ready()
    {
        if (GetViewport() is Window window)
        {
            window.ContentScaleSize = new Vector2I(320, 240);
            window.ContentScaleMode = Window.ContentScaleModeEnum.Viewport;
        }
        Vector2 position = new(160, 180);
        if (UseGpu)
        {
            Gpu = Particle2DPreset.Gpu(Count, OneShotBurst, LocalSpace);
            Gpu.Position = position;
            AddChild(Gpu);
        }
        else
        {
            Cpu = Particle2DPreset.Cpu(Count, OneShotBurst, LocalSpace);
            Cpu.Position = position;
            AddChild(Cpu);
        }
        RestartEffect();
    }

    public void RestartEffect()
    {
        Gpu?.Restart(keepSeed: true);
        Cpu?.Restart(keepSeed: true);
    }

    public void StopEmission()
    {
        if (Gpu != null) Gpu.Emitting = false;
        if (Cpu != null) Cpu.Emitting = false;
    }

    public override void _UnhandledInput(InputEvent input)
    {
        if (input.IsActionPressed("ui_accept")) RestartEffect();
        else if (input.IsActionPressed("ui_cancel")) StopEmission();
    }
}

Distinguish three operations

OperationResult
StopEmission / Emitting=falseNo new particles; existing particles remain until their lifetimes end
RestartEffect / Restart(keepSeed:true)Reset effect state and start emission again
Natural OneShot completionFinished signals after the particles finish

Emitting=false does not prove an empty screen. With staggered OneShot emission, the last particle can outlive emission itself. Observe Finished for completion; define owner lifetime separately if the entity and its resources should be removed.

Lifetime=2 in the sample. With continuous emission, Explosiveness=0 and the remaining defaults, Amount=128 gives approximately 64 births per second and up to 128 live particles after filling. OneShotBurst=true uses Explosiveness=1 to group births into one burst. Lifetime is lifespan rather than a delay before starting.

Restart retains the seed with keepSeed=true or UseFixedSeed=true. Passing keepSeed=false does not force a new seed when UseFixedSeed is enabled. This controls randomness; it does not guarantee cross-platform or CPU/GPU bit-identical replay.

Choose local or world coordinates

LocalCoords=true makes emitted particles follow emitter transforms. With false, moving the emitter does not move existing particles, which suits a trail left behind. Select the space before starting; changing it during simulation needs separate verification.

Do not multiply the velocity settings by delta in your own _Process: the particle system performs simulation. Change effect commands and parameters from C#, rather than the position of every visual point.

Compare actual output

These current-engine frames use Count=128, OneShotBurst=true and LocalSpace=true at roughly 0.6 seconds of simulation. They are illustrations, not an FPS comparison or a promise of matching CPU/GPU sequences.

CpuParticles2D

CPU particle burst in the current engine

GpuParticles2D

GPU particle burst in the current engine

Diagnose missing or incorrect effects

  • Empty GPU system: check ProcessMaterial, Amount, Emitting, Texture and VisibilityRect. A drawing material does not replace a simulation material.
  • Particles remain after StopEmission: expected; allow their Lifetime to expire instead of treating emission stop as clearing.
  • The trail moves with its emitter: check LocalCoords.
  • A burst does not repeat: call Restart rather than relying on another Emitting=true assignment during an unfinished cycle.
  • UseGpu changed at runtime but the type stayed the same: this sample root selects its type only at _Ready.

Material, trajectory and bounds · 2D coordinates

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.