diff --git a/Basalt.Core/Basalt.Core.csproj b/Basalt.Core/Basalt.Core.csproj
index eefeb29..a031b73 100644
--- a/Basalt.Core/Basalt.Core.csproj
+++ b/Basalt.Core/Basalt.Core.csproj
@@ -4,7 +4,7 @@
net8.0
enable
enable
- 1.8.1
+ 1.9.0
True
BasaltLogoBg.png
README.md
diff --git a/Basalt.Raylib/Basalt.Raylib.csproj b/Basalt.Raylib/Basalt.Raylib.csproj
index 16a629c..952ccd7 100644
--- a/Basalt.Raylib/Basalt.Raylib.csproj
+++ b/Basalt.Raylib/Basalt.Raylib.csproj
@@ -5,7 +5,7 @@
enable
enable
true
- 1.8.1
+ 1.9.0
True
BasaltLogoBg.png
README.md
diff --git a/Basalt.Raylib/Components/FirstPersonCameraController.cs b/Basalt.Raylib/Components/FirstPersonCameraController.cs
new file mode 100644
index 0000000..8cf10ea
--- /dev/null
+++ b/Basalt.Raylib/Components/FirstPersonCameraController.cs
@@ -0,0 +1,45 @@
+using Basalt.Common.Entities;
+using Basalt.Math;
+using System.Numerics;
+using static Raylib_cs.Raylib;
+
+namespace Basalt.Raylib.Components
+{
+ ///
+ /// Represents a first-person camera controller for Raylib.
+ ///
+ public class FirstPersonCameraController : RayCameraController
+ {
+ ///
+ /// The sensitivity of the camera controller.
+ ///
+ public float Sensitivity = 0.1f;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The entity associated with the camera controller.
+ public FirstPersonCameraController(Entity entity) : base(entity)
+ {
+ }
+
+ public override void OnUpdate()
+ {
+ if (!Enabled)
+ return;
+
+ Vector3 rotation = new(GetMouseDelta().X * Sensitivity, // Rotation: yaw
+ GetMouseDelta().Y * Sensitivity, // Rotation: pitch
+ 0.0f); // Rotation: roll
+
+ // Update the camera in raylib
+
+ camera.Position = Entity.Transform.Position;
+ camera.Target = camera.Position + Entity.Transform.Forward;
+
+ UpdateCameraPro(ref camera, Vector3.Zero, rotation, 0);
+
+ Entity.Transform.Rotation = BasaltMath.LookAtRotation(camera.Position, camera.Target, camera.Up);
+ }
+ }
+}
diff --git a/Basalt.Raylib/Components/Image.cs b/Basalt.Raylib/Components/Image.cs
index 4578538..34adec4 100644
--- a/Basalt.Raylib/Components/Image.cs
+++ b/Basalt.Raylib/Components/Image.cs
@@ -3,8 +3,8 @@
using Basalt.Common.Utils;
using Basalt.Raylib.Graphics;
using Raylib_cs;
-using static Raylib_cs.Raylib;
using System.Numerics;
+using static Raylib_cs.Raylib;
namespace Basalt.Raylib.Components
{
diff --git a/Basalt.Raylib/Components/ModelRenderer.cs b/Basalt.Raylib/Components/ModelRenderer.cs
index 78af068..5b140f5 100644
--- a/Basalt.Raylib/Components/ModelRenderer.cs
+++ b/Basalt.Raylib/Components/ModelRenderer.cs
@@ -72,7 +72,7 @@ public override unsafe void OnRender()
cube = ResourceCache.Instance.GetModel(ModelCacheKey)!.Value;
else
{
- throw new InvalidResourceKeyException(nameof(ModelCacheKey));
+ throw new InvalidResourceKeyException(nameof(ModelCacheKey), ModelCacheKey);
}
init = true;
}
diff --git a/Basalt.Raylib/Components/RayCameraController.cs b/Basalt.Raylib/Components/RayCameraController.cs
new file mode 100644
index 0000000..6593e35
--- /dev/null
+++ b/Basalt.Raylib/Components/RayCameraController.cs
@@ -0,0 +1,39 @@
+using Basalt.Common.Components;
+using Basalt.Common.Entities;
+using Raylib_cs;
+using System.Numerics;
+
+namespace Basalt.Raylib.Components
+{
+ ///
+ /// Represents a camera controller for Raylib using Camera3D.
+ ///
+ public class RayCameraController : CameraControllerBase
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The entity associated with the camera controller.
+ public RayCameraController(Entity entity) : base(entity)
+ {
+ Camera = new Camera3D
+ {
+ Position = Entity.Transform.Position,
+ Target = Entity.Transform.Position + Entity.Transform.Forward,
+ Up = new Vector3(0f, 1f, 0f),
+ FovY = 60f,
+ Projection = CameraProjection.Perspective
+ };
+
+ // Set the camera as the active camera in raylib
+ Raylib_cs.Raylib.UpdateCamera(ref camera, CameraMode.FirstPerson);
+ }
+
+ protected Camera3D camera;
+
+ ///
+ /// Gets or sets the camera.
+ ///
+ public override Camera3D Camera { get => camera; set => camera = value; }
+ }
+}
diff --git a/Basalt.Raylib/Components/RaylibParticleSystem.cs b/Basalt.Raylib/Components/RaylibParticleSystem.cs
new file mode 100644
index 0000000..7256c71
--- /dev/null
+++ b/Basalt.Raylib/Components/RaylibParticleSystem.cs
@@ -0,0 +1,55 @@
+using Basalt.Common.Components;
+using Basalt.Common.Entities;
+using Basalt.Common.Exceptions;
+using Basalt.Common.Utils;
+using Basalt.Raylib.Graphics;
+using Raylib_cs;
+using System.Numerics;
+
+namespace Basalt.Raylib.Components
+{
+ ///
+ /// A particle system that renders particles using Raylib.
+ ///
+ public class RaylibParticleSystem : BaseParticleSystem
+ {
+ private string _modelCacheKey = "sphere";
+ ///
+ /// The cache key for the model to use for rendering particles.
+ ///
+ public string ModelCacheKey
+ {
+ get => _modelCacheKey;
+ set
+ {
+ _modelCacheKey = value;
+ init = false;
+ }
+ }
+ bool init = false;
+ Model model;
+ public RaylibParticleSystem(Entity entity) : base(entity)
+ {
+ }
+
+ protected override void RenderParticles()
+ {
+ if (!init)
+ {
+ init = true;
+ var m = ResourceCache.Instance.GetModel(ModelCacheKey);
+ if (m == null)
+ {
+ throw new InvalidResourceKeyException(nameof(ModelCacheKey), ModelCacheKey);
+ }
+ model = m.Value;
+
+ }
+ foreach (var particle in _particles)
+ {
+ model.Transform = Raymath.MatrixRotateXYZ(Raymath.QuaternionToEuler(particle.Rotation));
+ Raylib_cs.Raylib.DrawModelEx(model, particle.Position, Vector3.UnitY, 0, particle.Scale, particle.Color.ToRaylibColor());
+ }
+ }
+ }
+}
diff --git a/Basalt.Raylib/ExtensionMethods.cs b/Basalt.Raylib/ExtensionMethods.cs
index 2f0d8bd..c461f37 100644
--- a/Basalt.Raylib/ExtensionMethods.cs
+++ b/Basalt.Raylib/ExtensionMethods.cs
@@ -65,5 +65,13 @@ public static EngineBuilder UseRaylibPreset(this EngineBuilder builder, WindowIn
builder.AddComponent();
return builder;
}
+
+
+ ///
+ /// Converts a to a .
+ ///
+ /// The color to be converted to a raylib color type
+ /// The original color as a
+ public static Raylib_cs.Color ToRaylibColor(this System.Drawing.Color color) => new(color.R, color.G, color.B, color.A);
}
}
diff --git a/Basalt.Raylib/Graphics/RaylibGraphicsEngine.cs b/Basalt.Raylib/Graphics/RaylibGraphicsEngine.cs
index 28ec454..5776ee6 100644
--- a/Basalt.Raylib/Graphics/RaylibGraphicsEngine.cs
+++ b/Basalt.Raylib/Graphics/RaylibGraphicsEngine.cs
@@ -28,6 +28,7 @@ public class RaylibGraphicsEngine : IGraphicsEngine
private EntityManager entityManager;
private ISoundSystem? soundSystem;
private IEventBus eventBus;
+ private RayCameraController? cameraController;
Shader PostProcessShader, LightShader;
bool ShouldRun = true;
@@ -107,14 +108,19 @@ public unsafe void Initialize()
public unsafe void Render()
{
Camera3D camera = new();
- var control = entityManager.GetEntities().FirstOrDefault(e => e.GetComponent() != null)?.GetComponent() ?? null;
+ var control = Engine.Instance.EntityManager.GetEntities().FirstOrDefault(e => e.GetComponents().FirstOrDefault(c => c.GetType().IsSubclassOf(typeof(RayCameraController))) is not null);
if (control == null)
{
- throw new NullReferenceException("No camera controller found in the scene.");
+ Engine.Instance.Logger?.LogError("No camera controller found. Using default controller instead.");
+ var entity = new Entity();
+ entity.AddComponent(new FirstPersonCameraController(entity));
+ Engine.CreateEntity(entity);
+ cameraController = entity.GetComponent()!;
+ control = entity;
}
-
- camera = control!.camera;
- control.OnStart();
+ else
+ cameraController = control!.GetComponents().FirstOrDefault(c => c.GetType().IsSubclassOf(typeof(RayCameraController))) as RayCameraController;
+ camera = cameraController!.Camera;
RenderTexture2D target = LoadRenderTexture(GetScreenWidth(), GetScreenHeight());
@@ -140,7 +146,6 @@ public unsafe void Render()
// Main game loop
while (ShouldRun)
{
- control.OnUpdate();
Time.DeltaTime = GetFrameTime();
@@ -173,7 +178,7 @@ public unsafe void Render()
foreach (var source in sources)
Rlights.UpdateLightValues(LightShader, source.Source);
- SetShaderValue(LightShader, LightShader.Locs[(int)ShaderLocationIndex.VectorView], control.Entity.Transform.Position, ShaderUniformDataType.Vec3);
+ SetShaderValue(LightShader, LightShader.Locs[(int)ShaderLocationIndex.VectorView], cameraController.Entity.Transform.Position, ShaderUniformDataType.Vec3);
}
//----------------------------------------------------------------------------------
@@ -184,7 +189,7 @@ public unsafe void Render()
BeginTextureMode(target);
ClearBackground(Color.Black);
- BeginMode3D(control.camera);
+ BeginMode3D(cameraController.Camera);
eventBus?.TriggerEvent(BasaltConstants.RenderEventKey);
@@ -259,13 +264,16 @@ public static T InvokeOnThread(Func delegateFunc)
private T invoke(Func delegateFunc) => delegateFunc();
-
-
public void Shutdown()
{
ShouldRun = false;
ResourceCache.Instance.UnloadRaylib();
logger?.LogWarning("Shutting down graphics engine...");
}
+
+ public void SetCameraController(RayCameraController newController)
+ {
+ cameraController = newController;
+ }
}
}
diff --git a/Basalt.Raylib/Sound/RaylibSoundSystem.cs b/Basalt.Raylib/Sound/RaylibSoundSystem.cs
index 6261c39..aedc15c 100644
--- a/Basalt.Raylib/Sound/RaylibSoundSystem.cs
+++ b/Basalt.Raylib/Sound/RaylibSoundSystem.cs
@@ -28,7 +28,7 @@ public void Initialize()
private void UpdateStream(object? sender, EventArgs args)
{
- if(MusicPlaying is not null)
+ if (MusicPlaying is not null)
{
UpdateMusicStream(MusicPlaying.Value);
}
@@ -49,7 +49,7 @@ public void Shutdown()
///
/// The logger to use for logging.
public RaylibSoundSystem()
- {
+ {
}
///
diff --git a/Basalt.TestField/Basalt.TestField.csproj b/Basalt.TestField/Basalt.TestField.csproj
index e4135ed..306d5ed 100644
--- a/Basalt.TestField/Basalt.TestField.csproj
+++ b/Basalt.TestField/Basalt.TestField.csproj
@@ -5,6 +5,7 @@
net8.0
enable
enable
+ true
diff --git a/Basalt.TestField/Components/DebugInfo.cs b/Basalt.TestField/Components/DebugInfo.cs
index d3ba5f2..770307b 100644
--- a/Basalt.TestField/Components/DebugInfo.cs
+++ b/Basalt.TestField/Components/DebugInfo.cs
@@ -1,8 +1,6 @@
using Basalt.Common;
using Basalt.Common.Components;
using Basalt.Common.Entities;
-using Raylib_cs;
-using System.Diagnostics;
using static Raylib_cs.Raylib;
namespace Basalt.TestField.Components
diff --git a/Basalt.TestField/Components/TestTrigger.cs b/Basalt.TestField/Components/TestTrigger.cs
index f6c3eaa..0e8e351 100644
--- a/Basalt.TestField/Components/TestTrigger.cs
+++ b/Basalt.TestField/Components/TestTrigger.cs
@@ -12,7 +12,7 @@ public TestTrigger(Entity entity) : base(entity)
public override void OnCollision(Collider other)
{
- if(other.Entity.Id == "entity.player")
+ if (other.Entity.Id == "entity.player")
{
Console.WriteLine($"Trigger collided with {other.Entity.Id}");
Entity.GetComponent().ModelCacheKey = "sphere";
diff --git a/Basalt.TestField/Program.cs b/Basalt.TestField/Program.cs
index 3d2e90f..0650bb7 100644
--- a/Basalt.TestField/Program.cs
+++ b/Basalt.TestField/Program.cs
@@ -15,7 +15,6 @@
using Basalt.Raylib.Sound;
using Basalt.Raylib.Utils;
using Basalt.TestField;
-using Basalt.TestField.Components;
using Basalt.Types;
using Raylib_cs;
using System.Numerics;
@@ -54,7 +53,7 @@
var player = new Entity();
-player.AddComponent(new CameraController(player));
+player.AddComponent(new FirstPersonCameraController(player));
player.Id = "entity.player";
Vector3 offset = Vector3.UnitY * -1;
player.Transform.Position = new Vector3(0, 5, 0);
@@ -66,15 +65,20 @@
Engine.CreateEntity(player);
-var trigger = new Entity();
-trigger.Id = "entity.trigger";
-trigger.Transform.Position = new Vector3(10, 2.5f, 0);
-trigger.AddComponent(new BoxCollider(trigger) { Size = new Vector3(5), IsTrigger = true });
-trigger.AddComponent(new ModelRenderer(trigger) { ModelCacheKey = "cube", Size = new Vector3(5), ColorTint = Color.Blue });
-trigger.AddComponent(new Rigidbody(trigger) { IsKinematic = true });
-trigger.AddComponent(new TestTrigger(trigger));
-Engine.CreateEntity(trigger);
+var emitter = new Entity();
+emitter.Transform.Position = new Vector3(0, 10, 0);
+emitter.AddComponent(new RaylibParticleSystem(emitter) { ModelCacheKey = "cube" });
+Engine.CreateEntity(emitter);
+var ps = emitter.GetComponent()!;
+
+ps.SubscribeOnParticleReset((ref Particle p) =>
+{
+ p.Velocity = new(Random.Shared.NextSingle() * 10 - 5, Random.Shared.NextSingle() * 10 - 5, Random.Shared.NextSingle() * 10 - 5);
+ // Apply random rotation
+ p.Rotation = Quaternion.CreateFromYawPitchRoll(Random.Shared.NextSingle() * MathF.PI * 2, Random.Shared.NextSingle() * MathF.PI * 2, Random.Shared.NextSingle() * MathF.PI * 2);
+
+});
TestingUtils.SetupTestingScene(250);
TestingUtils.SetupDebugInfo();
\ No newline at end of file
diff --git a/Basalt.Tests/CircularBufferTests.cs b/Basalt.Tests/CircularBufferTests.cs
index b912d5f..4602fa2 100644
--- a/Basalt.Tests/CircularBufferTests.cs
+++ b/Basalt.Tests/CircularBufferTests.cs
@@ -1,6 +1,4 @@
using Basalt.Utility;
-using NUnit.Framework;
-using System;
namespace Basalt.Tests
{
diff --git a/Basalt.Tests/Common/TestComponent.cs b/Basalt.Tests/Common/TestComponent.cs
index b96f108..b606ad5 100644
--- a/Basalt.Tests/Common/TestComponent.cs
+++ b/Basalt.Tests/Common/TestComponent.cs
@@ -29,8 +29,12 @@ public Entity? Target
}
}
}
- public bool HasStarted;
- public int OnStartCount = 0, OnUpdateCount = 0, OnRenderCount = 0, OnPhysicsUpdateCount = 0;
+ public bool HasStarted {get; set;}
+ public int OnStartCount { get; set; } = 0;
+ public int OnUpdateCount { get; set; } = 0;
+ public int OnRenderCount {get; set;} = 0;
+ public int OnPhysicsUpdateCount { get; set;} = 0;
+ public int Foo { get; private set; } = 0;
public override void OnStart()
{
HasStarted = true;
@@ -57,5 +61,8 @@ public override void OnPhysicsUpdate()
OnPhysicsUpdateCount++;
}
+ public int Get() => Foo;
+ public void Set(int value) => Foo = value;
+
}
}
diff --git a/Basalt.Tests/Integration/EntityIntegrationTests.cs b/Basalt.Tests/Integration/EntityIntegrationTests.cs
index 86af1f9..b4dd029 100644
--- a/Basalt.Tests/Integration/EntityIntegrationTests.cs
+++ b/Basalt.Tests/Integration/EntityIntegrationTests.cs
@@ -316,5 +316,81 @@ public void EntityEnabled_WhenDisabled_ShouldNotCallEvents()
Assert.That(entity.GetComponent()!.OnRenderCount, Is.EqualTo(0), "Render was called");
Assert.That(entity.GetComponent()!.OnPhysicsUpdateCount, Is.EqualTo(0), "Physics Update was called");
}
+
+ [Test]
+ public void CloneEntity_ShouldReturnNewInstances()
+ {
+ // Arrange
+ var entity = new Entity();
+ entity.AddComponent(new TestComponent(entity));
+ entity.AddComponent(new Rigidbody(entity));
+ var tc = entity.GetComponent()!;
+ var rb = entity.GetComponent()!;
+
+ // Act
+ Engine.Instance.Initialize();
+ Engine.CreateEntity(entity);
+ var clone = entity.Clone();
+ var ctc = clone.GetComponent()!;
+ var crb = clone.GetComponent()!;
+
+ // Assert
+ Assert.That(clone, Is.Not.Null);
+ Assert.That(clone, Is.Not.EqualTo(entity));
+ Assert.That(ctc, Is.Not.EqualTo(tc));
+ Assert.That(crb, Is.Not.EqualTo(rb));
+ Assert.That(ctc.Entity, Is.EqualTo(clone));
+ Assert.That(crb.Entity, Is.EqualTo(clone));
+ }
+
+ [Test]
+ public void CloneEntity_WhenCloning_ShouldCloneAllComponents()
+ {
+ // Arrange
+ var entity = new Entity();
+ entity.AddComponent(new TestComponent(entity));
+ entity.AddComponent(new Rigidbody(entity));
+
+ // Act
+ Engine.Instance.Initialize();
+ Engine.CreateEntity(entity);
+ var clone = entity.Clone();
+
+ // Assert
+ Assert.IsNotNull(clone);
+ Assert.IsNotNull(clone.GetComponent());
+ Assert.IsNotNull(clone.GetComponent());
+ }
+
+
+ [Test]
+ public void CloneEntity_WhenCloning_ShouldCopyProperties()
+ {
+ // Arrange
+ var entity = new Entity();
+ entity.Transform.Position = Vector3.One;
+ entity.AddComponent(new TestComponent(entity) { OnRenderCount = 42 });
+ entity.AddComponent(new Rigidbody(entity) { Mass = 10, IsKinematic = true, Drag = 1.1f});
+
+ var rb = entity.GetComponent()!;
+ var tc = entity.GetComponent()!;
+
+ tc.Set(69);
+
+ // Act
+ Engine.Instance.Initialize();
+ Engine.CreateEntity(entity);
+ var clone = entity.Clone();
+ var crb = clone.GetComponent()!;
+ var ctc = clone.GetComponent()!;
+
+ // Assert
+ Assert.That(clone.Transform.Position, Is.EqualTo(entity.Transform.Position).Using((IEqualityComparer) new Vector3EqualityComparer()));
+ Assert.That(clone.GetComponent()!.OnRenderCount, Is.EqualTo(entity.GetComponent()!.OnRenderCount));
+ Assert.That(crb.IsKinematic, Is.EqualTo(rb.IsKinematic));
+ Assert.That(crb.Mass, Is.EqualTo(rb.Mass));
+ Assert.That(crb.Drag, Is.EqualTo(rb.Drag));
+ Assert.That(ctc.Foo, Is.Not.EqualTo(tc.Foo));
+ }
}
}
diff --git a/Basalt.Tests/StateMachineTests.cs b/Basalt.Tests/StateMachineTests.cs
index 257a3a7..46b1496 100644
--- a/Basalt.Tests/StateMachineTests.cs
+++ b/Basalt.Tests/StateMachineTests.cs
@@ -1,6 +1,5 @@
-using NUnit.Framework;
-using Moq;
using Basalt.Utility;
+using Moq;
namespace Basalt.Tests
{
diff --git a/Basalt/Basalt.csproj b/Basalt/Basalt.csproj
index bf9ed54..9a957ea 100644
--- a/Basalt/Basalt.csproj
+++ b/Basalt/Basalt.csproj
@@ -5,7 +5,7 @@
enable
enable
true
- 1.8.1
+ 1.9.0
True
Basalt
BasaltLogoBg.png
diff --git a/Basalt/Common/Attributes/ComponentDependentOnAttribute.cs b/Basalt/Common/Attributes/ComponentDependentOnAttribute.cs
index 358bca6..a95067a 100644
--- a/Basalt/Common/Attributes/ComponentDependentOnAttribute.cs
+++ b/Basalt/Common/Attributes/ComponentDependentOnAttribute.cs
@@ -1,6 +1,4 @@
-using Basalt.Common.Components;
-
-namespace Basalt.Common.Attributes
+namespace Basalt.Common.Attributes
{
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
diff --git a/Basalt/Common/Components/BaseParticleSystem.cs b/Basalt/Common/Components/BaseParticleSystem.cs
new file mode 100644
index 0000000..5b02ffa
--- /dev/null
+++ b/Basalt/Common/Components/BaseParticleSystem.cs
@@ -0,0 +1,214 @@
+using Basalt.Common.Entities;
+using Basalt.Types;
+using System.Drawing;
+using System.Numerics;
+
+namespace Basalt.Common.Components
+{
+ ///
+ /// A base class for particle system components.
+ ///
+ public abstract class BaseParticleSystem : Component
+ {
+ ///
+ /// The particle pool used by the particle system.
+ ///
+ protected Particle[] _particles;
+
+ ///
+ /// The default particle values to be set on reset.
+ ///
+ protected Particle defaults;
+
+ private int _length;
+ private float _emissionRate = 5, _particleLifetime = 5, _systemLifetime = 0;
+
+
+ ///
+ /// Gets or sets the emission rate of the particle system.
+ ///
+ ///
+ /// Modifying this value will resize the particle pool.
+ ///
+ public float EmissionRate
+ {
+ get => _emissionRate;
+ set
+ {
+ _emissionRate = value;
+ ResizePool();
+ }
+ }
+
+ ///
+ /// Gets or sets the lifetime of the particles in the system.
+ ///
+ ///
+ /// Modifying this value will resize the particle pool.
+ ///
+ public float ParticleLifetime
+ {
+ get => _particleLifetime;
+ set
+ {
+ _particleLifetime = value;
+ ResizePool();
+ }
+ }
+
+ ///
+ /// Gets or sets the duration of the particle system. The system will reset and stop after this duration.
+ ///
+ public float SystemDuration { get; set; } = 5f;
+
+ ///
+ /// Gets or sets whether the particle system should loop.
+ ///
+ public bool Looping { get; set; } = false;
+
+ ///
+ /// A delegate for updating particles.
+ ///
+ /// The reference to the particle being updated
+ public delegate void ParticleUpdateDelegate(ref Particle particle);
+ private ParticleUpdateDelegate? _particleUpdate;
+ private ParticleUpdateDelegate? _onParticleReset;
+ protected BaseParticleSystem(Entity entity) : base(entity)
+ {
+ ResizePool();
+ for (int i = 0; i < _particles.Length; i++)
+ {
+ _particles[i].Lifetime = ParticleLifetime / _length * i;
+ }
+ defaults = new(Entity.Transform.Position, Quaternion.Identity, Vector3.Zero, 0);
+ }
+
+ ///
+ /// Renders the particles in the system.
+ ///
+ protected abstract void RenderParticles();
+ public sealed override void OnRender()
+ {
+ RenderParticles();
+ }
+ public override void OnUpdate()
+ {
+ var dt = Time.DeltaTime;
+ _systemLifetime += dt;
+
+ if (!Looping && _systemLifetime > SystemDuration + ParticleLifetime)
+ return;
+
+ for (int i = 0; i < _particles.Length; i++)
+ {
+ _particles[i].Lifetime += dt;
+ _particles[i].Position += _particles[i].Velocity * dt;
+ _particleUpdate?.Invoke(ref _particles[i]);
+ if (_particles[i].Lifetime > ParticleLifetime)
+ {
+ if (!Looping && _systemLifetime > SystemDuration)
+ {
+ _particles[i] = defaults;
+ _particles[i].Color = Color.FromArgb(0x00000000);
+ continue;
+ }
+ _particles[i] = defaults;
+ _onParticleReset?.Invoke(ref _particles[i]);
+ }
+ }
+ }
+
+ private void ResizePool()
+ {
+ int oldLength = _length;
+ _length = (int)(EmissionRate * ParticleLifetime);
+ var newPool = new Particle[_length];
+ if (_length < oldLength)
+ {
+ for (int i = 0; i < _length; i++)
+ {
+ newPool[i] = _particles[i];
+ }
+ }
+ else
+ {
+ for (int i = 0; i < oldLength; i++)
+ {
+ newPool[i] = _particles[i];
+ }
+ for (int i = oldLength; i < _length; i++)
+ {
+ newPool[i] = defaults;
+ }
+ }
+ _particles = newPool;
+ }
+
+ ///
+ /// Subscribes a delegate to be called when a particle is updated every frame.
+ ///
+ /// The target delegate
+ public void SubscribeUpdate(ParticleUpdateDelegate update)
+ {
+ _particleUpdate += update;
+ }
+
+ ///
+ /// Unsubscribes a delegate from being called when a particle is updated every frame.
+ ///
+ /// The target delegate
+ public void UnsubscribeUpdate(ParticleUpdateDelegate update)
+ {
+ _particleUpdate -= update;
+ }
+ ///
+ /// Subscribes a delegate to be called when a particle is reset.
+ ///
+ /// The target delegate
+
+ public void SubscribeOnParticleReset(ParticleUpdateDelegate update)
+ {
+ _onParticleReset += update;
+ }
+
+ ///
+ /// Unsubscribes a delegate from being called when a particle is reset.
+ ///
+ /// The target delegate
+ public void UnsubscribeOnParticleReset(ParticleUpdateDelegate update)
+ {
+ _onParticleReset -= update;
+ }
+
+ ///
+ /// Changes the default values of the particles in the system.
+ ///
+ /// The new default particle value
+ public void UpdateDefaults(Particle particle)
+ {
+ defaults = particle;
+ }
+
+ ///
+ /// Resets the particle system to it's initial state.
+ ///
+ public void Reset()
+ {
+ _systemLifetime = 0;
+ for (int i = 0; i < _particles.Length; i++)
+ {
+ _particles[i] = defaults;
+ }
+ }
+
+
+ ///
+ /// Stop the particle system from emitting new particles. Already existing particles will continue to update until end of lifetime.
+ ///
+ public void Stop()
+ {
+ _systemLifetime = SystemDuration;
+ }
+
+ }
+}
diff --git a/Basalt/Common/Components/BoxCollider.cs b/Basalt/Common/Components/BoxCollider.cs
index f2f22c9..c2e749f 100644
--- a/Basalt/Common/Components/BoxCollider.cs
+++ b/Basalt/Common/Components/BoxCollider.cs
@@ -1,5 +1,4 @@
-using Basalt.Common.Attributes;
-using Basalt.Common.Entities;
+using Basalt.Common.Entities;
using Newtonsoft.Json;
using System.Numerics;
diff --git a/Basalt/Common/Components/CameraControllerBase.cs b/Basalt/Common/Components/CameraControllerBase.cs
new file mode 100644
index 0000000..0e89490
--- /dev/null
+++ b/Basalt/Common/Components/CameraControllerBase.cs
@@ -0,0 +1,24 @@
+using Basalt.Common.Entities;
+
+namespace Basalt.Common.Components
+{
+ ///
+ /// Base class for camera controllers.
+ ///
+ /// The type that represents a camera.
+ public abstract class CameraControllerBase : Component
+ {
+ ///
+ /// Gets or sets the camera.
+ ///
+ public abstract T Camera { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The entity.
+ public CameraControllerBase(Entity entity) : base(entity)
+ {
+ }
+ }
+}
diff --git a/Basalt/Common/Components/Transform.cs b/Basalt/Common/Components/Transform.cs
index eb9dc20..afed5db 100644
--- a/Basalt/Common/Components/Transform.cs
+++ b/Basalt/Common/Components/Transform.cs
@@ -1,5 +1,4 @@
using Basalt.Common.Entities;
-using Basalt.Common.Physics;
using Basalt.Core.Common.Attributes;
using Basalt.Math;
using Newtonsoft.Json;
@@ -28,9 +27,9 @@ public Vector3 Position
return;
}
- if(Engine.Instance.EntityManager != null)
+ if (Engine.Instance.EntityManager != null)
Engine.Instance.EntityManager.ChunkingMechanism.MarkForUpdate(Entity);
-
+
var offset = value - position;
position = value;
diff --git a/Basalt/Common/Entities/Entity.cs b/Basalt/Common/Entities/Entity.cs
index 4521506..4f3b401 100644
--- a/Basalt/Common/Entities/Entity.cs
+++ b/Basalt/Common/Entities/Entity.cs
@@ -63,6 +63,9 @@ public bool Enabled
}
}
+ [JsonIgnore]
+ public bool Destroyed { get; private set; } = false;
+
public Entity()
{
Transform = new Transform(this);
@@ -173,11 +176,17 @@ public static Entity DeserializeFromJson(string json)
/// Adds a component to the entity.
///
/// The component to add
- public void AddComponent(Component component)
+ public void AddComponent(Component component, bool overwrite = false)
{
// Check for singleton attribute
if (components.Any(c => c.GetType() == component.GetType()) && component.GetType().GetCustomAttribute() != null)
- return;
+ {
+ if(!overwrite)
+ return;
+
+ // Replace the existing component
+ components.Remove(components.First(c => c.GetType() == component.GetType()));
+ }
components.Add(component);
switch (component)
@@ -201,6 +210,10 @@ public void AddComponent(Component component)
}
+ ///
+ /// Works identical to AddComponent, however forces changes to singleton components by overwritting them.
+ ///
+ ///
private void ForceAddComponent(Component component)
{
@@ -213,14 +226,23 @@ private void ForceAddComponent(Component component)
components.Add(component);
- if (Rigidbody == null && component is Rigidbody rb)
+ switch (component)
{
- Rigidbody = rb;
- }
+ case Rigidbody rb:
+ Rigidbody = rb;
+ break;
- else if (component is Transform t)
- {
- Transform = t;
+ case Transform t:
+ Transform = t;
+ break;
+
+ case Collider c:
+ Collider = c;
+ break;
+
+ default:
+ // Handle other cases if necessary
+ break;
}
}
@@ -320,6 +342,7 @@ public void RemoveChildren(Entity child)
///
public void Destroy()
{
+ Destroyed = true;
Engine.RemoveEntity(this);
foreach (var child in Children)
{
@@ -332,9 +355,22 @@ public void Destroy()
}
}
+ ///
+ /// Adds the entity to the engine by calling and recursively adds all children.
+ ///
+ public void Create()
+ {
+ Engine.CreateEntity(this);
+ foreach (var child in Children)
+ {
+ child.Create();
+ }
+ }
- internal void CallOnCollision(Collider other)
+ public void CallOnCollision(Collider other)
{
+ if (Destroyed)
+ return;
foreach (var component in components)
component.OnCollision(other);
}
@@ -366,7 +402,7 @@ internal void CallStart()
if (dependencyAttribute != null)
{
var missing = dependencyAttribute.Dependencies.Where(d => !HasComponent(d));
- if(missing.Any())
+ if (missing.Any())
{
Engine.Instance.Logger?.LogError($"Component \"{component.GetType().Name}\" is missing component dependencies: {string.Join(", ", missing.Select(m => $"\"{m.Name}\""))}");
}
@@ -375,5 +411,53 @@ internal void CallStart()
}
}
}
+
+ ///
+ /// Creates a deep copy of the entity and all of its children and components.
+ ///
+ ///
+ /// The clone entity will not be added to the engine automatically. Use or to add it to the engine.
+ /// The result will have a randomly generated Id, and all components will be cloned, with a few limitations:
+ ///
+ /// - Will only copy public fields;
+ /// - Will only copy properties if the setter is public;
+ /// - Will not copy static fields or properties;
+ /// - Will not copy init-only fields or properties;
+ /// - Reference-type fields and properties will have their references copied;
+ ///
+ ///
+ /// A deep copy of the entity
+ public Entity Clone()
+ {
+ var result = new Entity();
+ result.Id = Id + Guid.NewGuid().ToString();
+ foreach (var component in components)
+ {
+ var c = Activator.CreateInstance(component.GetType(), result) as Component;
+ foreach (var prop in c.GetType().GetProperties())
+ {
+ // Only copy values if the property has a setter, is not static and is public
+
+ if (prop.SetMethod != null && prop.CanWrite && !prop.SetMethod.IsStatic && prop.SetMethod.IsPublic)
+ prop.SetValue(c, prop.GetValue(component));
+ }
+
+ foreach(var field in c.GetType().GetFields())
+ {
+ if(field.IsPublic && !field.IsStatic && !field.IsInitOnly && !field.IsLiteral)
+ field.SetValue(c, field.GetValue(component));
+ }
+
+ c.Entity = result;
+ c.started = false;
+
+ result.ForceAddComponent(c);
+ }
+ foreach(var child in Children)
+ {
+ result.AddChildren(child.Clone());
+ }
+ return result;
+ }
}
}
diff --git a/Basalt/Common/Entities/EntityManager.cs b/Basalt/Common/Entities/EntityManager.cs
index dcce452..d8e4288 100644
--- a/Basalt/Common/Entities/EntityManager.cs
+++ b/Basalt/Common/Entities/EntityManager.cs
@@ -1,5 +1,4 @@
using Basalt.Common.Physics;
-using Basalt.Core.Common.Abstractions.Engine;
namespace Basalt.Common.Entities
{
diff --git a/Basalt/Common/Exceptions/InvalidResourceKeyException.cs b/Basalt/Common/Exceptions/InvalidResourceKeyException.cs
index 5140aa2..a1d5d76 100644
--- a/Basalt/Common/Exceptions/InvalidResourceKeyException.cs
+++ b/Basalt/Common/Exceptions/InvalidResourceKeyException.cs
@@ -2,7 +2,8 @@
{
public class InvalidResourceKeyException : Exception
{
- public InvalidResourceKeyException(string key) : base($"The resource key '{key}' is invalid or not found. Make sure the key is correctly typed or whether the resource was loaded or not.")
+ public InvalidResourceKeyException(string paramName, string paramValue)
+ : base($"The resource key '{paramValue}' for '{paramName}' is invalid or not found. Make sure the key is correctly typed or whether the resource was loaded or not.")
{
}
}
diff --git a/Basalt/Common/Physics/CollisionHandler.cs b/Basalt/Common/Physics/CollisionHandler.cs
index cbb48aa..389148a 100644
--- a/Basalt/Common/Physics/CollisionHandler.cs
+++ b/Basalt/Common/Physics/CollisionHandler.cs
@@ -49,7 +49,7 @@ public static void Handle(Collider col1, Collider col2)
/// The second box collider.
private static void BoxBoxCollision(Collider col1, Collider col2)
{
- if (!col1.Enabled || !col2.Enabled)
+ if (!col1.Enabled || !col2.Enabled || col1.Entity.Destroyed || col2.Entity.Destroyed)
return;
BoxCollider box1 = (BoxCollider)col1;
@@ -95,7 +95,7 @@ private static void BoxBoxCollision(Collider col1, Collider col2)
return;
// Handle collisions and separate them from here
-
+
// Calculate the direction of least penetration
Vector3 separationDirection = Vector3.Zero;
float minOverlap = Min(overlapX, Min(overlapY, overlapZ));
diff --git a/Basalt/Common/Physics/PhysicsEngine.cs b/Basalt/Common/Physics/PhysicsEngine.cs
index 08ec3e6..1b46d65 100644
--- a/Basalt/Common/Physics/PhysicsEngine.cs
+++ b/Basalt/Common/Physics/PhysicsEngine.cs
@@ -1,5 +1,4 @@
-using Basalt.Common.Components;
-using Basalt.Common.Entities;
+using Basalt.Common.Entities;
using Basalt.Common.Utils;
using Basalt.Core.Common.Abstractions.Engine;
@@ -20,7 +19,6 @@ public class PhysicsEngine : IPhysicsEngine
private IEventBus eventBus;
private ILogger? logger;
private bool ShouldRun = true;
- private List entities;
///
/// Gets or sets the gravity value for the physics engine.
///
@@ -33,7 +31,7 @@ public class PhysicsEngine : IPhysicsEngine
public PhysicsEngine()
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
{
-
+
}
///
@@ -76,11 +74,10 @@ public void Simulate()
eventBus?.TriggerEvent(BasaltConstants.PhysicsUpdateEventKey);
- Engine.Instance.EntityManager.ChunkingMechanism.Update();
-
// Check for collisions
DetectCollisions(Engine.Instance.EntityManager.ChunkingMechanism.GetEntitiesChunked());
+ Engine.Instance.EntityManager.ChunkingMechanism.Update();
elapsedTime = DateTimeOffset.Now.ToUnixTimeMilliseconds() - startTime;
diff --git a/Basalt/Common/Utils/ResourceCache.cs b/Basalt/Common/Utils/ResourceCache.cs
index 62c4953..1dc6347 100644
--- a/Basalt/Common/Utils/ResourceCache.cs
+++ b/Basalt/Common/Utils/ResourceCache.cs
@@ -84,7 +84,7 @@ public static bool TryGetResource(string resourceName, out T resource)
if (Instance.resourceCache.ContainsKey(resourceName))
{
var r = Instance.resourceCache[resourceName];
- if(r is T t)
+ if (r is T t)
{
resource = t;
return true;
diff --git a/Basalt/Engine.cs b/Basalt/Engine.cs
index 3e04a2a..99628fa 100644
--- a/Basalt/Engine.cs
+++ b/Basalt/Engine.cs
@@ -56,13 +56,13 @@ public bool Running
///
public EntityManager EntityManager
{
- get
- {
- if(_entityManager == null)
+ get
+ {
+ if (_entityManager == null)
{
_entityManager = new();
}
- return _entityManager;
+ return _entityManager;
}
private set { _entityManager = value; }
}
diff --git a/Basalt/Types/Particle.cs b/Basalt/Types/Particle.cs
new file mode 100644
index 0000000..2acbbe1
--- /dev/null
+++ b/Basalt/Types/Particle.cs
@@ -0,0 +1,125 @@
+using Basalt.Common.Components;
+using System.Drawing;
+using System.Numerics;
+
+namespace Basalt.Types
+{
+ ///
+ /// A struct representing a particle used by derived types.
+ ///
+ public struct Particle
+ {
+ ///
+ /// The current position of the particle.
+ ///
+ public Vector3 Position = Vector3.Zero;
+
+ ///
+ /// The rotation of the particle.
+ ///
+ public Quaternion Rotation = Quaternion.Identity;
+
+ ///
+ /// The velocity of the particle.
+ ///
+ public Vector3 Velocity = Vector3.Zero;
+
+ ///
+ /// The lifetime of the particle.
+ ///
+ public float Lifetime = 0;
+
+ ///
+ /// The scale of the particle.
+ ///
+ public Vector3 Scale { get; set; } = Vector3.One;
+
+ ///
+ /// The color of the particle.
+ ///
+ public Color Color { get; set; } = Color.HotPink;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public Particle()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of the struct with the specified position and rotation.
+ ///
+ /// The position of the particle.
+ /// The rotation of the particle.
+ public Particle(Vector3 position, Quaternion rotation)
+ {
+ Position = position;
+ Rotation = rotation;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with the specified position, rotation, and velocity.
+ ///
+ /// The position of the particle.
+ /// The rotation of the particle.
+ /// The velocity of the particle.
+ public Particle(Vector3 position, Quaternion rotation, Vector3 velocity)
+ {
+ Position = position;
+ Rotation = rotation;
+ Velocity = velocity;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with the specified position, rotation, velocity, and lifetime.
+ ///
+ /// The position of the particle.
+ /// The rotation of the particle.
+ /// The velocity of the particle.
+ /// The lifetime of the particle.
+ public Particle(Vector3 position, Quaternion rotation, Vector3 velocity, float lifetime)
+ {
+ Position = position;
+ Rotation = rotation;
+ Velocity = velocity;
+ Lifetime = lifetime;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with the specified position, rotation, velocity, lifetime, and scale.
+ ///
+ /// The position of the particle.
+ /// The rotation of the particle.
+ /// The velocity of the particle.
+ /// The lifetime of the particle.
+ /// The scale of the particle.
+ public Particle(Vector3 position, Quaternion rotation, Vector3 velocity, float lifetime, Vector3 scale)
+ {
+ Position = position;
+ Rotation = rotation;
+ Velocity = velocity;
+ Lifetime = lifetime;
+ Scale = scale;
+ }
+
+ ///
+ /// Initializes a new instance of the struct with the specified position, rotation, velocity, lifetime, scale, and color.
+ ///
+ /// The position of the particle.
+ /// The rotation of the particle.
+ /// The velocity of the particle.
+ /// The lifetime of the particle.
+ /// The scale of the particle.
+ /// The color of the particle.
+ public Particle(Vector3 position, Quaternion rotation, Vector3 velocity, float lifetime, Vector3 scale, Color color)
+ {
+ Position = position;
+ Rotation = rotation;
+ Velocity = velocity;
+ Lifetime = lifetime;
+ Scale = scale;
+ Color = color;
+ }
+ }
+}
diff --git a/Basalt/Utility/CircularBuffer.cs b/Basalt/Utility/CircularBuffer.cs
index afc4314..e44226a 100644
--- a/Basalt/Utility/CircularBuffer.cs
+++ b/Basalt/Utility/CircularBuffer.cs
@@ -79,7 +79,7 @@ public CircularBuffer(int size, Func generator)
}
_index = 0;
}
-
+
#endregion
///
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69bb867..52f11c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,29 @@
All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines.
+
+## [1.9.0](https://www.github.com/thiagomvas/Basalt/releases/tag/v1.9.0) (2024-06-18)
+
+### Features
+
+* Add base particle system component. ([d33c620](https://www.github.com/thiagomvas/Basalt/commit/d33c6201f4589925310b987b83736b49d512fea0))
+* Add base Raylib camera controller class ([2568b60](https://www.github.com/thiagomvas/Basalt/commit/2568b609786fbee19a3c445a74bcec0b044d4411))
+* Add CameraControllerBase generic class ([0099ea8](https://www.github.com/thiagomvas/Basalt/commit/0099ea85347604e6749e0a2e58fdd1aec30b1ba1))
+* Add Entity.Clone() and Entity.Create() ([6b1d15a](https://www.github.com/thiagomvas/Basalt/commit/6b1d15a55c7f2ccdb5424c470677ac6171b9c246))
+* Add First Person camera controller class ([57c6f74](https://www.github.com/thiagomvas/Basalt/commit/57c6f74f4d9ca29fc8802633b56bba3624df3a39))
+* Add RaylibParticleSystem component ([3615e2a](https://www.github.com/thiagomvas/Basalt/commit/3615e2ab304d60357448e15267bbf9b32eb02db9))
+* Add System.Drawing.Color.ToRaylibColor() extension method ([7acdade](https://www.github.com/thiagomvas/Basalt/commit/7acdadee93a1f9ee79fec03577b3565df120ee86))
+* CameraController can now be changed for RaylibGraphicsEngine ([0cd2cab](https://www.github.com/thiagomvas/Basalt/commit/0cd2cab1cc853e496f08492679fabebd651e2a5f))
+* Particle Systems can be looping or have limited emission duration. ([00bc197](https://www.github.com/thiagomvas/Basalt/commit/00bc197b6db7c4cbdf60b260fd2f0226059b91f6))
+* RaylibGraphicsEngine updated to support new camera system. ([7ec5323](https://www.github.com/thiagomvas/Basalt/commit/7ec532351f8efac7ffa4aa26f2409ea9ec4eacd4))
+
+### Bug Fixes
+
+* InvalidResourceKeyException now contains the param name and value that caused the exception ([32de895](https://www.github.com/thiagomvas/Basalt/commit/32de89577af550199992a5b8413546a7149123bc))
+* OnCollision no longer called when entity is destroyed. ([0aa34fb](https://www.github.com/thiagomvas/Basalt/commit/0aa34fb04739f155e495895d83f935fca4231899))
+* Possible null reference exception when setting transform position ([35f7d32](https://www.github.com/thiagomvas/Basalt/commit/35f7d320bd374805363cab3c16930da9a6ce3217))
+* Properly reference new chunking ([be6bcf9](https://www.github.com/thiagomvas/Basalt/commit/be6bcf98f370b693589fe802344d613515efcf6e))
+
## [1.8.1](https://www.github.com/thiagomvas/Basalt/releases/tag/v1.8.1) (2024-06-10)