diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1e21b704..f09d73c631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - `CustomSpatialOSSendSystem` is no longer available. [#1308](https://github.com/spatialos/gdk-for-unity/pull/1308) - The Player Lifecycle feature module now provides an `EntityId` in its `CreatePlayerEntityTemplate` callback. [#1315](https://github.com/spatialos/gdk-for-unity/pull/1315) - You will have to change your callback from `(string clientWorkerId, byte[] serializedArguments)` to `(EntityId entityId, string clientWorkerId, byte[] serializedArguments)`. +- Added the `ComponentType[] MiniumComponentTypes { get; }` property to `IEntityGameObjectCreator`. [#1330](https://github.com/spatialos/gdk-for-unity/pull/1330) + - You will have to define the minimum set of components required on an entity to trigger the `OnEntityCreated` method on your custom GameObject creator. ### Added @@ -32,6 +34,8 @@ - Downgraded the level of several code generator logs from `Info` to `Trace`. [#1277](https://github.com/spatialos/gdk-for-unity/pull/1277) - Upgraded the Worker SDK to `14.5.0`. [#1317](https://github.com/spatialos/gdk-for-unity/pull/1317) - Upgraded the Platform SDK used by the Deployment Launcher to `14.5.0` [#1317](https://github.com/spatialos/gdk-for-unity/pull/1317) +- Changed the GameObject Creation module to run for entities that match the minimum component set required by the creator, instead of any entity that is newly added. + - This means that the module no longer cares if an entity is checked out in one frame or across multiple. ### Fixed @@ -59,6 +63,7 @@ - The Playground project now uses QBI instead of CBI. [#1370](https://github.com/spatialos/gdk-for-unity/pull/1307) - Added `MockWorld` and `MockBase` classes to the `Improbable.Gdk.TestUtils` package. These are designed as a framework for testing Core code. [#1305](https://github.com/spatialos/gdk-for-unity/pull/1305) - Switched internal profiling to use new `ProfilerMarker` API. [#1311](https://github.com/spatialos/gdk-for-unity/pull/1311) +- Changed `MockWorld.Options.AdditionalSystems` from `Type[]` to `Action`. [#1330](https://github.com/spatialos/gdk-for-unity/pull/1330) ## `0.3.3` - 2020-02-14 diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md index 4a29ac49e0..2723380f82 100644 --- a/UPGRADE_GUIDE.md +++ b/UPGRADE_GUIDE.md @@ -29,6 +29,22 @@ public static EntityTemplate Player(EntityId entityId, string workerId, byte[] a } ``` +### The `IEntityGameObjectCreator` now requires the `ComponentType[] MiniumComponentTypes { get; }` property + +If you have written custom GameObject creators implementing `IEntityGameObjectCreator`, you will have to define the minimum set of components required on an entity to trigger the `OnEntityCreated` method. + +For example, the following has been added to the `GameObjectCreatorFromMetadata` class: + +```csharp +public ComponentType[] MinimumComponentTypes { get; } = +{ + ComponentType.ReadOnly(), + ComponentType.ReadOnly() +}; +``` + +> You will need to add `using Unity.Entities;` to the top of the file and reference it in the assembly that your custom GameObject creator is in. + ## From `0.3.2` to `0.3.3` ### Building for Android now requires the NDK @@ -97,7 +113,7 @@ type Vector3d { You should then replace the import of `improbable/vector.schema` and usage of `improbable.Vector3f`/`improbable.Vector3d` with the schema file you defined. > Note that methods such as `Vector3f.ToUnityVector();` are no longer available and you'll need to reimplement them yourself as extension/static methods. You can find the old implementations here: [`Vector3f`](https://github.com/spatialos/gdk-for-unity/blob/0.2.6/workers/unity/Packages/io.improbable.gdk.tools/.CodeGenerator/GdkCodeGenerator/Partials/Improbable.Vector3f) and [`Vector3d`](https://github.com/spatialos/gdk-for-unity/blob/0.2.6/workers/unity/Packages/io.improbable.gdk.tools/.CodeGenerator/GdkCodeGenerator/Partials/Improbable.Vector3d). -> +> > You will be unable to reimplement the operators since C# lacks the ability to define operations via extension methods. > > Note that the `Coordinates` type can be used as a replacement for `Vector3d` as they are structurally the same. diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectCreatorFromMetadata.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectCreatorFromMetadata.cs index e3a203e5de..490692fa68 100644 --- a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectCreatorFromMetadata.cs +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectCreatorFromMetadata.cs @@ -3,6 +3,7 @@ using System.IO; using Improbable.Gdk.Core; using Improbable.Gdk.Subscriptions; +using Unity.Entities; using UnityEngine; using Object = UnityEngine.Object; @@ -27,6 +28,12 @@ private readonly Dictionary cachedPrefabs typeof(MeshRenderer) }; + public ComponentType[] MinimumComponentTypes { get; } = + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }; + public GameObjectCreatorFromMetadata(string workerType, Vector3 workerOrigin, ILogDispatcher logger) { this.workerType = workerType; diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitSystemStateComponent.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitSystemStateComponent.cs new file mode 100644 index 0000000000..8f649b4dd4 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitSystemStateComponent.cs @@ -0,0 +1,10 @@ +using Improbable.Gdk.Core; +using Unity.Entities; + +namespace Improbable.Gdk.GameObjectCreation +{ + public struct GameObjectInitSystemStateComponent : ISystemStateComponentData + { + public EntityId EntityId; + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitSystemStateComponent.cs.meta b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitSystemStateComponent.cs.meta new file mode 100644 index 0000000000..5d041edfc5 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitSystemStateComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8c08a9c6deb7ee49bea161d49c872cb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitializationSystem.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitializationSystem.cs index 203f0dc3a2..e957548f96 100644 --- a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitializationSystem.cs +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/GameObjectInitializationSystem.cs @@ -1,3 +1,4 @@ +using System.Linq; using Improbable.Gdk.Core; using Improbable.Gdk.Subscriptions; using Unity.Entities; @@ -21,12 +22,27 @@ internal class GameObjectInitializationSystem : ComponentSystem private readonly GameObject workerGameObject; private EntitySystem entitySystem; - private WorkerSystem workerSystem; + + private EntityQuery newEntitiesQuery; + private EntityQuery removedEntitiesQuery; + + private ComponentType[] minimumComponentSet = new[] + { + ComponentType.ReadOnly() + }; public GameObjectInitializationSystem(IEntityGameObjectCreator gameObjectCreator, GameObject workerGameObject) { this.gameObjectCreator = gameObjectCreator; this.workerGameObject = workerGameObject; + + var minCreatorComponentSet = gameObjectCreator.MinimumComponentTypes; + if (minCreatorComponentSet != null) + { + minimumComponentSet = minimumComponentSet + .Concat(minCreatorComponentSet) + .ToArray(); + } } protected override void OnCreate() @@ -34,7 +50,6 @@ protected override void OnCreate() base.OnCreate(); entitySystem = World.GetExistingSystem(); - workerSystem = World.GetExistingSystem(); Linker = new EntityGameObjectLinker(World); @@ -42,10 +57,24 @@ protected override void OnCreate() { Linker.LinkGameObjectToSpatialOSEntity(new EntityId(0), workerGameObject); } + + newEntitiesQuery = GetEntityQuery(new EntityQueryDesc() + { + All = minimumComponentSet, + None = new[] { ComponentType.ReadOnly() } + }); + + removedEntitiesQuery = GetEntityQuery(new EntityQueryDesc() + { + All = new[] { ComponentType.ReadOnly() }, + None = minimumComponentSet + }); } protected override void OnDestroy() { + EntityManager.RemoveComponent(GetEntityQuery(typeof(GameObjectInitSystemStateComponent))); + Linker.UnlinkAllGameObjects(); foreach (var entityId in entitySystem.GetEntitiesInView()) @@ -58,24 +87,24 @@ protected override void OnDestroy() protected override void OnUpdate() { - foreach (var entityId in entitySystem.GetEntitiesAdded()) + Entities.With(newEntitiesQuery).ForEach((Entity entity, ref SpatialEntityId spatialEntityId) => { - var entity = workerSystem.GetEntity(entityId); gameObjectCreator.OnEntityCreated(new SpatialOSEntity(entity, EntityManager), Linker); - } + PostUpdateCommands.AddComponent(entity, new GameObjectInitSystemStateComponent + { + EntityId = spatialEntityId.EntityId + }); + }); - var removedEntities = entitySystem.GetEntitiesRemoved(); - foreach (var entityId in removedEntities) + Entities.With(removedEntitiesQuery).ForEach((ref GameObjectInitSystemStateComponent state) => { - Linker.UnlinkAllGameObjectsFromEntityId(entityId); - } + Linker.UnlinkAllGameObjectsFromEntityId(state.EntityId); + gameObjectCreator.OnEntityRemoved(state.EntityId); + }); Linker.FlushCommandBuffer(); - foreach (var entityId in removedEntities) - { - gameObjectCreator.OnEntityRemoved(entityId); - } + EntityManager.RemoveComponent(removedEntitiesQuery); } } } diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/IEntityGameObjectCreator.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/IEntityGameObjectCreator.cs index f230c54b1b..591370361a 100644 --- a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/IEntityGameObjectCreator.cs +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/IEntityGameObjectCreator.cs @@ -1,7 +1,6 @@ using Improbable.Gdk.Core; -using Improbable.Worker.CInterop; using Improbable.Gdk.Subscriptions; -using UnityEngine; +using Unity.Entities; namespace Improbable.Gdk.GameObjectCreation { @@ -11,6 +10,11 @@ namespace Improbable.Gdk.GameObjectCreation /// public interface IEntityGameObjectCreator { + /// + /// The minimum set of components required on an entity to create a GameObject. + /// + ComponentType[] MinimumComponentTypes { get; } + /// /// Called when a new SpatialOS Entity is checked out by the worker. /// diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/GameObjectCreationTests.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/GameObjectCreationTests.cs new file mode 100644 index 0000000000..7d5bf6a45c --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/GameObjectCreationTests.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using Improbable.Gdk.Core; +using Improbable.Gdk.Subscriptions; +using Improbable.Gdk.TestUtils; +using NUnit.Framework; +using Unity.Entities; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace Improbable.Gdk.GameObjectCreation.EditmodeTests +{ + public class GameObjectCreationTests : MockBase + { + private long entityId = 100; + + private const string WorkerType = "TestWorkerType"; + + private EntityGameObjectLinker linker; + + protected override MockWorld.Options GetOptions() + { + return new MockWorld.Options + { + AdditionalSystems = world => + { + var testGameObjectCreator = new TestGameObjectCreator(WorkerType); + GameObjectCreationHelper.EnableStandardGameObjectCreation(world, testGameObjectCreator); + } + }; + } + + [SetUp] + public new void Setup() + { + base.Setup(); + + var goInitSystem = World.GetSystem(); + linker = goInitSystem.Linker; + } + + [TearDown] + public new void TearDown() + { + World.Dispose(); + } + + [Test] + public void Create_GameObject_after_required_components_arrive_in_same_frame() + { + World + .Step(world => + { + world.Connection.CreateEntity(entityId, new EntityTemplate()); + + world.Connection.AddComponent(entityId, Position.ComponentId, + new Position.Update { Coords = Coordinates.Zero }); + + world.Connection.AddComponent(entityId, Metadata.ComponentId, + new Metadata.Update { EntityType = "TestObject" }); + + var gameObjectExists = linker.EntityIdToGameObjects + .TryGetValue(new EntityId(entityId), out var gameObjects); + + Assert.IsFalse(gameObjectExists); + }) + .Step(world => + { + var gameObjectExists = linker.EntityIdToGameObjects + .TryGetValue(new EntityId(entityId), out var gameObjects); + + Assert.IsTrue(gameObjectExists); + }); + } + + [Test] + public void Create_GameObject_after_required_components_arrive_in_multiple_frames() + { + World + .Step(world => + { + world.Connection.CreateEntity(entityId, new EntityTemplate()); + }) + .Step(world => + { + world.Connection.AddComponent(entityId, Position.ComponentId, + new Position.Update { Coords = Coordinates.Zero }); + + var gameObjectExists = linker.EntityIdToGameObjects + .TryGetValue(new EntityId(entityId), out var gameObjects); + + Assert.IsFalse(gameObjectExists); + }) + .Step(world => + { + world.Connection.AddComponent(entityId, Metadata.ComponentId, + new Metadata.Update { EntityType = "TestObject" }); + + var gameObjectExists = linker.EntityIdToGameObjects + .TryGetValue(new EntityId(entityId), out var gameObjects); + + Assert.IsFalse(gameObjectExists); + }) + .Step(world => + { + var gameObjectExists = linker.EntityIdToGameObjects + .TryGetValue(new EntityId(entityId), out var gameObjects); + + Assert.IsTrue(gameObjectExists); + }); + } + + private class TestGameObjectCreator : IEntityGameObjectCreator + { + private readonly string workerType; + + private readonly Dictionary entityIdToGameObject = new Dictionary(); + + public ComponentType[] MinimumComponentTypes { get; } = + { + ComponentType.ReadOnly(), + ComponentType.ReadOnly() + }; + + public TestGameObjectCreator(string workerType) + { + this.workerType = workerType; + } + + public void OnEntityCreated(SpatialOSEntity entity, EntityGameObjectLinker linker) + { + var gameObject = new GameObject(); + gameObject.transform.position = Vector3.one; + gameObject.transform.rotation = Quaternion.identity; + gameObject.name = $"TestObject(SpatialOS: {entity.SpatialOSEntityId}, Worker: {workerType})"; + + entityIdToGameObject.Add(entity.SpatialOSEntityId, gameObject); + linker.LinkGameObjectToSpatialOSEntity(entity.SpatialOSEntityId, gameObject); + } + + public void OnEntityRemoved(EntityId entityId) + { + if (!entityIdToGameObject.TryGetValue(entityId, out var go)) + { + return; + } + + Object.DestroyImmediate(go); + entityIdToGameObject.Remove(entityId); + } + } + } +} diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/GameObjectCreationTests.cs.meta b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/GameObjectCreationTests.cs.meta new file mode 100644 index 0000000000..a2ece31a88 --- /dev/null +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/GameObjectCreationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 62932c56e25d424dafbb2d06dd8850e4 +timeCreated: 1584709255 \ No newline at end of file diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/Improbable.Gdk.GameObjectCreation.EditmodeTests.asmdef b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/Improbable.Gdk.GameObjectCreation.EditmodeTests.asmdef index 23438866b4..1b266a25b3 100644 --- a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/Improbable.Gdk.GameObjectCreation.EditmodeTests.asmdef +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/Improbable.Gdk.GameObjectCreation.EditmodeTests.asmdef @@ -1,12 +1,14 @@ { "name": "Improbable.Gdk.GameObjectCreation.EditmodeTests", "references": [ - "GUID:0603bcb6cd766ea40a053a38fff8a84b", - "GUID:edb3612c44ad0d24988d387581fd5fbe", - "GUID:6e87be7cacbd7264c9a5b9efc59a4030", - "GUID:734d92eba21c94caba915361bd5ac177", - "GUID:27619889b8ba8c24980f49ee34dbb44a", - "GUID:0acc523941302664db1f4e527237feb3" + "Improbable.Gdk.Core.EditmodeTests", + "Improbable.Gdk.Core", + "Improbable.Gdk.GameObjectCreation", + "Unity.Entities", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner", + "Improbable.Gdk.TestUtils", + "Improbable.Gdk.Generated" ], "includePlatforms": [ "Editor" diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/LinkedGameObjectMapSubscriptionTests.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/LinkedGameObjectMapSubscriptionTests.cs index 7163513438..3046d0eaa1 100644 --- a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/LinkedGameObjectMapSubscriptionTests.cs +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/LinkedGameObjectMapSubscriptionTests.cs @@ -1,29 +1,48 @@ using Improbable.Gdk.Core; -using Improbable.Gdk.Core.EditmodeTests.Subscriptions; using Improbable.Gdk.Subscriptions; +using Improbable.Gdk.TestUtils; using NUnit.Framework; namespace Improbable.Gdk.GameObjectCreation.EditmodeTests { - public class LinkedGameObjectMapSubscriptionTests : SubscriptionTestBase + public class LinkedGameObjectMapSubscriptionTests : MockBase { private EntityId entityId = new EntityId(100); [Test] public void Subscribe_to_LinkedGameObjectMap_should_not_be_available_if_GameObjectCreation_systems_are_not_present() { - var goMapSubscription = SubscriptionSystem.Subscribe(entityId); - Assert.IsFalse(goMapSubscription.HasValue); + World + .Step((world) => + { + var subscriptionSystem = world.GetSystem(); + var goMapSubscription = subscriptionSystem.Subscribe(entityId); + + return goMapSubscription; + }) + .Step((world, goMapSubscription) => + { + Assert.IsFalse(goMapSubscription.HasValue); + }); } [Test] public void Subscribe_to_LinkedGameObjectMap_should_be_available_if_GameObjectCreation_systems_are_added() { - GameObjectCreationHelper.EnableStandardGameObjectCreation(World, new MockGameObjectCreator()); + World + .Step((world) => + { + GameObjectCreationHelper.EnableStandardGameObjectCreation(world.Worker.World, new MockGameObjectCreator()); + var subscriptionSystem = world.GetSystem(); + var goMapSubscription = subscriptionSystem.Subscribe(entityId); - var goMapSubscription = SubscriptionSystem.Subscribe(entityId); - Assert.IsTrue(goMapSubscription.HasValue); - Assert.IsNotNull(goMapSubscription.Value); + return goMapSubscription; + }) + .Step((world, goMapSubscription) => + { + Assert.IsTrue(goMapSubscription.HasValue); + Assert.IsNotNull(goMapSubscription.Value); + }); } } } diff --git a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/MockGameObjectCreator.cs b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/MockGameObjectCreator.cs index 2da4f5d62f..6b942931fd 100644 --- a/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/MockGameObjectCreator.cs +++ b/workers/unity/Packages/io.improbable.gdk.gameobjectcreation/Tests/Editmode/MockGameObjectCreator.cs @@ -1,10 +1,14 @@ +using System; using Improbable.Gdk.Core; using Improbable.Gdk.Subscriptions; +using Unity.Entities; namespace Improbable.Gdk.GameObjectCreation.EditmodeTests { public class MockGameObjectCreator : IEntityGameObjectCreator { + public ComponentType[] MinimumComponentTypes { get; } = { }; + public void OnEntityCreated(SpatialOSEntity entity, EntityGameObjectLinker linker) { throw new System.NotImplementedException(); diff --git a/workers/unity/Packages/io.improbable.gdk.testutils/MockWorld.cs b/workers/unity/Packages/io.improbable.gdk.testutils/MockWorld.cs index fd2fba3e6e..abb5c5c5ed 100644 --- a/workers/unity/Packages/io.improbable.gdk.testutils/MockWorld.cs +++ b/workers/unity/Packages/io.improbable.gdk.testutils/MockWorld.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using Improbable.Gdk.Core; using Improbable.Gdk.Subscriptions; +using NUnit.Framework; using Unity.Entities; using UnityEditor; using UnityEngine; @@ -13,7 +15,7 @@ public class MockWorld : IDisposable public struct Options { public string WorkerType; - public Type[] AdditionalSystems; + public Action AdditionalSystems; public ILogDispatcher Logger; } @@ -38,10 +40,7 @@ public static MockWorld Create(Options options) Vector3.zero) .Result; - foreach (var type in options.AdditionalSystems ?? new Type[] { }) - { - mockWorld.Worker.World.CreateSystem(type); - } + options.AdditionalSystems?.Invoke(mockWorld.Worker.World); mockWorld.Linker = new EntityGameObjectLinker(mockWorld.Worker.World);