From 851abd7c90b4a1e431fbd7d2d0bbe82c5fbee7fc Mon Sep 17 00:00:00 2001 From: dnesov Date: Tue, 15 Oct 2024 02:44:12 +0200 Subject: [PATCH] WIP: ResourceManager refactor, hot reloading using ResourceRef, API changes. --- TODO.md | 4 +- TestGame/Resources/test_emitter.toml | 11 ++ TestGame/TestGame.cs | 33 ++-- Voile/Source/Resources/Font.cs | 7 +- Voile/Source/Resources/Loaders/FontLoader.cs | 8 +- .../Resources/Loaders/IResourceLoader.cs | 8 - .../Resources/Loaders/ResourceLoader.cs | 69 ++++++++ Voile/Source/Resources/Loaders/SoundLoader.cs | 6 +- .../Resources/Loaders/Texture2dLoader.cs | 6 +- Voile/Source/Resources/Resource.cs | 40 ++--- Voile/Source/Resources/ResourceManager.cs | 152 +++++++++++++----- Voile/Source/Resources/Sound.cs | 6 +- Voile/Source/Resources/Texture2d.cs | 7 +- .../SceneGraph/Resources/SerializedScene.cs | 6 +- Voile/Source/Systems/ParticleSystem.cs | 147 +++++++++++++---- Voile/Voile.csproj | 1 + 16 files changed, 394 insertions(+), 117 deletions(-) create mode 100644 TestGame/Resources/test_emitter.toml delete mode 100644 Voile/Source/Resources/Loaders/IResourceLoader.cs create mode 100644 Voile/Source/Resources/Loaders/ResourceLoader.cs diff --git a/TODO.md b/TODO.md index 209be8b..db1326a 100644 --- a/TODO.md +++ b/TODO.md @@ -4,9 +4,9 @@ - ~~Add and implement interfaces for systems (ISystem, IUpdatableSystem, etc.)~~ - Minimize amount of possible null references. -- Serialization and deserialization of Resources from and to TOML files. +- TextDataResource providing a convenient wrapper around TOML data files. - Add documentation for common classes. -- Hot reloading of resources. +- ~~Hot reloading of resources.~~ ## I/O diff --git a/TestGame/Resources/test_emitter.toml b/TestGame/Resources/test_emitter.toml new file mode 100644 index 0000000..e697410 --- /dev/null +++ b/TestGame/Resources/test_emitter.toml @@ -0,0 +1,11 @@ +[ParticleEmitterSettings] + +MaxParticles = 1024 +EmitRadius = 128 +LifeTime = 0.5 +Direction = { x = 0.0, y = 1.0 } +LinearVelocity = 980.0 +ScaleBegin = 1.0 +ScaleEnd = 0.0 +ColorBegin = { r = 0.0, g = 1.0, b = 0.0, a = 1.0 } +ColorEnd = { r = 1.0, g = 0.0, b = 0.0, a = 1.0 } diff --git a/TestGame/TestGame.cs b/TestGame/TestGame.cs index 2c96d0e..1f11579 100644 --- a/TestGame/TestGame.cs +++ b/TestGame/TestGame.cs @@ -15,16 +15,25 @@ public class TestGame : Game { InitializeDefault(); _particleSystem = new ParticleSystem(); + + ResourceManager.AddResourceLoaderAssociation(new ParticleEmitterSettingsResourceLoader()); + + Input.AddInputMapping("reload", new InputAction[] { new KeyInputAction(KeyboardKey.R) }); } protected override void LoadResources() { - if (!ResourceManager.TryLoad("my_sound", "sounds/test_sound.ogg", out Sound? _testSound)) - { + // if (!ResourceManager.TryLoad("my_sound", "sounds/test_sound.ogg", out Sound? _testSound)) + // { - } + // } - if (!ResourceManager.TryLoad("inter_regular", "fonts/Inter-Regular.ttf", out Font? _font)) + // if (!ResourceManager.TryLoad("inter_regular", "fonts/Inter-Regular.ttf", out Font? _font)) + // { + + // } + + if (!ResourceManager.TryLoad("test_emitter.toml", out _emitterSettings)) { } @@ -32,19 +41,19 @@ public class TestGame : Game protected override void Ready() { - _particleSystem!.CreateEmitter(Renderer.WindowSize / 2, new ParticleEmitterSettings() - { - ColorBegin = Color.Green, - ColorEnd = Color.Red, - EmitRadius = 128, - MaxParticles = 256 - }); + _emitterId = _particleSystem!.CreateEmitter(Renderer.WindowSize / 2, _emitterSettings); } protected override void Run() { while (Renderer.ShouldRun) { + if (Input.IsActionPressed("reload")) + { + ResourceManager.Reload(); + _particleSystem!.RestartEmitter(_emitterId); + } + _particleSystem!.Update(Renderer.FrameTime); Renderer.BeginFrame(); @@ -77,5 +86,7 @@ public class TestGame : Game } private ParticleSystem? _particleSystem; + private int _emitterId; + private ResourceRef? _emitterSettings; private Logger _logger = new(nameof(TestGame)); } \ No newline at end of file diff --git a/Voile/Source/Resources/Font.cs b/Voile/Source/Resources/Font.cs index 1a1b5e6..b0f0e9d 100644 --- a/Voile/Source/Resources/Font.cs +++ b/Voile/Source/Resources/Font.cs @@ -7,7 +7,12 @@ public class Font : Resource /// internal int Handle { get; set; } = -1; public int Size { get; set; } = 16; - public Font(string path, byte[] buffer) : base(path, buffer) + + public byte[]? Buffer { get; private set; } + public long BufferSize { get; set; } + + public Font(string path, byte[] buffer) : base(path) { + Buffer = buffer; } } \ No newline at end of file diff --git a/Voile/Source/Resources/Loaders/FontLoader.cs b/Voile/Source/Resources/Loaders/FontLoader.cs index 52bf30f..da0923d 100644 --- a/Voile/Source/Resources/Loaders/FontLoader.cs +++ b/Voile/Source/Resources/Loaders/FontLoader.cs @@ -1,13 +1,15 @@ + namespace Voile.Resources; -public class FontLoader : IResourceLoader +public class FontLoader : ResourceLoader { - public IEnumerable SupportedExtensions => new string[] + public override IEnumerable SupportedExtensions => new string[] { "ttf" }; - public Font Load(string path) + + protected override Font LoadResource(string path) { byte[] fileBuffer = File.ReadAllBytes(path); var result = new Font(path, fileBuffer); diff --git a/Voile/Source/Resources/Loaders/IResourceLoader.cs b/Voile/Source/Resources/Loaders/IResourceLoader.cs deleted file mode 100644 index c67cb1e..0000000 --- a/Voile/Source/Resources/Loaders/IResourceLoader.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Voile.Resources -{ - public interface IResourceLoader where T : Resource - { - public IEnumerable SupportedExtensions { get; } - public T Load(string path); - } -} \ No newline at end of file diff --git a/Voile/Source/Resources/Loaders/ResourceLoader.cs b/Voile/Source/Resources/Loaders/ResourceLoader.cs new file mode 100644 index 0000000..23a556a --- /dev/null +++ b/Voile/Source/Resources/Loaders/ResourceLoader.cs @@ -0,0 +1,69 @@ + +using System.Diagnostics.CodeAnalysis; + +namespace Voile.Resources +{ + public abstract class ResourceLoader where T : Resource + { + public abstract IEnumerable SupportedExtensions { get; } + + public Guid Load(string path) + { + var resource = LoadResource(path); + var guid = Guid.NewGuid(); + + var oldResourceGuid = _loadedResources.FirstOrDefault(loadedResource => loadedResource.Value.Path == path).Key; + + if (_loadedResources.ContainsKey(oldResourceGuid)) + { + _loadedResources[oldResourceGuid] = resource; + return oldResourceGuid; + } + + _loadedResources.Add(guid, resource); + + return guid; + } + + public void Reload() + { + foreach (var loadedResource in _loadedResources) + { + Load(loadedResource.Value.Path); + } + } + + public bool TryGet(Guid resourceGuid, [NotNullWhen(true)] out T? resource) + { + resource = default; + + if (!_loadedResources.ContainsKey(resourceGuid)) + { + return false; + } + + resource = _loadedResources[resourceGuid]; + + return true; + } + + public bool TryUnload(Guid resourceGuid) + { + if (!_loadedResources.ContainsKey(resourceGuid)) + { + return false; + } + + var resource = _loadedResources[resourceGuid]; + + _loadedResources.Remove(resourceGuid); + resource.Dispose(); + + return true; + } + + protected abstract T LoadResource(string path); + + protected Dictionary _loadedResources = new(); + } +} \ No newline at end of file diff --git a/Voile/Source/Resources/Loaders/SoundLoader.cs b/Voile/Source/Resources/Loaders/SoundLoader.cs index 294390a..c8e2e5b 100644 --- a/Voile/Source/Resources/Loaders/SoundLoader.cs +++ b/Voile/Source/Resources/Loaders/SoundLoader.cs @@ -2,14 +2,14 @@ using StbVorbisSharp; namespace Voile.Resources { - public class SoundLoader : IResourceLoader + public class SoundLoader : ResourceLoader { - public IEnumerable SupportedExtensions => new string[] + public override IEnumerable SupportedExtensions => new string[] { "ogg" }; - public Sound Load(string path) + protected override Sound LoadResource(string path) { Vorbis vorbis; Sound result; diff --git a/Voile/Source/Resources/Loaders/Texture2dLoader.cs b/Voile/Source/Resources/Loaders/Texture2dLoader.cs index 7715942..cffaf47 100644 --- a/Voile/Source/Resources/Loaders/Texture2dLoader.cs +++ b/Voile/Source/Resources/Loaders/Texture2dLoader.cs @@ -3,16 +3,16 @@ using StbImageSharp; namespace Voile { - public class Texture2dLoader : IResourceLoader + public class Texture2dLoader : ResourceLoader { - public IEnumerable SupportedExtensions => new string[] + public override IEnumerable SupportedExtensions => new string[] { ".png", ".jpg", ".jpeg" }; - public Texture2d Load(string path) + protected override Texture2d LoadResource(string path) { ImageResult image; using (var stream = File.OpenRead(path)) diff --git a/Voile/Source/Resources/Resource.cs b/Voile/Source/Resources/Resource.cs index 32d829c..bb6b78d 100644 --- a/Voile/Source/Resources/Resource.cs +++ b/Voile/Source/Resources/Resource.cs @@ -1,34 +1,34 @@ -using System.Text.Json.Serialization; +using Voile.Resources; namespace Voile { + /// + /// Wraps a reference to an asset of a given type. + /// + /// + public sealed class ResourceRef where T : Resource + { + public readonly Guid Guid = Guid.Empty; + public bool HasValue => Guid != Guid.Empty; + public T Value => ResourceManager.GetResource(Guid); + + public ResourceRef(Guid guid) + { + Guid = guid; + } + } + public abstract class Resource : IDisposable { - public Guid Guid { get; set; } = Guid.NewGuid(); + public string Path { get; private set; } = string.Empty; - public string? Path { get => _path; set => _path = value; } - - [JsonIgnore] - public byte[]? Buffer { get => _buffer; set => _buffer = value; } - - [JsonIgnore] - public long BufferSize { get; set; } - - public Resource(string path, byte[] buffer) + public Resource(string path) { - _path = path; - _buffer = buffer; - - BufferSize = buffer.Length; + Path = path; } public void Dispose() { - Buffer = null; - Path = null; } - - private string? _path; - private byte[]? _buffer; } } \ No newline at end of file diff --git a/Voile/Source/Resources/ResourceManager.cs b/Voile/Source/Resources/ResourceManager.cs index be9de2c..c71a560 100644 --- a/Voile/Source/Resources/ResourceManager.cs +++ b/Voile/Source/Resources/ResourceManager.cs @@ -6,9 +6,13 @@ namespace Voile.Resources { public class ResourceManager : IDisposable { - public string ResourceRoot { get; set; } = "Resources/"; + public static string ResourceRoot { get; set; } = "Resources/"; - public bool TryLoad(string resourceId, string path, [NotNullWhen(true)] out T? result) where T : Resource + public static Action? OnLoadRequested; + public static Action? OnUnloadRequested; + public static Action? OnReloaded; + + public static bool TryLoad(string path, [NotNullWhen(true)] out ResourceRef? result) where T : Resource { T? resource = default; result = null; @@ -22,9 +26,9 @@ namespace Voile.Resources return false; } - _logger.Info($"Loading {path} as {typeof(T)} with id \"{resourceId}\"..."); + _logger.Info($"Loading {path} as {typeof(T)}..."); - if (!TryGetLoader(out IResourceLoader? loader)) + if (!TryGetLoader(out ResourceLoader? loader)) { return false; } @@ -37,34 +41,58 @@ namespace Voile.Resources _logger.Error($"Extension {extension} is not supported!"); } - if (loader.Load(fullPath) is not T loadedResource) + var resourceGuid = loader.Load(fullPath); + + if (!loader.TryGet(resourceGuid, out T? loadedResource)) { + _logger.Error($"Failed to load resource at path \"{path}\"!"); return false; } resource = loadedResource; - _loadedResources.Add(resourceId, resource); - _logger.Info($"\"{resourceId}\" was loaded successfully."); + _resourcePathMap.Add(path, resourceGuid); - result = loadedResource; + _logger.Info($"\"{path}\" ({resourceGuid}) was loaded successfully."); + + result = new ResourceRef(resourceGuid); return true; } + public void Reload() + { + _logger.Info("Reloading resources."); + OnReloaded?.Invoke(); + } + + // TODO public bool TryUnload(string resourceId) { _logger.Info($"Unloading resource with id \"{resourceId}\"..."); - if (!_loadedResources.ContainsKey(resourceId)) - { - _logger.Error($"Cannot unload resource with id \"{resourceId}\": resource doesn't exist!"); - return false; - } + // if (!_resourceStringMap.TryGetValue(resourceId, out Guid guid)) + // { + // _logger.Error($"Resource with ID \"{resourceId}\" doesn't exist!"); + // return false; + // } - var resource = _loadedResources[resourceId]; + return true; + } - _loadedResources.Remove(resourceId); - resource.Dispose(); + // TODO + public bool TryUnload(Guid resourceGuid) + { + _logger.Info($"Unloading resource with guid \"{resourceGuid}\"..."); + + // if (!_loadedResources.ContainsKey(resourceGuid)) + // { + // _logger.Error($"Cannot unload resource with id \"{resourceGuid}\": resource doesn't exist!"); + // return false; + // } + // var resource = _loadedResources[resourceGuid]; + + // _loadedResources.Remove(resourceGuid); + // resource.Dispose(); return true; } @@ -94,11 +122,17 @@ namespace Voile.Resources return false; } - var expectedResource = _loadedResources[resourceId]; + var resourceGuid = _resourcePathMap[resourceId]; - if (expectedResource is not T loadedResource) + if (!TryGetLoader(out ResourceLoader? loader)) { - _logger.Error($"Given resource is of wrong type: provided {typeof(T)}, expected {expectedResource.GetType()}!"); + _logger.Error($"No loader available for type {typeof(T)}!"); + return false; + } + + if (!loader.TryGet(resourceGuid, out T? loadedResource)) + { + _logger.Error($"No resource with id {resourceId} found!"); return false; } @@ -107,11 +141,29 @@ namespace Voile.Resources return true; } - public bool IsResourceLoaded(string resourceId) => _loadedResources.ContainsKey(resourceId); + public static T GetResource(Guid resourceGuid) where T : Resource + { + if (!TryGetLoader(out ResourceLoader? loader)) + { + throw new Exception($"No loader available for type {typeof(T)}!"); + } - public void AddResourceLoaderAssociation(IResourceLoader loader) where T : Resource + if (!loader.TryGet(resourceGuid, out T? loadedResource)) + { + throw new Exception($"No resource with GUID \"{resourceGuid}\" found!"); + } + + return loadedResource; + } + + public bool IsResourceLoaded(string resourceId) => _resourcePathMap.ContainsKey(resourceId); + + public static void AddResourceLoaderAssociation(ResourceLoader loader) where T : Resource { _logger.Info($"Added resource loader association for {typeof(T)}."); + + OnReloaded += loader.Reload; + _resourceLoaderAssociations.Add(typeof(T), loader); } @@ -121,7 +173,28 @@ namespace Voile.Resources _resourceSaverAssociations.Add(typeof(T), saver); } - private bool TryGetLoader([NotNullWhen(true)] out IResourceLoader? loader) where T : Resource + public void EnableFileWatching() + { + _fileWatcher = new FileSystemWatcher(ResourceRoot); + + _fileWatcher.NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Security + | NotifyFilters.Size; + + _fileWatcher.IncludeSubdirectories = true; + _fileWatcher.EnableRaisingEvents = true; + + _fileWatcher.Changed += FileSystemChanged; + + _logger.Info("File watching enabled."); + } + + private static bool TryGetLoader([NotNullWhen(true)] out ResourceLoader? loader) where T : Resource { loader = null; @@ -131,13 +204,9 @@ namespace Voile.Resources return false; } - loader = _resourceLoaderAssociations[typeof(T)] as IResourceLoader; + loader = _resourceLoaderAssociations[typeof(T)] as ResourceLoader; - if (loader is not null) - { - _logger.Info($"Using {loader.GetType()} for loading..."); - } - else + if (loader is null) { _logger.Error($"No loader association found for {typeof(T)}."); return false; @@ -171,31 +240,42 @@ namespace Voile.Resources return true; } - public void Dispose() + private void FileSystemChanged(object sender, FileSystemEventArgs e) { - foreach (var resource in _loadedResources) + if (e.ChangeType != WatcherChangeTypes.Changed) { - TryUnload(resource.Key); + return; } - GC.SuppressFinalize(this); + Reload(); + } + + public void Dispose() + { + // foreach (var loader in _) + // { + // TryUnload(resource.Key); + // } + + // GC.SuppressFinalize(this); } - private Logger _logger = new(nameof(ResourceManager)); + private static Logger _logger = new(nameof(ResourceManager)); - private readonly Dictionary _resourceLoaderAssociations = new() + private static readonly Dictionary _resourceLoaderAssociations = new() { {typeof(Sound), new SoundLoader()}, {typeof(Texture2d), new Texture2dLoader()}, {typeof(Font), new FontLoader()} }; - private readonly Dictionary _resourceSaverAssociations = new() + private static readonly Dictionary _resourceSaverAssociations = new() { }; - private Dictionary _loadedResources = new(); + private FileSystemWatcher? _fileWatcher; + private static Dictionary _resourcePathMap = new(); } } \ No newline at end of file diff --git a/Voile/Source/Resources/Sound.cs b/Voile/Source/Resources/Sound.cs index aeac10a..9db8739 100644 --- a/Voile/Source/Resources/Sound.cs +++ b/Voile/Source/Resources/Sound.cs @@ -5,8 +5,12 @@ namespace Voile public SoundFormat Format { get; set; } public int SampleRate { get; set; } - public Sound(string path, byte[] buffer) : base(path, buffer) + public byte[]? Buffer { get; private set; } + public long BufferSize { get; set; } + + public Sound(string path, byte[] buffer) : base(path) { + Buffer = buffer; } } diff --git a/Voile/Source/Resources/Texture2d.cs b/Voile/Source/Resources/Texture2d.cs index 2f02202..dbea271 100644 --- a/Voile/Source/Resources/Texture2d.cs +++ b/Voile/Source/Resources/Texture2d.cs @@ -9,9 +9,14 @@ namespace Voile public int Width { get; set; } public int Height { get; set; } public int Mipmaps { get; set; } = 1; + + public byte[]? Buffer { get; private set; } + public long BufferSize { get; set; } + public TextureFormat Format { get; set; } = TextureFormat.UncompressedR8G8B8A8; - public Texture2d(string path, byte[] buffer) : base(path, buffer) + public Texture2d(string path, byte[] buffer) : base(path) { + Buffer = buffer; } public static Texture2d Empty => new Texture2d(string.Empty, new byte[] { }); diff --git a/Voile/Source/SceneGraph/Resources/SerializedScene.cs b/Voile/Source/SceneGraph/Resources/SerializedScene.cs index 40fecae..4ef3834 100644 --- a/Voile/Source/SceneGraph/Resources/SerializedScene.cs +++ b/Voile/Source/SceneGraph/Resources/SerializedScene.cs @@ -6,8 +6,12 @@ namespace Voile.SceneGraph { public Dictionary Layers { get; set; } - public SerializedScene(string path, byte[] buffer) : base(path, buffer) + public byte[]? Buffer { get; set; } + public long BufferSize { get; set; } + + public SerializedScene(string path, byte[] buffer) : base(path) { + Buffer = buffer; } } diff --git a/Voile/Source/Systems/ParticleSystem.cs b/Voile/Source/Systems/ParticleSystem.cs index 76e54fb..f0b51e7 100644 --- a/Voile/Source/Systems/ParticleSystem.cs +++ b/Voile/Source/Systems/ParticleSystem.cs @@ -1,5 +1,7 @@ +using System.ComponentModel; using System.Numerics; -using Voile.Rendering; +using Tommy; +using Voile.Resources; using Voile.Utils; namespace Voile.Systems; @@ -41,18 +43,71 @@ public class ParticleEmitterSettings public float Damping { get; set; } = 1.0f; } +public class ParticleEmitterSettingsResource : Resource +{ + public ParticleEmitterSettings Settings { get; private set; } + public ParticleEmitterSettingsResource(string path, ParticleEmitterSettings settings) : base(path) + { + Settings = settings; + } + +} + +public class ParticleEmitterSettingsResourceLoader : ResourceLoader +{ + public override IEnumerable SupportedExtensions => new string[] { + "toml" + }; + + protected override ParticleEmitterSettingsResource LoadResource(string path) + { + // TODO: this is ugly, better to make some sort of wrapper API for TOML files. + var settings = new ParticleEmitterSettings(); + // Parse into a node + using (StreamReader reader = File.OpenText(path)) + { + // Parse the table + TomlTable table = TOML.Parse(reader); + + if (!table.HasKey("ParticleEmitterSettings")) + { + Console.WriteLine("Particle emitter settings doesnt have a header!"); + } + + if (table["ParticleEmitterSettings"]["MaxParticles"] is TomlInteger maxParticles) + { + settings.MaxParticles = (int)maxParticles.Value; + } + + if (table["ParticleEmitterSettings"]["EmitRadius"] is TomlInteger emitRadius) + { + settings.EmitRadius = (int)emitRadius.Value; + } + + if (table["ParticleEmitterSettings"]["LifeTime"] is TomlFloat lifetime) + { + settings.LifeTime = (float)lifetime.Value; + } + + } + + return new ParticleEmitterSettingsResource(path, settings); + } +} + public class ParticleEmitter : IUpdatableSystem { public ReadOnlySpan Particles => _particles.AsSpan(); public Vector2 OriginPosition => _originPosition; - public ParticleEmitterSettings Settings => _settings; + public ParticleEmitterSettings Settings => _settingsResource.Value.Settings; + public int ParticleArrayOffset => _particles.Offset; - public ParticleEmitter(Vector2 originPosition, ParticleEmitterSettings settings, ArraySegment particles) + public ParticleEmitter(Vector2 originPosition, ResourceRef settingsResource, ArraySegment particles) { _originPosition = originPosition; - _settings = settings; - _maxParticles = _settings.MaxParticles; + _settingsResource = settingsResource; + _maxParticles = _settingsResource.Value.Settings.MaxParticles; _particleIndex = _maxParticles - 1; _particles = particles; @@ -60,9 +115,29 @@ public class ParticleEmitter : IUpdatableSystem _random = new LehmerRandom(); } + public void Restart(ArraySegment particles) + { + // foreach (var particle in _particles) + // { + // particle.LifeTimeRemaining = 0.0f; + // } + + for (int i = 0; i < _particles.Count; i++) + { + var particle = _particles[i]; + particle.LifeTimeRemaining = 0.0f; + } + + _particles = particles; + + _maxParticles = Settings.MaxParticles; + _particleIndex = _maxParticles - 1; + } + public void Update(double deltaTime) { - var rate = (int)MathUtils.Lerp(1, _maxParticles, _settings.Explosiveness); + var rate = (int)MathUtils.Lerp(1, _maxParticles, Settings.Explosiveness); + for (int i = 0; i < rate; i++) { Emit(); @@ -71,19 +146,19 @@ public class ParticleEmitter : IUpdatableSystem for (int i = 0; i < _maxParticles; i++) { var particle = _particles[i]; - if (!particle.Alive) continue; + // if (!particle.Alive) continue; - if (particle.LifeTimeRemaining <= 0.0f) - { - particle.Alive = false; - 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.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); @@ -91,7 +166,7 @@ public class ParticleEmitter : IUpdatableSystem var color = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t); particle.ColorArgb = color.Argb; - particle.Velocity -= particle.Velocity * _settings.Damping * (float)deltaTime; + particle.Velocity -= particle.Velocity * Settings.Damping * (float)deltaTime; _particles[i] = particle; } @@ -101,16 +176,17 @@ public class ParticleEmitter : IUpdatableSystem { Particle particle = _particles[_particleIndex]; if (!(particle.LifeTimeRemaining <= 0)) return; - particle.Alive = true; + + // particle.Alive = true; particle.Position = GetEmitPosition(); - particle.Velocity = _settings.Direction * _settings.LinearVelocity; + particle.Velocity = Settings.Direction * Settings.LinearVelocity; - particle.Velocity += Vector2.One * _settings.LinearVelocityRandom * ((float)_random.NextDouble() - 0.5f); + 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.AngularVelocity = Settings.AngularVelocity; + particle.AngularVelocity += 1f * Settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f); - particle.LifeTime = _settings.LifeTime; + particle.LifeTime = Settings.LifeTime; particle.LifeTimeRemaining = particle.LifeTime; _particles[_particleIndex] = particle; @@ -119,9 +195,11 @@ public class ParticleEmitter : IUpdatableSystem private Vector2 GetEmitPosition() { + var settings = _settingsResource.Value.Settings; + // 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 radius = (float)Math.Sqrt(_random.NextDouble()) * settings.EmitRadius; float x = radius * (float)Math.Cos(angle); float y = radius * (float)Math.Sin(angle); @@ -134,7 +212,7 @@ public class ParticleEmitter : IUpdatableSystem private int _particleIndex; private Vector2 _originPosition = Vector2.Zero; private ArraySegment _particles; - private ParticleEmitterSettings _settings; + private ResourceRef _settingsResource; } public class ParticleSystem : IUpdatableSystem, IDisposable @@ -152,12 +230,14 @@ public class ParticleSystem : IUpdatableSystem, IDisposable _particles = new Particle[ParticleLimit]; } - public void CreateEmitter(Vector2 originPosition, ParticleEmitterSettings settings) + 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; + return -1; } var particles = new ArraySegment(_particles, _emitterSliceOffset, settings.MaxParticles); @@ -173,11 +253,25 @@ public class ParticleSystem : IUpdatableSystem, IDisposable particle.LifeTime = settings.LifeTime; } - var emitter = new ParticleEmitter(originPosition, settings, particles); + var emitter = new ParticleEmitter(originPosition, settingsResource, particles); _emitters.Add(emitter); - _emitterSliceOffset += settings.MaxParticles; + + return _emitters.Count - 1; + } + + public void RestartEmitter(int id) + { + if (id > _emitters.Count - 1) + { + throw new ArgumentException($"Emitter with id {id} doesn't exist!"); + } + + var emitter = _emitters[id]; + var particles = new ArraySegment(_particles, emitter.ParticleArrayOffset, emitter.Settings.MaxParticles); + + emitter.Restart(particles); } public void Update(double deltaTime) @@ -196,7 +290,6 @@ public class ParticleSystem : IUpdatableSystem, IDisposable private void CleanupParticles() => Array.Clear(_particles); private Particle[] _particles; private int _particleIndex; - private int _emitterSliceOffset; private List _emitters = new(); diff --git a/Voile/Voile.csproj b/Voile/Voile.csproj index 9f98fa8..dc9d981 100644 --- a/Voile/Voile.csproj +++ b/Voile/Voile.csproj @@ -12,6 +12,7 @@ +