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)