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:
2025-06-07 20:54:07 +09:00
parent bab3be2508
commit 40d333b004
123 changed files with 1441 additions and 740 deletions

View File

@@ -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")]

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)]

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -25,4 +25,9 @@ internal static class TypeHandle
{
return type.TypeHandle.Value;
}
public static Type? ToType(nint handle)
{
return Type.GetTypeFromHandle(RuntimeTypeHandle.FromIntPtr(handle));
}
}