From 775b973eb224f6b47d93295669a91c3b29924ec4 Mon Sep 17 00:00:00 2001 From: dnesov Date: Thu, 17 Oct 2024 00:50:49 +0200 Subject: [PATCH] Finalize ParticleSystem, add angular velocity, multiple emitters, GetBool for IDataReader, use Lerp in LerpColor. --- TestGame/Resources/fire_effect.toml | 20 +++ TestGame/Resources/test_emitter.toml | 14 -- TestGame/TestGame.cs | 31 ++-- .../Resources/DataReaders/IDataReader.cs | 7 + .../Resources/DataReaders/TomlDataReader.cs | 24 +++ Voile/Source/Systems/ParticleSystem.cs | 144 ++++++++---------- Voile/Source/Utils/MathUtils.cs | 8 +- 7 files changed, 139 insertions(+), 109 deletions(-) create mode 100644 TestGame/Resources/fire_effect.toml delete mode 100644 TestGame/Resources/test_emitter.toml diff --git a/TestGame/Resources/fire_effect.toml b/TestGame/Resources/fire_effect.toml new file mode 100644 index 0000000..17ad69a --- /dev/null +++ b/TestGame/Resources/fire_effect.toml @@ -0,0 +1,20 @@ +# Fire effect example +[ParticleEmitterSettings] + +Local = true +MaxParticles = 128 +EmitRadius = 8 +Explosiveness = 0 +LifeTime = 1.0 +Direction = [0.0, -1.0] +LinearVelocity = 200 +LinearVelocityDamping = 0.0 +AngularVelocity = 230 +AngularVelocityDamping = 0.0 +AngularVelocityRandom = 1.0 +Gravity = [0.0, 0.0] +LinearVelocityRandom = 0.5 +ScaleBegin = 0.1 +ScaleEnd = 5.0 +ColorBegin = [255, 162, 0] +ColorEnd = [64, 64, 64, 0] diff --git a/TestGame/Resources/test_emitter.toml b/TestGame/Resources/test_emitter.toml deleted file mode 100644 index e7bdd93..0000000 --- a/TestGame/Resources/test_emitter.toml +++ /dev/null @@ -1,14 +0,0 @@ -[ParticleEmitterSettings] - -MaxParticles = 1024 -EmitRadius = 128 -Explosiveness = 1.0 -LifeTime = 2.0 -Direction = [0.0, -1.0] -LinearVelocity = 200 -Gravity = [0.0, 0.0] -LinearVelocityRandom = 0.5 -ScaleBegin = 1.0 -ScaleEnd = 5.0 -ColorBegin = [255, 0, 0, 0] -ColorEnd = [0, 255, 0, 255] diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 5b14554..70beeb0 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -26,11 +26,6 @@ public class TestGame : Game protected override void LoadResources() { - // if (!ResourceManager.TryLoad("my_sound", "sounds/test_sound.ogg", out Sound? _testSound)) - // { - - // } - if (!ResourceManager.TryLoad("fonts/Inter-Regular.ttf", out _font)) { @@ -38,7 +33,7 @@ public class TestGame : Game ResourceManager.TryLoad("icon.png", out _icon); - if (!ResourceManager.TryLoad("test_emitter.toml", out _emitterSettings)) + if (!ResourceManager.TryLoad("fire_effect.toml", out _fireEffect)) { throw new Exception("Failed to load emitter settings!"); } @@ -46,7 +41,7 @@ public class TestGame : Game protected override void Ready() { - _emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _emitterSettings); + _emitterId = _particleSystem.CreateEmitter(Renderer.WindowSize / 2, _fireEffect); } protected override void Run() @@ -59,6 +54,16 @@ public class TestGame : Game _particleSystem!.RestartEmitter(_emitterId); } + if (Input.KeyboardKeyJustPressed(KeyboardKey.One)) + { + _particleSystem.CreateEmitter(Input.GetMousePosition(), _fireEffect); + } + + if (Input.IsMouseButtonDown(MouseButton.Left)) + { + _particleSystem.SetEmitterPosition(_emitterId, Input.GetMousePosition()); + } + _particleSimStopwatch = Stopwatch.StartNew(); _particleSystem!.Update(Renderer.FrameTime); _particleSimStopwatch.Stop(); @@ -97,15 +102,15 @@ public class TestGame : Game Renderer.BeginBlended(Voile.Rendering.BlendMode.BlendAlpha); var maxParticles = emitter.Settings.MaxParticles; + var particleSize = new Vector2(16.0f, 16.0f); + var pivot = particleSize / 2; - for (int i = maxParticles - 1; i > 0; i--) + for (int i = maxParticles - 1; i >= 0; i--) { var particle = emitter.GetParticle(i); - var color = new Color(particle.ColorArgb); - - Renderer.SetTransform(emitter.OriginPosition + particle.Position, Vector2.Zero); - Renderer.DrawRectangle(Vector2.One * 16.0f * particle.Scale, color); + Renderer.SetTransform(particle.Position, pivot, particle.Rotation); + Renderer.DrawRectangle(particleSize * particle.Scale, particle.Color); } Renderer.EndBlended(); @@ -116,7 +121,7 @@ public class TestGame : Game private Stopwatch _particleSimStopwatch, _renderStopwatch; private long _lastRenderTime; private int _emitterId; - private ResourceRef _emitterSettings; + private ResourceRef _fireEffect; private ResourceRef _font; private ResourceRef _icon; private Logger _logger = new(nameof(TestGame)); diff --git a/Voile/Source/Resources/DataReaders/IDataReader.cs b/Voile/Source/Resources/DataReaders/IDataReader.cs index 1a08434..09aca08 100644 --- a/Voile/Source/Resources/DataReaders/IDataReader.cs +++ b/Voile/Source/Resources/DataReaders/IDataReader.cs @@ -30,6 +30,13 @@ namespace Voile.Resources.DataReaders /// public interface IStreamKeyValueGetter { + /// + /// Get a boolean from this data getter. + /// + /// Key of the value. + /// Default value in case this getter fails to get data. + /// + bool GetBool(string key, bool defaultValue = false); /// /// Get an int from this data getter. /// diff --git a/Voile/Source/Resources/DataReaders/TomlDataReader.cs b/Voile/Source/Resources/DataReaders/TomlDataReader.cs index 7bfc1f7..da18592 100644 --- a/Voile/Source/Resources/DataReaders/TomlDataReader.cs +++ b/Voile/Source/Resources/DataReaders/TomlDataReader.cs @@ -31,6 +31,30 @@ public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValue } } + public bool GetBool(string key, bool defaultValue = false) + { + if (_table is null) + { + return defaultValue; + } + + var dataTable = _table[ExpectedHeader]; + + if (!dataTable.HasKey(key)) + { + return defaultValue; + } + + var tableValue = dataTable[key]; + + if (!tableValue.IsBoolean) + { + return defaultValue; + } + + return tableValue.AsBoolean; + } + public int GetInt(string key, int defaultValue = 0) { if (_table is null) diff --git a/Voile/Source/Systems/ParticleSystem.cs b/Voile/Source/Systems/ParticleSystem.cs index 1778322..e91be06 100644 --- a/Voile/Source/Systems/ParticleSystem.cs +++ b/Voile/Source/Systems/ParticleSystem.cs @@ -12,7 +12,7 @@ public struct Particle } public int EmitterIndex { get; set; } - public int ColorArgb { get; set; } + public Color Color { get; set; } public Vector2 Position { get; set; } public float LifeTime { get; set; } = 1.0f; public float Scale { get; set; } @@ -21,6 +21,7 @@ public struct Particle public class ParticleEmitterSettings { + public bool Local { get; set; } = true; public int MaxParticles { get; set; } = 16; public float EmitRadius { get; set; } public float LifeTime { get; set; } = 1.0f; @@ -28,14 +29,15 @@ public class ParticleEmitterSettings 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 AngularVelocityRandom { get; set; } = 0.5f; 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 float LinearVelocityDamping { get; set; } = 1.0f; + public float AngularVelocityDamping { get; set; } = 0.1f; } public class ParticleEmitterSettingsResource : Resource @@ -45,7 +47,6 @@ public class ParticleEmitterSettingsResource : Resource { Settings = settings; } - } /// @@ -65,6 +66,7 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader /// Origin position in the world of this emitter. /// - public Vector2 OriginPosition => _originPosition; + public Vector2 OriginPosition { get; set; } /// /// for this emitter. /// public ParticleEmitterSettings Settings => _settingsResource.Value.Settings; - public int ArrayOffset => _lifetimes.Offset; /// /// Constructs a new . @@ -107,29 +109,39 @@ public class ParticleEmitter : IUpdatableSystem /// World origin position. /// Emitter settings resource. /// Particle segment that this emitter will simulate. - public ParticleEmitter(Vector2 originPosition, ResourceRef settingsResource, ArraySegment positionsSlice, ArraySegment velocitiesSlice, ArraySegment lifetimesSlice) + public ParticleEmitter(Vector2 originPosition, ResourceRef settingsResource) { - _originPosition = originPosition; + OriginPosition = originPosition; _settingsResource = settingsResource; _maxParticles = _settingsResource.Value.Settings.MaxParticles; - // _particleIndex = _maxParticles - 1; - _positions = positionsSlice; - _velocities = velocitiesSlice; - _lifetimes = lifetimesSlice; + _positions = new Vector2[_maxParticles]; + _velocities = new Vector2[_maxParticles]; + _angularVelocities = new float[_maxParticles]; + _lifetimes = new float[_maxParticles]; + _rotations = new float[_maxParticles]; _random = new LehmerRandom(); } public Particle GetParticle(int idx) { + if (idx > _lifetimes.Length) + { + return new Particle(); + } + var t = _lifetimes[idx] / Settings.LifeTime; + var scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t); + var color = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t); + return new Particle() { Position = _positions[idx], - Scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t), - ColorArgb = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t).Argb + Scale = scale, + Color = color, + Rotation = _rotations[idx] }; } @@ -139,12 +151,14 @@ public class ParticleEmitter : IUpdatableSystem /// New particle segment. public void Restart() { - for (int i = 0; i < _lifetimes.Count; i++) - { - _lifetimes[i] = 0.0f; - } - _maxParticles = Settings.MaxParticles; + + _positions = new Vector2[_maxParticles]; + _velocities = new Vector2[_maxParticles]; + _angularVelocities = new float[_maxParticles]; + _lifetimes = new float[_maxParticles]; + _rotations = new float[_maxParticles]; + _particleIndex = 0; } @@ -156,23 +170,28 @@ public class ParticleEmitter : IUpdatableSystem { var rate = (int)MathUtils.Lerp(1, _maxParticles, Settings.Explosiveness); - for (int i = 0; i < rate; i++) - { - Emit(); - } - var deltaTimeVector = new Vector2((float)deltaTime); var gravityVector = Settings.Gravity * (float)deltaTime; - var dampingFactor = Settings.Damping * (float)deltaTime; + + var linearVelocityDampingFactor = Settings.LinearVelocityDamping * (float)deltaTime; + var angularVelocityDampingFactor = Settings.AngularVelocityDamping * (float)deltaTime; for (int i = 0; i < _maxParticles; i++) { _lifetimes[i] = Math.Max(0.0f, _lifetimes[i] - (float)deltaTime); _velocities[i] += gravityVector; - _positions[i] += _velocities[i] * deltaTimeVector; - _velocities[i] -= _velocities[i] * dampingFactor; + _positions[i] += _velocities[i] * deltaTimeVector; + _rotations[i] += _angularVelocities[i] * (float)deltaTime; + + _velocities[i] -= _velocities[i] * linearVelocityDampingFactor; + _angularVelocities[i] -= _angularVelocities[i] * angularVelocityDampingFactor; + } + + for (int i = 0; i < rate; i++) + { + Emit(); } } @@ -184,6 +203,7 @@ public class ParticleEmitter : IUpdatableSystem _velocities[_particleIndex] = Settings.Direction * Settings.LinearVelocity; _velocities[_particleIndex] += Vector2.One * Settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f); + _angularVelocities[_particleIndex] = Settings.AngularVelocity * Settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f); _lifetimes[_particleIndex] = Settings.LifeTime; @@ -193,6 +213,7 @@ public class ParticleEmitter : IUpdatableSystem private Vector2 GetEmitPosition() { var settings = _settingsResource.Value.Settings; + Vector2 originOffset = Settings.Local ? OriginPosition : Vector2.Zero; // https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image var angle = _random.NextDouble() * Math.PI * 2; @@ -201,15 +222,16 @@ public class ParticleEmitter : IUpdatableSystem float x = radius * (float)Math.Cos(angle); float y = radius * (float)Math.Sin(angle); - return new Vector2(x, y); + var point = new Vector2(x, y); + + return point + originOffset; } private LehmerRandom _random; private int _maxParticles; private int _particleIndex; - private Vector2 _originPosition = Vector2.Zero; - private ArraySegment _positions, _velocities; - private ArraySegment _lifetimes; + private Vector2[] _positions, _velocities; + private float[] _lifetimes, _rotations, _angularVelocities; private ResourceRef _settingsResource; } @@ -218,26 +240,11 @@ public class ParticleEmitter : IUpdatableSystem /// public class ParticleSystem : IUpdatableSystem, IDisposable { - /// - /// Maximum amount of particles emittable by the system. - /// - public int ParticleLimit { get; set; } = short.MaxValue; - /// /// List of particle emitters created for this ParticleSystem. /// public IReadOnlyList Emitters => _emitters; - /// - /// Constructs a new . - /// - public ParticleSystem() - { - _particlePositions = new Vector2[ParticleLimit]; - _particleVelocities = new Vector2[ParticleLimit]; - _particleLifetimes = new float[ParticleLimit]; - } - /// /// Creates an emitter from a . /// @@ -246,31 +253,22 @@ public class ParticleSystem : IUpdatableSystem, IDisposable /// public int CreateEmitter(Vector2 originPosition, ResourceRef settingsResource) { - var settings = settingsResource.Value.Settings; - - if (_emitterSliceOffset + settings.MaxParticles >= ParticleLimit - 1) - { - _logger.Error("Cannot create an emitter! Reached particle limit."); - return -1; - } - - var positionsSlice = new ArraySegment(_particlePositions, _emitterSliceOffset, settings.MaxParticles); - var velocitiesSlice = new ArraySegment(_particleVelocities, _emitterSliceOffset, settings.MaxParticles); - var lifetimesSlice = new ArraySegment(_particleLifetimes, _emitterSliceOffset, settings.MaxParticles); - - for (int i = 0; i < lifetimesSlice.Count; i++) - { - lifetimesSlice[i] = settings.LifeTime; - } - - var emitter = new ParticleEmitter(originPosition, settingsResource, positionsSlice, velocitiesSlice, lifetimesSlice); - + var emitter = new ParticleEmitter(originPosition, settingsResource); _emitters.Add(emitter); - _emitterSliceOffset += settings.MaxParticles; - return _emitters.Count - 1; } + public void SetEmitterPosition(int emitterIdx, Vector2 position) + { + if (emitterIdx >= _emitters.Count) + { + throw new ArgumentException($"Emitter with idx {emitterIdx} is not present!"); + } + + var emitter = _emitters[emitterIdx]; + emitter.OriginPosition = position; + } + /// /// Restarts an emitter. /// @@ -284,7 +282,6 @@ public class ParticleSystem : IUpdatableSystem, IDisposable } var emitter = _emitters[id]; - emitter.Restart(); } @@ -302,16 +299,7 @@ public class ParticleSystem : IUpdatableSystem, IDisposable public void Dispose() { - Array.Clear(_particlePositions); - Array.Clear(_particleVelocities); - Array.Clear(_particleLifetimes); } - private Vector2[] _particlePositions, _particleVelocities; - private float[] _particleLifetimes; - 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/MathUtils.cs b/Voile/Source/Utils/MathUtils.cs index 551166b..9f8fcbc 100644 --- a/Voile/Source/Utils/MathUtils.cs +++ b/Voile/Source/Utils/MathUtils.cs @@ -19,10 +19,10 @@ namespace Voile { t = Math.Clamp(t, 0f, 1f); - byte r = (byte)(colorA.R + (colorB.R - colorA.R) * t); - byte g = (byte)(colorA.G + (colorB.G - colorA.G) * t); - byte b = (byte)(colorA.B + (colorB.B - colorA.B) * t); - byte a = (byte)(colorA.A + (colorB.A - colorA.A) * t); + byte r = (byte)Lerp(colorA.R, colorB.R, t); + byte g = (byte)Lerp(colorA.G, colorB.G, t); + byte b = (byte)Lerp(colorA.B, colorB.B, t); + byte a = (byte)Lerp(colorA.A, colorB.A, t); return new Color(r, g, b, a); }