diff --git a/workers/unity/Assets/Playground/Editor/SnapshotGenerator/SnapshotGenerator.cs b/workers/unity/Assets/Playground/Editor/SnapshotGenerator/SnapshotGenerator.cs index ef4045364d..594b86065e 100644 --- a/workers/unity/Assets/Playground/Editor/SnapshotGenerator/SnapshotGenerator.cs +++ b/workers/unity/Assets/Playground/Editor/SnapshotGenerator/SnapshotGenerator.cs @@ -19,10 +19,11 @@ public struct Arguments public static void Generate(Arguments arguments) { Debug.Log("Generating snapshot."); - var snapshot = CreateSnapshot(arguments.NumberEntities); - - Debug.Log($"Writing snapshot to: {arguments.OutputPath}"); - snapshot.WriteToFile(arguments.OutputPath); + using (var snapshot = CreateSnapshot(arguments.NumberEntities)) + { + Debug.Log($"Writing snapshot to: {arguments.OutputPath}"); + snapshot.WriteToFile(arguments.OutputPath); + } } private static Snapshot CreateSnapshot(int cubeCount) diff --git a/workers/unity/Assets/Tests/EditmodeTests/Correctness/SceneAuthoring/SceneConverterTests.cs b/workers/unity/Assets/Tests/EditmodeTests/Correctness/SceneAuthoring/SceneConverterTests.cs new file mode 100644 index 0000000000..1838112510 --- /dev/null +++ b/workers/unity/Assets/Tests/EditmodeTests/Correctness/SceneAuthoring/SceneConverterTests.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using Improbable.Gdk.Core; +using Improbable.Gdk.Core.SceneAuthoring; +using Improbable.Gdk.Core.SceneAuthoring.AuthoringComponents; +using Improbable.Gdk.Core.SceneAuthoring.Editor; +using Improbable.Worker.CInterop; +using NUnit.Framework; +using UnityEngine; + +namespace Improbable.Gdk.EditmodeTests.SceneAuthoring +{ + [TestFixture] + public class SceneConverterTests + { + [Test] + public void GameObjects_with_duplicate_entity_ids_throws_exception() + { + var entityId = new EntityId(1); + var gameObjects = new[] + { + CreateGameObject(desiredEntityId: entityId), CreateGameObject(desiredEntityId: entityId) + }; + Assert.Throws(() => SceneConverter.Convert(gameObjects)); + } + + [Test] + public void GameObjects_with_multiple_converters_are_rejected() + { + var gameObject = CreateGameObject(); + gameObject.AddComponent(); + + Assert.Throws(() => SceneConverter.Convert(new[] { gameObject })); + } + + [Test] + public void Child_gameobjects_are_not_considered_if_includeChildren_is_false() + { + var gameobject = CreateGameObject(); + var child = CreateGameObject(); + child.transform.SetParent(gameobject.transform); + + using (var snapshot = SceneConverter.Convert(new[] { gameobject })) + { + Assert.AreEqual(1, snapshot.Count); + } + } + + [Test] + public void Child_gameobjects_are_considered_if_includeChildren_is_true() + { + var gameobject = CreateGameObject(); + var child = CreateGameObject(); + var grandChild = CreateGameObject(); + + child.transform.SetParent(gameobject.transform); + grandChild.transform.SetParent(child.transform); + + using (var snapshot = SceneConverter.Convert(new[] { gameobject }, includeChildren: true)) + { + Assert.AreEqual(3, snapshot.Count); + } + } + + [Test] + public void GameObjects_with_specific_entity_id_are_always_put_there() + { + var firstPosition = new Vector3(1, 0, 0); + var secondPosition = new Vector3(0, 1, 0); + var restPosition = new Vector3(0, 0, 1); + + var firstEntityId = new EntityId(1); + var secondEntityId = new EntityId(2); + + var gameObjects = new[] + { + CreateGameObject(position: restPosition), + CreateGameObject(desiredEntityId: firstEntityId, position: firstPosition), + CreateGameObject(position: restPosition), + CreateGameObject(desiredEntityId: secondEntityId, position: secondPosition), + CreateGameObject(position: restPosition) + }; + + using (var snapshot = SceneConverter.Convert(gameObjects)) + { + var firstEntity = snapshot[firstEntityId]; + var firstEntityPosition = GetPosition(firstEntity).Coords.ToUnityVector(); + Assert.AreEqual(firstPosition, firstEntityPosition); + + var secondEntity = snapshot[secondEntityId]; + var secondEntityPosition = GetPosition(secondEntity).Coords.ToUnityVector(); + Assert.AreEqual(secondPosition, secondEntityPosition); + } + } + + private static GameObject CreateGameObject(EntityId desiredEntityId = default, Vector3 position = default) + { + var go = new GameObject(); + go.transform.position = position; + + go.AddComponent(); + var conversion = go.AddComponent(); + + if (desiredEntityId.IsValid()) + { + conversion.UseSpecificEntityId = true; + conversion.DesiredEntityId = desiredEntityId.Id; + } + + return go; + } + + private static Position.Snapshot GetPosition(Entity entity) + { + var schemaObject = entity.Get(Position.ComponentId).Value.SchemaData.Value.GetFields(); + return Position.Serialization.DeserializeSnapshot(schemaObject); + } + + private class CustomConverter : MonoBehaviour, IConvertGameObjectToSpatialOsEntity + { + public List Convert() + { + throw new NotImplementedException(); + } + } + } +} diff --git a/workers/unity/Assets/Tests/EditmodeTests/Correctness/SceneAuthoring/SceneConverterTests.cs.meta b/workers/unity/Assets/Tests/EditmodeTests/Correctness/SceneAuthoring/SceneConverterTests.cs.meta new file mode 100644 index 0000000000..753d638333 --- /dev/null +++ b/workers/unity/Assets/Tests/EditmodeTests/Correctness/SceneAuthoring/SceneConverterTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e084d31c789d41669d999d5d3a99ae30 +timeCreated: 1599126128 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.core/SceneAuthoring/SceneConverter.cs b/workers/unity/Packages/io.improbable.gdk.core/SceneAuthoring/SceneConverter.cs new file mode 100644 index 0000000000..cf1b56fee1 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.core/SceneAuthoring/SceneConverter.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Improbable.Gdk.Core.SceneAuthoring.Editor +{ + public static class SceneConverter + { + public static Snapshot Convert(Scene scene, bool includeChildren = false) + { + return Convert(scene.GetRootGameObjects(), includeChildren); + } + + public static Snapshot Convert(IEnumerable gameObjects, bool includeChildren = false) + { + var (entities, entitiesWithIds) = gameObjects + .SelectMany(gameObject => CollectGameObjects(gameObject, includeChildren)) + .SelectMany(GetEntities) + .Partition(); + + var snapshot = new Snapshot(); + + foreach (var kvp in entitiesWithIds) + { + snapshot.AddEntity(kvp.Key, kvp.Value); + } + + var nextId = 1; + + foreach (var entity in entities) + { + while (entitiesWithIds.ContainsKey(new EntityId(nextId))) + { + nextId++; + } + + snapshot.AddEntity(new EntityId(nextId), entity); + nextId++; + } + + return snapshot; + } + + private static IEnumerable CollectGameObjects(GameObject root, bool includeChildren) + { + yield return root; + + if (!includeChildren) + { + yield break; + } + + foreach (Transform childTransform in root.transform) + { + foreach (var child in CollectGameObjects(childTransform.gameObject, true)) + { + yield return child; + } + } + } + + private static IEnumerable GetEntities(GameObject gameObject) + { + var converters = gameObject.GetComponents(); + + switch (converters.Length) + { + case 0: + return Enumerable.Empty(); + case 1: + return converters[0].Convert(); + default: + var componentNames = string.Join(", ", converters.Select(c => c.GetType().Name)); + throw new InvalidOperationException($"GameObject {gameObject} has more than one component that implements {nameof(IConvertGameObjectToSpatialOsEntity)}: '{componentNames}'"); + } + } + + private static (List, Dictionary) Partition( + this IEnumerable convertedEntities) + { + var entities = new List(); + var entitiesWithIds = new Dictionary(); + + foreach (var convertedEntity in convertedEntities) + { + if (!convertedEntity.EntityId.HasValue) + { + entities.Add(convertedEntity.Template); + continue; + } + + var entityId = convertedEntity.EntityId.Value; + + if (entitiesWithIds.ContainsKey(entityId)) + { + throw new InvalidOperationException($"More than one entity is specified with EntityId {entityId}"); + } + + entitiesWithIds[entityId] = convertedEntity.Template; + } + + return (entities, entitiesWithIds); + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.core/SceneAuthoring/SceneConverter.cs.meta b/workers/unity/Packages/io.improbable.gdk.core/SceneAuthoring/SceneConverter.cs.meta new file mode 100644 index 0000000000..717f4d6520 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.core/SceneAuthoring/SceneConverter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23400c5caa183b14d9394b88145c5e99 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/workers/unity/Packages/io.improbable.gdk.core/Utility/Snapshot.cs b/workers/unity/Packages/io.improbable.gdk.core/Utility/Snapshot.cs index 74a07e09d1..6717e5535d 100644 --- a/workers/unity/Packages/io.improbable.gdk.core/Utility/Snapshot.cs +++ b/workers/unity/Packages/io.improbable.gdk.core/Utility/Snapshot.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Improbable.Worker.CInterop; using UnityEngine; @@ -7,8 +8,10 @@ namespace Improbable.Gdk.Core /// /// Convenience wrapper around the WorkerSDK Snapshot API. /// - public class Snapshot + public class Snapshot : IDisposable { + private const int PersistenceComponentId = 55; + private readonly Dictionary entities = new Dictionary(); public int Count => entities.Count; @@ -50,7 +53,10 @@ public EntityId AddEntity(EntityTemplate entityTemplate) /// public void AddEntity(EntityId entityId, EntityTemplate entityTemplate) { - entities[entityId] = entityTemplate.GetEntity(); + var entity = entityTemplate.GetEntity(); + // This is a no-op if the entity already has persistence. + entity.Add(new ComponentData(PersistenceComponentId, SchemaComponentData.Create())); + entities[entityId] = entity; } /// @@ -79,5 +85,21 @@ public void WriteToFile(string path) } } } + + public void Dispose() + { + foreach (var kvp in entities) + { + var entity = kvp.Value; + + foreach (var id in entity.GetComponentIds()) + { + var componentData = entity.Get(id).Value; + componentData.SchemaData?.Destroy(); + } + } + } + + internal Entity this[EntityId entityId] => entities[entityId]; } }