diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs
index 2b68c53..2c96d0e 100644
--- a/TestGame/TestGame.cs
+++ b/TestGame/TestGame.cs
@@ -4,6 +4,8 @@ using Voile.Resources;
using Voile.SceneGraph;
using Voile.Utils;
using Voile.Input;
+using Voile.Systems;
+using System.Numerics;
public class TestGame : Game
{
@@ -12,9 +14,7 @@ public class TestGame : Game
public override void Initialize()
{
InitializeDefault();
-
- _audioSystem = new FmodAudioSystem();
- _audioSystem.Start();
+ _particleSystem = new ParticleSystem();
}
protected override void LoadResources()
@@ -32,62 +32,50 @@ public class TestGame : Game
protected override void Ready()
{
- _scene = new Scene(new SceneSettings()
+ _particleSystem!.CreateEmitter(Renderer.WindowSize / 2, new ParticleEmitterSettings()
{
- Renderer = Renderer,
- AudioBackend = _audioSystem!,
- InputHandler = Input,
- ResourceManager = ResourceManager
+ ColorBegin = Color.Green,
+ ColorEnd = Color.Red,
+ EmitRadius = 128,
+ MaxParticles = 256
});
-
- _uiLayer = new UiLayer();
- _worldLayer = new EntityLayer();
-
- _testSoundInstance = _audioSystem!.CreateInstance(_testSound!);
-
- Input.AddInputMapping("play", new InputAction[] { new KeyInputAction(KeyboardKey.Spacebar) });
- Input.AddInputMapping("sprint", new InputAction[] { new KeyInputAction(KeyboardKey.LeftShift) });
- Input.AddInputMapping("toggle_fullscreen", new InputAction[] { new KeyInputAction(KeyboardKey.F11) });
-
- _scene!.AddLayer("World", _worldLayer!);
-
- _worldLayer!.AddEntity(new World());
- _worldLayer.AddEntity(new TestPlayer());
-
- _scene.AddLayer("UI", _uiLayer!);
- _scene.Start();
}
protected override void Run()
{
while (Renderer.ShouldRun)
{
- if (Input.IsActionPressed("play"))
- {
- _testSoundInstance!.PitchVariation(0.9f, 1.1f)
- .VolumeVariation(0.98f, 1.02f);
+ _particleSystem!.Update(Renderer.FrameTime);
- _testSoundInstance!.Play();
+ Renderer.BeginFrame();
+ Renderer.ClearBackground(Color.Black);
+ foreach (var emitter in _particleSystem!.Emitters)
+ {
+ DrawEmitter(emitter);
}
- _scene.Update();
- _scene.BeginDraw();
- _scene.EndDraw();
+
+ Renderer.EndFrame();
}
}
public override void Shutdown()
{
ShutdownDefault();
- _audioSystem!.Dispose();
}
- private Sound? _testSound;
- private SoundInstance? _testSoundInstance;
- private Font? _font;
- private FmodAudioSystem? _audioSystem;
- private Scene? _scene;
+ private void DrawEmitter(ParticleEmitter emitter)
+ {
+ for (int i = 0; i < emitter.Particles.Length; i++)
+ {
+ var particle = emitter.Particles[i];
- private UiLayer? _uiLayer;
- private EntityLayer? _worldLayer;
+ var color = new Color(particle.ColorArgb);
+
+ Renderer.SetTransform(emitter.OriginPosition + particle.Position, Vector2.Zero);
+ Renderer.DrawCircle(16f * particle.Scale, color);
+ }
+ }
+
+ private ParticleSystem? _particleSystem;
private Logger _logger = new(nameof(TestGame));
}
\ No newline at end of file
diff --git a/Voile/Source/SceneGraph/Entities/Particles2d.cs b/Voile/Source/SceneGraph/Entities/Particles2d.cs
deleted file mode 100644
index 09931a5..0000000
--- a/Voile/Source/SceneGraph/Entities/Particles2d.cs
+++ /dev/null
@@ -1,162 +0,0 @@
-using System.Drawing;
-using System.Numerics;
-using Voile.Rendering;
-
-namespace Voile.SceneGraph
-{
- // TODO: add oneshot parameter.
- public class Particles2d : Drawable2d
- {
- public int MaxParticles => _maxParticles;
- public ParticleSettings Settings => _settings;
-
- public Particles2d(ParticleSettings settings)
- {
- _settings = settings;
- _maxParticles = _settings.MaxParticles;
- _particleIndex = _maxParticles - 1;
-
- _particles = new Particle[_maxParticles];
- }
-
- public void Restart()
- {
- CleanupParticles();
-
- // Allocate a new particle array if max particles property got changed.
- if (_maxParticles != _settings.MaxParticles)
- {
- _particles = new Particle[_maxParticles];
- }
- }
-
- public override void OnDraw(RenderSystem renderer)
- {
- foreach (var particle in _particles)
- {
- if (!particle.Alive) continue;
-
- var t = particle.LifeTimeRemaining / particle.LifeTime;
- var scale = MathUtils.Lerp(_settings.ScaleEnd, _settings.ScaleBegin, t);
- var color = MathUtils.LerpColor(_settings.ColorEnd, _settings.ColorBegin, t);
-
- renderer.SetTransform(particle.Position, Vector2.Zero, particle.Rotation);
- renderer.DrawRectangle(Vector2.One * scale, color);
- }
- }
- protected override void OnStart()
- {
- base.OnStart();
- // Emit();
- }
- protected override void OnUpdate(double dt)
- {
- base.OnUpdate(dt);
-
- var rate = (int)MathUtils.Lerp(1, _maxParticles, _settings.Explosiveness);
- for (int i = 0; i < rate; i++)
- {
- Emit();
- }
-
- for (int i = 0; i < _maxParticles; i++)
- {
- var particle = _particles[i];
- if (!particle.Alive) continue;
-
- if (particle.LifeTimeRemaining <= 0.0f)
- {
- particle.Alive = false;
- continue;
- }
-
- particle.LifeTimeRemaining -= (float)dt;
- particle.Velocity += _settings.Gravity * (float)dt;
- particle.Position += particle.Velocity * (float)dt;
- particle.Rotation += particle.AngularVelocity * (float)dt;
-
- particle.Velocity -= particle.Velocity * _settings.Damping * (float)dt;
-
- _particles[i] = particle;
- }
- }
-
- private void Emit()
- {
- Particle particle = _particles[_particleIndex];
- if (!(particle.LifeTimeRemaining <= 0)) return;
- particle.Alive = true;
- particle.Position = GetEmitPosition();
- particle.Velocity = _settings.Direction * _settings.LinearVelocity;
-
- particle.Velocity += Vector2.One * _settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f);
-
- particle.AngularVelocity = _settings.AngularVelocity;
- particle.AngularVelocity += 1f * _settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
-
- particle.LifeTime = _settings.LifeTime;
- particle.LifeTimeRemaining = particle.LifeTime;
-
- _particles[_particleIndex] = particle;
- _particleIndex = --_particleIndex <= 0 ? _maxParticles - 1 : --_particleIndex;
- }
-
- private void CleanupParticles() => Array.Clear(_particles);
- private Vector2 GetEmitPosition()
- {
- // https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
- var angle = _random.NextDouble() * Math.PI * 2;
- float radius = (float)Math.Sqrt(_random.NextDouble()) * _settings.EmitRadius;
-
- float x = Position.X + radius * (float)Math.Cos(angle);
- float y = Position.Y + radius * (float)Math.Sin(angle);
-
- return new Vector2(x, y);
- }
-
- private ParticleSettings _settings;
- private int _maxParticles;
- private Particle[] _particles;
- private int _particleIndex;
-
- // TODO: replace a random function for better distribution and performance.
- private LehmerRandom _random = new LehmerRandom();
- }
-
- public struct ParticleSettings
- {
- public ParticleSettings()
- {
- }
- public float EmitRadius;
- public float LifeTime;
- public float Explosiveness;
- public int MaxParticles;
- public Vector2 Direction;
- public float LinearVelocity;
- public float AngularVelocity = 0.0f;
- public float AngularVelocityRandom;
- public float LinearVelocityRandom;
- public Vector2 Gravity;
- public float ScaleBegin = 16f;
- public float ScaleEnd = 0.0f;
- public Color ColorBegin = Color.White;
- public Color ColorEnd = Color.Black;
- public float Damping = 0.0f;
- }
-
- public struct Particle
- {
- public Particle()
- {
- }
- public Vector2 Position;
- public Vector2 Velocity;
- public float AngularVelocity;
- public float LifeTime;
- public float LifeTimeRemaining;
- public float Scale;
- public float Rotation;
- public bool Alive = true;
- }
-}
\ No newline at end of file
diff --git a/Voile/Source/Systems/ISystem.cs b/Voile/Source/Systems/ISystem.cs
index 9cb3eb6..98109cc 100644
--- a/Voile/Source/Systems/ISystem.cs
+++ b/Voile/Source/Systems/ISystem.cs
@@ -2,6 +2,9 @@ namespace Voile;
public interface IStartableSystem
{
+ ///
+ /// Starts this system.
+ ///
void Start();
}
diff --git a/Voile/Source/Systems/ParticleSystem.cs b/Voile/Source/Systems/ParticleSystem.cs
new file mode 100644
index 0000000..76e54fb
--- /dev/null
+++ b/Voile/Source/Systems/ParticleSystem.cs
@@ -0,0 +1,205 @@
+using System.Numerics;
+using Voile.Rendering;
+using Voile.Utils;
+
+namespace Voile.Systems;
+
+public struct Particle
+{
+ public Particle()
+ {
+ }
+
+ public int EmitterIndex { get; set; }
+ public int ColorArgb { get; set; }
+ public Vector2 Position { get; set; }
+ public Vector2 Velocity { get; set; }
+ public float AngularVelocity { get; set; }
+ public float LifeTime { get; set; } = 1.0f;
+ public float LifeTimeRemaining { get; set; }
+ public float Scale { get; set; }
+ public float Rotation { get; set; }
+ public bool Alive { get; set; } = true;
+}
+
+public class ParticleEmitterSettings
+{
+ public int MaxParticles { get; set; } = 16;
+ public float EmitRadius { get; set; }
+ public float LifeTime { get; set; } = 1.0f;
+ public float Explosiveness { get; set; }
+ public Vector2 Direction { get; set; } = -Vector2.UnitY;
+ public float LinearVelocity { get; set; } = 980.0f;
+ public float AngularVelocity { get; set; }
+ public float AngularVelocityRandom { get; set; }
+ public float LinearVelocityRandom { get; set; } = 0.5f;
+ public Vector2 Gravity { get; set; } = Vector2.UnitY * 980f;
+ public float ScaleBegin { get; set; } = 1.0f;
+ public float ScaleEnd { get; set; } = 0.0f;
+ public Color ColorBegin { get; set; } = Color.White;
+ public Color ColorEnd { get; set; } = Color.Black;
+ public float Damping { get; set; } = 1.0f;
+}
+
+public class ParticleEmitter : IUpdatableSystem
+{
+ public ReadOnlySpan Particles => _particles.AsSpan();
+ public Vector2 OriginPosition => _originPosition;
+ public ParticleEmitterSettings Settings => _settings;
+
+ public ParticleEmitter(Vector2 originPosition, ParticleEmitterSettings settings, ArraySegment particles)
+ {
+ _originPosition = originPosition;
+
+ _settings = settings;
+ _maxParticles = _settings.MaxParticles;
+ _particleIndex = _maxParticles - 1;
+
+ _particles = particles;
+
+ _random = new LehmerRandom();
+ }
+
+ public void Update(double deltaTime)
+ {
+ var rate = (int)MathUtils.Lerp(1, _maxParticles, _settings.Explosiveness);
+ for (int i = 0; i < rate; i++)
+ {
+ Emit();
+ }
+
+ for (int i = 0; i < _maxParticles; i++)
+ {
+ var particle = _particles[i];
+ if (!particle.Alive) continue;
+
+ if (particle.LifeTimeRemaining <= 0.0f)
+ {
+ particle.Alive = false;
+ continue;
+ }
+
+ particle.LifeTimeRemaining = Math.Clamp(particle.LifeTimeRemaining - (float)deltaTime, 0.0f, particle.LifeTime);
+
+ var t = particle.LifeTimeRemaining / particle.LifeTime;
+
+ particle.Velocity += _settings.Gravity * (float)deltaTime;
+ particle.Position += particle.Velocity * (float)deltaTime;
+ particle.Rotation += particle.AngularVelocity * (float)deltaTime;
+ particle.Scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t);
+
+ var color = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t);
+ particle.ColorArgb = color.Argb;
+
+ particle.Velocity -= particle.Velocity * _settings.Damping * (float)deltaTime;
+
+ _particles[i] = particle;
+ }
+ }
+
+ private void Emit()
+ {
+ Particle particle = _particles[_particleIndex];
+ if (!(particle.LifeTimeRemaining <= 0)) return;
+ particle.Alive = true;
+ particle.Position = GetEmitPosition();
+ particle.Velocity = _settings.Direction * _settings.LinearVelocity;
+
+ particle.Velocity += Vector2.One * _settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f);
+
+ particle.AngularVelocity = _settings.AngularVelocity;
+ particle.AngularVelocity += 1f * _settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
+
+ particle.LifeTime = _settings.LifeTime;
+ particle.LifeTimeRemaining = particle.LifeTime;
+
+ _particles[_particleIndex] = particle;
+ _particleIndex = --_particleIndex <= 0 ? _maxParticles - 1 : --_particleIndex;
+ }
+
+ private Vector2 GetEmitPosition()
+ {
+ // https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
+ var angle = _random.NextDouble() * Math.PI * 2;
+ float radius = (float)Math.Sqrt(_random.NextDouble()) * _settings.EmitRadius;
+
+ float x = radius * (float)Math.Cos(angle);
+ float y = radius * (float)Math.Sin(angle);
+
+ return new Vector2(x, y);
+ }
+
+ private LehmerRandom _random;
+ private int _maxParticles;
+ private int _particleIndex;
+ private Vector2 _originPosition = Vector2.Zero;
+ private ArraySegment _particles;
+ private ParticleEmitterSettings _settings;
+}
+
+public class ParticleSystem : IUpdatableSystem, IDisposable
+{
+ ///
+ /// Maximum amount of particles emittable by the system.
+ ///
+ public int ParticleLimit { get; set; } = 8192;
+
+ public IReadOnlyList Emitters => _emitters;
+
+ public ParticleSystem()
+ {
+ _particleIndex = ParticleLimit - 1;
+ _particles = new Particle[ParticleLimit];
+ }
+
+ public void CreateEmitter(Vector2 originPosition, ParticleEmitterSettings settings)
+ {
+ if (_emitterSliceOffset + settings.MaxParticles >= ParticleLimit - 1)
+ {
+ _logger.Error("Cannot create an emitter! Reached particle limit.");
+ return;
+ }
+
+ var particles = new ArraySegment(_particles, _emitterSliceOffset, settings.MaxParticles);
+
+ // foreach (var particle in particles)
+ // {
+ // particle.LifeTime = settings.LifeTime;
+ // }
+
+ for (int i = 0; i < particles.Count; i++)
+ {
+ var particle = particles[i];
+ particle.LifeTime = settings.LifeTime;
+ }
+
+ var emitter = new ParticleEmitter(originPosition, settings, particles);
+
+ _emitters.Add(emitter);
+
+ _emitterSliceOffset += settings.MaxParticles;
+ }
+
+ public void Update(double deltaTime)
+ {
+ foreach (var emitter in _emitters)
+ {
+ emitter.Update(deltaTime);
+ }
+ }
+
+ public void Dispose()
+ {
+ CleanupParticles();
+ }
+
+ private void CleanupParticles() => Array.Clear(_particles);
+ private Particle[] _particles;
+ private int _particleIndex;
+
+ private int _emitterSliceOffset;
+
+ private List _emitters = new();
+
+ private Logger _logger = new(nameof(ParticleSystem));
+}
\ No newline at end of file
diff --git a/Voile/Source/Utils/Color.cs b/Voile/Source/Utils/Color.cs
index abb1b42..938fb92 100644
--- a/Voile/Source/Utils/Color.cs
+++ b/Voile/Source/Utils/Color.cs
@@ -37,21 +37,18 @@ namespace Voile
public float R { get; set; }
public float G { get; set; }
public float B { get; set; }
- public float A { get; set; }
+ public float A { get; set; } = 1.0f;
public int Argb
{
get
{
- int c = (ushort)Math.Round(A * 255f);
- c <<= 8;
- c |= (ushort)Math.Round(R * 255f);
- c <<= 8;
- c |= (ushort)Math.Round(G * 255f);
- c <<= 8;
- c |= (ushort)Math.Round(B * 255f);
+ int a = (int)Math.Round(A * 255f) << 24;
+ int r = (int)Math.Round(R * 255f) << 16;
+ int g = (int)Math.Round(G * 255f) << 8;
+ int b = (int)Math.Round(B * 255f);
- return c;
+ return a | r | g | b;
}
}
diff --git a/Voile/Source/Utils/MathUtils.cs b/Voile/Source/Utils/MathUtils.cs
index bc6fcdb..043e87a 100644
--- a/Voile/Source/Utils/MathUtils.cs
+++ b/Voile/Source/Utils/MathUtils.cs
@@ -8,10 +8,10 @@ namespace Voile
public static Color LerpColor(Color colorA, Color colorB, double t)
{
- var r = (byte)Lerp(colorA.R, colorB.R, t);
- var g = (byte)Lerp(colorA.G, colorB.G, t);
- var b = (byte)Lerp(colorA.B, colorB.B, t);
- var a = (byte)Lerp(colorA.A, colorB.A, t);
+ var r = Lerp(colorA.R, colorB.R, t);
+ var g = Lerp(colorA.G, colorB.G, t);
+ var b = Lerp(colorA.B, colorB.B, t);
+ var a = Lerp(colorA.A, colorB.A, t);
return new Color(r, g, b, a);
}