diff --git a/.gitignore b/.gitignore index adcee675..fc81be36 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ .vscode/ .idea/ +.junie/ bits/ cmake-build-*/ bin/ build/ graph/ +obj/ CMakeUserPresets.json +*.DotSettings.user diff --git a/Draconic.slnx b/Draconic.slnx new file mode 100644 index 00000000..23dd1ad7 --- /dev/null +++ b/Draconic.slnx @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/Engine/csharp/Draconic.Engine.Tests/Draconic.Engine.Tests.csproj b/Engine/csharp/Draconic.Engine.Tests/Draconic.Engine.Tests.csproj new file mode 100644 index 00000000..f46f40bd --- /dev/null +++ b/Engine/csharp/Draconic.Engine.Tests/Draconic.Engine.Tests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Engine/csharp/Draconic.Engine.Tests/WorldTests.cs b/Engine/csharp/Draconic.Engine.Tests/WorldTests.cs new file mode 100644 index 00000000..a5851989 --- /dev/null +++ b/Engine/csharp/Draconic.Engine.Tests/WorldTests.cs @@ -0,0 +1,305 @@ +using Draconic.Engine; + +namespace Draconic.Engine.Tests; + +public class WorldTests +{ + private readonly struct TestComponent(int value) : IComponent + { + public int Value { get; } = value; + } + + private sealed class TrackingBehavior : Behavior + { + public int UpdateCalls { get; private set; } + public int FixedUpdateCalls { get; private set; } + public int LateUpdateCalls { get; private set; } + public float LastDeltaTime { get; private set; } + public Entity AssignedEntity => Entity; + public World AssignedWorld => World; + + public TrackingBehavior() : base(new Entity(), new World()) + { + } + + public override void Update(float deltaTime) + { + UpdateCalls++; + LastDeltaTime = deltaTime; + } + + public override void FixedUpdate(float deltaTime) + { + FixedUpdateCalls++; + LastDeltaTime = deltaTime; + } + + public override void LateUpdate(float deltaTime) + { + LateUpdateCalls++; + LastDeltaTime = deltaTime; + } + } + + private sealed class ThrowingBehavior : Behavior + { + public ThrowingBehavior() : base(new Entity(), new World()) + { + } + + public override void Update(float deltaTime) + { + throw new InvalidOperationException("Expected test exception."); + } + } + + [Fact] + public void CreateEntity_WithoutId_CreatesEntityAndAllowsComponentAndBehaviorOperations() + { + var world = new World(); + var entity = world.CreateEntity(); + + world.CreateComponent(entity.EntityId); + var behavior = new TrackingBehavior(); + world.AddBehavior(entity.EntityId, behavior); + + Assert.True(world.TryGetComponent(entity.EntityId, out _)); + Assert.True(world.TryGetBehavior(entity.EntityId, out var fetchedBehavior)); + Assert.Same(behavior, fetchedBehavior); + } + + [Fact] + public void CreateEntity_WithProvidedId_UsesProvidedId() + { + var world = new World(); + const ulong entityId = 42; + + var entity = world.CreateEntity(entityId); + + Assert.Equal(entityId, entity.EntityId); + } + + [Fact] + public void CreateEntity_WithDuplicateProvidedId_ThrowsInvalidOperationException() + { + var world = new World(); + const ulong entityId = 42; + world.CreateEntity(entityId); + + Assert.Throws(() => world.CreateEntity(entityId)); + } + + [Fact] + public void CreateEntity_WithProvidedId_AdvancesGeneratedEntityIdPastProvidedId() + { + var world = new World(); + const ulong providedId = 1_000_000; + + world.CreateEntity(providedId); + var generatedEntity = world.CreateEntity(); + + Assert.True(generatedEntity.EntityId > providedId); + } + + [Fact] + public void DestroyEntity_ExistingEntity_RemovesEntityDataAndReturnsTrue() + { + var world = new World(); + var entity = world.CreateEntity(); + world.CreateComponent(entity.EntityId); + world.AddBehavior(entity.EntityId, new TrackingBehavior()); + + var destroyed = world.DestroyEntity(entity.EntityId); + + Assert.True(destroyed); + Assert.False(world.TryGetComponent(entity.EntityId, out _)); + Assert.False(world.TryGetBehavior(entity.EntityId, out _)); + Assert.False(world.RemoveComponent(entity.EntityId)); + Assert.False(world.RemoveBehavior(entity.EntityId)); + } + + [Fact] + public void DestroyEntity_NonExistentEntity_ReturnsFalse() + { + var world = new World(); + + var destroyed = world.DestroyEntity(1234); + + Assert.False(destroyed); + } + + [Fact] + public void CreateComponent_ExistingEntity_CanBeRetrieved() + { + var world = new World(); + var entity = world.CreateEntity(); + + world.CreateComponent(entity.EntityId); + + Assert.True(world.TryGetComponent(entity.EntityId, out var component)); + Assert.Equal(0, component.Value); + } + + [Fact] + public void CreateComponent_DuplicateType_ThrowsInvalidOperationException() + { + var world = new World(); + var entity = world.CreateEntity(); + world.CreateComponent(entity.EntityId); + + Assert.Throws(() => world.CreateComponent(entity.EntityId)); + } + + [Fact] + public void CreateComponent_ForMissingEntity_ThrowsKeyNotFoundException() + { + var world = new World(); + + Assert.Throws(() => world.CreateComponent(999)); + } + + [Fact] + public void TryUpdateComponent_ExistingEntity_UpdatesStoredValue() + { + var world = new World(); + var entity = world.CreateEntity(); + world.CreateComponent(entity.EntityId); + var updated = new TestComponent(77); + + var updateResult = world.TryUpdateComponent(entity.EntityId, ref updated); + var getResult = world.TryGetComponent(entity.EntityId, out var component); + + Assert.True(updateResult); + Assert.True(getResult); + Assert.Equal(77, component.Value); + } + + [Fact] + public void TryUpdateComponent_MissingEntity_ReturnsFalse() + { + var world = new World(); + var component = new TestComponent(11); + + var result = world.TryUpdateComponent(444, ref component); + + Assert.False(result); + } + + [Fact] + public void RemoveComponent_ExistingComponent_RemovesAndReturnsTrue() + { + var world = new World(); + var entity = world.CreateEntity(); + world.CreateComponent(entity.EntityId); + + var removed = world.RemoveComponent(entity.EntityId); + + Assert.True(removed); + Assert.False(world.TryGetComponent(entity.EntityId, out _)); + } + + [Fact] + public void AddBehavior_ExistingEntity_SetsWorldAndEntityAndCanBeRetrieved() + { + var world = new World(); + var entity = world.CreateEntity(); + var behavior = new TrackingBehavior(); + + world.AddBehavior(entity.EntityId, behavior); + + Assert.True(world.TryGetBehavior(entity.EntityId, out var fetched)); + Assert.Same(behavior, fetched); + Assert.Same(entity, behavior.AssignedEntity); + Assert.Same(world, behavior.AssignedWorld); + } + + [Fact] + public void AddBehavior_DuplicateType_ThrowsInvalidOperationException() + { + var world = new World(); + var entity = world.CreateEntity(); + world.AddBehavior(entity.EntityId, new TrackingBehavior()); + + Assert.Throws(() => world.AddBehavior(entity.EntityId, new TrackingBehavior())); + } + + [Fact] + public void AddBehavior_ForMissingEntity_ThrowsKeyNotFoundException() + { + var world = new World(); + + Assert.Throws(() => world.AddBehavior(99, new TrackingBehavior())); + } + + [Fact] + public void RemoveBehavior_ExistingBehavior_RemovesAndReturnsTrue() + { + var world = new World(); + var entity = world.CreateEntity(); + world.AddBehavior(entity.EntityId, new TrackingBehavior()); + + var removed = world.RemoveBehavior(entity.EntityId); + + Assert.True(removed); + Assert.False(world.TryGetBehavior(entity.EntityId, out _)); + } + + [Fact] + public void ExecuteUpdate_CallsUpdateForRegisteredBehaviors() + { + var world = new World(); + var entity = world.CreateEntity(); + var behavior = new TrackingBehavior(); + world.AddBehavior(entity.EntityId, behavior); + + world.ExecuteUpdate(0.25f); + + Assert.Equal(1, behavior.UpdateCalls); + Assert.Equal(0.25f, behavior.LastDeltaTime); + } + + [Fact] + public void ExecuteFixedUpdate_CallsFixedUpdateForRegisteredBehaviors() + { + var world = new World(); + var entity = world.CreateEntity(); + var behavior = new TrackingBehavior(); + world.AddBehavior(entity.EntityId, behavior); + + world.ExecuteFixedUpdate(0.5f); + + Assert.Equal(1, behavior.FixedUpdateCalls); + Assert.Equal(0.5f, behavior.LastDeltaTime); + } + + [Fact] + public void ExecuteLateUpdate_CallsLateUpdateForRegisteredBehaviors() + { + var world = new World(); + var entity = world.CreateEntity(); + var behavior = new TrackingBehavior(); + world.AddBehavior(entity.EntityId, behavior); + + world.ExecuteLateUpdate(1f); + + Assert.Equal(1, behavior.LateUpdateCalls); + Assert.Equal(1f, behavior.LastDeltaTime); + } + + [Fact] + public void ExecuteUpdate_WhenBehaviorThrows_ContinuesProcessingOthers() + { + var world = new World(); + var first = world.CreateEntity(); + var second = world.CreateEntity(); + var trackingBehavior = new TrackingBehavior(); + + world.AddBehavior(first.EntityId, new ThrowingBehavior()); + world.AddBehavior(second.EntityId, trackingBehavior); + + var exception = Record.Exception(() => world.ExecuteUpdate(0.16f)); + + Assert.Null(exception); + Assert.Equal(1, trackingBehavior.UpdateCalls); + } +} \ No newline at end of file diff --git a/Engine/csharp/Draconic.Engine/Behavior.cs b/Engine/csharp/Draconic.Engine/Behavior.cs new file mode 100644 index 00000000..77ed890c --- /dev/null +++ b/Engine/csharp/Draconic.Engine/Behavior.cs @@ -0,0 +1,104 @@ +namespace Draconic.Engine; + +/// +/// Represents the base class for all behaviors that can be associated with an entity within a game world. +/// +/// +/// A behavior encapsulates logic and functionality that can be attached to an entity in the world. +/// It provides methods for handling lifecycle events such as initialization, updates, and destruction, +/// which can be overridden by derived classes to implement specific behavior. +/// +public abstract class Behavior +{ + /// + /// Represents a fundamental unit within the game engine architecture. + /// + /// + /// The class serves as the cornerstone for organizing objects in the game world. + /// Each entity is uniquely identified by its . + /// Entities can contain components and behaviors to define their characteristics and functionality. + /// + protected internal Entity Entity { get; internal set; } + + /// + /// Provides the primary context for managing entities, components, and behaviors in the gaming engine. + /// + /// + /// The World property links behaviors to the World instance that governs the lifecycle and interaction + /// of all entities and components in the system. It acts as an interface for behaviors to access and + /// interact with the overarching simulation environment. + /// + protected internal World World { get; internal set; } + + /// + /// Represents an abstract base class for behaviors attached to entities within the game world. + /// A behavior defines specific functionality or actions that can be applied to an entity. + /// + protected Behavior(Entity entity, World world) + { + Entity = entity; + World = world; + } + + /// Updates the behavior with the logic defined by derived classes. + /// This method is called during the world's update cycle and should + /// be overridden by subclasses to implement specific behavior logic. + /// + /// The time, in seconds, since the last update. + public virtual void Update(float deltaTime) + { + + } + + /// + /// Invoked when a behavior is initialized and attached to an entity within the game world. + /// This method is primarily intended for performing setup logic, initializing state, or registering any required resources + /// specific to the behavior instance. + /// Override this method in derived classes to implement custom initialization logic. + /// + public virtual void Awake() + { + + } + + /// + /// Executes logic for a fixed update cycle. This method is used in for processing + /// physics-related updates or other time-dependent logic intended to run + /// at a consistent interval, independent of the frame rate. + /// + /// + /// The fixed time-step duration since the last fixed update cycle, typically used for + /// calculations requiring a consistent time interval. + /// + public virtual void FixedUpdate(float deltaTime) + { + + } + + /// + /// Invoked during the LateUpdate phase of the game loop for this behavior. + /// This method is called after all Update methods have run in the current frame, + /// allowing behaviors to perform operations that should occur later in the frame. + /// + /// + /// The time elapsed, in seconds, since the last frame. Use this value to perform + /// time-based calculations and ensure frame-rate independent behavior. + /// + public virtual void LateUpdate(float deltaTime) + { + + } + + /// + /// Called when the behavior's associated entity is being destroyed. + /// This method provides an opportunity to perform cleanup or finalization tasks + /// related to the behavior before the entity's destruction is completed. + /// By default, this method performs no operation, but it can be overridden + /// in derived classes to implement specific destruction logic. + /// + public virtual void OnDestroyed() + { + + } + +} \ No newline at end of file diff --git a/Engine/csharp/Draconic.Engine/Draconic.Engine.csproj b/Engine/csharp/Draconic.Engine/Draconic.Engine.csproj new file mode 100644 index 00000000..237d6616 --- /dev/null +++ b/Engine/csharp/Draconic.Engine/Draconic.Engine.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Engine/csharp/Draconic.Engine/Entity.cs b/Engine/csharp/Draconic.Engine/Entity.cs new file mode 100644 index 00000000..6c9d12d3 --- /dev/null +++ b/Engine/csharp/Draconic.Engine/Entity.cs @@ -0,0 +1,44 @@ +using System.Runtime.InteropServices; + +namespace Draconic.Engine; + +/// +/// Represents a fundamental unit within the game engine. +/// Each instance of the class is uniquely identified by an . +/// Entities can serve as containers for components and behaviors, enabling flexible and modular interaction within the game world. +/// +public class Entity +{ + /// + /// Gets or sets the unique identifier for an entity within the game engine. + /// + public ulong EntityId { get; set; } + + private static ulong _lastEntityId = 0; + private static readonly Lock _lock = new Lock(); + + public Entity() + { + lock (_lock) + { + EntityId = ++_lastEntityId; + } + } + public Entity(ulong entityId) + { + lock (_lock) + { + EntityId = entityId; + + if (entityId > _lastEntityId) + { + _lastEntityId = entityId; + } + } + } + + public static Entity Create() + { + return new Entity(); + } +} \ No newline at end of file diff --git a/Engine/csharp/Draconic.Engine/IComponent.cs b/Engine/csharp/Draconic.Engine/IComponent.cs new file mode 100644 index 00000000..1092d740 --- /dev/null +++ b/Engine/csharp/Draconic.Engine/IComponent.cs @@ -0,0 +1,14 @@ +namespace Draconic.Engine; + +/// +/// Represents a marker interface for defining components in the Draconic Engine. +/// +/// +/// Components are used to store data in the entity-component-system (ECS) architecture. +/// Implementing this interface allows a structure or class to represent an element of data +/// associated with an entity in the world. +/// +public interface IComponent +{ + +} \ No newline at end of file diff --git a/Engine/csharp/Draconic.Engine/Transform.cs b/Engine/csharp/Draconic.Engine/Transform.cs new file mode 100644 index 00000000..b17ff74c --- /dev/null +++ b/Engine/csharp/Draconic.Engine/Transform.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Draconic.Engine; + +/// +/// Represents the transformation data for an entity, including its position, scale, and rotation. +/// +[StructLayout(LayoutKind.Sequential)] +public struct Transform : IComponent +{ + /// + /// Represents the position of a transform in 3D space. + /// + public Vector3 Position { get; set; } + + /// + /// Represents the scaling factor applied to an object in 3D space. + /// + public Vector3 Scale { get; set; } + + /// + /// Represents the rotational orientation of the transform in 3D space. + /// + public Quaternion Rotation { get; set; } + +} \ No newline at end of file diff --git a/Engine/csharp/Draconic.Engine/World.cs b/Engine/csharp/Draconic.Engine/World.cs new file mode 100644 index 00000000..8f30eff9 --- /dev/null +++ b/Engine/csharp/Draconic.Engine/World.cs @@ -0,0 +1,423 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Draconic.Engine; + +/// +/// Represents the main context for managing entities, components, and behaviors within the Draconic Engine. +/// +/// +/// The World class provides functionality for creating and destroying entities, attaching and managing components, +/// and adding, retrieving, or removing behaviors associated with entities. It also handles the update cycles +/// (Update, FixedUpdate, LateUpdate) for behaviors. +/// +public class World +{ + //ToDo: refactor as a sparse set. + + #region Fields + + /// + /// Stores a collection of all entities in the current world context, + /// providing fast, thread-safe access to entity instances by their unique identifiers. + /// + private readonly ConcurrentDictionary _entities = new(); + + /// + /// Stores all components associated with entities in the world. + /// + private readonly ConcurrentDictionary> _components = new(); + + /// + /// Maintains a thread-safe collection of behaviors associated with each entity in the . + /// + private readonly ConcurrentDictionary> _behaviors = new(); + + #endregion + + #region Entity Management + + /// Creates a new entity within the world and initializes its associated data structures + /// for components and behaviors. + /// The method generates a new entity, adds it to the world's collection of entities, + /// and registers empty sets of components and behaviors associated with the entity's ID. + /// + /// An instance of the newly created entity. + /// + public Entity CreateEntity() + { + var entity = Entity.Create(); + + _entities[entity.EntityId] = entity; + _components.TryAdd(entity.EntityId, new()); + _behaviors.TryAdd(entity.EntityId, new()); + return entity; + } + + /// + /// Creates a new entity with the specified unique identifier. + /// + /// The unique identifier to associate with the new entity. + /// A newly created with the specified identifier. + public Entity CreateEntity(ulong entityId) + { + if (_entities.ContainsKey(entityId)) + { + throw new InvalidOperationException($"Entity {entityId} already exists in world."); + } + + var entity = new Entity(entityId); + _entities[entity.EntityId] = entity; + _components.TryAdd(entity.EntityId, new()); + _behaviors.TryAdd(entity.EntityId, new()); + return entity; + } + + /// + /// Removes the specified entity and all associated components and behaviors from the world. + /// + /// The unique identifier of the entity to be removed. + /// + /// A boolean value indicating whether the entity was successfully removed. + /// Returns true if the entity existed and was removed; otherwise, false. + /// + public bool DestroyEntity(ulong entityId) + { + var removedEntity = _entities.TryRemove(entityId, out _); + _components.TryRemove(entityId, out _); + _behaviors.TryRemove(entityId, out _); + + return removedEntity; + } + + #endregion + + #region Component Management + + /// Adds a component of the specified type to the given entity within the world. + /// This method registers the provided component instance and associates it with the entity ID. + /// If a component of the same type already exists for the entity, an exception is thrown. + /// The type of the component to add, implementing IComponent. + /// The unique identifier of the entity to which the component will be added. + /// A reference to the component instance that will be added to the entity. + /// Thrown if the entity with the specified ID does not exist in the world. + /// Thrown if a component of the same type already exists for the entity. + internal void AddComponent(ulong entityId, ref T component) where T : struct, IComponent + { + if (!_entities.ContainsKey(entityId)) + { + throw new KeyNotFoundException($"Entity {entityId} does not exist in world."); + } + + if (!_components.TryGetValue(entityId, out var components)) + { + components = new(); + + _components[entityId] = components; + } + + + if (!components.TryAdd(component.GetType(), component)) + { + throw new InvalidOperationException($"Component of type {component.GetType()} already exists for entity {entityId}"); + } + } + + /// Creates and registers a new component of the specified type for the given entity. + /// The method initializes the component with its default values and associates it with + /// the provided entity ID. This allows the component to be included in the entity's data + /// structure, making it accessible for subsequent operations such as retrieval or updates. + /// Throws an exception if a component of the same type is already associated with the entity. + /// + /// The type of the component to create and associate with the entity. The type must + /// implement the IComponent interface and be a value type. + /// + /// + /// The unique identifier of the entity to which the new component will be associated. + /// + public void CreateComponent(ulong entityId) where T : struct, IComponent + { + T component = new T(); + AddComponent(entityId, ref component); + } + + /// Removes a specified component from the entity identified by the given entity ID. + /// This method ensures that the component of the specified type is removed only if the entity exists + /// and the component of the given type is currently associated with the entity. + /// + /// The type of the component to be removed. Must implement the IComponent interface. + /// + /// + /// The unique identifier of the entity from which the component is to be removed. + /// + /// + /// True if the component was successfully removed; otherwise, false. Returns false if the entity + /// does not exist or if the specified component is not associated with the entity. + /// + public bool RemoveComponent(ulong entityId) where T : IComponent + { + if (!_entities.ContainsKey(entityId)) + { + return false; + } + + if (_components.TryGetValue(entityId, out var components)) + { + + return components.TryRemove(typeof(T), out _); + } + + return false; + } + + /// Attempts to retrieve a component of the specified type associated with the given entity ID. + /// If the entity exists and has a component of the specified type, the method outputs the component + /// and returns true. Otherwise, the method outputs the default value for the component type and returns false. + /// + /// The type of the component to retrieve. Must be a struct and implement the IComponent interface. + /// + /// + /// The ID of the entity from which the component should be retrieved. + /// + /// + /// When the method returns, contains the component of the specified type if retrieval was successful; + /// otherwise, contains the default value for the specified type. + /// + /// + /// True if the entity exists and has a component of the specified type; otherwise, false. + /// + public bool TryGetComponent(ulong entityId, out T component) where T : struct, IComponent + { + if (!_entities.ContainsKey(entityId)) + { + component = default; + return false; + } + + if (_components.TryGetValue(entityId, out var components)) + { + components.TryGetValue(typeof(T), out var componentObj); + if (componentObj is not null && componentObj is T typedComponent) + { + component = typedComponent; + return true; + } + + } + + component = default; + return false; + } + + /// Attempts to update a component of the specified type for the given entity with the provided value. + /// If the entity exists and the component type is associated with the entity, the component value is updated, + /// and the method returns true. If the entity does not exist or the component type is not associated with the entity, + /// the method returns false. + /// + /// The unique identifier of the entity for which the component is to be updated. + /// + /// + /// The new value of the component to update. Passed by reference. + /// + /// + /// The type of the component to be updated. Must implement the IComponent interface. + /// + /// + /// true if the component was successfully updated; otherwise, false. + /// + public bool TryUpdateComponent(ulong entityId, ref T component) where T : struct, IComponent + { + if (!_entities.ContainsKey(entityId)) + { + return false; + } + + if (_components.TryGetValue(entityId, out var components)) + { + components[typeof(T)] = component; + return true; + } + + return false; + } + + #endregion + + #region Behavior Management + + /// Adds a behavior to the specified entity within the world. + /// The behavior is associated with the provided entity ID and stored in the world's behavior collection. + /// The method ensures that the behavior is assigned to both the entity and the world, + /// and validates that there are no duplicate behaviors of the same type for the entity. + /// + /// The type of the behavior to be added. Must be a subclass of Behavior. + /// + /// + /// The unique identifier of the entity to which the behavior is added. + /// + /// + /// The behavior instance to be added to the entity. + /// + /// + /// Thrown if no entity with the specified ID exists in the world. + /// + /// + /// Thrown if a behavior of the same type already exists for the specified entity. + /// + public void AddBehavior(ulong entityId, T behavior) where T : Behavior + { + if (!_entities.TryGetValue(entityId, out var entity)) + { + throw new KeyNotFoundException($"Entity {entityId} does not exist in world."); + } + + if (!_behaviors.TryGetValue(entityId, out var behaviors)) + { + behaviors = new(); + _behaviors[entityId] = behaviors; + } + + behavior.Entity = entity; + behavior.World = this; + + if (!behaviors.TryAdd(behavior.GetType(), behavior)) + { + throw new InvalidOperationException($"Behavior of type {behavior.GetType()} already exists for entity {entityId}"); + } + } + + /// Attempts to retrieve a behavior of the specified type associated with a given entity ID. + /// If the behavior exists and is of the requested type, it is returned in the output parameter. + /// Otherwise, the output parameter is set to null, and the method returns false. + /// The type of the behavior to retrieve. Must inherit from the Behavior class. + /// The unique identifier for the entity whose behavior is being retrieved. + /// + /// When the method completes, this parameter contains the behavior of the requested type if it exists; + /// otherwise, it is set to null. + /// + /// + /// True if the specified behavior is found and is of the requested type; otherwise, false. + /// + public bool TryGetBehavior(ulong entityId, [NotNullWhen(true)] out T? behavior) where T : Behavior + { + if (!_entities.ContainsKey(entityId)) + { + behavior = null; + return false; + } + + if (_behaviors.TryGetValue(entityId, out var behaviors)) + { + behaviors.TryGetValue(typeof(T), out var behaviorObj); + if (behaviorObj is T typedBehavior) + { + behavior = typedBehavior; + return true; + } + } + + behavior = null; + return false; + } + + /// Removes a behavior of the specified type from the given entity. + /// If the entity exists and the behavior is found, it is removed from the + /// collection of behaviors associated with the entity. + /// + /// The type of behavior to be removed, which must inherit from the Behavior class. + /// + /// + /// The unique identifier of the entity from which the behavior should be removed. + /// + /// + /// True if the behavior was successfully removed; otherwise, false. + /// + public bool RemoveBehavior(ulong entityId) where T : Behavior + { + if (!_entities.ContainsKey(entityId)) + { + return false; + } + + if (_behaviors.TryGetValue(entityId, out var behaviors)) + { + return behaviors.TryRemove(typeof(T), out _); + } + + return false; + } + + /// Executes the update process for all behaviors registered within the world. + /// Each behavior's `Update` method is called with the provided delta time to + /// perform time-based logic or operations. If a behavior throws an exception + /// during its update, the process continues with the remaining behaviors. + /// The time, in seconds, since the last update. + public void ExecuteUpdate(float deltaTime) + { + foreach (var behaviors in _behaviors.Values) + foreach (var behavior in behaviors.Values) + { + try + { + behavior.Update(deltaTime); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + } + + } + } + + /// Executes the FixedUpdate lifecycle step for all registered behaviors in the world. + /// This method iterates through all behaviors associated with entities and invokes their + /// FixedUpdate method with the provided delta time. + /// If an exception is thrown during the execution of a behavior's FixedUpdate method, + /// it is caught and logged to the error output stream without interrupting the remaining updates. + /// + /// The time interval, in seconds, that has elapsed since the previous FixedUpdate call. + /// This value is passed to each behavior's FixedUpdate method to support time-dependent updates. + /// + public void ExecuteFixedUpdate(float deltaTime) + { + foreach (var behaviors in _behaviors.Values) + foreach (var behavior in behaviors.Values) + { + try + { + behavior.FixedUpdate(deltaTime); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + } + } + } + + /// Executes the LateUpdate phase for all registered behaviors in the world. + /// This method iterates through all behaviors associated with entities in the world + /// and invokes their LateUpdate method, passing the provided delta time. + /// Any exceptions thrown during the execution of a behavior's LateUpdate method + /// are caught and logged to the error output. + /// + /// The time interval, in seconds, since the last LateUpdate cycle. + /// This value is provided to each behavior to adjust their logic based on the elapsed time. + /// + public void ExecuteLateUpdate(float deltaTime) + { + foreach (var behaviors in _behaviors.Values) + foreach (var behavior in behaviors.Values) + { + try + { + behavior.LateUpdate(deltaTime); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + } + } + } + + #endregion +} \ No newline at end of file diff --git a/SourceGenerators/Draconic.Dynamics/Draconic.Dynamics.Sourcegen/Draconic.Dynamics.Sourcegen.csproj b/SourceGenerators/Draconic.Dynamics/Draconic.Dynamics.Sourcegen/Draconic.Dynamics.Sourcegen.csproj new file mode 100644 index 00000000..1894925d --- /dev/null +++ b/SourceGenerators/Draconic.Dynamics/Draconic.Dynamics.Sourcegen/Draconic.Dynamics.Sourcegen.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + false + enable + latest + + true + true + Draconic.Dynamics.Sourcegen + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/SourceGenerators/Draconic.Dynamics/Draconic.Dynamics.Tests/Draconic.Dynamics.Tests.csproj b/SourceGenerators/Draconic.Dynamics/Draconic.Dynamics.Tests/Draconic.Dynamics.Tests.csproj new file mode 100644 index 00000000..f8ae0a4d --- /dev/null +++ b/SourceGenerators/Draconic.Dynamics/Draconic.Dynamics.Tests/Draconic.Dynamics.Tests.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + + false + + Draconic.Dynamics.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + +