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

View File

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

View File

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

View File

@@ -3,12 +3,35 @@ using Ghost.Graphics;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Engine.Streaming; 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 internal static class AssetEntryFactory
{ {
public static AssetEntry CreateNewEntry(AssetManager manager, IResourceDatabase resourceDatabase, ResourceManager resourceManager, Guid assetId, AssetType assetType, Guid[] dependencies) 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;
using Ghost.Core.Utilities;
using Ghost.Graphics.Services;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections; using Misaki.HighPerformance.LowLevel.Collections;
using Misaki.HighPerformance.LowLevel.Utilities;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace Ghost.Engine.Streaming; 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 public interface IContentProvider
{ {
bool HasAsset(Guid guid); bool HasAsset(Guid guid);
@@ -263,5 +236,7 @@ public partial class AssetManager : IDisposable
} }
_entries.Clear(); _entries.Clear();
GC.SuppressFinalize(this);
} }
} }

View File

@@ -9,7 +9,7 @@ namespace Ghost.Engine.Streaming;
internal class ResourceStreamingProcessor : IResourceStreamingProcessor 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<ProcessableAssetEntry> _pendingProcess;
private readonly ConcurrentQueue<UploadableAssetEntry> _pendingUpload; private readonly ConcurrentQueue<UploadableAssetEntry> _pendingUpload;
@@ -101,7 +101,7 @@ internal class ResourceStreamingProcessor : IResourceStreamingProcessor
context.CopyPipeline.Begin(); context.CopyPipeline.Begin();
var uploadCount = 0; 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) if (entry.State != AssetState.Loaded)
{ {

View File

@@ -5,6 +5,7 @@ using Ghost.Entities;
using Ghost.Graphics.RHI; using Ghost.Graphics.RHI;
using Ghost.Graphics.Services; using Ghost.Graphics.Services;
using Misaki.HighPerformance.Jobs; using Misaki.HighPerformance.Jobs;
using Misaki.HighPerformance.LowLevel.Buffer;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Ghost.Engine.Streaming; namespace Ghost.Engine.Streaming;
@@ -21,6 +22,7 @@ internal struct SceneContentHeader
public int entityCount; 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 partial class AssetManager
{ {
public Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType) public Result<JobHandle> LoadScene(World world, AssetRef<Scene> sceneAsset, SceneLoadingType loadingType)
@@ -56,11 +58,7 @@ public partial class AssetManager
world.Reset(); world.Reset();
} }
var loadResult = SceneManager.LoadSceneIntoWorld(world, header, stream); var loadResult = SceneManager.ParseSceneData(header, stream, AllocationHandle.Persistent);
if (loadResult.IsFailure)
{
return Result.Failure(loadResult.Message);
}
return JobHandle.Invalid; return JobHandle.Invalid;
} }

View File

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

View File

@@ -12,6 +12,7 @@ public unsafe struct EntityCommandBuffer : IDisposable
CreateEntity, CreateEntity,
CreateEntityWithComponents, CreateEntityWithComponents,
DestroyEntity, DestroyEntity,
DestroyEntities,
AddComponent, AddComponent,
RemoveComponent, RemoveComponent,
SetComponent, SetComponent,
@@ -52,6 +53,14 @@ public unsafe struct EntityCommandBuffer : IDisposable
_writer.Write(entity); _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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddComponent<T>(Entity entity, T component = default) public void AddComponent<T>(Entity entity, T component = default)
where T : unmanaged, IComponent where T : unmanaged, IComponent
@@ -151,6 +160,12 @@ public unsafe struct EntityCommandBuffer : IDisposable
entityManager.DestroyEntity(entityToDestroy); entityManager.DestroyEntity(entityToDestroy);
break; break;
case ECBOpCode.DestroyEntities:
var removeCount = reader.Read<int>();
var entitiesToRemove = reader.ReadSpan<Entity>(removeCount);
entityManager.DestroyEntities(entitiesToRemove);
break;
case ECBOpCode.AddComponent: case ECBOpCode.AddComponent:
var entityToAdd = reader.Read<Entity>(); var entityToAdd = reader.Read<Entity>();
var addCompTypeID = reader.Read<Identifier<IComponent>>(); 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; _world = world;
_entityLocations = new UnsafeSlotMap<EntityLocation>(initialCapacity, AllocationHandle.Persistent, AllocationOption.Clear); _entityLocations = new UnsafeSlotMap<EntityLocation>(initialCapacity, AllocationHandle.Persistent, AllocationOption.Clear);
_scriptComponents = new SlotMap<List<ScriptComponent>>(initialCapacity / 2);
} }
~EntityManager() ~EntityManager()
@@ -93,6 +92,78 @@ public unsafe partial class EntityManager : IDisposable
_entityLocations.Clear(); _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, private static void CopyData(ref Archetype oldArch, int oldChunk, int oldRow,
ref Archetype newArch, int newChunk, int newRow) ref Archetype newArch, int newChunk, int newRow)
{ {
@@ -303,48 +374,7 @@ public unsafe partial class EntityManager : IDisposable
} }
else else
{ {
Identifier<Archetype> newArcID = default; var newArcID = GetOrCreateCleanupArchetype(ref archetype);
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;
}
ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID); ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID);
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex); newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
@@ -361,7 +391,7 @@ public unsafe partial class EntityManager : IDisposable
/// Destroy the specified entities. /// Destroy the specified entities.
/// </summary> /// </summary>
/// <param name="entities">The entities to destroy.</param> /// <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) if (entities.Length == 0)
{ {
@@ -394,48 +424,7 @@ public unsafe partial class EntityManager : IDisposable
else else
{ {
// Archetype has ICleanupComponent — move entity to cleanup archetype. // Archetype has ICleanupComponent — move entity to cleanup archetype.
Identifier<Archetype> newArcID; var newArcID = GetOrCreateCleanupArchetype(ref archetype);
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;
}
ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID); ref var newArchetype = ref _world.ComponentManager.GetArchetypeReference(newArcID);
newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex); newArchetype.AllocateEntity(out var newChunkIndex, out var newRowIndex);
@@ -717,22 +706,7 @@ public unsafe partial class EntityManager : IDisposable
newSignature.SetBit(componentID); newSignature.SetBit(componentID);
// Find or create new archetype // Find or create new archetype
var newSignatureHash = newSignature.GetHashCode(); newArcID = FindOrCreateArchetype(ref newSignature, compCount);
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);
}
oldArchetype.AddEdgeAdd(componentID, newArcID); oldArchetype.AddEdgeAdd(componentID, newArcID);
} }
@@ -847,22 +821,7 @@ public unsafe partial class EntityManager : IDisposable
} }
// Find or create new archetype // Find or create new archetype
var newSignatureHash = newSignature.GetHashCode(); newArcID = FindOrCreateArchetype(ref newSignature, compCount);
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);
}
oldArchetype.AddEdgeRemove(componentID, newArcID); oldArchetype.AddEdgeRemove(componentID, newArcID);
} }
@@ -1121,20 +1080,7 @@ public unsafe partial class EntityManager : IDisposable
compCount++; compCount++;
newSignature.SetBit(componentID); newSignature.SetBit(componentID);
var newSignatureHash = newSignature.GetHashCode(); newArcID = FindOrCreateArchetype(ref newSignature, compCount);
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);
}
oldArchetype.AddEdgeAdd(componentID, newArcID); oldArchetype.AddEdgeAdd(componentID, newArcID);
} }
@@ -1227,20 +1173,7 @@ public unsafe partial class EntityManager : IDisposable
return DestroyEntity_Internal(entity, location); return DestroyEntity_Internal(entity, location);
} }
var newSignatureHash = newSignature.GetHashCode(); newArcID = FindOrCreateArchetype(ref newSignature, compCount);
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);
}
oldArchetype.AddEdgeRemove(componentID, newArcID); 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(); var eB = worldB.EntityManager.CreateEntity();
Assert.AreEqual(eA, eB); 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 finally
{ {