TomlDataReader, documentation updates, move ParticleSystem to Voile.Systems.Particles.

This commit is contained in:
2024-10-15 19:56:30 +02:00
parent ecd752e961
commit d5601c9dea
78 changed files with 18013 additions and 1143 deletions

View File

@@ -26,11 +26,11 @@ namespace Voile.Audio
int channels = 0;
if (sound.Format == SoundFormat.Mono)
if (sound.Channel == SoundChannel.Mono)
{
channels = 1;
}
else if (sound.Format == SoundFormat.Stereo)
else if (sound.Channel == SoundChannel.Stereo)
{
channels = 2;
}

View File

@@ -4,6 +4,9 @@ using Voile.Resources;
namespace Voile
{
/// <summary>
/// Entry point for the Voile game.
/// </summary>
public abstract class Game
{
/// <summary>

View File

@@ -4,7 +4,7 @@ using Raylib_cs;
namespace Voile.Input
{
/// <summary>
/// An input system implemented using Raylib's API. Used by default together with <see cref="RaylibRenderSystem"/>.
/// An input system implemented using Raylib's API. Used by default together with <see cref="Rendering.RaylibRenderSystem"/>.
/// </summary>
public class RaylibInputSystem : InputSystem
{

View File

@@ -123,7 +123,8 @@ namespace Voile.Rendering
public override void BeginBlended(BlendMode blendMode)
{
Raylib.BeginBlendMode((Raylib_cs.BlendMode)blendMode);
var rayBlend = (Raylib_cs.BlendMode)blendMode;
Raylib.BeginBlendMode(rayBlend);
}
public override void EndBlended()

View File

@@ -0,0 +1,76 @@
using System.Numerics;
namespace Voile.Resources.DataReaders
{
/// <summary>
/// Reads data from a specified stream.
/// </summary>
public interface IStreamDataReader
{
/// <summary>
/// Read data from a specified stream.
/// </summary>
/// <param name="data">Stream with data.</param>
void Read(Stream data);
}
/// <summary>
/// Validates data integrity or schema.
/// </summary>
public interface IDataValidator
{
/// <summary>
/// Determines if data specified is valid and can be safely read.
/// </summary>
/// <returns>Returns true if data is valid.</returns>
bool Valid();
}
/// <summary>
/// Gets primitive type data from a key/value based format.
/// </summary>
public interface IStreamKeyValueGetter
{
/// <summary>
/// Get an int from this data getter.
/// </summary>
/// <param name="key">Key of the value.</param>
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
/// <returns></returns>
int GetInt(string key, int defaultValue = 0);
/// <summary>
/// Get a long from this data getter.
/// </summary>
/// <param name="key">Key of the value.</param>
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
/// <returns></returns>
long GetLong(string key, long defaultValue = 0);
/// <summary>
/// Get a float from this data getter.
/// </summary>
/// <param name="key">Key of the value.</param>
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
/// <returns></returns>
float GetFloat(string key, float defaultValue = 0.0f);
/// <summary>
/// Get a double from this data getter.
/// </summary>
/// <param name="key">Key of the value.</param>
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
/// <returns></returns>
double GetDouble(string key, double defaultValue = 0.0);
/// <summary>
/// Get a Voile.Color from this data getter.
/// </summary>
/// <param name="key">Key of the value.</param>
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
/// <returns></returns>
Color GetColor(string key, Color defaultValue);
/// <summary>
/// Get a System.Numerics.Vector2 from this data getter.
/// </summary>
/// <param name="key">Key of the value.</param>
/// <param name="defaultValue">Default value in case this getter fails to get data.</param>
/// <returns></returns>
Vector2 GetVector2(string key, Vector2 defaultValue);
}
}

View File

@@ -0,0 +1,218 @@
using System.Numerics;
using Tommy;
namespace Voile.Resources.DataReaders;
/// <summary>
/// Reads key/value data from a TOML file.
/// </summary>
public class TomlDataReader : IStreamDataReader, IDataValidator, IStreamKeyValueGetter, IDisposable
{
public string ExpectedHeader { get; private set; } = string.Empty;
public TomlDataReader(string expectedHeader)
{
ExpectedHeader = expectedHeader;
}
public void Read(Stream data)
{
if (data is not FileStream fs)
{
throw new ArgumentException("Toml data reader only supports file streams.");
}
_fs = fs;
using (var reader = new StreamReader(_fs))
{
_table = TOML.Parse(reader);
_valid = _table.HasKey(ExpectedHeader);
}
}
public int GetInt(string key, int defaultValue = 0)
{
if (_table is null)
{
return defaultValue;
}
var dataTable = _table[ExpectedHeader];
if (!dataTable.HasKey(key))
{
return defaultValue;
}
var tableValue = dataTable[key];
if (!tableValue.IsInteger)
{
return defaultValue;
}
return tableValue.AsInteger;
}
public long GetLong(string key, long defaultValue = 0)
{
if (_table is null)
{
return defaultValue;
}
var dataTable = _table[ExpectedHeader];
if (!dataTable.HasKey(key))
{
return defaultValue;
}
var tableValue = dataTable[key];
if (!tableValue.IsInteger)
{
return defaultValue;
}
return tableValue.AsInteger.Value;
}
public float GetFloat(string key, float defaultValue = 0)
{
if (_table is null)
{
return defaultValue;
}
var dataTable = _table[ExpectedHeader];
if (!dataTable.HasKey(key))
{
return defaultValue;
}
var tableValue = dataTable[key];
if (!tableValue.IsFloat)
{
if (tableValue.IsInteger) return (float)tableValue.AsInteger.Value;
return defaultValue;
}
return tableValue.AsFloat;
}
public double GetDouble(string key, double defaultValue = 0)
{
if (_table is null)
{
return defaultValue;
}
var dataTable = _table[ExpectedHeader];
if (!dataTable.HasKey(key))
{
return defaultValue;
}
if (!dataTable.IsFloat)
{
return defaultValue;
}
return dataTable.AsFloat.Value;
}
public Color GetColor(string key, Color defaultValue)
{
if (_table is null)
{
return defaultValue;
}
var dataTable = _table[ExpectedHeader];
if (!dataTable.HasKey(key))
{
return defaultValue;
}
var colorNode = dataTable[key];
if (colorNode.IsInteger)
{
return new Color(colorNode.AsInteger);
}
else if (colorNode.IsArray)
{
var colorArray = colorNode.AsArray;
var rNode = colorArray[0];
var gNode = colorArray[1];
var bNode = colorArray[2];
var r = rNode.IsInteger ? rNode.AsInteger : 0;
var g = gNode.IsInteger ? gNode.AsInteger : 0;
var b = bNode.IsInteger ? bNode.AsInteger : 0;
int a = 255;
if (colorArray.RawArray.Count == 4)
{
var aNode = colorArray[3];
a = aNode.IsInteger ? aNode.AsInteger : 0;
}
return new Color((byte)r, (byte)g, (byte)b, (byte)a);
}
else if (colorNode.IsString)
{
var colorHexString = colorNode.AsString.Value;
return Color.FromHexString(colorHexString);
}
else
{
throw new ArgumentException("Color can only be represented as an array of integers in the range of 0-255, array of floats (0-1), hexadecimal, or hex string.");
}
}
public Vector2 GetVector2(string key, Vector2 defaultValue)
{
if (_table is null)
{
return defaultValue;
}
var dataTable = _table[ExpectedHeader];
if (!dataTable.HasKey(key))
{
return defaultValue;
}
var vector2Node = dataTable[key];
if (!vector2Node.IsArray)
{
return defaultValue;
}
var vector2Array = vector2Node.AsArray;
return new Vector2(vector2Array[0], vector2Array[1]);
}
public bool Valid() => _valid;
public void Dispose()
{
_fs?.Dispose();
}
private TomlTable? _table;
private FileStream? _fs;
private bool _valid;
}

View File

@@ -1,5 +1,8 @@
namespace Voile;
/// <summary>
/// Represents font data.
/// </summary>
public class Font : Resource
{
/// <summary>

View File

@@ -3,10 +3,22 @@ using System.Diagnostics.CodeAnalysis;
namespace Voile.Resources
{
/// <summary>
/// Loads resources from various sources and prepares them to be used for Voile.
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class ResourceLoader<T> where T : Resource
{
/// <summary>
/// File extensions that are supported by this loader.
/// </summary>
public abstract IEnumerable<string> SupportedExtensions { get; }
/// <summary>
/// Loads a resource to this resource loader's resource list.
/// </summary>
/// <param name="path">File system path to the resource to load.</param>
/// <returns>A <see cref="Guid"/> of the loaded resource that can be later retrieved with <see cref="TryGet"/>.</returns>
public Guid Load(string path)
{
var resource = LoadResource(path);
@@ -25,6 +37,9 @@ namespace Voile.Resources
return guid;
}
/// <summary>
/// Reloads resources loaded by this resource loader.
/// </summary>
public void Reload()
{
foreach (var loadedResource in _loadedResources)
@@ -33,6 +48,12 @@ namespace Voile.Resources
}
}
/// <summary>
/// Gets a resource from a GUID.
/// </summary>
/// <param name="resourceGuid">GUID of the resource to get.</param>
/// <param name="resource">Retrieved resource. Will return a default resource if resource retrieval was not successful.</param>
/// <returns>True if resource retrieval was successful.</returns>
public bool TryGet(Guid resourceGuid, [NotNullWhen(true)] out T? resource)
{
resource = default;
@@ -47,6 +68,11 @@ namespace Voile.Resources
return true;
}
/// <summary>
/// Unloads a resource.
/// </summary>
/// <param name="resourceGuid">GUID of a resource to unload.</param>
/// <returns>True if unloading was successful, otherwise false.</returns>
public bool TryUnload(Guid resourceGuid)
{
if (!_loadedResources.ContainsKey(resourceGuid))
@@ -62,6 +88,11 @@ namespace Voile.Resources
return true;
}
/// <summary>
/// Load steps specific to this resource loader.
/// </summary>
/// <param name="path">File system path to the resource to load.</param>
/// <returns>Loaded resource.</returns>
protected abstract T LoadResource(string path);
protected Dictionary<Guid, T> _loadedResources = new();

View File

@@ -42,7 +42,7 @@ namespace Voile.Resources
result = new Sound(path, audioData)
{
Format = (SoundFormat)vorbis.Channels - 1,
Channel = (SoundChannel)vorbis.Channels - 1,
SampleRate = vorbis.SampleRate,
BufferSize = length
};

View File

@@ -3,6 +3,9 @@ using StbImageSharp;
namespace Voile
{
/// <summary>
/// Loads a 2D texture from PNG or JPG files using StbImageSharp.
/// </summary>
public class Texture2dLoader : ResourceLoader<Texture2d>
{
public override IEnumerable<string> SupportedExtensions => new string[]

View File

@@ -8,8 +8,14 @@ namespace Voile
/// <typeparam name="T"></typeparam>
public sealed class ResourceRef<T> where T : Resource
{
/// <summary>
/// Resource GUID this ResourceRef maps to.
/// </summary>
public readonly Guid Guid = Guid.Empty;
public bool HasValue => Guid != Guid.Empty;
/// <summary>
/// Retrieve a reference.
/// </summary>
public T Value => ResourceManager.GetResource<T>(Guid);
public ResourceRef(Guid guid)
@@ -18,8 +24,14 @@ namespace Voile
}
}
/// <summary>
/// Represents data usable by Voile.
/// </summary>
public abstract class Resource : IDisposable
{
/// <summary>
/// Path to this resource.
/// </summary>
public string Path { get; private set; } = string.Empty;
public Resource(string path)

View File

@@ -4,14 +4,36 @@ using Voile.Utils;
namespace Voile.Resources
{
/// <summary>
/// Manages resource loading and lifetime, and wraps around multiple available ResourceLoaders.
/// </summary>
public class ResourceManager : IDisposable
{
/// <summary>
/// Root path for resources to manipulate resources at.
/// </summary>
public static string ResourceRoot { get; set; } = "Resources/";
/// <summary>
/// Emits when a resource gets loaded.
/// </summary>
public static Action<string>? OnLoadRequested;
/// <summary>
/// Emits when a resource gets unloaded.
/// </summary>
public static Action<Guid>? OnUnloadRequested;
/// <summary>
/// Emits when <see cref="ResourceManager"/> requested reload of all loaded resources.
/// </summary>
public static Action? OnReloaded;
/// <summary>
/// Loads a resource from a given file system path.
/// </summary>
/// <typeparam name="T">Resource type to load</typeparam>
/// <param name="path">File system path to the resource to load.</param>
/// <param name="result">A ResourceRef for the loaded resource.</param>
/// <returns>True if resource was loaded successfully, otherwise will log an error and return false.</returns>
public static bool TryLoad<T>(string path, [NotNullWhen(true)] out ResourceRef<T>? result) where T : Resource
{
T? resource = default;
@@ -59,6 +81,9 @@ namespace Voile.Resources
return true;
}
/// <summary>
/// Reload all resources that are currently loaded.
/// </summary>
public void Reload()
{
_logger.Info("Reloading resources.");
@@ -112,17 +137,24 @@ namespace Voile.Resources
return true;
}
public bool TryGetResource<T>(string resourceId, [NotNullWhen(true)] out T? resource) where T : Resource
/// <summary>
/// Gets a <see cref="Resource"/> by a file system path.
/// </summary>
/// <typeparam name="T">Type of <see cref="Resource"/> to load.</typeparam>
/// <param name="path">Path to the resource.</param>
/// <param name="resource">Retrieved resource. Otherwise null if nothing got retrieved.</param>
/// <returns>True if resource got successfully retrieved, otherwise false.</returns>
public bool TryGetResource<T>(string path, [NotNullWhen(true)] out T? resource) where T : Resource
{
resource = null;
if (!IsResourceLoaded(resourceId))
if (!IsResourceLoaded(path))
{
_logger.Error($"Resource \"{resourceId}\" has not yet been loaded!");
_logger.Error($"Resource \"{path}\" has not yet been loaded!");
return false;
}
var resourceGuid = _resourcePathMap[resourceId];
var resourceGuid = _resourcePathMap[path];
if (!TryGetLoader(out ResourceLoader<T>? loader))
{
@@ -132,7 +164,7 @@ namespace Voile.Resources
if (!loader.TryGet(resourceGuid, out T? loadedResource))
{
_logger.Error($"No resource with id {resourceId} found!");
_logger.Error($"No resource with id {path} found!");
return false;
}
@@ -141,6 +173,13 @@ namespace Voile.Resources
return true;
}
/// <summary>
/// Gets a <see cref="Resource"/> by resource's <see cref="Guid"/>.
/// </summary>
/// <typeparam name="T">Type of <see cref="Resource"/> to load.</typeparam>
/// <param name="resourceGuid">Resource's GUID.</param>
/// <param name="resource">Retrieved resource. Otherwise null if nothing got retrieved.</param>
/// <returns>True if resource got successfully retrieved, otherwise false.</returns>
public static T GetResource<T>(Guid resourceGuid) where T : Resource
{
if (!TryGetLoader(out ResourceLoader<T>? loader))
@@ -155,9 +194,18 @@ namespace Voile.Resources
return loadedResource;
}
/// <summary>
/// Determines if a resource is currently loaded.
/// </summary>
/// <param name="path">Path to the resource.</param>
/// <returns>True if a resource at the specified path is loaded, otherwise false.</returns>
public bool IsResourceLoaded(string path) => _resourcePathMap.ContainsKey(path);
public bool IsResourceLoaded(string resourceId) => _resourcePathMap.ContainsKey(resourceId);
/// <summary>
/// Adds a resource loader associated with a resource type.
/// </summary>
/// <typeparam name="T">A type of <see cref="Resource"/>.</typeparam>
/// <param name="loader">Loader to use for loading this resource type.</param>
public static void AddResourceLoaderAssociation<T>(ResourceLoader<T> loader) where T : Resource
{
_logger.Info($"Added resource loader association for {typeof(T)}.");
@@ -167,12 +215,20 @@ namespace Voile.Resources
_resourceLoaderAssociations.Add(typeof(T), loader);
}
/// <summary>
/// Adds a resource saver associated with a resource type.
/// </summary>
/// <typeparam name="T">A type of <see cref="Resource"/>.</typeparam>
/// <param name="saver">saver to use for saving this resource type.</param>
public void AddResourceSaverAssociation<T>(IResourceSaver<T> saver) where T : Resource
{
_logger.Info($"Added resource saver association for {typeof(T)}.");
_resourceSaverAssociations.Add(typeof(T), saver);
}
/// <summary>
/// Enables file watching. <see cref="ResourceManager"/> will automaticallly reload resources when they get changed.
/// </summary>
public void EnableFileWatching()
{
_fileWatcher = new FileSystemWatcher(ResourceRoot);
@@ -252,15 +308,9 @@ namespace Voile.Resources
public void Dispose()
{
// foreach (var loader in _)
// {
// TryUnload(resource.Key);
// }
// GC.SuppressFinalize(this);
}
private static Logger _logger = new(nameof(ResourceManager));
private static readonly Dictionary<Type, object> _resourceLoaderAssociations = new()

View File

@@ -1,8 +1,11 @@
namespace Voile
{
/// <summary>
/// Represents raw audio samples.
/// </summary>
public class Sound : Resource
{
public SoundFormat Format { get; set; }
public SoundChannel Channel { get; set; }
public int SampleRate { get; set; }
public byte[]? Buffer { get; private set; }
@@ -14,7 +17,10 @@ namespace Voile
}
}
public enum SoundFormat
/// <summary>
/// Channel type contained within a sound.
/// </summary>
public enum SoundChannel
{
Mono,
Stereo

View File

@@ -0,0 +1,18 @@
namespace Voile.Resources;
public class TextDataResource : Resource
{
public TextDataResource(string path) : base(path)
{
}
public void AddValue<T>(string key, T value) where T : struct
{
}
public void GetValue<T>(string key, T value) where T : struct
{
}
}

View File

@@ -1,5 +1,8 @@
namespace Voile
{
/// <summary>
/// A two-dimensional texture stored on the GPU.
/// </summary>
public class Texture2d : Resource
{
/// <summary>

View File

@@ -19,5 +19,9 @@ public interface IStartableSystem<T>
public interface IUpdatableSystem
{
/// <summary>
/// Updates this system.
/// </summary>
/// <param name="deltaTime">Time step.</param>
void Update(double deltaTime);
}

View File

@@ -1,10 +1,9 @@
using System.ComponentModel;
using System.Numerics;
using Tommy;
using Voile.Resources;
using Voile.Resources.DataReaders;
using Voile.Utils;
namespace Voile.Systems;
namespace Voile.Systems.Particles;
public struct Particle
{
@@ -53,6 +52,9 @@ public class ParticleEmitterSettingsResource : Resource
}
/// <summary>
/// Loads <see cref="ParticleEmitterSettingsResource"/> from a provided TOML data file.
/// </summary>
public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmitterSettingsResource>
{
public override IEnumerable<string> SupportedExtensions => new string[] {
@@ -63,45 +65,57 @@ public class ParticleEmitterSettingsResourceLoader : ResourceLoader<ParticleEmit
{
// 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))
using (var reader = new TomlDataReader("ParticleEmitterSettings"))
{
// 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;
}
reader.Read(File.Open(path, FileMode.Open));
settings.MaxParticles = reader.GetInt("MaxParticles");
settings.EmitRadius = reader.GetInt("EmitRadius");
settings.LifeTime = reader.GetFloat("LifeTime", 1.0f);
settings.Explosiveness = reader.GetFloat("Explosiveness");
settings.Direction = reader.GetVector2("Direction", Vector2.Zero);
settings.LinearVelocity = reader.GetFloat("LinearVelocity", 980f);
settings.AngularVelocity = reader.GetFloat("AngularVelocity");
settings.AngularVelocityRandom = reader.GetFloat("AngularVelocityRandom", 1.0f);
settings.LinearVelocityRandom = reader.GetFloat("LinearVelocityRandom", 1.0f);
settings.Gravity = reader.GetVector2("Gravity", Vector2.UnitY * 980f);
settings.ScaleBegin = reader.GetFloat("ScaleBegin");
settings.ScaleEnd = reader.GetFloat("ScaleEnd");
settings.ColorBegin = reader.GetColor("ColorBegin", Color.White);
settings.ColorEnd = reader.GetColor("ColorEnd", Color.Black);
settings.Damping = reader.GetFloat("Damping", 1.0f);
}
return new ParticleEmitterSettingsResource(path, settings);
}
}
/// <summary>
/// Emits and simulates particles from a provided particle segment.
/// </summary>
public class ParticleEmitter : IUpdatableSystem
{
/// <summary>
/// A segment of particles this emitter simulates.
/// </summary>
public ReadOnlySpan<Particle> Particles => _particles.AsSpan();
/// <summary>
/// Origin position in the world of this emitter.
/// </summary>
public Vector2 OriginPosition => _originPosition;
/// <summary>
/// <see cref="ParticleEmitterSettings"/> for this emitter.
/// </summary>
public ParticleEmitterSettings Settings => _settingsResource.Value.Settings;
public int ParticleArrayOffset => _particles.Offset;
/// <summary>
/// Constructs a new <see cref="ParticleEmitter"/>.
/// </summary>
/// <param name="originPosition">World origin position.</param>
/// <param name="settingsResource">Emitter settings resource.</param>
/// <param name="particles">Particle segment that this emitter will simulate.</param>
public ParticleEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource, ArraySegment<Particle> particles)
{
_originPosition = originPosition;
@@ -115,13 +129,12 @@ public class ParticleEmitter : IUpdatableSystem
_random = new LehmerRandom();
}
/// <summary>
/// Restart this emitter.
/// </summary>
/// <param name="particles">New particle segment.</param>
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];
@@ -134,6 +147,10 @@ public class ParticleEmitter : IUpdatableSystem
_particleIndex = _maxParticles - 1;
}
/// <summary>
/// Updates this emitter's simulation.
/// </summary>
/// <param name="deltaTime"></param>
public void Update(double deltaTime)
{
var rate = (int)MathUtils.Lerp(1, _maxParticles, Settings.Explosiveness);
@@ -146,13 +163,6 @@ public class ParticleEmitter : IUpdatableSystem
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);
@@ -215,6 +225,9 @@ public class ParticleEmitter : IUpdatableSystem
private ResourceRef<ParticleEmitterSettingsResource> _settingsResource;
}
/// <summary>
/// CPU based particle simulator.
/// </summary>
public class ParticleSystem : IUpdatableSystem, IDisposable
{
/// <summary>
@@ -222,14 +235,26 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
/// </summary>
public int ParticleLimit { get; set; } = 8192;
/// <summary>
/// List of particle emitters created for this ParticleSystem.
/// </summary>
public IReadOnlyList<ParticleEmitter> Emitters => _emitters;
/// <summary>
/// Constructs a new <see cref="ParticleSystem"/>.
/// </summary>
public ParticleSystem()
{
_particleIndex = ParticleLimit - 1;
_particles = new Particle[ParticleLimit];
}
/// <summary>
/// Creates an emitter from a <see cref="ParticleEmitterSettingsResource"/>.
/// </summary>
/// <param name="originPosition"></param>
/// <param name="settingsResource"></param>
/// <returns></returns>
public int CreateEmitter(Vector2 originPosition, ResourceRef<ParticleEmitterSettingsResource> settingsResource)
{
var settings = settingsResource.Value.Settings;
@@ -242,11 +267,6 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
var particles = new ArraySegment<Particle>(_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];
@@ -261,6 +281,11 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
return _emitters.Count - 1;
}
/// <summary>
/// Restarts an emitter.
/// </summary>
/// <param name="id">Id of an emitter to restart.</param>
/// <exception cref="ArgumentException"></exception>
public void RestartEmitter(int id)
{
if (id > _emitters.Count - 1)
@@ -274,6 +299,10 @@ public class ParticleSystem : IUpdatableSystem, IDisposable
emitter.Restart(particles);
}
/// <summary>
/// Updates this particle simulation. It is recommended to use a fixed timestep for the particle simulation for performance reasons.
/// </summary>
/// <param name="deltaTime"></param>
public void Update(double deltaTime)
{
foreach (var emitter in _emitters)

View File

@@ -78,6 +78,29 @@ namespace Voile
R = (hex & 0xFF) / 255.0f;
}
public static Color FromHexString(string hex)
{
if (hex.StartsWith("#"))
{
hex = hex[1..];
}
if (hex.Length == 6)
{
int rgb = int.Parse(hex, System.Globalization.NumberStyles.HexNumber);
return new Color(rgb);
}
else if (hex.Length == 8)
{
int rgba = int.Parse(hex, System.Globalization.NumberStyles.HexNumber);
return new Color(rgba);
}
else
{
throw new ArgumentException("Invalid hex color format. Use #RRGGBB or #RRGGBBAA.");
}
}
public Color Lightened(float amount)
{
var result = this;