Частицы 2D: CPU, GPU и жизненный цикл эффекта
Полный пример с управлением эмиссией, перезапуском и пространством движения.
Выбрать систему
| Система | Где настраивается движение | Что учитывать |
|---|---|---|
| CpuParticles2D | Свойства самой Entity | Симуляция на CPU; отображение всё равно проходит через рендерер |
| GpuParticles2D | ProcessMaterial | GPU-симуляция; доступность дополнительных эффектов зависит от backend |
Обе системы создают визуальные частицы, а не по Entity на каждую точку. У отдельной частицы здесь нет C# callback, физического тела или сетевого ID. Для игровых снарядов с индивидуальной логикой нужен другой подход.
Базовые GPU-частицы в приведённом примере работают в Compatibility/OpenGL. Это не означает поддержку в этом backend trails и sub-emitters: эти возможности ограничены отдельно. CPU и GPU не обязаны выдавать одинаковые пиксели при равных параметрах и seed. Выбирайте по требуемым эффектам и замерам на целевой системе, а не по обещанию «GPU всегда быстрее».
Запустить полный пример
- Создайте C#-проект Reckfell.NET.Sdk 1.0.0, net10.0 с рендерером Compatibility.
- Сохраните два файла ниже и выполните dotnet build.
- Создайте уровень с корнем Entity2D, прикрепите Particle2DExample.cs, сохраните particles.rscene и запустите.
- Сначала частицы постепенно заполняют фонтан. ui_accept перезапускает его, ui_cancel прекращает новую эмиссию. В стандартных назначениях это Enter/Space и Escape; дайте окну игры фокус.
UseGpu=false выбирает CPU, true — GPU. OneShotBurst=true создаёт разовую вспышку, false — непрерывную эмиссию. LocalSpace выбирает пространство симуляции. Count допустим от 1 до 2048; верхняя граница — защита примера, не лимит движка. Эти параметры корневого скрипта читаются при _Ready: меняйте их до запуска. Для изменения уже созданной системы обращайтесь к её Cpu или Gpu.
Пример сам создаёт эмиттер и текстуру 8×8, задаёт логический размер окна игры 320×240 во время запуска и не требует сторонних ассетов.
Particle2DPreset.cs
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
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();
}
}Различать три операции
| Операция | Что происходит |
|---|---|
| StopEmission / Emitting=false | Новые частицы не появляются; уже живущие остаются до конца жизни |
| RestartEffect / Restart(keepSeed:true) | Состояние эффекта сбрасывается и эмиссия начинается заново |
| Естественное завершение OneShot | После завершения частиц приходит Finished |
Emitting=false не доказывает пустой экран. При OneShot с растянутым появлением последняя частица может жить после остановки эмиссии. Для завершения используйте Finished; для удаления Entity и ресурсов задайте отдельное правило времени жизни владельца.
В примере Lifetime=2. При непрерывной эмиссии с Explosiveness=0 и стандартными остальными параметрами Amount=128 даёт примерно 64 рождения в секунду и до 128 живых частиц после заполнения. При OneShotBurst=true Explosiveness=1 собирает рождение в один выброс. Lifetime — время жизни, не команда задержать запуск.
Restart сохраняет seed при keepSeed=true или UseFixedSeed=true. false у keepSeed не заставляет выбрать новый seed, когда UseFixedSeed уже включён. Это управление случайностью, а не гарантия межплатформенного или CPU/GPU-побитового повтора.
Выбрать локальные или мировые координаты
LocalCoords=true заставляет уже выпущенные частицы следовать трансформации эмиттера. При false перемещение эмиттера не переносит существующие частицы: это подходит для оставляемого следа. Выбирайте пространство до начала эффекта; смена параметра посреди симуляции требует отдельной проверки.
Не рассчитывайте траекторию умножением настроек скорости на delta в своём _Process: движение выполняет система частиц. В C# меняйте команды и параметры эффекта, а не позицию каждой визуальной точки.
Сравнить реальный результат
Ниже — кадры текущего движка с Count=128, OneShotBurst=true, LocalSpace=true примерно через 0,6 секунды симуляции. Это иллюстрации, не сравнение FPS и не обещание одинаковой последовательности CPU/GPU.
CpuParticles2D
GpuParticles2D
Если эффект не виден или ведёт себя неверно
- GPU-система пустая: проверьте ProcessMaterial, Amount, Emitting, текстуру и VisibilityRect. Материал отрисовки не заменяет материал симуляции.
- Частицы не исчезли после StopEmission: это ожидаемо; дождитесь их Lifetime, не считайте остановку эмиссии очисткой.
- После движения эмиттера след уезжает вместе с ним: проверьте LocalCoords.
- Вспышка не повторяется: вызывайте Restart, а не полагайтесь на повторное присваивание Emitting=true в незавершённом цикле.
- Переключили UseGpu во время игры, но тип остался прежним: этот флаг корневого примера выбирает тип только при _Ready.