Improbable Icon

GDK Questions

unity-gdk

#1

Question 1: Linking GameObjects to ECS Entities

I am trying to understand how the TransformSyncSystem does its business. I understand that it iterates over entities on the “Spatial side”, but how are those linked to the Unity GameObjects? (In the GDK) The GameObjects in the example project do not have a GameObjectEntity component attached, so how can an ECS System iterate over them? I see that a Group is created:

private struct TransformData
{
    public readonly int Length;

    // This I understand, components that are generated from Schema.
    public ComponentDataArray<Transform.Component> Transform;
    public ComponentDataArray<Position.Component> Position;
  
    // How?? The GameObjects with the Rigidbodies don't have a "GameObjectEntity"
    [ReadOnly] public ComponentArray<Rigidbody> GameObjectRigidBody; 
}

Where in the codebase are these Rigidbodies connected to their entities? I thought that was the purpose of a GameObjectEntity: to link Unity GameObjects and their MonoBehaviours/Components to an ECS entity? But the sample project doesn’t use them?

Question 2: PositionSendSystem

I see that the PositionSendSystem is taking data from the schema-generated Position component, and then sending ComponentUpdates manually. This system applies rate-limiting too which makes sense for Position updates. However, doesn’t the GDK automatically send updates for all changed Components?

I think I have also found that the answer is the CustomSpatialOSSendSystem, which prevents the default replicator from running… That makes sense.


#3

Thanks for having a look at the GDK - we really appreciate any and all feedback :smile:

Question 1:
You are right, we don’t use the GameObjectEntity component provided by Unity. The main reason for this is because of how our flow of spawning entities happens vs how Unity expects you to use the GameObjectEntity component.

In the GDK all entities received are initially created as an ECS entity. We then have two systems: GameObjectInitializationSystem and EntityGameObjectLinker which will create a corresponding GameObject (if one is required!) and link the ECS entity and that GameObject.

In Unity’s Hybrid ECS approach with the GameObjectEntity component, they expect you to have the opposite workflow where you have the GameObject first and you wish to create an ECS entity from that. There is no supported API for linking the GameObject to an existing entity.

Ultimately, the our code does the same thing as the GameObjectEntity (finds all components on the GameObject and inserts them onto the ECS entity) but it just supports a different workflow.

Question 2:
Yup you are right again - the CustomSpatialOSSendSystem disables the standard generated replication from firing, allowing the custom replication full control of when to replicate state and events and what to replicate. In the future, we hope to extend this further and give you a bit more control (do you want to modify replication for all state updates, events and commands or just state updates, etc.) as currently it hands over everything to you.


#4

Thanks Jamie! So if I understand correctly, what it really comes down to is:

  1. GameObjectInitializationSystem receives new (SpatialOS)Entities. (User Code)
  2. Creates a GameObject from a Prefab using the EntityGameObjectCreator (GDK Core)
  3. Uses the EntityGameObjectLinker to add the Components on the GameObject to the entity. (GDK Core)
  4. The EntityGameObjectLinker causes a call to EntityManager.AddComponent(entity, componentType). (Unity ECS)
    (This was hidden somewhere deep in ViewCommandBuffer, took me some snooping around to find it)

So is this also correct? : in order to add a Rigidbody (or other Component/MonoBehaviour) as an ECS Component, you need to call

  • ComponentType.Create<Rigidbody>(), and then you can do
  • EntityManager.AddComponent(). (which takes a ComponentType, not a Type)
  • At which point you can use a ComponentArray<RigidBody> directly in a group and Inject it?

and this is part of what the GameObjectEntity normally does for you?


#5

Yes that’s mostly correct! Although its changing fairly rapidly at the moment - so future readers beware!

There’s a bit more nuance to it however, if you have a (GameObject) component that you want to add to the ECS entity you have to:

  1. Call entityManager.AddComponent(entity, ComponentType.Create<T>());
  2. Then call entityManager.SetComponentObject(entity, component); (noting the generic is inferred by the type of component))

That latter call is the important one as Components are handled differently to IComponentData in the ECS (since they are reference types). That method is strictly internal - although we’ve exposed it (as an extension method) to replicate the behaviour of the GameObjectEntity script.

Its also important to note that the Component will be “Unity null” unless its properly instantiated by Unity on a GameObject. I’d generally advise to not try and hack around it too much (more than we already are :joy:).

In the future, once Unity fleshes out some of the Hybrid workflows, we are hoping to remove that small amount of reflection and use whatever workflow Unity provides.

The TL;DR of what GameObjectEntity and our implementation does is:

for component in gameObject.GetAllComponents():
    entityManager.AddComponent(entity, ComponentType.Create(component.GetType()))
    entityManager.SetComponentObject(entity, component)

Feel free to DM me here or on Discord if you want to go a bit deeper into current implementation details :smile:


#6

Thank you again!

I have another question. I hope that it’s OK for me to post them here? I will gladly move to discord or DM if you prefer but I figured that it’s nice to have things public & with better formatting (than discord).

Question 3: What is ComponentData?

In the SDK, each generated component MyThing got a corresponding struct MyThingData with fields corresponding to the fields defined in the schema. This made a lot of sense.

However, in the GDK, I see CreateSchemaComponentData which returns a generic ComponentData object.

var cubeTargetVelocity = CubeTargetVelocity.Component.CreateSchemaComponentData(new Vector3f { X = -2.0f });

What does this object do and why?
Is it only used for Initialisation? And at runtime it’s the Generated.MyThing.Component that is used?


#7

More general questions definitely are better in public :smile:

The CreateSchemaComponentData method is a helper method that will serialise a bundle of data into a format (ComponentData) that the WorkerSDK and the runtime can understand (based on schema).

The CubeTargetVelocity.Component is the ECS component that Unity understands.

An example usage of this currently is with the EntityBuilder. This requires a ComponentData object which would be frustrating to manually create and serialise each time.

Unfortunately, we have a lot of terminology overloading at the moment!

The reason we have this alongside the Serialization static class generated with each component is that when the component is not blittable we do some obfuscation to make it appear to be so from Unity’s point of view. This means that creating some components naively can cause some problems - so we sidestep it by giving the user these helper methods instead :smile:

There is definitely an argument to move the helper method from: CubeTargetVelocity.Component.CreateSchemaComponentData to CubeTargetVelocity.Serialization.CreateSchemaComponentData.

Please keep them coming! :wink:


#8

yes.

a lot…

like. A LOT.

:rofl:

is anything not a component?

Ah gotcha. I’m starting to understand why there would be two different formats (one for Unity and one for the runtime)


#9

Question 4: Archetypes

So UnityECS has this definition of an Archetype as an array of ComponentTypes. This I understand.

And from what I understand, when an Entity is created through the Spatial runtime, the SpatialOSReceiveSystem creates a new Entity and adds the ECS Components corresponding to the Spatial Components (for each AddComponentOp)

  1. So where do Unity’s EntityArchetypes come into play? I though they were obligatory for the ECS Chunk storage? (I did a CTRL+F for EntityManager.CreateArchetype but came up empty)

  2. And the answer is probably related to: What does the Schema Archetype component do in the GDK Sample?
    I also find it hard to understand what the ArchetypeInitializationSystem does for each of the listed ArchetypeNames

  3. I see that the ArchetypeName is mapped to a Resources/ Prefab location path. So then what does the Schema Prefab component do?

Feedback

And this leads to perhaps my first bit of feedback. I wanted to create a new type of object and add it to the GDK sample, and to me it seems like I have to write ‘definitions’ for this new entity type in many different places:

Where What
A Prefab Attached Monobehaviours / Components
The snapshot creation code The added SpatialOS Schema Components (and thus ECS components)
ArchetypeInitializationSystem Something related to the ArchetypeName, but I’m not sure what
Somewhere? Create an EntityArchetype for ECS Components belonging to an entity?

It would be nice perhaps to have a single place where all components for a generic (SpatialOS/Unity) EntityType are defined. Like a prefab with a bunch of placeholder MonoBehaviours that translate to Schema components (instead of doing this in code in SnapshotGenerator or similar). I do realise that would mean going back to having a Prefab Preprocessor that removes/translates those components so not ideal.


#10

More overloading again :sob:

Unity’s concept of an archetype are obligatory for ECS chunks, but it will manage them for you under the hood. They do provide some benefits like faster batch instantiation of ECS entities and some fun low-level APIs. The Unity Archetype changes every time you add/remove components - so in the GDK they change fairly often (updates/authority changes/events received/commands received).


The archetype schema component is (annoyingly) unrelated, it is used to add local ECS components to Spatial entities upon instantiation.

So taking the example from ArchetypeInitializationSystem:

                switch (worker.WorkerType)
                {
                    case WorkerUtils.UnityClient when archetype == ArchetypeConfig.CharacterArchetype:
                    case WorkerUtils.UnityClient when archetype == ArchetypeConfig.CubeArchetype:
                    case WorkerUtils.UnityClient when archetype == ArchetypeConfig.SpinnerArchetype:
                    case WorkerUtils.UnityGameLogic when archetype == ArchetypeConfig.CharacterArchetype:
                    case WorkerUtils.UnityGameLogic when archetype == ArchetypeConfig.CubeArchetype:
                    case WorkerUtils.UnityGameLogic when archetype == ArchetypeConfig.SpinnerArchetype:
                        PostUpdateCommands.AddBuffer<BufferedTransform>(entity);
                        break;
                    default:
                        break;
                }

Any new Cube, Spinner, and Character on any worker (in its currently implementation) will have BufferedTransform Buffer (a special type of ECS component) added to it- this is for use in the Transform feature module.

The current implementation is probably not the best example but you can imagine a different use case where a particular entity would require some special local components on a particular worker type - that’s what the archetype schema component is for :smile:

Off the top of my head, you could use something like this in a player heart beating feature:

                switch (worker.WorkerType)
                {
                    case WorkerUtils.UnityClient when archetype == ArchetypeConfig.CharacterArchetype:
                    case WorkerUtils.UnityClient when archetype == ArchetypeConfig.CubeArchetype:
                    case WorkerUtils.UnityClient when archetype == ArchetypeConfig.SpinnerArchetype:
                    case WorkerUtils.UnityGameLogic when archetype == ArchetypeConfig.CharacterArchetype:
                        PostUpdateCommands.AddComponentData<NeedsHeartbeatSent>(entity);
                    case WorkerUtils.UnityGameLogic when archetype == ArchetypeConfig.CubeArchetype:
                    case WorkerUtils.UnityGameLogic when archetype == ArchetypeConfig.SpinnerArchetype:
                        PostUpdateCommands.AddBuffer<BufferedTransform>(entity);
                        break;
                    default:
                        break;
                }

This now says any new Cube, Spinner, and Character on any worker will have BufferedTransform added and any Character on the GameLogic worker will have NeedsHeartbeatSent added to it.


You’ll be happy to know that the Prefab schema component no longer serves any purpose and was removed earlier today :wink:


As for the multiple definitions of entities - this is something we are painfully aware of. Currently you have to do the following:

  1. Define an Entity template function somewhere - either for use in snapshot generation or dynamically at runtime (with CreateEntity commands)
  2. (Optionally) Create a Prefab and add Monobehaviours
  3. (Optionally) Add entries to the ArchetypeIntializationSystem

For me the easiest way to think about this to link each of these to different aspects of entities:

  1. The initial state of the entity
  2. User code which consumes and writes SpatialOS state to interact with the world.
  3. Extra instantiation logic for non-replicated properties that you want access/need in the ECS.

The bare minimum is small - but the more you want to do with the entity (especially if you want a GameObject), the more places you have to touch - which is obviously not ideal.

Keep in mind the ArchetypeInitialisationSystem is currently user code and is not set in stone - there is the opinion that if you have a use case for such initialisation code - its possibly better to write a specialised system that handles just that use case rather than introducing larger opinions.

We’ve floated the idea around of having a feature module for snapshot generation which would do something similar to the Prefab Preprocessor - but this would only be for snapshot generation (in the current design spec).

Bridging the gap between ECS and GameObjects is unfortunately fairly difficult - it will be interesting to see what sort of tooling Unity introduce for this in the future (there’s some inklings of ECS prefabs in the source code of the packages) and how we can adapt this into the GDK to make the workflow nicer.


#11

Thank you again for taking the time to write this up! Cleared up so much for me again! :star_struck:


I think that switch statement is … not the best? In the original code it only adds the same component to every entity, so it’s understandable, but your amendment — I think — shows how it would go wrong.

I don’t think that’s what it does now? Because of the switch/case fall through? C# only falls through empty cases? Actually I am having 100% trouble parsing what exactly it would do in your example :sweat_smile:.

Of course this is really way too specific of a comment for code that’s in flux, I realise that.

But the general takeaway for me is that this code (logic for adding non-replicated components — your #3) is altogether too procedural instead of declarative. The same goes to a degree for entity initialisation (#1).


#12

TIL things about C#! This is why you should run code before posting it :joy:

Its fairly likely that the Archetype stuff is going to be rewritten/replaced/removed in the near future anyway.


The procedural vs declarative nature could be addressed in a similar way to GameObject prefabs:

  • Declare a set ECS Entity Prefab (with default values - currently kind of possible)
  • Create the SpatialOS entity from off the wire
  • Merge the SpatialOS entity along with a (set of?) ECS Entity Prefabs based on some context. (Merging entities is not currently a supported API in the Unity ECS)

At least then the approach is consistent with that of GameObject creation.


I think with two different workflows supported we are going to have these kind of redundancies unless we can figure out a more elegant solution - any suggestions greatly appreciated :wink:


#13

Do you mean using GameObjectEntity and Unity’s ComponentDataWrapper?

Yeah I’m starting to see why it works like it does now. Unity’s GameObjectEntity and the SpatialOS entities are coming at it from a different angle.


The BufferedTransform that is added in the GDK Sample is a different thing. Because even though the documentation says that

A dynamic buffer is a type of component data

you can’t actually use ComponentDataWrapper<BufferedTransform>.

I find it kind of weird that a DynamicBuffer IS a Component. Instead of having a ‘regular’ component with a Buffer as a field type. (Kind of like lists in Schema)


For “normal” non-replicated ECS components it might work though:

Adding a ComponentDataWrapper MonoBehaviour to the Prefab, and then scanning the prefab and actually adding the ECS component when it’s created?

I guess this wouldn’t work for SpatialOS entities that aren’t GameObjects though.


#14

Adding a ComponentDataWrapper MonoBehaviour to the Prefab, and then scanning the prefab and actually adding the ECS component when it’s created?

Yeah this could be a viable option - but like you say, this doesn’t work if you have SpatialOS entities which aren’t GameObjects. Equally - that isn’t a reason that to not add this as a small feature to exactly replicate the GameObjectEntity script.

The kicker for that might be introducing too many different workflows - which can become counterproductive and increases the maintenance burden.

  • Declare a set ECS Entity Prefab (with default values - currently kind of possible)

We’ve seen APIs for marking an ECS Entity instance as a Prefab (literally drowning in terminology overloading now :sob: ) in the new Unity.Entities package which appears like you can seed them with initial data (as opposed to EntityArchetype where everything is default constructed). But we’ve not experimented with it yet to determine how useful it may/may not be. :smile:


#15

A way comes to mind to structure this in my current project.

My old SDK-based workflow was as follows:

  • SpatialOS (the entity pipeline) automatically instantiates a Prefab for each entity using the Metadata Component’s entity_type string.

  • I created an ObjectType component with a single int field that fully specifies the object. I suppose it’s somewhat similar to the Archetype in the GDK. (except an int/enum instead of a string) However:

    • The ObjectType is used to look up a ScriptableObject Template with further properties that are then applied to the Instantiated GameObject: different meshes/materials etc. But the Template also stores the Prefab just in case.
    class Template : ScriptableObject {
        GameObject prefab;
        Material material;
        Mesh mesh;
        // Other initialization stuff.
    }
    

For example: There are different things just passively lying around in the world that are just “WorldItem” prefabs. The ObjectType is an int that specifies whether it’s a CementBag, WoodPlanks ,PlasticBarrel, etc. Some items are uniquely specified by the Metadata.entity_type (the Prefab name string) but some are not.

However, this roundtrip to find a Template is a little convoluted because Spatial already created a Prefab:


With the SDK Pipeline “out of the way”, though, a more sensible and ‘linear’ entity creation pipeline comes to mind, one that goes through the Template first, before Instantiating a Prefab.

  • A new entity comes in from the runtime, it has an ObjectType component.
  • The Template is found based on the ObjectType.
  • The Template stores a link to the prefab, which is instantiated.
  • The Template is responsible for setting other properties on the instantiated object. (Perhaps adding other ECS components)
  • Use LinkGameObjectToEntity to link the Template to the Entity I suppose?

Sorry for the long detour, but my point is: for a GameObject-based workflow, this Template could be a sensible place to do much of the definition-y stuff that we talked about before.

For example, the Template could hold references to the different Prefabs used on the different worker types, replacing PrefabConfig.cs

My question is: Is this something I can elaborate on? Or will the GDK design possibly break this in the future? (By, for example, insisting on instantiating Prefabs internally, or having the Archetype as a core concept)


#16

First off - thanks for all the awesome detail, its so useful for us to understand how our customers interact with/use various features :smile:

So - yes you can elaborate on this approach with the current design, in fact it dovetails almost perfectly.


(By, for example, insisting on instantiating Prefabs internally, or having the Archetype as a core concept)

I will address each of these in turn - but the general idea is that we want as much of the GDK to be opt-in and replaceable as possible. This is one of the core tenets of the GDK and a lesson learned from the SDK - don’t force users into your workflow. Hence - feature modules, these will be fully opt-in and we want to expose lots of hooks for functionality within these feature modules to allow users to tweak how they work without forcing you to rewrite them (acknowledging that these hooks can only tweak so much - eventually you may need to rewrite).

Prefab Instantiation

We currently have a feature module: com.improbable.gdk.gameobjectcreation which is responsible for instantiating GameObjects. This contains a few things:

  1. An ECS system which does the following. Creates GameObjects and calls LinkGameObjectToEntity when new entities are received. Removes GameObjects and calls UnlinkGameObjectToEntity whenever entities are removed.
  2. An interface IEntityGameObjectCreator which exposes two methods:
    • GameObject OnEntityCreated(SpatialOSEntity entity);
    • void OnEntityRemoved(EntityId entityId, GameObject linkedGameObject);
  3. A default implementation of IEntityGameObjectCreator which uses the Metadata component.
  4. Some helper structs/methods

Of those helper methods, these will be of particular interest to you -

    public static class GameObjectCreationSystemHelper
    {
        public static void EnableStandardGameObjectCreation(World world)
        {
            var workerSystem = world.GetOrCreateManager<WorkerSystem>();
            EnableStandardGameObjectCreation(world, new GameObjectCreatorFromMetadata(workerSystem.WorkerType,
                workerSystem.Origin, workerSystem.LogDispatcher));
        }

        public static void EnableStandardGameObjectCreation(World world, IEntityGameObjectCreator creator)
        {
            world.CreateManager<GameObjectInitializationSystem>(creator);
        }
    }

Following the philosophy of feature modules - just adding a feature module package to your Unity project does not automatically inject itself into the project runtime, you have to explicitly opt in by adding ECS systems to the world or similar.

In this case - you can choose the default implementation (the top) or a custom one (the bottom).

Given the workflow that you described to me, I think the IEntityGameObjectCreator actually fits your use case perfectly - for example you might have something like this (and no - I didn’t try running this code either :stuck_out_tongue:)

public class MyGameObjectCreator : IEntityGameObjectCreator
{
    private readonly Dictionary<uint, Template> templates = new Dictionary<uint, Template>();

    public MyGameObjectCreator()
    {
        PopulateTemplateMap();
    }

    public GameObject OnEntityCreated(SpatialOSEntity entity)
    {
        if (!entity.HasComponent<ObjectType.Component>())
        {
            return null;
        }
        
        var objectTypeIndex = entity.GetComponent<ObjectType.Component>().Index;

        if (!templates.TryGetValue(objectTypeIndex, out var template))
        {
            throw new ArgumentException($"Unknown object type index: {objectTypeIndex}");
        }

        // Do something with template and return the instantiated prefab.
    }

    public void OnEntityRemoved(EntityId entityId, GameObject linkedGameObject)
    {
        // Do any cleanup required.
    }

    private void PopulateTemplateMap()
    {
        // Find all scriptable objects - add them to the map.
    }
}

So the workflow would look something like the following:

Untitled%20Diagram

Alternatively - if you wanted even more control, you can opt out entirely of the com.improbable.gdk.gameobjectcreation feature module and do whatever you want to get GameObjects in the world. :smile:

Archetype as a Core Concept

This again relates to the concept of a feature module - if we do end up moving this from Playground it will be into a feature module, which is entirely opt-in :smile:.


#17

We are getting public docs ready for these sorts of thing (in particular the Monobehaviour workflow) - so you should be seeing those appear soon :smile:


#18

:two_hearts:

That looks great! Very understandable. I look forward to trying it out.

When moving from the SDK to the GDK, what other major differences should I consider?

It seems that:

  • The GDK has Readers/Writers so I can start with my existing MonoBehaviour based code.
  • Entity creation can be handled as above.
  • ?? What else is there?

I assume I have to do some work to move from the SDK’s Scene-based workers to the new Worker/WorkerSystem stuff?


#19

The GDK does have Reader/Writers but they are different from the SDK Reader/Writers - for example, sending commands is entirely different.

We haven’t started work on a migration path/compatibility layer yet - so there might be some pains if you try to migrate across right now.

There are going to be a number of differences (just off the top of my head):

  • Snapshot generation will have to be adjusted (different worker attributes) + EntityBuilder has a slightly different API
  • As you mentioned worker connection and Scene is another difference. The example currently in Playground is a good resource - we have the SampleScene for iteration in the editor with both workers, GameLogicScene for a singular UnityGameLogic worker and ClientScene for a singular UnityClient worker.
  • Semantics around Reader/Writers are different
  • Generated code no longer uses Improbable.Collection objects.

There are very likely to be more differences that I’ve not thought of.