Refactor project structure and enhance functionality
Changed the project namespace from `Ghost.Editor` to `Ghost.App` across multiple files. Changed the `InternalsVisibleTo` attribute in `AssemblyInfo.cs` to include `Ghost.App`. Changed the `ProjectRepository` class to add new asynchronous methods for retrieving projects by ID, name, and metadata path. Changed the `ProjectService` class to utilize the new asynchronous project loading methods. Changed the `SceneGraph` classes to improve node management and serialization. Changed the `EntityManager` class to enhance entity management with new component handling methods. Added new test classes, `EntityTest` and `SerializationTest`, to ensure reliability in entity and serialization systems. Added the `Ghost.App` project file to establish a modular project structure. Added the `Ghost.Generator` project for automated component serialization code generation. Updated UI components to reflect the new namespace for proper functionality.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
global using EntityID = System.Int32;
|
||||
global using GenerationID = System.Byte;
|
||||
global using GenerationID = System.UInt16;
|
||||
global using WorldID = System.UInt16;
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Ghost.App")]
|
||||
[assembly: InternalsVisibleTo("Ghost.Engine")]
|
||||
[assembly: InternalsVisibleTo("Ghost.Editor")]
|
||||
[assembly: InternalsVisibleTo("Ghost.Test")]
|
||||
@@ -18,8 +18,12 @@ internal interface IComponentPool : IDisposable
|
||||
get;
|
||||
}
|
||||
|
||||
public void Add(Entity entity, IComponentData component);
|
||||
public bool Remove(Entity entity);
|
||||
public bool Has(Entity entity);
|
||||
public IComponentData Get(Entity entity);
|
||||
|
||||
public IEnumerable<(Entity entity, IComponentData component)> Enumerate();
|
||||
}
|
||||
|
||||
internal interface IComponentPool<T> : IComponentPool
|
||||
@@ -56,6 +60,10 @@ internal class ComponentPool<T> : IComponentPool<T>
|
||||
_lookup.AsSpan().Fill(Entity.INVALID_ID);
|
||||
}
|
||||
|
||||
public ComponentPool() : this(16)
|
||||
{
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static EntityID GetLookupIndex(Entity entity)
|
||||
{
|
||||
@@ -68,6 +76,16 @@ internal class ComponentPool<T> : IComponentPool<T>
|
||||
return _lookup[GetLookupIndex(entity)];
|
||||
}
|
||||
|
||||
public void Add(Entity entity, IComponentData component)
|
||||
{
|
||||
if (component is not T typedComponent)
|
||||
{
|
||||
throw new ArgumentException($"Component type mismatch. Expected {typeof(T)}, but got {component.GetType()}.");
|
||||
}
|
||||
|
||||
Add(entity, typedComponent);
|
||||
}
|
||||
|
||||
public void Add(Entity entity, T component)
|
||||
{
|
||||
if (!entity.IsValid)
|
||||
@@ -117,6 +135,11 @@ internal class ComponentPool<T> : IComponentPool<T>
|
||||
return true;
|
||||
}
|
||||
|
||||
public IComponentData Get(Entity entity)
|
||||
{
|
||||
return GetRef(entity);
|
||||
}
|
||||
|
||||
public ref T GetRef(Entity entity)
|
||||
{
|
||||
if (!entity.IsValid)
|
||||
@@ -128,6 +151,17 @@ internal class ComponentPool<T> : IComponentPool<T>
|
||||
return ref _components[index].data;
|
||||
}
|
||||
|
||||
public IEnumerable<(Entity entity, IComponentData component)> Enumerate()
|
||||
{
|
||||
for (var i = 0; i < _nextId; i++)
|
||||
{
|
||||
if (_components[i].owner.IsValid)
|
||||
{
|
||||
yield return (_components[i].owner, _components[i].data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Has(Entity entity)
|
||||
{
|
||||
if (entity.ID >= _lookup.Length)
|
||||
@@ -194,6 +228,16 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
|
||||
_executionList.Sort((a, b) => a.ExecutionOrder.CompareTo(b.ExecutionOrder));
|
||||
}
|
||||
|
||||
public void Add(Entity entity, IComponentData component)
|
||||
{
|
||||
if (component is not ScriptComponent scriptComponent)
|
||||
{
|
||||
throw new ArgumentException($"Component type mismatch. Expected {typeof(ScriptComponent)}, but got {component.GetType()}.");
|
||||
}
|
||||
|
||||
Add(entity, scriptComponent);
|
||||
}
|
||||
|
||||
public void Add(Entity entity, ScriptComponent component)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
@@ -283,12 +327,33 @@ internal class ScriptComponentPool : IComponentPool<ScriptComponent>
|
||||
return _scriptComponents?.ContainsKey(entity) ?? false;
|
||||
}
|
||||
|
||||
public List<ScriptComponent>? Get(Entity entity)
|
||||
public IComponentData Get(Entity entity)
|
||||
{
|
||||
throw new NotSupportedException("Use GetAll instead of Get for ScriptComponentPool.");
|
||||
}
|
||||
|
||||
public IEnumerable<(Entity entity, IComponentData component)> Enumerate()
|
||||
{
|
||||
if (_scriptComponents == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var kvp in _scriptComponents)
|
||||
{
|
||||
foreach (var script in kvp.Value)
|
||||
{
|
||||
yield return (kvp.Key, script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<IComponentData> GetAll(Entity entity)
|
||||
{
|
||||
if (_scriptComponents == null
|
||||
|| !_scriptComponents.TryGetValue(entity, out var scriptList))
|
||||
{
|
||||
return null;
|
||||
return Enumerable.Empty<ScriptComponent>();
|
||||
}
|
||||
|
||||
return scriptList;
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ghost.Entities;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public struct Entity : IEquatable<Entity>, IComparable<Entity>
|
||||
{
|
||||
// Is 256 generations enough? If not, increase the size of GenerationID or make generation as a separate int field.
|
||||
public const int GENERATION_BITS = sizeof(GenerationID) * 8;
|
||||
public const int INDEX_BITS = sizeof(EntityID) * 8 - GENERATION_BITS;
|
||||
|
||||
private const int _GENERATION_MASK = (1 << GENERATION_BITS) - 1;
|
||||
private const int _INDEX_MASK = (1 << INDEX_BITS) - 1;
|
||||
|
||||
public const EntityID INVALID_ID = -1;
|
||||
|
||||
[JsonInclude]
|
||||
private EntityID _id;
|
||||
private GenerationID _generation;
|
||||
|
||||
public readonly EntityID ID
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _id;
|
||||
}
|
||||
|
||||
public readonly GenerationID Generation
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _generation;
|
||||
}
|
||||
|
||||
public readonly bool IsValid
|
||||
{
|
||||
@@ -22,39 +30,20 @@ public struct Entity : IEquatable<Entity>, IComparable<Entity>
|
||||
get => ID != INVALID_ID;
|
||||
}
|
||||
|
||||
public readonly EntityID ID
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => _id & _INDEX_MASK;
|
||||
}
|
||||
|
||||
public readonly GenerationID Generation
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => (GenerationID)(_id >> INDEX_BITS & _GENERATION_MASK);
|
||||
}
|
||||
|
||||
public static Entity Invalid
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => new(INVALID_ID, 0);
|
||||
get => new(INVALID_ID, GenerationID.MaxValue);
|
||||
}
|
||||
|
||||
internal Entity(EntityID id, GenerationID generation)
|
||||
{
|
||||
_id = generation << INDEX_BITS | id;
|
||||
_id = id;
|
||||
_generation = generation;
|
||||
}
|
||||
|
||||
internal void IncrementGeneration()
|
||||
{
|
||||
var generation = Generation + 1;
|
||||
if (generation >= _GENERATION_MASK)
|
||||
{
|
||||
throw new InvalidOperationException("Generation overflow");
|
||||
}
|
||||
|
||||
_id = _id & ~(_GENERATION_MASK << INDEX_BITS) | generation << INDEX_BITS;
|
||||
}
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal void IncrementGeneration() => _generation++;
|
||||
|
||||
public readonly bool Equals(Entity other)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Entities;
|
||||
public struct EntityManager : IDisposable
|
||||
public readonly struct EntityManager : IDisposable
|
||||
{
|
||||
private readonly List<Entity> _entities;
|
||||
private readonly Queue<EntityID> _freeEntitySlots;
|
||||
@@ -15,11 +15,6 @@ public struct EntityManager : IDisposable
|
||||
public readonly int EntityCount => _entities.Count;
|
||||
public readonly ReadOnlySpan<Entity> Entities => CollectionsMarshal.AsSpan(_entities);
|
||||
|
||||
public event Action<Entity>? OnEntityCreated;
|
||||
public event Action<EntityID>? OnEntityRemoved;
|
||||
public event Action<Entity, Type>? OnComponentAdded;
|
||||
public event Action<Entity, Type>? OnComponentRemoved;
|
||||
|
||||
internal EntityManager(World world, int initialCapacity)
|
||||
{
|
||||
_entities = new(initialCapacity);
|
||||
@@ -45,10 +40,14 @@ public struct EntityManager : IDisposable
|
||||
_entities.Add(entity);
|
||||
}
|
||||
|
||||
OnEntityCreated?.Invoke(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
internal readonly void AddEntityInternal(Entity entity)
|
||||
{
|
||||
_entities.Add(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified <see cref="Entity"/> from the world.
|
||||
/// </summary>
|
||||
@@ -67,7 +66,6 @@ public struct EntityManager : IDisposable
|
||||
_entities[entity.ID] = slot;
|
||||
_freeEntitySlots.Enqueue(entity.ID);
|
||||
|
||||
OnEntityRemoved?.Invoke(entity.ID);
|
||||
entity = Entity.Invalid;
|
||||
}
|
||||
|
||||
@@ -88,6 +86,20 @@ public struct EntityManager : IDisposable
|
||||
return _entities[entity.ID].Generation == entity.Generation;
|
||||
}
|
||||
|
||||
public readonly void AddComponent(Entity entity, IComponentData component, Type type)
|
||||
{
|
||||
var typeHandle = TypeHandle.Get(type);
|
||||
ref var pool = ref CollectionsMarshal.GetValueRefOrAddDefault(_world.ComponentStorage.ComponentPools, typeHandle, out var exists);
|
||||
if (!exists)
|
||||
{
|
||||
var poolType = typeof(ComponentPool<>).MakeGenericType(type);
|
||||
pool = (IComponentPool)(Activator.CreateInstance(poolType) ?? throw new InvalidOperationException($"Failed to create component pool for type {type}."));
|
||||
}
|
||||
|
||||
pool!.Add(entity, component);
|
||||
_world.ComponentStorage.GetOrCreateMask(typeHandle).SetBit(entity.ID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a component of type <typeparamref name="T"/> to the given <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
@@ -100,7 +112,6 @@ public struct EntityManager : IDisposable
|
||||
{
|
||||
_world.ComponentStorage.GetOrCreateComponentPool<T>().Add(entity, component);
|
||||
_world.ComponentStorage.GetOrCreateMask(TypeHandle.Get<T>()).SetBit(entity.ID);
|
||||
OnComponentAdded?.Invoke(entity, typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -123,7 +134,6 @@ public struct EntityManager : IDisposable
|
||||
}
|
||||
|
||||
_world.ComponentStorage.GetOrCreateMask(TypeHandle.Get<T>()).ClearBit(entity.ID);
|
||||
OnComponentRemoved?.Invoke(entity, typeof(T));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -183,7 +193,6 @@ public struct EntityManager : IDisposable
|
||||
where T : ScriptComponent, new()
|
||||
{
|
||||
_world.ComponentStorage.ScriptComponentPool.Add(entity, new T());
|
||||
OnComponentAdded?.Invoke(entity, typeof(ScriptComponent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -203,9 +212,14 @@ public struct EntityManager : IDisposable
|
||||
|
||||
var instance = (ScriptComponent?)Activator.CreateInstance(type) ?? throw new InvalidOperationException($"Failed to create instance of {type}.");
|
||||
_world.ComponentStorage.ScriptComponentPool.Add(entity, instance);
|
||||
OnComponentAdded?.Invoke(entity, typeof(ScriptComponent));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a script of type <typeparamref name="T"/> from the given <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the script to remove.</typeparam>
|
||||
/// <param name="entity">The entity from which the script is to be removed.</param>
|
||||
/// <returns>True if the script was successfully removed; otherwise, false.</returns>
|
||||
public readonly bool RemoveScript<T>(Entity entity)
|
||||
where T : ScriptComponent
|
||||
{
|
||||
@@ -214,10 +228,15 @@ public struct EntityManager : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
OnComponentRemoved?.Invoke(entity, typeof(ScriptComponent));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a script at the specified index from the given <see cref="Entity"/>.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity from which the script is to be removed.</param>
|
||||
/// <param name="index">The index of the script to remove.</param>
|
||||
/// <returns>True if the script was successfully removed; otherwise, false.</returns>
|
||||
public readonly bool RemoveScriptAt(Entity entity, int index)
|
||||
{
|
||||
if (!_world.ComponentStorage.ScriptComponentPool.RemoveAt(entity, index))
|
||||
@@ -225,7 +244,6 @@ public struct EntityManager : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
OnComponentRemoved?.Invoke(entity, typeof(ScriptComponent));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -238,7 +256,7 @@ public struct EntityManager : IDisposable
|
||||
public readonly T? GetScript<T>(Entity entity)
|
||||
where T : ScriptComponent
|
||||
{
|
||||
return (T?)_world.ComponentStorage.ScriptComponentPool.Get(entity)?
|
||||
return (T?)_world.ComponentStorage.ScriptComponentPool.GetAll(entity)?
|
||||
.FirstOrDefault(script => script is T tScript);
|
||||
}
|
||||
|
||||
@@ -251,7 +269,7 @@ public struct EntityManager : IDisposable
|
||||
public readonly IEnumerable<T> GetScripts<T>(Entity entity)
|
||||
where T : ScriptComponent
|
||||
{
|
||||
return (IEnumerable<T>?)_world.ComponentStorage.ScriptComponentPool.Get(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty<T>();
|
||||
return (IEnumerable<T>?)_world.ComponentStorage.ScriptComponentPool.GetAll(entity)?.Where(script => script is T tScript) ?? Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
public readonly void Dispose()
|
||||
|
||||
@@ -24,11 +24,11 @@ internal struct QueryFilter()
|
||||
internal List<nint> _absent = new(6);
|
||||
internal List<nint> _disabled = new(6);
|
||||
|
||||
public readonly void ComputeFilterBitMask(World world, ref BitSet result)
|
||||
public readonly void ComputeFilterBitMask(World world, BitSet result)
|
||||
{
|
||||
BitSet allMask = default;
|
||||
BitSet anyMask = default;
|
||||
BitSet absentMask = default;
|
||||
BitSet allMask = new();
|
||||
BitSet anyMask = new();
|
||||
BitSet absentMask = new();
|
||||
|
||||
var hasAll = false;
|
||||
var hasAny = false;
|
||||
@@ -77,7 +77,6 @@ internal struct QueryFilter()
|
||||
absentMask |= mask;
|
||||
}
|
||||
|
||||
result = new BitSet(world.EntityManager.EntityCount);
|
||||
result.SetAll();
|
||||
|
||||
if (hasAll)
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ghost.Entities.Systems;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public struct SystemStorage
|
||||
public readonly struct SystemStorage
|
||||
{
|
||||
private readonly List<Type> _systems = new();
|
||||
private readonly List<ISystem> _executionList = new();
|
||||
|
||||
private readonly World _world;
|
||||
|
||||
public event Action<Type>? SystemAdded;
|
||||
public event Action<Type>? SystemRemoved;
|
||||
internal ReadOnlySpan<Type> Systems => CollectionsMarshal.AsSpan(_systems);
|
||||
|
||||
internal SystemStorage(World world)
|
||||
{
|
||||
@@ -21,7 +21,6 @@ public struct SystemStorage
|
||||
public readonly void AddSystem(Type systemType)
|
||||
{
|
||||
_systems.Add(systemType);
|
||||
SystemAdded?.Invoke(systemType);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
@@ -34,7 +33,6 @@ public struct SystemStorage
|
||||
public readonly void RemoveSystem(Type systemType)
|
||||
{
|
||||
_systems.Remove(systemType);
|
||||
SystemRemoved?.Invoke(systemType);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -59,7 +59,8 @@ public struct QueryEnumerable<T0>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -231,7 +232,8 @@ public struct QueryEnumerable<T0, T1>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -408,7 +410,8 @@ public struct QueryEnumerable<T0, T1, T2>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -590,7 +593,8 @@ public struct QueryEnumerable<T0, T1, T2, T3>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -777,7 +781,8 @@ public struct QueryEnumerable<T0, T1, T2, T3, T4>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -969,7 +974,8 @@ public struct QueryEnumerable<T0, T1, T2, T3, T4, T5>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -1166,7 +1172,8 @@ public struct QueryEnumerable<T0, T1, T2, T3, T4, T5, T6>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
@@ -1368,7 +1375,8 @@ public struct QueryEnumerable<T0, T1, T2, T3, T4, T5, T6, T7>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@ public struct QueryEnumerable<<#= generics #>>
|
||||
|
||||
_count = count;
|
||||
_index = -1;
|
||||
filters.ComputeFilterBitMask(_world, ref _filterMask);
|
||||
_filterMask = new BitSet(_world.EntityManager.EntityCount);
|
||||
filters.ComputeFilterBitMask(_world, _filterMask);
|
||||
|
||||
Current = default;
|
||||
}
|
||||
|
||||
@@ -25,4 +25,9 @@ internal static class TypeHandle
|
||||
{
|
||||
return type.TypeHandle.Value;
|
||||
}
|
||||
|
||||
public static Type? ToType(nint handle)
|
||||
{
|
||||
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(handle));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user