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); }