WIP: ResourceManager refactor, hot reloading using ResourceRef, API changes.
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
- ~~Add and implement interfaces for systems (ISystem, IUpdatableSystem, etc.)~~
|
- ~~Add and implement interfaces for systems (ISystem, IUpdatableSystem, etc.)~~
|
||||||
- Minimize amount of possible null references.
|
- 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.
|
- Add documentation for common classes.
|
||||||
- Hot reloading of resources.
|
- ~~Hot reloading of resources.~~
|
||||||
|
|
||||||
## I/O
|
## I/O
|
||||||
|
|
||||||
|
|||||||
11
TestGame/Resources/test_emitter.toml
Normal file
11
TestGame/Resources/test_emitter.toml
Normal file
@@ -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 }
|
||||||
@@ -15,16 +15,25 @@ public class TestGame : Game
|
|||||||
{
|
{
|
||||||
InitializeDefault();
|
InitializeDefault();
|
||||||
_particleSystem = new ParticleSystem();
|
_particleSystem = new ParticleSystem();
|
||||||
|
|
||||||
|
ResourceManager.AddResourceLoaderAssociation(new ParticleEmitterSettingsResourceLoader());
|
||||||
|
|
||||||
|
Input.AddInputMapping("reload", new InputAction[] { new KeyInputAction(KeyboardKey.R) });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadResources()
|
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()
|
protected override void Ready()
|
||||||
{
|
{
|
||||||
_particleSystem!.CreateEmitter(Renderer.WindowSize / 2, new ParticleEmitterSettings()
|
_emitterId = _particleSystem!.CreateEmitter(Renderer.WindowSize / 2, _emitterSettings);
|
||||||
{
|
|
||||||
ColorBegin = Color.Green,
|
|
||||||
ColorEnd = Color.Red,
|
|
||||||
EmitRadius = 128,
|
|
||||||
MaxParticles = 256
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Run()
|
protected override void Run()
|
||||||
{
|
{
|
||||||
while (Renderer.ShouldRun)
|
while (Renderer.ShouldRun)
|
||||||
{
|
{
|
||||||
|
if (Input.IsActionPressed("reload"))
|
||||||
|
{
|
||||||
|
ResourceManager.Reload();
|
||||||
|
_particleSystem!.RestartEmitter(_emitterId);
|
||||||
|
}
|
||||||
|
|
||||||
_particleSystem!.Update(Renderer.FrameTime);
|
_particleSystem!.Update(Renderer.FrameTime);
|
||||||
|
|
||||||
Renderer.BeginFrame();
|
Renderer.BeginFrame();
|
||||||
@@ -77,5 +86,7 @@ public class TestGame : Game
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ParticleSystem? _particleSystem;
|
private ParticleSystem? _particleSystem;
|
||||||
|
private int _emitterId;
|
||||||
|
private ResourceRef<ParticleEmitterSettingsResource>? _emitterSettings;
|
||||||
private Logger _logger = new(nameof(TestGame));
|
private Logger _logger = new(nameof(TestGame));
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,12 @@ public class Font : Resource
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal int Handle { get; set; } = -1;
|
internal int Handle { get; set; } = -1;
|
||||||
public int Size { get; set; } = 16;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
|
|
||||||
namespace Voile.Resources;
|
namespace Voile.Resources;
|
||||||
|
|
||||||
public class FontLoader : IResourceLoader<Font>
|
public class FontLoader : ResourceLoader<Font>
|
||||||
{
|
{
|
||||||
public IEnumerable<string> SupportedExtensions => new string[]
|
public override IEnumerable<string> SupportedExtensions => new string[]
|
||||||
{
|
{
|
||||||
"ttf"
|
"ttf"
|
||||||
};
|
};
|
||||||
|
|
||||||
public Font Load(string path)
|
|
||||||
|
protected override Font LoadResource(string path)
|
||||||
{
|
{
|
||||||
byte[] fileBuffer = File.ReadAllBytes(path);
|
byte[] fileBuffer = File.ReadAllBytes(path);
|
||||||
var result = new Font(path, fileBuffer);
|
var result = new Font(path, fileBuffer);
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Voile.Resources
|
|
||||||
{
|
|
||||||
public interface IResourceLoader<T> where T : Resource
|
|
||||||
{
|
|
||||||
public IEnumerable<string> SupportedExtensions { get; }
|
|
||||||
public T Load(string path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
69
Voile/Source/Resources/Loaders/ResourceLoader.cs
Normal file
69
Voile/Source/Resources/Loaders/ResourceLoader.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
|
namespace Voile.Resources
|
||||||
|
{
|
||||||
|
public abstract class ResourceLoader<T> where T : Resource
|
||||||
|
{
|
||||||
|
public abstract IEnumerable<string> 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<Guid, T> _loadedResources = new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@ using StbVorbisSharp;
|
|||||||
|
|
||||||
namespace Voile.Resources
|
namespace Voile.Resources
|
||||||
{
|
{
|
||||||
public class SoundLoader : IResourceLoader<Sound>
|
public class SoundLoader : ResourceLoader<Sound>
|
||||||
{
|
{
|
||||||
public IEnumerable<string> SupportedExtensions => new string[]
|
public override IEnumerable<string> SupportedExtensions => new string[]
|
||||||
{
|
{
|
||||||
"ogg"
|
"ogg"
|
||||||
};
|
};
|
||||||
|
|
||||||
public Sound Load(string path)
|
protected override Sound LoadResource(string path)
|
||||||
{
|
{
|
||||||
Vorbis vorbis;
|
Vorbis vorbis;
|
||||||
Sound result;
|
Sound result;
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ using StbImageSharp;
|
|||||||
|
|
||||||
namespace Voile
|
namespace Voile
|
||||||
{
|
{
|
||||||
public class Texture2dLoader : IResourceLoader<Texture2d>
|
public class Texture2dLoader : ResourceLoader<Texture2d>
|
||||||
{
|
{
|
||||||
public IEnumerable<string> SupportedExtensions => new string[]
|
public override IEnumerable<string> SupportedExtensions => new string[]
|
||||||
{
|
{
|
||||||
".png",
|
".png",
|
||||||
".jpg",
|
".jpg",
|
||||||
".jpeg"
|
".jpeg"
|
||||||
};
|
};
|
||||||
|
|
||||||
public Texture2d Load(string path)
|
protected override Texture2d LoadResource(string path)
|
||||||
{
|
{
|
||||||
ImageResult image;
|
ImageResult image;
|
||||||
using (var stream = File.OpenRead(path))
|
using (var stream = File.OpenRead(path))
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
using System.Text.Json.Serialization;
|
using Voile.Resources;
|
||||||
|
|
||||||
namespace Voile
|
namespace Voile
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Wraps a reference to an asset of a given type.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
public sealed class ResourceRef<T> where T : Resource
|
||||||
|
{
|
||||||
|
public readonly Guid Guid = Guid.Empty;
|
||||||
|
public bool HasValue => Guid != Guid.Empty;
|
||||||
|
public T Value => ResourceManager.GetResource<T>(Guid);
|
||||||
|
|
||||||
|
public ResourceRef(Guid guid)
|
||||||
|
{
|
||||||
|
Guid = guid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class Resource : IDisposable
|
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; }
|
public Resource(string path)
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public byte[]? Buffer { get => _buffer; set => _buffer = value; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
public long BufferSize { get; set; }
|
|
||||||
|
|
||||||
public Resource(string path, byte[] buffer)
|
|
||||||
{
|
{
|
||||||
_path = path;
|
Path = path;
|
||||||
_buffer = buffer;
|
|
||||||
|
|
||||||
BufferSize = buffer.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Buffer = null;
|
}
|
||||||
Path = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? _path;
|
|
||||||
private byte[]? _buffer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,9 +6,13 @@ namespace Voile.Resources
|
|||||||
{
|
{
|
||||||
public class ResourceManager : IDisposable
|
public class ResourceManager : IDisposable
|
||||||
{
|
{
|
||||||
public string ResourceRoot { get; set; } = "Resources/";
|
public static string ResourceRoot { get; set; } = "Resources/";
|
||||||
|
|
||||||
public bool TryLoad<T>(string resourceId, string path, [NotNullWhen(true)] out T? result) where T : Resource
|
public static Action<string>? OnLoadRequested;
|
||||||
|
public static Action<Guid>? OnUnloadRequested;
|
||||||
|
public static Action? OnReloaded;
|
||||||
|
|
||||||
|
public static bool TryLoad<T>(string path, [NotNullWhen(true)] out ResourceRef<T>? result) where T : Resource
|
||||||
{
|
{
|
||||||
T? resource = default;
|
T? resource = default;
|
||||||
result = null;
|
result = null;
|
||||||
@@ -22,9 +26,9 @@ namespace Voile.Resources
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.Info($"Loading {path} as {typeof(T)} with id \"{resourceId}\"...");
|
_logger.Info($"Loading {path} as {typeof(T)}...");
|
||||||
|
|
||||||
if (!TryGetLoader(out IResourceLoader<T>? loader))
|
if (!TryGetLoader(out ResourceLoader<T>? loader))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -37,34 +41,58 @@ namespace Voile.Resources
|
|||||||
_logger.Error($"Extension {extension} is not supported!");
|
_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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
resource = loadedResource;
|
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<T>(resourceGuid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Reload()
|
||||||
|
{
|
||||||
|
_logger.Info("Reloading resources.");
|
||||||
|
OnReloaded?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
public bool TryUnload(string resourceId)
|
public bool TryUnload(string resourceId)
|
||||||
{
|
{
|
||||||
_logger.Info($"Unloading resource with id \"{resourceId}\"...");
|
_logger.Info($"Unloading resource with id \"{resourceId}\"...");
|
||||||
|
|
||||||
if (!_loadedResources.ContainsKey(resourceId))
|
// if (!_resourceStringMap.TryGetValue(resourceId, out Guid guid))
|
||||||
{
|
// {
|
||||||
_logger.Error($"Cannot unload resource with id \"{resourceId}\": resource doesn't exist!");
|
// _logger.Error($"Resource with ID \"{resourceId}\" doesn't exist!");
|
||||||
return false;
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resource = _loadedResources[resourceId];
|
// TODO
|
||||||
|
public bool TryUnload(Guid resourceGuid)
|
||||||
|
{
|
||||||
|
_logger.Info($"Unloading resource with guid \"{resourceGuid}\"...");
|
||||||
|
|
||||||
_loadedResources.Remove(resourceId);
|
// if (!_loadedResources.ContainsKey(resourceGuid))
|
||||||
resource.Dispose();
|
// {
|
||||||
|
// _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;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -94,11 +122,17 @@ namespace Voile.Resources
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedResource = _loadedResources[resourceId];
|
var resourceGuid = _resourcePathMap[resourceId];
|
||||||
|
|
||||||
if (expectedResource is not T loadedResource)
|
if (!TryGetLoader(out ResourceLoader<T>? 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +141,29 @@ namespace Voile.Resources
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsResourceLoaded(string resourceId) => _loadedResources.ContainsKey(resourceId);
|
public static T GetResource<T>(Guid resourceGuid) where T : Resource
|
||||||
|
{
|
||||||
|
if (!TryGetLoader(out ResourceLoader<T>? loader))
|
||||||
|
{
|
||||||
|
throw new Exception($"No loader available for type {typeof(T)}!");
|
||||||
|
}
|
||||||
|
|
||||||
public void AddResourceLoaderAssociation<T>(IResourceLoader<T> 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<T>(ResourceLoader<T> loader) where T : Resource
|
||||||
{
|
{
|
||||||
_logger.Info($"Added resource loader association for {typeof(T)}.");
|
_logger.Info($"Added resource loader association for {typeof(T)}.");
|
||||||
|
|
||||||
|
OnReloaded += loader.Reload;
|
||||||
|
|
||||||
_resourceLoaderAssociations.Add(typeof(T), loader);
|
_resourceLoaderAssociations.Add(typeof(T), loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,7 +173,28 @@ namespace Voile.Resources
|
|||||||
_resourceSaverAssociations.Add(typeof(T), saver);
|
_resourceSaverAssociations.Add(typeof(T), saver);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetLoader<T>([NotNullWhen(true)] out IResourceLoader<T>? 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<T>([NotNullWhen(true)] out ResourceLoader<T>? loader) where T : Resource
|
||||||
{
|
{
|
||||||
loader = null;
|
loader = null;
|
||||||
|
|
||||||
@@ -131,13 +204,9 @@ namespace Voile.Resources
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loader = _resourceLoaderAssociations[typeof(T)] as IResourceLoader<T>;
|
loader = _resourceLoaderAssociations[typeof(T)] as ResourceLoader<T>;
|
||||||
|
|
||||||
if (loader is not null)
|
if (loader is null)
|
||||||
{
|
|
||||||
_logger.Info($"Using {loader.GetType()} for loading...");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
_logger.Error($"No loader association found for {typeof(T)}.");
|
_logger.Error($"No loader association found for {typeof(T)}.");
|
||||||
return false;
|
return false;
|
||||||
@@ -171,31 +240,42 @@ namespace Voile.Resources
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void FileSystemChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.ChangeType != WatcherChangeTypes.Changed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Reload();
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
foreach (var resource in _loadedResources)
|
// foreach (var loader in _)
|
||||||
{
|
// {
|
||||||
TryUnload(resource.Key);
|
// TryUnload(resource.Key);
|
||||||
}
|
// }
|
||||||
|
|
||||||
GC.SuppressFinalize(this);
|
// GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Logger _logger = new(nameof(ResourceManager));
|
private static Logger _logger = new(nameof(ResourceManager));
|
||||||
|
|
||||||
private readonly Dictionary<Type, object> _resourceLoaderAssociations = new()
|
private static readonly Dictionary<Type, object> _resourceLoaderAssociations = new()
|
||||||
{
|
{
|
||||||
{typeof(Sound), new SoundLoader()},
|
{typeof(Sound), new SoundLoader()},
|
||||||
{typeof(Texture2d), new Texture2dLoader()},
|
{typeof(Texture2d), new Texture2dLoader()},
|
||||||
{typeof(Font), new FontLoader()}
|
{typeof(Font), new FontLoader()}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly Dictionary<Type, object> _resourceSaverAssociations = new()
|
private static readonly Dictionary<Type, object> _resourceSaverAssociations = new()
|
||||||
{
|
{
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private Dictionary<string, Resource> _loadedResources = new();
|
private FileSystemWatcher? _fileWatcher;
|
||||||
|
private static Dictionary<string, Guid> _resourcePathMap = new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,12 @@ namespace Voile
|
|||||||
public SoundFormat Format { get; set; }
|
public SoundFormat Format { get; set; }
|
||||||
public int SampleRate { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,14 @@ namespace Voile
|
|||||||
public int Width { get; set; }
|
public int Width { get; set; }
|
||||||
public int Height { get; set; }
|
public int Height { get; set; }
|
||||||
public int Mipmaps { get; set; } = 1;
|
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 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[] { });
|
public static Texture2d Empty => new Texture2d(string.Empty, new byte[] { });
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ namespace Voile.SceneGraph
|
|||||||
{
|
{
|
||||||
public Dictionary<string, Layer> Layers { get; set; }
|
public Dictionary<string, Layer> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Voile.Rendering;
|
using Tommy;
|
||||||
|
using Voile.Resources;
|
||||||
using Voile.Utils;
|
using Voile.Utils;
|
||||||
|
|
||||||
namespace Voile.Systems;
|
namespace Voile.Systems;
|
||||||
@@ -41,18 +43,71 @@ public class ParticleEmitterSettings
|
|||||||
public float Damping { get; set; } = 1.0f;
|
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<ParticleEmitterSettingsResource>
|
||||||
|
{
|
||||||
|
public override IEnumerable<string> 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 class ParticleEmitter : IUpdatableSystem
|
||||||
{
|
{
|
||||||
public ReadOnlySpan<Particle> Particles => _particles.AsSpan();
|
public ReadOnlySpan<Particle> Particles => _particles.AsSpan();
|
||||||
public Vector2 OriginPosition => _originPosition;
|
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<Particle> particles)
|
public ParticleEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource, ArraySegment<Particle> particles)
|
||||||
{
|
{
|
||||||
_originPosition = originPosition;
|
_originPosition = originPosition;
|
||||||
|
|
||||||
_settings = settings;
|
_settingsResource = settingsResource;
|
||||||
_maxParticles = _settings.MaxParticles;
|
_maxParticles = _settingsResource.Value.Settings.MaxParticles;
|
||||||
_particleIndex = _maxParticles - 1;
|
_particleIndex = _maxParticles - 1;
|
||||||
|
|
||||||
_particles = particles;
|
_particles = particles;
|
||||||
@@ -60,9 +115,29 @@ public class ParticleEmitter : IUpdatableSystem
|
|||||||
_random = new LehmerRandom();
|
_random = new LehmerRandom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Restart(ArraySegment<Particle> 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)
|
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++)
|
for (int i = 0; i < rate; i++)
|
||||||
{
|
{
|
||||||
Emit();
|
Emit();
|
||||||
@@ -71,19 +146,19 @@ public class ParticleEmitter : IUpdatableSystem
|
|||||||
for (int i = 0; i < _maxParticles; i++)
|
for (int i = 0; i < _maxParticles; i++)
|
||||||
{
|
{
|
||||||
var particle = _particles[i];
|
var particle = _particles[i];
|
||||||
if (!particle.Alive) continue;
|
// if (!particle.Alive) continue;
|
||||||
|
|
||||||
if (particle.LifeTimeRemaining <= 0.0f)
|
// if (particle.LifeTimeRemaining <= 0.0f)
|
||||||
{
|
// {
|
||||||
particle.Alive = false;
|
// particle.Alive = false;
|
||||||
continue;
|
// continue;
|
||||||
}
|
// }
|
||||||
|
|
||||||
particle.LifeTimeRemaining = Math.Clamp(particle.LifeTimeRemaining - (float)deltaTime, 0.0f, particle.LifeTime);
|
particle.LifeTimeRemaining = Math.Clamp(particle.LifeTimeRemaining - (float)deltaTime, 0.0f, particle.LifeTime);
|
||||||
|
|
||||||
var t = particle.LifeTimeRemaining / 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.Position += particle.Velocity * (float)deltaTime;
|
||||||
particle.Rotation += particle.AngularVelocity * (float)deltaTime;
|
particle.Rotation += particle.AngularVelocity * (float)deltaTime;
|
||||||
particle.Scale = MathUtils.Lerp(Settings.ScaleEnd, Settings.ScaleBegin, t);
|
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);
|
var color = MathUtils.LerpColor(Settings.ColorEnd, Settings.ColorBegin, t);
|
||||||
particle.ColorArgb = color.Argb;
|
particle.ColorArgb = color.Argb;
|
||||||
|
|
||||||
particle.Velocity -= particle.Velocity * _settings.Damping * (float)deltaTime;
|
particle.Velocity -= particle.Velocity * Settings.Damping * (float)deltaTime;
|
||||||
|
|
||||||
_particles[i] = particle;
|
_particles[i] = particle;
|
||||||
}
|
}
|
||||||
@@ -101,16 +176,17 @@ public class ParticleEmitter : IUpdatableSystem
|
|||||||
{
|
{
|
||||||
Particle particle = _particles[_particleIndex];
|
Particle particle = _particles[_particleIndex];
|
||||||
if (!(particle.LifeTimeRemaining <= 0)) return;
|
if (!(particle.LifeTimeRemaining <= 0)) return;
|
||||||
particle.Alive = true;
|
|
||||||
|
// particle.Alive = true;
|
||||||
particle.Position = GetEmitPosition();
|
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 = Settings.AngularVelocity;
|
||||||
particle.AngularVelocity += 1f * _settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
|
particle.AngularVelocity += 1f * Settings.AngularVelocityRandom * ((float)_random.NextDouble() - 0.5f);
|
||||||
|
|
||||||
particle.LifeTime = _settings.LifeTime;
|
particle.LifeTime = Settings.LifeTime;
|
||||||
particle.LifeTimeRemaining = particle.LifeTime;
|
particle.LifeTimeRemaining = particle.LifeTime;
|
||||||
|
|
||||||
_particles[_particleIndex] = particle;
|
_particles[_particleIndex] = particle;
|
||||||
@@ -119,9 +195,11 @@ public class ParticleEmitter : IUpdatableSystem
|
|||||||
|
|
||||||
private Vector2 GetEmitPosition()
|
private Vector2 GetEmitPosition()
|
||||||
{
|
{
|
||||||
|
var settings = _settingsResource.Value.Settings;
|
||||||
|
|
||||||
// https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
|
// https://gamedev.stackexchange.com/questions/26713/calculate-random-points-pixel-within-a-circle-image
|
||||||
var angle = _random.NextDouble() * Math.PI * 2;
|
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 x = radius * (float)Math.Cos(angle);
|
||||||
float y = radius * (float)Math.Sin(angle);
|
float y = radius * (float)Math.Sin(angle);
|
||||||
@@ -134,7 +212,7 @@ public class ParticleEmitter : IUpdatableSystem
|
|||||||
private int _particleIndex;
|
private int _particleIndex;
|
||||||
private Vector2 _originPosition = Vector2.Zero;
|
private Vector2 _originPosition = Vector2.Zero;
|
||||||
private ArraySegment<Particle> _particles;
|
private ArraySegment<Particle> _particles;
|
||||||
private ParticleEmitterSettings _settings;
|
private ResourceRef<ParticleEmitterSettingsResource> _settingsResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ParticleSystem : IUpdatableSystem, IDisposable
|
public class ParticleSystem : IUpdatableSystem, IDisposable
|
||||||
@@ -152,12 +230,14 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
|
|||||||
_particles = new Particle[ParticleLimit];
|
_particles = new Particle[ParticleLimit];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void CreateEmitter(Vector2 originPosition, ParticleEmitterSettings settings)
|
public int CreateEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource)
|
||||||
{
|
{
|
||||||
|
var settings = settingsResource.Value.Settings;
|
||||||
|
|
||||||
if (_emitterSliceOffset + settings.MaxParticles >= ParticleLimit - 1)
|
if (_emitterSliceOffset + settings.MaxParticles >= ParticleLimit - 1)
|
||||||
{
|
{
|
||||||
_logger.Error("Cannot create an emitter! Reached particle limit.");
|
_logger.Error("Cannot create an emitter! Reached particle limit.");
|
||||||
return;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var particles = new ArraySegment<Particle>(_particles, _emitterSliceOffset, settings.MaxParticles);
|
var particles = new ArraySegment<Particle>(_particles, _emitterSliceOffset, settings.MaxParticles);
|
||||||
@@ -173,11 +253,25 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
|
|||||||
particle.LifeTime = settings.LifeTime;
|
particle.LifeTime = settings.LifeTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
var emitter = new ParticleEmitter(originPosition, settings, particles);
|
var emitter = new ParticleEmitter(originPosition, settingsResource, particles);
|
||||||
|
|
||||||
_emitters.Add(emitter);
|
_emitters.Add(emitter);
|
||||||
|
|
||||||
_emitterSliceOffset += settings.MaxParticles;
|
_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<Particle>(_particles, emitter.ParticleArrayOffset, emitter.Settings.MaxParticles);
|
||||||
|
|
||||||
|
emitter.Restart(particles);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(double deltaTime)
|
public void Update(double deltaTime)
|
||||||
@@ -196,7 +290,6 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
|
|||||||
private void CleanupParticles() => Array.Clear(_particles);
|
private void CleanupParticles() => Array.Clear(_particles);
|
||||||
private Particle[] _particles;
|
private Particle[] _particles;
|
||||||
private int _particleIndex;
|
private int _particleIndex;
|
||||||
|
|
||||||
private int _emitterSliceOffset;
|
private int _emitterSliceOffset;
|
||||||
|
|
||||||
private List<ParticleEmitter> _emitters = new();
|
private List<ParticleEmitter> _emitters = new();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<PackageReference Include="Silk.NET.WebGPU" Version="2.20.0" />
|
<PackageReference Include="Silk.NET.WebGPU" Version="2.20.0" />
|
||||||
<PackageReference Include="Silk.NET.WebGPU.Native.WGPU" Version="2.20.0" />
|
<PackageReference Include="Silk.NET.WebGPU.Native.WGPU" Version="2.20.0" />
|
||||||
<PackageReference Include="Silk.NET.Windowing" Version="2.20.0" />
|
<PackageReference Include="Silk.NET.Windowing" Version="2.20.0" />
|
||||||
|
<PackageReference Include="Tommy" Version="3.1.2" />
|
||||||
<PackageReference Include="Voile.Fmod" Version="0.2.2.8" />
|
<PackageReference Include="Voile.Fmod" Version="0.2.2.8" />
|
||||||
<PackageReference Include="ImGui.NET" Version="1.89.4" />
|
<PackageReference Include="ImGui.NET" Version="1.89.4" />
|
||||||
<PackageReference Include="Raylib-cs" Version="4.2.0.1" />
|
<PackageReference Include="Raylib-cs" Version="4.2.0.1" />
|
||||||
|
|||||||
Reference in New Issue
Block a user