fix(scene): avoid scene ID collision on load

Do not serialize the SceneID component.
Generate a new SceneID when loading a scene file dynamically to prevent ID collisions.
Update related tests and scene graph builders.
This commit is contained in:
2026-05-10 22:50:05 +09:00
parent a95ff01366
commit 314b0111f0
14 changed files with 200 additions and 156 deletions

View File

@@ -99,7 +99,7 @@ Used for the **initial full build** of the scene graph from an ECS `World`. Afte
**Algorithm:** **Algorithm:**
1. Query all entities with `SceneID` component. 1. Query all entities with `SceneID` component.
2. Group entities by `SceneID.scene.ID`. 2. Group entities by `SceneID.scene.id`.
3. For each scene group: 3. For each scene group:
- Create a `SceneNode` (name comes from editor metadata, not from runtime). - Create a `SceneNode` (name comes from editor metadata, not from runtime).
- Walk the `Hierarchy` component linked-list to build the tree: - Walk the `Hierarchy` component linked-list to build the tree:
@@ -238,7 +238,7 @@ public class EditorWorldService : IDisposable
1. Check `EditorWorld.Version` — if unchanged, skip. 1. Check `EditorWorld.Version` — if unchanged, skip.
2. Query all entities with `SceneID` component from the editor world. 2. Query all entities with `SceneID` component from the editor world.
3. Group by `SceneID.scene.ID`. 3. Group by `SceneID.scene.id`.
4. **For each scene group:** 4. **For each scene group:**
- Find or create the `SceneNode` in `RootNodes` (match by `Scene.ID`). - Find or create the `SceneNode` in `RootNodes` (match by `Scene.ID`).
- Walk the `Hierarchy` linked-list of roots. - Walk the `Hierarchy` linked-list of roots.

View File

@@ -151,6 +151,6 @@ public static class SceneGraphBuilder
private static string GetDefaultSceneName(Scene scene) private static string GetDefaultSceneName(Scene scene)
{ {
return $"NewScene ({scene.ID})"; return $"NewScene ({scene.id})";
} }
} }

View File

@@ -23,15 +23,15 @@ public class EditorWorldService : IDisposable
public EditorWorldService() public EditorWorldService()
{ {
EditorWorld = World.Create(entityCapacity: DEFAULT_ENTITY_CAPACITY); EditorWorld = World.Create(entityCapacity: DEFAULT_ENTITY_CAPACITY);
CreateDefaultScene(); // CreateDefaultScene();
RebuildSceneGraph(); // RebuildSceneGraph();
} }
public void CreateDefaultScene() public void CreateDefaultScene()
{ {
var scene = SceneManager.CreateScene(); var scene = SceneManager.CreateScene();
var entity = EditorWorld.EntityManager.CreateEntity(); var entity = EditorWorld.EntityManager.CreateEntity();
EditorWorld.EntityManager.AddComponent(entity, new Ghost.Engine.Components.SceneID EditorWorld.EntityManager.AddComponent(entity, new Engine.Components.SceneID
{ {
scene = scene scene = scene
}); });

View File

@@ -55,7 +55,7 @@ public class SceneGraphSyncService
} }
} }
var sceneName = $"NewScene ({scene.ID})"; var sceneName = $"NewScene ({scene.id})";
var newSceneNode = new SceneNode(world, scene, sceneName); var newSceneNode = new SceneNode(world, scene, sceneName);
rootNodes.Add(newSceneNode); rootNodes.Add(newSceneNode);
return newSceneNode; return newSceneNode;

View File

@@ -136,29 +136,6 @@ public class SceneSerializationService : IDisposable
return hash; return hash;
} }
private static int[] GetEntityFieldOffsetsFromJson(string typeName, string componentJson)
{
var type = Type.GetType(typeName);
if (type == null)
{
return Array.Empty<int>();
}
var entityFields = GetEntityFields(type);
if (entityFields.Length == 0)
{
return Array.Empty<int>();
}
var offsets = new int[entityFields.Length];
for (var i = 0; i < entityFields.Length; i++)
{
offsets[i] = (int)Marshal.OffsetOf(type, entityFields[i].Name);
}
return offsets;
}
public static unsafe void SerializeToBinary(SceneSaveData data, Stream targetStream) public static unsafe void SerializeToBinary(SceneSaveData data, Stream targetStream)
{ {
using var writer = new BinaryWriter(targetStream, Encoding.UTF8, true); using var writer = new BinaryWriter(targetStream, Encoding.UTF8, true);
@@ -185,7 +162,11 @@ public class SceneSerializationService : IDisposable
foreach (var (typeName, componentElement) in entity.Components) foreach (var (typeName, componentElement) in entity.Components)
{ {
var typeHash = GetTypeNameHash(typeName); var typeHash = GetTypeNameHash(typeName);
var componentType = Type.GetType(typeName); var componentType = TypeCache.GetTypes(typeName);
if (componentType == typeof(SceneID))
{
continue;
}
if (componentType == null) if (componentType == null)
{ {
@@ -276,7 +257,7 @@ public class SceneSerializationService : IDisposable
#region Load Scene into Editor World #region Load Scene into Editor World
public unsafe Result LoadSceneIntoEditorWorld(SceneSaveData data, SceneLoadingType loadingType = SceneLoadingType.Single) public unsafe Result<Scene> LoadSceneIntoEditorWorld(SceneSaveData data, SceneLoadingType loadingType = SceneLoadingType.Single)
{ {
if (loadingType == SceneLoadingType.Single) if (loadingType == SceneLoadingType.Single)
{ {
@@ -284,6 +265,7 @@ public class SceneSerializationService : IDisposable
} }
var world = _worldService.EditorWorld; var world = _worldService.EditorWorld;
var activeScene = SceneManager.CreateScene();
var entityCount = data.Entities.Count; var entityCount = data.Entities.Count;
if (entityCount == 0) if (entityCount == 0)
@@ -307,6 +289,8 @@ public class SceneSerializationService : IDisposable
var entityData = data.Entities[fileIndex]; var entityData = data.Entities[fileIndex];
ref var list = ref typeIds[fileIndex]; ref var list = ref typeIds[fileIndex];
list.Add(ComponentRegistry.GetOrRegisterComponentID<SceneID>());
foreach (var (typeName, _) in entityData.Components) foreach (var (typeName, _) in entityData.Components)
{ {
var compId = ComponentRegistry.GetComponentIDByName(typeName); var compId = ComponentRegistry.GetComponentIDByName(typeName);
@@ -324,12 +308,7 @@ public class SceneSerializationService : IDisposable
list.Add(compId); list.Add(compId);
} }
if (list.Count == 0) var componentSet = new ComponentSetView(list);
{
continue;
}
using var componentSet = new ComponentSet(scope.AllocationHandle, list);
var entity = world.EntityManager.CreateEntity(componentSet); var entity = world.EntityManager.CreateEntity(componentSet);
forwardMap[fileIndex] = entity; forwardMap[fileIndex] = entity;
} }
@@ -342,9 +321,11 @@ public class SceneSerializationService : IDisposable
continue; continue;
} }
world.EntityManager.SetComponent(entity, new SceneID { scene = activeScene });
var entityData = data.Entities[fileIndex]; var entityData = data.Entities[fileIndex];
ref var list = ref typeIds[fileIndex]; ref var list = ref typeIds[fileIndex];
var idx = 0; var idx = 1;
foreach (var (typeName, componentElement) in entityData.Components) foreach (var (typeName, componentElement) in entityData.Components)
{ {
@@ -384,7 +365,7 @@ public class SceneSerializationService : IDisposable
RebuildAndReturn: RebuildAndReturn:
_worldService.RebuildSceneGraph(); _worldService.RebuildSceneGraph();
return Result.Success(); return activeScene;
} }
private static Identifier<IComponent> RegisterComponentByType(Type type) private static Identifier<IComponent> RegisterComponentByType(Type type)
@@ -489,6 +470,11 @@ public class SceneSerializationService : IDisposable
foreach (var layout in archetype._layouts) foreach (var layout in archetype._layouts)
{ {
var type = ComponentRegistry.s_runtimeIDToType[layout.componentID]; var type = ComponentRegistry.s_runtimeIDToType[layout.componentID];
if (type == typeof(SceneID))
{
continue;
}
var fullName = type.FullName ?? type.Name; var fullName = type.FullName ?? type.Name;
var compInfo = ComponentRegistry.GetComponentInfo(layout.componentID); var compInfo = ComponentRegistry.GetComponentInfo(layout.componentID);

View File

@@ -103,6 +103,11 @@ public static class TypeCache
return s_types; return s_types;
} }
public static TypeInfo? GetTypes(string typeFullName)
{
return s_types.FirstOrDefault(t => t.FullName == typeFullName);
}
public static IEnumerable<MethodInfo>? GetMethodsWithAttribute<T>() public static IEnumerable<MethodInfo>? GetMethodsWithAttribute<T>()
where T : DiscoverableAttributeBase where T : DiscoverableAttributeBase
{ {

View File

@@ -22,7 +22,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Misaki.HighPerformance" Version="1.0.9" /> <PackageReference Include="Misaki.HighPerformance" Version="1.0.9" />
<PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.6" /> <PackageReference Include="Misaki.HighPerformance.Jobs" Version="3.1.6" />
<PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.24"> <PackageReference Include="Misaki.HighPerformance.LowLevel" Version="1.6.26">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

View File

@@ -1,6 +1,10 @@
using Ghost.Core; using Ghost.Core;
using Ghost.Core.Utilities;
using Ghost.Engine.Components;
using Ghost.Engine.Core;
using Ghost.Entities; using Ghost.Entities;
using Misaki.HighPerformance.LowLevel.Buffer; using Misaki.HighPerformance.LowLevel.Buffer;
using Misaki.HighPerformance.LowLevel.Collections;
using System.Text; using System.Text;
namespace Ghost.Engine; namespace Ghost.Engine;
@@ -9,19 +13,11 @@ internal partial class AssetEntry
{ {
private static void RegisterSceneCallback() private static void RegisterSceneCallback()
{ {
s_onCreation[(int)AssetType.Scene] = static (e) => s_onCreation[(int)AssetType.Scene] = null;
{
};
s_onParseRawData[(int)AssetType.Scene] = static (e) => e.ParseSceneData(); s_onParseRawData[(int)AssetType.Scene] = static (e) => e.ParseSceneData();
s_onRecordUpload[(int)AssetType.Scene] = static (e, ctx) => Result.Success(); s_onRecordUpload[(int)AssetType.Scene] = static (e, ctx) => Result.Success();
s_onUploadComplete[(int)AssetType.Scene] = static (e, ctx) => s_onUploadComplete[(int)AssetType.Scene] = null;
{ s_onReleaseResource[(int)AssetType.Scene] = null;
};
s_onReleaseResource[(int)AssetType.Scene] = static (e) =>
{
};
} }
private unsafe Result ParseSceneData() private unsafe Result ParseSceneData()
@@ -73,69 +69,102 @@ internal partial class AssetManager
public static class SceneLoader public static class SceneLoader
{ {
private struct BinaryEntityInfo private struct BinaryEntityInfo : IDisposable
{ {
public int entityIndex; public int entityIndex;
public int componentCount; public int componentCount;
public struct ComponentInfo public struct ComponentInfo
{ {
public uint typeHash; public uint typeHash;
public string typeName;
public Identifier<IComponent> typeID; public Identifier<IComponent> typeID;
public int dataSize; public int dataSize;
public int dataOffset; public int dataOffset;
public int entityFieldCount; public int entityFieldCount;
public int[] entityFieldOffsets; public UnsafeArray<int> entityFieldOffsets;
} }
public ComponentInfo[] components; 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();
}
} }
public static unsafe Result<int> LoadSceneIntoWorld(World world, void* pRawData, int dataSize) public static unsafe Result<int> LoadSceneIntoWorld(World world, void* pRawData, int dataSize)
{ {
RegisterKnownComponentTypes(); var reader = new SpanReader(new ReadOnlySpan<byte>(pRawData, dataSize));
var pData = (byte*)pRawData; var magic = Encoding.UTF8.GetString(reader.ReadSpan<byte>(4));
var offset = 0;
var magic = Encoding.UTF8.GetString(pData + offset, 4);
offset += 4;
if (magic != "GSCN") if (magic != "GSCN")
{ {
return Result.Failure("Invalid scene binary magic."); return Result.Failure("Invalid scene binary magic.");
} }
var version = ReadInt32(pData, ref offset); var version = reader.Read<int>();
if (version != 1) if (version != 1)
{ {
return Result.Failure($"Unsupported scene binary version: {version}"); return Result.Failure($"Unsupported scene binary version: {version}");
} }
var entityCount = ReadInt32(pData, ref offset); using var scope = AllocationManager.CreateStackScope();
var entityInfos = new BinaryEntityInfo[entityCount];
var forwardMap = new Dictionary<int, Entity>(entityCount); var entityCount = reader.Read<int>();
using var entityInfos = new BinaryEntityInfoArray(entityCount, scope.AllocationHandle);
using var forwardMap = new UnsafeHashMap<int, Entity>(entityCount, scope.AllocationHandle);
for (var i = 0; i < entityCount; i++) for (var i = 0; i < entityCount; i++)
{ {
var compCount = ReadInt32(pData, ref offset); var compCount = reader.Read<int>();
var comps = new BinaryEntityInfo.ComponentInfo[compCount]; if (compCount == 0)
{
continue;
}
var comps = new UnsafeArray<BinaryEntityInfo.ComponentInfo>(compCount, scope.AllocationHandle);
for (var j = 0; j < compCount; j++) for (var j = 0; j < compCount; j++)
{ {
var typeHash = (uint)ReadInt32(pData, ref offset); var typeHash = reader.Read<uint>();
var nameLength = ReadInt32(pData, ref offset); var nameLength = reader.Read<int>();
var typeName = Encoding.UTF8.GetString(pData + offset, nameLength); var typeName = Encoding.UTF8.GetString(reader.ReadSpan<byte>(nameLength));
offset += nameLength;
var dataSz = ReadInt32(pData, ref offset); var dataSz = reader.Read<int>();
var dataOff = offset; var dataOff = reader.Position;
offset += dataSz; reader.Position += dataSz;
var fieldCount = ReadInt32(pData, ref offset); var fieldCount = reader.Read<int>();
var fieldOffsets = new int[fieldCount]; var fieldOffsets = new UnsafeArray<int>(fieldCount, scope.AllocationHandle);
for (var f = 0; f < fieldCount; f++) for (var f = 0; f < fieldCount; f++)
{ {
fieldOffsets[f] = ReadInt32(pData, ref offset); fieldOffsets[f] = reader.Read<int>();
} }
var typeID = ComponentRegistry.GetComponentIDByName(typeName); var typeID = ComponentRegistry.GetComponentIDByName(typeName);
@@ -143,7 +172,6 @@ public static class SceneLoader
comps[j] = new BinaryEntityInfo.ComponentInfo comps[j] = new BinaryEntityInfo.ComponentInfo
{ {
typeHash = typeHash, typeHash = typeHash,
typeName = typeName,
typeID = typeID, typeID = typeID,
dataSize = dataSz, dataSize = dataSz,
dataOffset = dataOff, dataOffset = dataOff,
@@ -160,34 +188,31 @@ public static class SceneLoader
}; };
} }
var pTypeIds = stackalloc Identifier<IComponent>[32]; using var typeIds = new UnsafeList<Identifier<IComponent>>(32, scope.AllocationHandle);
typeIds.Add(ComponentTypeID<SceneID>.Value);
for (var i = 0; i < entityCount; i++) for (var i = 0; i < entityCount; i++)
{ {
var info = entityInfos[i]; ref var info = ref entityInfos[i];
var validCount = 0;
for (var j = 0; j < info.componentCount; j++) for (var j = 0; j < info.componentCount; j++)
{ {
if (info.components[j].typeID.IsValid) if (info.components[j].typeID.IsValid)
{ {
if (validCount < 32) typeIds.Add(info.components[j].typeID);
{
pTypeIds[validCount] = info.components[j].typeID;
}
validCount++;
} }
} }
if (validCount > 0 && validCount <= 32) var set = new ComponentSetView(typeIds);
{
using var scope = AllocationManager.CreateStackScope();
using var set = new ComponentSet(scope.AllocationHandle, new ReadOnlySpan<Identifier<IComponent>>(pTypeIds, validCount));
var entity = world.EntityManager.CreateEntity(set); var entity = world.EntityManager.CreateEntity(set);
forwardMap[i] = entity;
} forwardMap.TryAdd(i, entity);
typeIds.RemoveRange(1, typeIds.Count - 1);
} }
var activeScene = SceneManager.CreateScene();
for (var i = 0; i < entityCount; i++) for (var i = 0; i < entityCount; i++)
{ {
if (!forwardMap.TryGetValue(i, out var entity)) if (!forwardMap.TryGetValue(i, out var entity))
@@ -195,6 +220,8 @@ public static class SceneLoader
continue; continue;
} }
world.EntityManager.SetComponent(entity, new SceneID { scene = activeScene });
var info = entityInfos[i]; var info = entityInfos[i];
for (var j = 0; j < info.componentCount; j++) for (var j = 0; j < info.componentCount; j++)
{ {
@@ -205,7 +232,7 @@ public static class SceneLoader
} }
var compSize = ComponentRegistry.GetComponentInfo(comp.typeID).size; var compSize = ComponentRegistry.GetComponentInfo(comp.typeID).size;
var pSrc = pData + comp.dataOffset; var pSrc = (byte*)pRawData + comp.dataOffset;
world.EntityManager.SetComponent(entity, comp.typeID, pSrc); world.EntityManager.SetComponent(entity, comp.typeID, pSrc);
} }
@@ -250,18 +277,4 @@ public static class SceneLoader
return Result.Success(entityCount); return Result.Success(entityCount);
} }
private static void RegisterKnownComponentTypes()
{
_ = ComponentTypeID<Ghost.Engine.Components.Hierarchy>.Value;
_ = ComponentTypeID<Ghost.Engine.Components.LocalToWorld>.Value;
_ = ComponentTypeID<Ghost.Engine.Components.SceneID>.Value;
}
private static unsafe int ReadInt32(byte* pData, ref int offset)
{
var value = *(int*)(pData + offset);
offset += 4;
return value;
}
} }

View File

@@ -52,11 +52,11 @@ public interface IContentProvider
internal partial class AssetEntry internal partial class AssetEntry
{ {
private static readonly Action<AssetEntry>[] s_onCreation = new Action<AssetEntry>[(int)AssetType.Unknown + 1]; private static readonly Action<AssetEntry>?[] s_onCreation = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
private static readonly Func<AssetEntry, Result>[] s_onParseRawData = new Func<AssetEntry, Result>[(int)AssetType.Unknown + 1]; private static readonly Func<AssetEntry, Result>?[] s_onParseRawData = new Func<AssetEntry, Result>[(int)AssetType.Unknown + 1];
private static readonly Func<AssetEntry, ResourceStreamingContext, Result>[] s_onRecordUpload = new Func<AssetEntry, ResourceStreamingContext, Result>[(int)AssetType.Unknown + 1]; private static readonly Func<AssetEntry, ResourceStreamingContext, Result>?[] s_onRecordUpload = new Func<AssetEntry, ResourceStreamingContext, Result>[(int)AssetType.Unknown + 1];
private static readonly Action<AssetEntry, ResourceStreamingContext>[] s_onUploadComplete = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1]; private static readonly Action<AssetEntry, ResourceStreamingContext>?[] s_onUploadComplete = new Action<AssetEntry, ResourceStreamingContext>[(int)AssetType.Unknown + 1];
private static readonly Action<AssetEntry>[] s_onReleaseResource = new Action<AssetEntry>[(int)AssetType.Unknown + 1]; private static readonly Action<AssetEntry>?[] s_onReleaseResource = new Action<AssetEntry>[(int)AssetType.Unknown + 1];
static AssetEntry() static AssetEntry()
{ {
@@ -321,7 +321,7 @@ internal partial class AssetManager : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal bool RemoveEntry(Guid guid) internal bool RemoveEntry(Guid guid)
{ {
return _entries.TryRemove(guid, out var entry); return _entries.TryRemove(guid, out var _);
} }
private void EnsureScheduled(AssetEntry entry) private void EnsureScheduled(AssetEntry entry)

View File

@@ -8,35 +8,24 @@ namespace Ghost.Engine.Core;
/// <summary> /// <summary>
/// Represents a runtime scene - a collection of entities with the same SceneID. /// Represents a runtime scene - a collection of entities with the same SceneID.
/// </summary> /// </summary>
public readonly struct Scene : IEquatable<Scene> public struct Scene : IEquatable<Scene>
{ {
private readonly short _id; public byte id;
/// <summary>
/// Gets the unique identifier of this scene.
/// </summary>
public short ID => _id;
/// <summary> /// <summary>
/// Gets whether this scene is valid. /// Gets whether this scene is valid.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public bool IsValid => _id >= 0; public readonly bool IsValid => id != 255;
/// <summary> /// <summary>
/// Gets an invalid scene instance. /// Gets an invalid scene instance.
/// </summary> /// </summary>
public static Scene Invalid => new(-1); public static Scene Invalid => new Scene { id = 255 };
[JsonConstructor] public readonly bool Equals(Scene other)
public Scene(short id)
{ {
_id = id; return id == other.id;
}
public bool Equals(Scene other)
{
return _id == other._id;
} }
public override bool Equals(object? obj) public override bool Equals(object? obj)
@@ -44,9 +33,9 @@ public readonly struct Scene : IEquatable<Scene>
return obj is Scene other && Equals(other); return obj is Scene other && Equals(other);
} }
public override int GetHashCode() public readonly override int GetHashCode()
{ {
return _id.GetHashCode(); return id.GetHashCode();
} }
public static bool operator ==(Scene left, Scene right) public static bool operator ==(Scene left, Scene right)
@@ -59,9 +48,9 @@ public readonly struct Scene : IEquatable<Scene>
return !left.Equals(right); return !left.Equals(right);
} }
public override string ToString() public readonly override string ToString()
{ {
return $"Scene(ID: {_id})"; return $"Scene(ID: {id})";
} }
} }
@@ -74,8 +63,8 @@ public readonly struct Scene : IEquatable<Scene>
/// </remarks> /// </remarks>
public static class SceneManager public static class SceneManager
{ {
private static short s_nextSceneID; private static byte s_nextSceneID;
private static readonly Queue<short> s_recycledSceneIDs = new(); private static readonly Queue<byte> s_recycledSceneIDs = new();
/// <summary> /// <summary>
/// Creates a new scene in the world. /// Creates a new scene in the world.
@@ -88,7 +77,7 @@ public static class SceneManager
id = s_nextSceneID++; id = s_nextSceneID++;
} }
return new Scene(id); return new Scene { id = id };
} }
/// <summary> /// <summary>
@@ -112,7 +101,7 @@ public static class SceneManager
for (var i = 0; i < chunk.EntityCount; i++) for (var i = 0; i < chunk.EntityCount; i++)
{ {
if (sceneIDs[i].scene.ID == scene.ID) if (sceneIDs[i].scene.id == scene.id)
{ {
entitiesToDestroy.Add(entities[i]); entitiesToDestroy.Add(entities[i]);
} }
@@ -120,7 +109,7 @@ public static class SceneManager
} }
world.EntityManager.DestroyEntities(entitiesToDestroy.AsSpan()); world.EntityManager.DestroyEntities(entitiesToDestroy.AsSpan());
s_recycledSceneIDs.Enqueue(scene.ID); s_recycledSceneIDs.Enqueue(scene.id);
} }
/// <summary> /// <summary>
@@ -145,7 +134,7 @@ public static class SceneManager
for (var i = 0; i < chunk.EntityCount; i++) for (var i = 0; i < chunk.EntityCount; i++)
{ {
if (sceneIDs[i].scene.ID == scene.ID) if (sceneIDs[i].scene.id == scene.id)
{ {
entities.Add(chunkEntities[i]); entities.Add(chunkEntities[i]);
} }

View File

@@ -331,6 +331,11 @@ public struct ComponentSet : IDisposable, IEquatable<ComponentSet>
return _hashCode; return _hashCode;
} }
public void Dispose()
{
_components.Dispose();
}
public override readonly bool Equals(object? obj) public override readonly bool Equals(object? obj)
{ {
return obj is ComponentSet set && Equals(set); return obj is ComponentSet set && Equals(set);
@@ -346,8 +351,53 @@ public struct ComponentSet : IDisposable, IEquatable<ComponentSet>
return !(left == right); return !(left == right);
} }
public void Dispose() public static implicit operator ComponentSetView(in ComponentSet set)
{ {
_components.Dispose(); return new ComponentSetView(set.Components);
}
}
public ref struct ComponentSetView : IEquatable<ComponentSetView>
{
private readonly ReadOnlySpan<Identifier<IComponent>> _components;
private int _hashCode;
public readonly ReadOnlySpan<Identifier<IComponent>> Components => _components;
public ComponentSetView(ReadOnlySpan<Identifier<IComponent>> components)
{
_components = components;
_hashCode = -1;
}
public readonly bool Equals(ComponentSetView other)
{
return _hashCode == other._hashCode;
}
public override int GetHashCode()
{
if (_hashCode == -1)
{
_hashCode = ComponentRegistry.GetHashCode(_components);
}
return _hashCode;
}
public override readonly bool Equals(object? obj)
{
return false;
}
public static bool operator ==(ComponentSetView left, ComponentSetView right)
{
return left.Equals(right);
}
public static bool operator !=(ComponentSetView left, ComponentSetView right)
{
return !(left == right);
} }
} }

View File

@@ -125,7 +125,7 @@ public unsafe partial class EntityManager : IDisposable
/// </summary> /// </summary>
/// <param name="set">A set of component space IDs to add to the entities.</param> /// <param name="set">A set of component space IDs to add to the entities.</param>
/// <returns>The created entity.</returns> /// <returns>The created entity.</returns>
public Entity CreateEntity(ComponentSet set) public Entity CreateEntity(ComponentSetView set)
{ {
var entities = (Span<Entity>)stackalloc Entity[1]; var entities = (Span<Entity>)stackalloc Entity[1];
CreateEntities(entities, set); CreateEntities(entities, set);
@@ -187,7 +187,7 @@ public unsafe partial class EntityManager : IDisposable
/// <param name="entities">The span to store the created entities.</param> /// <param name="entities">The span to store the created entities.</param>
/// <param name="set">A set of component space IDs to add to the entities.</param> /// <param name="set">A set of component space IDs to add to the entities.</param>
/// <returns>An array of the created entities.</returns> /// <returns>An array of the created entities.</returns>
public void CreateEntities(Span<Entity> entities, ComponentSet set) public void CreateEntities(Span<Entity> entities, ComponentSetView set)
{ {
var hash = set.GetHashCode(); var hash = set.GetHashCode();
var arcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(hash); var arcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(hash);
@@ -222,7 +222,7 @@ public unsafe partial class EntityManager : IDisposable
/// </summary> /// </summary>
/// <param name="count">The number of entities to create.</param> /// <param name="count">The number of entities to create.</param>
/// <param name="set">A set of component space IDs to add to the entities.</param> /// <param name="set">A set of component space IDs to add to the entities.</param>
public void CreateEntities(int count, ComponentSet set) public void CreateEntities(int count, ComponentSetView set)
{ {
var hash = set.GetHashCode(); var hash = set.GetHashCode();
var arcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(hash); var arcID = _world.ComponentManager.GetArchetypeIDBySignatureHash(hash);

View File

@@ -117,16 +117,18 @@ public class SceneSerializationTests
Assert.AreEqual(3, data.Entities.Count, $"data contained {data.Entities.Count} entities"); Assert.AreEqual(3, data.Entities.Count, $"data contained {data.Entities.Count} entities");
foreach (var ent in data.Entities) foreach (var ent in data.Entities)
{ {
Assert.IsTrue(ent.Components.Count > 0, "Entity has no components"); Assert.IsTrue(ent.Components.Count >= 0, "Entity has no components"); // Can be 0 because we might have entities without components, but should not be negative
} }
var loadResult = _serializationService.LoadSceneIntoEditorWorld(data); var loadResult = _serializationService.LoadSceneIntoEditorWorld(data);
Assert.IsTrue(loadResult.IsSuccess, loadResult.Message); Assert.IsTrue(loadResult.IsSuccess, loadResult.Message);
var world = _worldService.EditorWorld; var world = _worldService.EditorWorld;
scene = loadResult.Value;
using var scope = AllocationManager.CreateStackScope(); using var scope = AllocationManager.CreateStackScope();
using var entities = SceneManager.GetSceneEntities(scene, world, scope.AllocationHandle); using var entities = SceneManager.GetSceneEntities(scene, world, scope.AllocationHandle);
Assert.AreEqual(3, entities.Count, $"Expected 3 entities for scene {scene.ID} but found {entities.Count}"); Assert.AreEqual(3, entities.Count, $"Expected 3 entities for scene {scene.id} but found {entities.Count}");
} }
[TestMethod] [TestMethod]
@@ -271,8 +273,7 @@ public class SceneSerializationTests
initialCount += chunk.EntityCount; initialCount += chunk.EntityCount;
} }
// EditorWorldService creates 1 default scene entity, plus 4 from this test = 5 Assert.AreEqual(4, initialCount, "Expected 4 entities.");
Assert.AreEqual(5, initialCount, "Expected 5 entities (1 default + 4 from test).");
var filePath = Path.Combine(_projectRoot, "SingleLoad.gscene"); var filePath = Path.Combine(_projectRoot, "SingleLoad.gscene");
_serializationService.SaveSceneFromEditorWorld(filePath, scene); _serializationService.SaveSceneFromEditorWorld(filePath, scene);