Skip to content
This repository has been archived by the owner on Jan 18, 2022. It is now read-only.

Allow multi-frame entity checkout for GameObject creation #1330

Merged
merged 8 commits into from
Mar 23, 2020
Merged
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<World>`. [#1330](https://github.com/spatialos/gdk-for-unity/pull/1330)

## `0.3.3` - 2020-02-14

18 changes: 17 additions & 1 deletion UPGRADE_GUIDE.md
Original file line number Diff line number Diff line change
@@ -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<Metadata.Component>(),
ComponentType.ReadOnly<Position.Component>()
};
```

> You wil need to add `using Unity.Entities;` to the top of the file.
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved
## 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.
Original file line number Diff line number Diff line change
@@ -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<string, GameObject> cachedPrefabs
typeof(MeshRenderer)
};

public ComponentType[] MinimumComponentTypes { get; } =
{
ComponentType.ReadOnly<Metadata.Component>(),
ComponentType.ReadOnly<Position.Component>()
};

public GameObjectCreatorFromMetadata(string workerType, Vector3 workerOrigin, ILogDispatcher logger)
{
this.workerType = workerType;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Improbable.Gdk.Core;
using Unity.Entities;

namespace Improbable.Gdk.GameObjectCreation
{
public struct GameObjectInitSystemStateComponent : ISystemStateComponentData
{
public EntityId EntityId;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Linq;
using Improbable.Gdk.Core;
using Improbable.Gdk.Subscriptions;
using Unity.Entities;
@@ -21,31 +22,59 @@ internal class GameObjectInitializationSystem : ComponentSystem
private readonly GameObject workerGameObject;

private EntitySystem entitySystem;
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved
private WorkerSystem workerSystem;

private EntityQuery newEntitiesQuery;
private EntityQuery removedEntitiesQuery;

private ComponentType[] minimumComponentSet = new[]
{
ComponentType.ReadOnly<SpatialEntityId>()
};

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()
{
base.OnCreate();

entitySystem = World.GetExistingSystem<EntitySystem>();
workerSystem = World.GetExistingSystem<WorkerSystem>();

Linker = new EntityGameObjectLinker(World);

if (workerGameObject != null)
{
Linker.LinkGameObjectToSpatialOSEntity(new EntityId(0), workerGameObject);
}

newEntitiesQuery = GetEntityQuery(new EntityQueryDesc()
{
All = minimumComponentSet,
None = new[] { ComponentType.ReadOnly<GameObjectInitSystemStateComponent>() }
});

removedEntitiesQuery = GetEntityQuery(new EntityQueryDesc()
{
All = new[] { ComponentType.ReadOnly<GameObjectInitSystemStateComponent>() },
None = minimumComponentSet
});
}

protected override void OnDestroy()
{
EntityManager.RemoveComponent<GameObjectInitSystemStateComponent>(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<GameObjectInitSystemStateComponent>(removedEntitiesQuery);
}
}
}
Original file line number Diff line number Diff line change
@@ -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
/// </summary>
public interface IEntityGameObjectCreator
{
/// <summary>
/// The minimum set of components required on an entity to create a GameObject.
/// </summary>
ComponentType[] MinimumComponentTypes { get; }

/// <summary>
/// Called when a new SpatialOS Entity is checked out by the worker.
/// </summary>
Original file line number Diff line number Diff line change
@@ -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<GameObjectInitializationSystem>();
linker = goInitSystem.Linker;
}
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved

[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
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved
.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<EntityId, GameObject> entityIdToGameObject = new Dictionary<EntityId, GameObject>();

public ComponentType[] MinimumComponentTypes { get; } =
{
ComponentType.ReadOnly<Position.Component>(),
ComponentType.ReadOnly<Metadata.Component>()
};

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);
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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",
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved
"Improbable.Gdk.Core",
"Improbable.Gdk.GameObjectCreation",
"Unity.Entities",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"Improbable.Gdk.TestUtils",
"Improbable.Gdk.Generated"
],
"includePlatforms": [
"Editor"
Original file line number Diff line number Diff line change
@@ -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<LinkedGameObjectMap>(entityId);
Assert.IsFalse(goMapSubscription.HasValue);
World
.Step((world) =>
{
var subscriptionSystem = world.GetSystem<SubscriptionSystem>();
var goMapSubscription = subscriptionSystem.Subscribe<LinkedGameObjectMap>(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<SubscriptionSystem>();
var goMapSubscription = subscriptionSystem.Subscribe<LinkedGameObjectMap>(entityId);

var goMapSubscription = SubscriptionSystem.Subscribe<LinkedGameObjectMap>(entityId);
Assert.IsTrue(goMapSubscription.HasValue);
Assert.IsNotNull(goMapSubscription.Value);
return goMapSubscription;
})
.Step((world, goMapSubscription) =>
{
Assert.IsTrue(goMapSubscription.HasValue);
Assert.IsNotNull(goMapSubscription.Value);
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
Original file line number Diff line number Diff line change
@@ -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<World> 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);