Refactor scene loading, shared components, and cleanup

- Split scene loading into parsing and materialization steps
- Make SceneID an ISharedComponent; add SharedComponentSet
- Centralize archetype/cleanup logic for entity destruction
- Add batch DestroyEntities to EntityCommandBuffer
- Use shared component filtering for SceneID queries
- Move AssetType/AssetState enums to AssetEntry.cs
- Remove ManagedEntity/ScriptComponent logic
- Misc: Write<T> signature, AsSpan, code style, GC fixes
This commit is contained in:
2026-05-16 21:07:54 +09:00
parent f85cf4edde
commit 18505cdff6
13 changed files with 287 additions and 669 deletions

View File

@@ -32,7 +32,7 @@ public unsafe struct BufferWriter : IDisposable
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write<T>(T value)
public void Write<T>(scoped in T value)
where T : unmanaged
{
EnsureCapacity(sizeof(T));
@@ -72,7 +72,7 @@ public unsafe struct BufferWriter : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Span<byte> AsSpan()
{
return _buffer.AsSpan();
return _buffer.AsSpan(0, Position);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -114,7 +114,7 @@ public unsafe ref struct SpanWriter
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write<T>(T value)
public void Write<T>(scoped in T value)
where T : unmanaged
{
Unsafe.WriteUnaligned(ref _buffer[_position], value);

View File

@@ -2,7 +2,7 @@ using Ghost.Entities;
namespace Ghost.Engine.Components;
public struct SceneID : IComponent // TODO: ISharedComponent
public struct SceneID : ISharedComponent
{
public ushort value;
}

View File

@@ -3,7 +3,6 @@ using Ghost.Core.Utilities;
using Ghost.Engine.Components;
using Ghost.Engine.Streaming;
using Ghost.Entities;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Text;
@@ -29,7 +28,7 @@ public struct Scene : IEquatable<Scene>
/// <summary>
/// Gets an invalid scene instance.
/// </summary>
public static Scene Invalid => new Scene { _id = INVALID_ID };
public static Scene Invalid => new() { _id = INVALID_ID };
internal Scene(ushort id)
{
@@ -76,105 +75,43 @@ public struct Scene : IEquatable<Scene>
/// </remarks>
public static class SceneManager
{
private struct BinaryEntityInfo : IDisposable
{
public int entityIndex;
public int componentCount;
public struct ComponentInfo
{
public UnsafeArray<int> entityFieldOffsets;
public long dataOffset;
public uint typeHash;
public Identifier<IComponent> typeID;
public int dataSize;
public int entityFieldCount;
}
public UnsafeArray<ComponentInfo> components;
public void Dispose()
{
for (var i = 0; i < components.Length; i++)
{
components[i].entityFieldOffsets.Dispose();
}
components.Dispose();
}
}
private struct BinaryEntityInfoArray : IDisposable
{
public UnsafeArray<BinaryEntityInfo> data;
public readonly ref BinaryEntityInfo this[int index] => ref data[index];
public BinaryEntityInfoArray(int count, AllocationHandle handle)
{
data = new UnsafeArray<BinaryEntityInfo>(count, handle, AllocationOption.Clear);
}
public void Dispose()
{
for (var i = 0; i < data.Length; i++)
{
data[i].Dispose();
}
data.Dispose();
}
}
internal struct SceneLoadResult : IDisposable
{
internal struct PendingEntity : IDisposable
{
public int fileLocalIndex;
public ComponentSet componentSet;
public UnsafeList<Identifier<IComponent>> componentTypeIDs;
public UnsafeList<(Identifier<IComponent> typeID, UnsafeArray<byte> data)> componentData;
public UnsafeList<(int componentIndex, UnsafeArray<int> fieldOffsets)> entityFields;
public UnsafeList<(int componentDataIndex, UnsafeArray<int> fieldOffsets)> entityFields;
public void Dispose()
{
componentTypeIDs.Dispose();
for (int i = 0; i < componentData.Count; i++)
{
componentData[i].data.Dispose();
}
componentSet.Dispose();
componentData.Dispose();
for (int i = 0; i < entityFields.Count; i++)
{
entityFields[i].fieldOffsets.Dispose();
}
entityFields.Dispose();
}
}
public UnsafeList<PendingEntity> entities;
public Scene scene;
public UnsafeArray<PendingEntity> entities;
public void Dispose()
{
for (int i = 0; i < entities.Count; i++)
for (int i = 0; i < entities.Length; i++)
{
entities[i].Dispose();
}
entities.Dispose();
}
}
internal unsafe struct LoadSceneJob : IJob
{
public SceneContentHeader header;
public Stream stream;
public SceneLoadResult* result;
public AllocationHandle allocationHandle;
public void Execute(ref readonly JobExecutionContext ctx)
{
}
}
private static ushort s_nextSceneID;
private static readonly Queue<ushort> s_recycledSceneIDs = new();
@@ -197,23 +134,39 @@ public static class SceneManager
}
}
internal static unsafe Result<JobHandle> LoadSceneIntoWorld(World world, SceneContentHeader header, Stream stream)
internal static SceneLoadResult ParseSceneData(SceneContentHeader header, Stream stream, AllocationHandle allocationHandle)
{
using var scope = AllocationManager.CreateStackScope();
var result = new SceneLoadResult
{
entities = new UnsafeArray<SceneLoadResult.PendingEntity>(header.entityCount, allocationHandle)
};
using var entityInfos = new BinaryEntityInfoArray(header.entityCount, scope.AllocationHandle);
using var forwardMap = new UnsafeHashMap<int, Entity>(header.entityCount, scope.AllocationHandle);
using var scope = AllocationManager.CreateStackScope();
using var str = new UnsafeArray<byte>(128, scope.AllocationHandle);
for (var i = 0; i < header.entityCount; i++)
{
var compCount = stream.Read<int>();
if (compCount == 0)
{
result.entities[i] = new SceneLoadResult.PendingEntity
{
fileLocalIndex = i,
componentTypeIDs = new UnsafeList<Identifier<IComponent>>(0, allocationHandle),
componentData = new UnsafeList<(Identifier<IComponent> typeID, UnsafeArray<byte> data)>(0, allocationHandle),
entityFields = new UnsafeList<(int componentDataIndex, UnsafeArray<int> fieldOffsets)>(0, allocationHandle)
};
continue;
}
var comps = new UnsafeArray<BinaryEntityInfo.ComponentInfo>(compCount, scope.AllocationHandle);
var pending = new SceneLoadResult.PendingEntity
{
fileLocalIndex = i,
componentTypeIDs = new UnsafeList<Identifier<IComponent>>(compCount, allocationHandle),
componentData = new UnsafeList<(Identifier<IComponent> typeID, UnsafeArray<byte> data)>(compCount, allocationHandle),
entityFields = new UnsafeList<(int componentDataIndex, UnsafeArray<int> fieldOffsets)>(compCount, allocationHandle)
};
for (var j = 0; j < compCount; j++)
{
@@ -226,119 +179,109 @@ public static class SceneManager
}
var strSpan = str.AsSpan(0, nameLength);
stream.ReadExactly(strSpan);
stream.ReadExactly(strSpan.Slice(0, nameLength));
var typeName = Encoding.UTF8.GetString(strSpan);
var dataSz = stream.Read<int>();
var dataOff = stream.Position;
stream.Position += dataSz;
var compData = new UnsafeArray<byte>(dataSz, allocationHandle);
stream.ReadExactly(compData);
var fieldCount = stream.Read<int>();
var fieldOffsets = new UnsafeArray<int>(fieldCount, scope.AllocationHandle);
for (var f = 0; f < fieldCount; f++)
UnsafeArray<int> fieldOffsets = default;
if (fieldCount > 0)
{
fieldOffsets[f] = stream.Read<int>();
fieldOffsets = new UnsafeArray<int>(fieldCount, allocationHandle);
for (var f = 0; f < fieldCount; f++)
{
fieldOffsets[f] = stream.Read<int>();
}
}
var typeID = ComponentRegistry.GetComponentIDByName(typeName);
comps[j] = new BinaryEntityInfo.ComponentInfo
if (typeID.IsValid)
{
dataOffset = dataOff,
entityFieldOffsets = fieldOffsets,
typeHash = typeHash,
typeID = typeID,
dataSize = dataSz,
entityFieldCount = fieldCount,
};
}
entityInfos[i] = new BinaryEntityInfo
{
entityIndex = i,
componentCount = compCount,
components = comps,
};
}
using var typeIds = new UnsafeList<Identifier<IComponent>>(32, scope.AllocationHandle);
typeIds.Add(ComponentTypeID<SceneID>.Value);
for (var i = 0; i < header.entityCount; i++)
{
ref var info = ref entityInfos[i];
for (var j = 0; j < info.componentCount; j++)
{
if (info.components[j].typeID.IsValid)
pending.componentTypeIDs.Add(typeID);
pending.componentData.Add((typeID, compData));
if (fieldCount > 0)
{
pending.entityFields.Add((pending.componentData.Count - 1, fieldOffsets));
}
}
else
{
typeIds.Add(info.components[j].typeID);
compData.Dispose();
if (fieldCount > 0) fieldOffsets.Dispose();
}
}
var set = new ComponentSetView(typeIds);
result.entities[i] = pending;
}
return result;
}
internal static unsafe Result<int> MaterializeScene(World world, ref SceneLoadResult result, Scene scene)
{
using var scope = AllocationManager.CreateStackScope();
using var forwardMap = new UnsafeHashMap<int, Entity>(result.entities.Length, scope.AllocationHandle);
using var sharedCom = new SharedComponentSet(256, scope.AllocationHandle);
// Create entities and set SceneID
for (var i = 0; i < result.entities.Length; i++)
{
ref var pending = ref result.entities[i];
using var typeIds = new UnsafeList<Identifier<IComponent>>(pending.componentTypeIDs.Count + 1, scope.AllocationHandle);
typeIds.Add(ComponentTypeID<SceneID>.Value);
for (int j = 0; j < pending.componentTypeIDs.Count; j++)
{
typeIds.Add(pending.componentTypeIDs[j]);
}
sharedCom.With(new SceneID { value = scene.ID });
var set = new ComponentSetView(typeIds, sharedCom);
var entity = world.EntityManager.CreateEntity(set);
forwardMap.TryAdd(pending.fileLocalIndex, entity);
forwardMap.TryAdd(i, entity);
typeIds.RemoveRange(1, typeIds.Count - 1);
sharedCom.Reset();
}
var activeScene = CreateScene();
for (var i = 0; i < header.entityCount; i++)
// Set component data
for (var i = 0; i < result.entities.Length; i++)
{
if (!forwardMap.TryGetValue(i, out var entity))
{
ref var pending = ref result.entities[i];
if (!forwardMap.TryGetValue(pending.fileLocalIndex, out var entity))
continue;
}
world.EntityManager.SetComponent(entity, new SceneID { value = activeScene.ID });
using var compScope = AllocationManager.CreateStackScope();
var info = entityInfos[i];
for (var j = 0; j < info.componentCount; j++)
for (var j = 0; j < pending.componentData.Count; j++)
{
var comp = info.components[j];
if (!comp.typeID.IsValid)
{
continue;
}
var compSize = ComponentRegistry.GetComponentInfo(comp.typeID).size;
stream.Position = comp.dataOffset;
using var src = stream.ReadMemory(compSize, compScope.AllocationHandle);
world.EntityManager.SetComponent(entity, comp.typeID, src.GetUnsafePtr());
var (typeID, data) = pending.componentData[j];
world.EntityManager.SetComponent(entity, typeID, data.GetUnsafePtr());
}
}
for (var i = 0; i < header.entityCount; i++)
// Remap entity references
for (var i = 0; i < result.entities.Length; i++)
{
if (!forwardMap.TryGetValue(i, out var entity))
{
ref var pending = ref result.entities[i];
if (!forwardMap.TryGetValue(pending.fileLocalIndex, out var entity))
continue;
}
var info = entityInfos[i];
for (var j = 0; j < info.componentCount; j++)
for (var j = 0; j < pending.entityFields.Count; j++)
{
var comp = info.components[j];
if (!comp.typeID.IsValid || comp.entityFieldCount == 0)
{
continue;
}
var (componentDataIndex, fieldOffsets) = pending.entityFields[j];
var compTypeID = pending.componentData[componentDataIndex].typeID;
var pComponent = world.EntityManager.GetComponent(entity, comp.typeID);
var pComponent = world.EntityManager.GetComponent(entity, compTypeID);
if (pComponent == null)
{
continue;
}
for (var f = 0; f < comp.entityFieldCount; f++)
for (var f = 0; f < fieldOffsets.Length; f++)
{
var fieldOffset = comp.entityFieldOffsets[f];
var fieldOffset = fieldOffsets[f];
var pField = (byte*)pComponent + fieldOffset;
var fileLocalIndex = *(int*)pField;
if (!forwardMap.TryGetValue(fileLocalIndex, out var remappedEntity))
@@ -351,7 +294,7 @@ public static class SceneManager
}
}
return Result.Success();
return Result.Success(result.entities.Length);
}
/// <summary>
@@ -365,29 +308,18 @@ public static class SceneManager
ref var query = ref world.ComponentManager.GetEntityQueryReference(queryID);
using var scope = AllocationManager.CreateStackScope();
var entitiesToDestroy = new UnsafeList<Entity>(128, scope.AllocationHandle);
using var ecb = new EntityCommandBuffer(512, scope.AllocationHandle);
// Iterate through all matching entities
foreach (var chunk in query.GetChunkIterator())
{
var entities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<SceneID>();
for (var i = 0; i < chunk.EntityCount; i++)
ref readonly var sceneID = ref chunk.GetSharedComponent<SceneID>();
if (sceneID.value == scene.ID)
{
if (sceneIDs[i].value == scene.ID)
{
entitiesToDestroy.Add(entities[i]);
}
ecb.DestroyEntities(chunk.GetEntities());
}
}
world.EntityManager.DestroyEntities(entitiesToDestroy.AsSpan());
s_recycledSceneIDs.Enqueue(scene.ID);
}
public static void ReleaseScene(Scene scene)
{
s_recycledSceneIDs.Enqueue(scene.ID);
}
@@ -408,15 +340,10 @@ public static class SceneManager
// Iterate through all matching entities
foreach (var chunk in query.GetChunkIterator())
{
var chunkEntities = chunk.GetEntities();
var sceneIDs = chunk.GetComponentData<SceneID>();
for (var i = 0; i < chunk.EntityCount; i++)
ref readonly var sceneID = ref chunk.GetSharedComponent<SceneID>();
if (sceneID.value == scene.ID)
{
if (sceneIDs[i].value == scene.ID)
{
entities.Add(chunkEntities[i]);
}
entities.AddRange(chunk.GetEntities());
}
}

View File

@@ -3,12 +3,35 @@ using Ghost.Graphics;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.CompilerServices;
namespace Ghost.Engine.Streaming;
public enum AssetType
{
Texture = 0,
Mesh = 1,
Material = 2,
Shader = 3,
Scene = 4,
Audio = 5,
Video = 6,
Json = 7,
Unknown = 64,
}
public enum AssetState
{
Unloaded = 0,
Scheduled = 1,
Loading = 2,
Loaded = 3,
Processing = 4,
Ready = 5,
Failed = 6,
}
internal static class AssetEntryFactory
{
public static AssetEntry CreateNewEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies)

View File

@@ -1,42 +1,15 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Graphics.Services;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace Ghost.Engine.Streaming;
public enum AssetType
{
Texture = 0,
Mesh = 1,
Material = 2,
Shader = 3,
Scene = 4,
Audio = 5,
Video = 6,
Json = 7,
Unknown = 64,
}
public enum AssetState
{
Unloaded = 0,
Scheduled = 1,
Loading = 2,
Loaded = 3,
Processing = 4,
Ready = 5,
Failed = 6,
}
public interface IContentProvider
{
bool HasAsset(Guid guid);
@@ -263,5 +236,7 @@ public partial class AssetManager : IDisposable
}
_entries.Clear();
GC.SuppressFinalize(this);
}
}

View File

@@ -9,7 +9,7 @@ namespace Ghost.Engine.Streaming;
internal class ResourceStreamingProcessor : IResourceStreamingProcessor
{
private const int _MAX_UPLOADS_PER_FRAME = 8;
private const int MAX_UPLOADS_PER_FRAME = 8;
private readonly ConcurrentQueue<ProcessableAssetEntry> _pendingProcess;
private readonly ConcurrentQueue<UploadableAssetEntry> _pendingUpload;
@@ -101,7 +101,7 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
context.CopyPipeline.Begin();
var uploadCount = 0;
while (uploadCount < _MAX_UPLOADS_PER_FRAME && _pendingUpload.TryDequeue(out var entry))
while (uploadCount < MAX_UPLOADS_PER_FRAME && _pendingUpload.TryDequeue(out var entry))
{
if (entry.State != AssetState.Loaded)
{

View File

@@ -5,6 +5,7 @@ using Ghost.Entities;
using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices;
namespace Ghost.Engine.Streaming;
@@ -21,6 +22,7 @@ internal struct SceneContentHeader
public int entityCount;
}
// TODO: We should have a dedicated scene loading service. Maybe we should make our SceneManager as a service.
public partial class AssetManager
{
public Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType)
@@ -56,11 +58,7 @@ public partial class AssetManager
world.Reset();
}
var loadResult = SceneManager.LoadSceneIntoWorld(world, header, stream);
if (loadResult.IsFailure)
{
return Result.Failure(loadResult.Message);
}
var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent);
return JobHandle.Invalid;
}

View File

@@ -1,4 +1,5 @@
using Ghost.Core;
using Ghost.Core.Utilities;
using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
@@ -52,11 +53,6 @@ internal static class ComponentRegistry
internal static readonly Dictionary<int, Type> s_runtimeIDToType = new();
#endif
static ComponentRegistry()
{
GetOrRegisterComponentID<ManagedEntityRef>();
}
public static unsafe Identifier<IComponent> GetOrRegisterComponentID<T>()
where T : unmanaged, IComponent
{
@@ -278,12 +274,12 @@ public partial class ComponentManager : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Clear()
{
for (int i = 0; i < _archetypes.Count; i++)
for (var i = 0; i < _archetypes.Count; i++)
{
_archetypes[i].Dispose();
}
for (int i = 0; i < _entityQueries.Count; i++)
for (var i = 0; i < _entityQueries.Count; i++)
{
_entityQueries[i].Dispose();
}
@@ -300,7 +296,7 @@ public partial class ComponentManager : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Collect()
{
for (int i = 0; i < _archetypes.Count; i++)
for (var i = 0; i < _archetypes.Count; i++)
{
_archetypes[i].Collect();
}
@@ -342,6 +338,37 @@ public partial class ComponentManager : IDisposable
}
}
public struct SharedComponentSet : IDisposable
{
private BufferWriter _writer;
public SharedComponentSet(int capacity, AllocationHandle allocationHandle)
{
_writer = new BufferWriter(capacity, allocationHandle);
}
public void With<T>(scoped in T data)
where T : unmanaged, ISharedComponent
{
_writer.Write(in data);
}
public readonly ReadOnlySpan<byte> AsSpan()
{
return _writer.AsSpan();
}
public void Reset()
{
_writer.Reset();
}
public void Dispose()
{
_writer.Dispose();
}
}
/// <summary>
/// Represents an immutable set of component identifiers used to define a group of components within an entity or system.
/// </summary>
@@ -404,6 +431,11 @@ public struct ComponentSet : IDisposable, IEquatable<ComponentSet>
_sharedHashCode = -1;
}
public ComponentSet(AllocationHandle allocationHandle, ReadOnlySpan<Identifier<IComponent>> components, SharedComponentSet sharedComponentSet)
: this(allocationHandle, components, sharedComponentSet.AsSpan())
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ComponentSetView AsView()
{
@@ -502,6 +534,11 @@ public ref struct ComponentSetView : IEquatable<ComponentSetView>
_sharedHashCode = -1;
}
public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components, SharedComponentSet sharedComponentSet)
: this(components, sharedComponentSet.AsSpan())
{
}
public readonly bool Equals(ComponentSetView other)
{
return _hashCode == other._hashCode && _sharedHashCode == other._sharedHashCode;

View File

@@ -12,6 +12,7 @@ public unsafe struct EntityCommandBuffer : IDisposable
CreateEntity,
CreateEntityWithComponents,
DestroyEntity,
DestroyEntities,
AddComponent,
RemoveComponent,
SetComponent,
@@ -52,6 +53,14 @@ public unsafe struct EntityCommandBuffer : IDisposable
_writer.Write(entity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DestroyEntities(params ReadOnlySpan<Entity> entities)
{
_writer.Write(ECBOpCode.DestroyEntities);
_writer.Write(entities.Length);
_writer.WriteSpan(entities);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T>(Entity entity, T component = default)
where T : unmanaged, IComponent
@@ -151,6 +160,12 @@ public unsafe struct EntityCommandBuffer : IDisposable
entityManager.DestroyEntity(entityToDestroy);
break;
case ECBOpCode.DestroyEntities:
var removeCount = reader.Read<int>();
var entitiesToRemove = reader.ReadSpan<Entity>(removeCount);
entityManager.DestroyEntities(entitiesToRemove);
break;
case ECBOpCode.AddComponent:
var entityToAdd = reader.Read<Entity>();
var addCompTypeID = reader.Read<Identifier<IComponent>>();

View File

@@ -1,224 +0,0 @@
using Misaki.HighPerformance.Collections;
using Misaki.HighPerformance.Utilities;
namespace Ghost.Entities;
public partial class EntityManager
{
private readonly SlotMap<List<ScriptComponent>> _scriptComponents;
internal SlotMap<List<ScriptComponent>> ScriptComponents => _scriptComponents;
/// <summary>
/// Creates a new ManagedEntity and associates it with the given Entity.
/// </summary>
/// <param name="entity">The Entity to associate with the ManagedEntity.</param>
/// <returns>The created ManagedEntity.</returns>
public ManagedEntity CreateManagedEntity(Entity entity)
{
var managedEntity = CreateManagedEntity();
AddComponent(entity, new ManagedEntityRef
{
entity = managedEntity
});
return managedEntity;
}
/// <summary>
/// Creates a new ManagedEntity.
/// </summary>
/// <remarks>
/// You must call this if you add <see cref="ManagedEntityRef"/> manually to an entity.
/// Otherwise, use <see cref="CreateManagedEntity(Entity)"/>.
/// </remarks>
/// <returns>The created ManagedEntity.</returns>
public ManagedEntity CreateManagedEntity()
{
var id = _scriptComponents.Add(new(8), out var generation);
var managedEntity = new ManagedEntity
{
id = id,
generation = generation
};
return managedEntity;
}
/// <summary>
/// Destroys the given ManagedEntity and calls OnDestroy on all associated ScriptComponents.
/// </summary>
/// <param name="managedEntity">The ManagedEntity to destroy.</param>
public void DestroyManagedEntity(ManagedEntity managedEntity)
{
if (_scriptComponents.TryGetElement(managedEntity.id, managedEntity.generation, out var scripts))
{
foreach (var script in scripts)
{
script.OnDestroy();
}
_scriptComponents.Remove(managedEntity.id, managedEntity.generation);
}
}
/// <summary>
/// Checks if the given ManagedEntity exists.
/// </summary>
/// <param name="managedEntity">The ManagedEntity to check.</param>
/// <returns>True if the ManagedEntity exists, false otherwise.</returns>
public bool Exists(ManagedEntity managedEntity)
{
return _scriptComponents.Contains(managedEntity.id, managedEntity.generation);
}
/// <summary>
/// Adds a ScriptComponent of space T to the given ManagedEntity and Entity.
/// </summary>
/// <typeparam name="T">The space of ScriptComponent to add.</typeparam>
/// <param name="managedEntity">The ManagedEntity to add the ScriptComponent to.</
/// <param name="entity">The Entity associated with the ManagedEntity.</param>
public void AddScriptComponent<T>(ManagedEntity managedEntity, Entity entity)
where T : ScriptComponent, new()
{
if (_scriptComponents.TryGetElement(managedEntity.id, managedEntity.generation, out var scripts))
{
var script = new T
{
_world = _world,
_entity = entity,
_managedEntity = managedEntity
};
scripts.Add(script);
script.OnCreate();
return;
}
throw new InvalidOperationException($"ManagedEntity {managedEntity} does not exist.");
}
/// <summary>
/// Adds a ScriptComponent of space T to the given Entity.
/// </summary>
/// <typeparam name="T">The space of ScriptComponent to add.</typeparam>
/// <param name="entity">The Entity to add the ScriptComponent to.</param>
public unsafe void AddScriptComponent<T>(Entity entity)
where T : ScriptComponent, new()
{
var location = _entityLocations.GetElementAt(entity.ID, entity.Generation);
ref var archetype = ref _world.ComponentManager.GetArchetypeReference(location.archetypeID);
var pManagedEntityRef = (ManagedEntityRef*)archetype.GetComponentData(location.chunkIndex, location.rowIndex, ComponentTypeID<ManagedEntityRef>.Value);
if (pManagedEntityRef == null)
{
throw new InvalidOperationException($"Entity {entity} does not have ManagedEntityRef component.");
}
AddScriptComponent<T>(pManagedEntityRef->entity, entity);
}
/// <summary>
/// Destroys the ScriptComponent of space T associated with the given ManagedEntity.
/// </summary>
/// <typeparam name="T">The space of ScriptComponent to destroy.</typeparam>
/// <param name="managedEntity">The ManagedEntity whose ScriptComponent is to be destroyed </param>
/// <returns>True if the ScriptComponent was found and destroyed, false otherwise.</returns
public bool DestroyScriptComponent<T>(ManagedEntity managedEntity)
where T : ScriptComponent
{
if (_scriptComponents.TryGetElement(managedEntity.id, managedEntity.generation, out var scripts))
{
for (var i = 0; i < scripts.Count; i++)
{
if (scripts[i] is T script)
{
script.OnDestroy();
scripts.RemoveAndSwapBack(i);
return true;
}
}
return false;
}
throw new InvalidOperationException($"ManagedEntity {managedEntity} does not exist.");
}
/// <summary>
/// Checks if the given ManagedEntity has a ScriptComponent of space T.
/// </summary>
/// <typeparam name="T">The space of ScriptComponent to check for.</typeparam>
/// <param name="managedEntity">The ManagedEntity to check.</param>
/// <returns>True if the ManagedEntity has a ScriptComponent of space T, false </returns>
public bool HasScriptComponent<T>(ManagedEntity managedEntity)
where T : ScriptComponent
{
if (_scriptComponents.TryGetElement(managedEntity.id, managedEntity.generation, out var scripts))
{
foreach (var script in scripts)
{
if (script is T)
{
return true;
}
}
return false;
}
throw new InvalidOperationException($"ManagedEntity {managedEntity} does not exist.");
}
/// <summary>
/// Gets the ScriptComponent of space T associated with the given ManagedEntity.
/// </summary>
/// <typeparam name="T">The space of ScriptComponent to get.</typeparam>
/// <param name="managedEntity">The ManagedEntity whose ScriptComponent is to be retrieved
/// <returns>The ScriptComponent of space T.</returns>
public T GetScriptComponent<T>(ManagedEntity managedEntity)
where T : ScriptComponent
{
if (_scriptComponents.TryGetElement(managedEntity.id, managedEntity.generation, out var scripts))
{
foreach (var script in scripts)
{
if (script is T typedScript)
{
return typedScript;
}
}
throw new InvalidOperationException($"ManagedEntity {managedEntity} does not have script component of type {typeof(T)}.");
}
throw new InvalidOperationException($"ManagedEntity {managedEntity} does not exist.");
}
/// <summary>
/// Gets all ScriptComponents of space T associated with the given ManagedEntity.
/// </summary>
/// <typeparam name="T">The space of ScriptComponent to get.</typeparam>
/// <param name="managedEntity">The ManagedEntity whose ScriptComponents are to be retrieved
/// <returns>The list of ScriptComponents of space T.</returns>
public List<T> GetScriptComponents<T>(ManagedEntity managedEntity)
where T : ScriptComponent
{
if (_scriptComponents.TryGetElement(managedEntity.id, managedEntity.generation, out var scripts))
{
var result = new List<T>();
foreach (var script in scripts)
{
if (script is T typedScript)
{
result.Add(typedScript);
}
}
return result;
}
throw new InvalidOperationException($"ManagedEntity {managedEntity} does not exist.");
}
}

View File

@@ -52,7 +52,6 @@ public unsafe partial class EntityManager : IDisposable
{
_world = world;
_entityLocations = new UnsafeSlotMap<EntityLocation>(initialCapacity, AllocationHandle.Persistent, AllocationOption.Clear);
_scriptComponents = new SlotMap<List<ScriptComponent>>(initialCapacity / 2);
}
~EntityManager()
@@ -93,6 +92,78 @@ public unsafe partial class EntityManager : IDisposable
_entityLocations.Clear();
}
/// <summary>
/// Get or compute the cleanup archetype for <paramref name="archetype"/>.
/// The cleanup archetype contains only <see cref="ICleanupComponent"/> components,
/// so they can get a final tick before the entity is fully destroyed.
/// </summary>
private Identifier<Archetype> GetOrCreateCleanupArchetype(ref Archetype archetype)
{
if (archetype._cleanupEdge >= 0)
{
return archetype._cleanupEdge;
}
ref var signature = ref archetype._signature;
using var scope = AllocationManager.CreateStackScope();
using var newSignature = new UnsafeBitSet(signature.Count, scope.AllocationHandle);
var compCount = 0;
var it = signature.GetIterator();
while (it.Next(out var componentID))
{
if (ComponentRegistry.GetComponentInfo(componentID).isCleanup)
{
newSignature.SetBit(componentID);
compCount++;
}
}
var newSignatureHash = newSignature.GetHashCode();
var newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var i = 0;
while (newIt.Next(out var cid))
{
componentTypeIDs[i++] = cid;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
archetype._cleanupEdge = newArcID;
return newArcID;
}
/// <summary>
/// Look up or create an archetype from a <see cref="SpanBitSet"/> signature.
/// </summary>
private Identifier<Archetype> FindOrCreateArchetype(ref readonly SpanBitSet signature, int componentCount)
{
var hash = signature.GetHashCode();
var arcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(hash);
if (arcID.IsInvalid)
{
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[componentCount];
var it = signature.GetIterator();
var i = 0;
while (it.Next(out var cid))
{
componentTypeIDs[i++] = cid;
}
arcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, hash);
}
return arcID;
}
private static void CopyData(ref Archetype oldArch, int oldChunk, int oldRow,
ref Archetype newArch, int newChunk, int newRow)
{
@@ -303,48 +374,7 @@ public unsafe partial class EntityManager : IDisposable
}
else
{
Identifier<Archetype> newArcID = default;
if (archetype._cleanupEdge < 0)
{
ref var signature = ref archetype._signature;
using var scope = AllocationManager.CreateStackScope();
using var newSignature = new UnsafeBitSet(signature.Count, scope.AllocationHandle);
var compCount = 0;
var it = signature.GetIterator();
while (it.Next(out var componentID))
{
if (ComponentRegistry.GetComponentInfo(componentID).isCleanup)
{
newSignature.SetBit(componentID);
compCount++;
}
}
var newSignatureHash = newSignature.GetHashCode();
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
// Create new archetype
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var i = 0;
while (newIt.Next(out var index))
{
componentTypeIDs[i++] = index;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
archetype._cleanupEdge = newArcID;
}
else
{
newArcID = archetype._cleanupEdge;
}
var newArcID = GetOrCreateCleanupArchetype(ref archetype);
ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID);
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
@@ -361,7 +391,7 @@ public unsafe partial class EntityManager : IDisposable
/// Destroy the specified entities.
/// </summary>
/// <param name="entities">The entities to destroy.</param>
public void DestroyEntities(ReadOnlySpan<Entity> entities)
public void DestroyEntities(params ReadOnlySpan<Entity> entities)
{
if (entities.Length == 0)
{
@@ -394,48 +424,7 @@ public unsafe partial class EntityManager : IDisposable
else
{
// Archetype has ICleanupComponent — move entity to cleanup archetype.
Identifier<Archetype> newArcID;
if (archetype._cleanupEdge < 0)
{
// Compute cleanup edge: build a signature containing only cleanup components.
ref var signature = ref archetype._signature;
using var inner = AllocationManager.CreateStackScope();
using var newSignature = new UnsafeBitSet(signature.Count, inner.AllocationHandle);
var compCount = 0;
var it = signature.GetIterator();
while (it.Next(out var componentID))
{
if (ComponentRegistry.GetComponentInfo(componentID).isCleanup)
{
newSignature.SetBit(componentID);
compCount++;
}
}
var newSignatureHash = newSignature.GetHashCode();
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var idx = 0;
while (newIt.Next(out var cid))
{
componentTypeIDs[idx++] = cid;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
archetype._cleanupEdge = newArcID;
}
else
{
newArcID = archetype._cleanupEdge;
}
var newArcID = GetOrCreateCleanupArchetype(ref archetype);
ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID);
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
@@ -717,22 +706,7 @@ public unsafe partial class EntityManager : IDisposable
newSignature.SetBit(componentID);
// Find or create new archetype
var newSignatureHash = newSignature.GetHashCode();
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
// Create new archetype
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var i = 0;
while (newIt.Next(out var index))
{
componentTypeIDs[i++] = index;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
newArcID = FindOrCreateArchetype(ref newSignature, compCount);
oldArchetype.AddEdgeAdd(componentID, newArcID);
}
@@ -847,22 +821,7 @@ public unsafe partial class EntityManager : IDisposable
}
// Find or create new archetype
var newSignatureHash = newSignature.GetHashCode();
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
// Create new archetype
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var i = 0;
while (newIt.Next(out var index))
{
componentTypeIDs[i++] = index;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
newArcID = FindOrCreateArchetype(ref newSignature, compCount);
oldArchetype.AddEdgeRemove(componentID, newArcID);
}
@@ -1121,20 +1080,7 @@ public unsafe partial class EntityManager : IDisposable
compCount++;
newSignature.SetBit(componentID);
var newSignatureHash = newSignature.GetHashCode();
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var i = 0;
while (newIt.Next(out var index))
{
componentTypeIDs[i++] = index;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
newArcID = FindOrCreateArchetype(ref newSignature, compCount);
oldArchetype.AddEdgeAdd(componentID, newArcID);
}
@@ -1227,20 +1173,7 @@ public unsafe partial class EntityManager : IDisposable
return DestroyEntity_Internal(entity, location);
}
var newSignatureHash = newSignature.GetHashCode();
newArcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(newSignatureHash);
if (newArcID.IsInvalid)
{
Span<Identifier<IComponent>> componentTypeIDs = stackalloc Identifier<IComponent>[compCount];
var newIt = newSignature.GetIterator();
var i = 0;
while (newIt.Next(out var index))
{
componentTypeIDs[i++] = index;
}
newArcID = _world.ComponentManager.CreateArchetype(componentTypeIDs, newSignatureHash);
}
newArcID = FindOrCreateArchetype(ref newSignature, compCount);
oldArchetype.AddEdgeRemove(componentID, newArcID);
}

View File

@@ -1,69 +0,0 @@
using System.Runtime.CompilerServices;
namespace Ghost.Entities;
public record struct ManagedEntity
{
public int id;
public int generation;
}
public struct ManagedEntityRef : IComponent
{
public ManagedEntity entity;
}
public abstract class ScriptComponent
{
internal World _world = null!;
internal Entity _entity;
internal ManagedEntity _managedEntity;
public World World => _world;
public Entity Entity => _entity;
public ManagedEntity ManagedEntity => _managedEntity;
protected ref T GetComponent<T>()
where T : unmanaged, IComponent
{
ref var value = ref _world.EntityManager.GetComponent<T>(_entity);
if (Unsafe.IsNullRef(ref value))
{
throw new InvalidOperationException($"Entity {_entity} does not have component of type {typeof(T)}");
}
return ref value;
}
public virtual void OnCreate()
{
}
public virtual void OnDestroy()
{
}
public virtual void OnEnable()
{
}
public virtual void OnDisable()
{
}
public virtual void Start()
{
}
public virtual void Update()
{
}
public virtual void FixedUpdate()
{
}
public virtual void LateUpdate()
{
}
}

View File

@@ -101,6 +101,9 @@ public class WorldTests
var eB = worldB.EntityManager.CreateEntity();
Assert.AreEqual(eA, eB);
// Entity does not store world reference, so we can't directly check if world a has eb or world b has ea, but we can check existence in each world.
Assert.IsTrue(_world.EntityManager.Exists(eA));
Assert.IsTrue(worldB.EntityManager.Exists(eB));
}
finally
{