Improbable Icon

Doing it: GDK Migration (logbook)

gdk
unity-gdk

#1

This is a logbook for the migration from the SpatialOS Unity SDK to the GDK for Unity.

Importing the GDK

Error with package details: Package has invalid dependencies:
com.unity.burst: Package [com.unity.burst@0.2.4-preview.26] cannot be found
com.unity.incrementalcompiler: Package [com.unity.incrementalcompiler@0.0.42-preview.20] cannot be found
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)

I just started with my existing SDK project and added the GDK dependencies in manifest.json . Then I got a complaint that I needed Burst and the Incremental Compiler, so I added those from the package manager.


Assembly has reference to non-existent assembly ‘Improbable.Gdk.Generated’ (Packages/com.improbable.gdk.playerlifecycle/Improbable.Gdk.PlayerLifecycle.asmdef)

It seems weird for a feature module to depend on Generated code? Because I can’t generate code until the GDK is properly imported?

I was looking for the SpatialOS menu to maybe do a codegen to get the Generated code? But I could only find the SDK menu. So I deleted the Plugins/Improbable folder, as I figured that is the SDK. ¯\(ツ)/¯ . Bit of cleanup.

I removed the two “Feature Module” packages from the manifest (Player Life Cycle and Transform Synchronization).

Then I no longer got the assembly errors.

Edit: :bulb: As soon as I got the Codegen working, SpatialOS created the Generated folder for me. Then I copied over the Improbable.Gdk.Generated.asmdef from the UnityGDK project.


Had a bunch of C#7 language errors. So I switched to the .NET4 runtime under Player Preferences.


Now I’m getting regular errors because of course all my code is using the SDK. Good stuff! :tada:

Let’s examine some of those, see if a few different imports can fix a bunch of them. There’s 656 :x: total.

I changed from using Concrete.Schema to using Generated.Concrete.Schema. Neat.

Down to 620 :x:.


I just let VSCode suggest some new “usings” for me. For a few errors.

  • using Improbable.Gdk.Core.Commands;
  • using Improbable.Gdk.GameObjectRepresentation

Had to add Requirable to the Readers and Writers:

[Require] PlayerCreation.Writer playerCreation -> [Require] PlayerCreation.Requirable.Writer playerCreation


Commands are now separate from Readers/Writers so I need to Inject the CommandSenders / CommandHandlers too.

The callbacks are now events.

playerCreationWriter.CommandReceiver.OnCreatePlayer.RegisterAsyncResponse(OnCreatePlayer);

// BECOMES

playerCreationCommandHandler.OnCreatePlayerRequest += OnCreatePlayer;

Ah good, I don’t have to Deregister (just like with component updates):

If your game logic dictates, you can simply unsubscribe from receiving requests by calling requestHandler -= myRequestProcessingMethod;. However, you should not do so in OnDisable(): when a MonoBehaviour is disabled, the RequestHandler is already invalidated and shouldn’t be accessed, anything you registered will be cleaned up automatically by the GDK.


SpatialOS.Commands.SendCommand needs to be rewritten to the new WorldCommands methods.

Adding the WorldCommandRequestSender and WorldCommandResponseHandler is easy enough, but the tricky thing is keeping track of the Responder, since the WorldCommands no longer have the convenient OnSuccess and OnFailure callbacks. So I had to cache the responder awaiting the response from the World Command.

First class migrated! Before and after:

using Concrete.Schema;
using Improbable.Entity.Component;
using Improbable.Unity;
using Improbable.Unity.Core;
using Improbable.Unity.Visualizer;
using Improbable.Worker;
using UnityEngine;

namespace Concrete
{
    [WorkerType(WorkerPlatform.UnityWorker)]
    public class PlayerCreator : MonoBehaviour
    {
        [Require]
        private PlayerCreation.Writer PlayerCreationWriter;

        private void OnEnable()
        {
            PlayerCreationWriter.CommandReceiver.OnCreatePlayer
            	.RegisterAsyncResponse(OnCreatePlayer);
        }

        private void OnDisable()
        {
            PlayerCreationWriter.CommandReceiver.OnCreatePlayer.DeregisterResponse();
        }

        private void OnCreatePlayer(ResponseHandle<PlayerCreation.Commands.CreatePlayer, 
        	CreatePlayerRequest, CreatePlayerResponse> responseHandle)
        {
            var clientWorkerId = responseHandle.CallerInfo.CallerWorkerId;
            var posFlat = Random.insideUnitCircle * 0.25f * Config.TerrainWidth;
            Vector3 pos = new Vector3(posFlat.x, 5 + Config.TerrainHeight / 2, posFlat.y);
            Debug.LogFormat("Creating player at {0}", pos);
            RaycastHit hit;
            if ( Physics.SphereCast(pos, Config.PlayerColliderRadius, 
            	-Vector3.up, out hit, Config.TerrainHeight * 2, Layers.WalkableSurface) )
            {
                pos = hit.point;
                pos.y += 1.5f;
            }
            var playerEntityTemplate = EntityTemplateFactory.
            	CreatePlayerTemplate(clientWorkerId, pos);
            	
            SpatialOS.Commands.CreateEntity (PlayerCreationWriter, playerEntityTemplate)
                .OnSuccess (_ => 
                	responseHandle.Respond (
                		new CreatePlayerResponse ((int) StatusCode.Success)))
                .OnFailure (failure => 
                	responseHandle.Respond (
                		new CreatePlayerResponse ((int) failure.StatusCode)));
        }
    }
}


AFTER:

using System;
using Generated.Concrete.Schema;
using Improbable.Gdk.Core.Commands;
using Improbable.Gdk.GameObjectRepresentation;
using Improbable.Worker;
using Improbable.Worker.Core;
using UnityEngine;

using Random = UnityEngine.Random;

namespace Concrete
{
    public class PlayerCreator : MonoBehaviour
    {
        [Require] 
        PlayerCreation.Requirable.Writer playerCreationWriter;
        [Require] 
        PlayerCreation.Requirable.CommandRequestHandler playerCreationCommandHandler;
        [Require] 
        WorldCommands.Requirable.WorldCommandRequestSender worldCommandRequestSender;
        [Require] 
        WorldCommands.Requirable.WorldCommandResponseHandler worldCommandResponseHandler;

        PlayerCreation.CreatePlayer.RequestResponder? pendingResponse;

        private void OnEnable()
        {
            playerCreationCommandHandler.OnCreatePlayerRequest += OnCreatePlayerRequest;
            worldCommandResponseHandler.OnCreateEntityResponse += OnCreateEntityResponse;
        }

 

        private void OnCreatePlayerRequest(PlayerCreation.CreatePlayer.RequestResponder responder)
        {
            
            var clientWorkerId = responder.Request.CallerWorkerId;
            var posFlat = Random.insideUnitCircle * 0.25f * Config.TerrainWidth;
            Vector3 pos = new Vector3(posFlat.x, 5 + Config.TerrainHeight / 2, posFlat.y);
            Debug.LogFormat("Creating player at {0}", pos);
            RaycastHit hit;
            if ( Physics.SphereCast(pos, Config.PlayerColliderRadius, 
            	-Vector3.up, out hit, Config.TerrainHeight * 2, Layers.WalkableSurface) )
            {
                pos = hit.point;
                pos.y += 1.5f;
            }
            var playerEntityTemplate = EntityTemplateFactory.
            	CreatePlayerTemplate(clientWorkerId, pos);

            pendingResponse = responder;
            worldCommandRequestSender.CreateEntity(playerEntityTemplate);
            
        }

        void OnCreateEntityResponse(WorldCommands.CreateEntity.ReceivedResponse response)
        {
            if (pendingResponse != null)
            {
                pendingResponse.Value.SendResponse(
                new CreatePlayerResponse((int) response.Op.StatusCode));
            }
        }
    }
}


SDK API that I can’t find anymore:

  • The [WorkerType(WorkerPlatform.UnityWorker)] attribute doesn’t exist anymore I guess. Must be a different way to enable components on certain worker types? In this case it’s actually not so bad because the MonoBehaviour has a Writer injected, so it’ll be disabled on Client workers anyway.

  • The LocalEntities API? Should I somehow lookup my entities up from the ECS?


#3

Starting on migrating a second class…


ToUnityVector convenience no longer exists. Had to create it.


GameObject.EntityId() no longer exists. Replaced it by:

var entityId = GetComponent<SpatialOSComponent>().SpatialEntityId;

#4

Hey @noio, :slight_smile:
Wow. This is amazing and seriously valuable feedback. Thanks for giving the GDK a try and sorry for the pitfalls so far. We are eagerly taking our notes of the problems and open questions here.

To answer a couple of your questions so far:

  • Yes, the [WorkerType(WorkerPlatform.UnityWorker)] attribute isn’t in the GDK at the moment. We are evaluating an internal prototype right now so it might be added in soon.
  • Yes, the LocalEntities API isn’t in the GDK at the moment. There is almost certainly going to be an API for that in the future.
  • The ToUnityVector() conversion method is provided as part of the Improbable.Gdk.TransformSynchronization package/feature module.
  • Yes, the current approach for getting a GameObjects’s EntityId is indeed to get it from the GetComponent<SpatialOSComponent>(). Note: There is a high chance that this API will change. Just as a heads-up.

#5

Re: the lack of OnSuccess and OnFailure callbacks on a command:

I created a CommandCallbackContext object that I pass into a command, which contains a single callback:

I ensure that the callback only gets called once (wasUsed) because the OnCommandResponse event can be delegated to multiple listeners.


public class CommandCallbackContext<T>
{
    public System.Action<T> OnResponse;
    bool wasUsed = false;
    
    public CommandCallbackContext(System.Action<T> onResponse)
    {
        OnResponse = onResponse;
    }

    public void DoResponse(T responseObject)
    {
        if (wasUsed == false)
        {
            OnResponse(responseObject);
            wasUsed = true;
        }
    }
}
private void OnCreatePlayerRequest(PlayerCreation.CreatePlayer.RequestResponder responder)
{
    //....
    var playerEntityTemplate = EntityTemplateFactory.CreatePlayerTemplate(clientWorkerId, pos);

    var context = new CommandCallbackContext<WorldCommands.CreateEntity.ReceivedResponse>(
        response =>
        {
            responder.SendResponse(new CreatePlayerResponse((int)response.Op.StatusCode));
        }
    );

    worldCommandRequestSender.CreateEntity(playerEntityTemplate, context: context);
    
}

void OnCreateEntityResponse(WorldCommands.CreateEntity.ReceivedResponse response)
{
    var callbackContext = (CommandCallbackContext<WorldCommands.CreateEntity.ReceivedResponse>) response.Context;

    callbackContext.DoResponse(response);
}

#6

Not having to send updates manually (writer.Send(new Update()) with the MonoBehaviour workflow is pretty awesome! :sunglasses:

Combine the fact that component fields set — through a writer — immediately return the new value is really awesome.
:sunglasses: :sunglasses: :sunglasses:

i.e.:

myWriter.Data.MyField = 3;
myWriter.Data.MyField == 3; // true

This is going to make parts of my code a lot simpler.

Edit: ehhh, nope. I totally assumed wrong :point_down:


#7

Hey @Noio,
As a word of warning, currently it is not possible to change component values using the approach you described above in the MonoBehaviour API. myWriter.Data currently returns a copy of the internally stored component values of an entity. By modifying the struct that myWriter.Data returns, you are just modifying a copy without truly changing the ground-truth (unless you are modifying a reference type like a list or map, we don’t recommend users taking advantage of this “hack” :wink: ). As such, myWriter.Data.MyField = 3 does not actually result in a component update. This is different from the ECS API of the Unity GDK where we indeed automatically send SpatialOS component updates when you change the values of an entity component locally. For MonoBehaviours, this feature currently doesn’t exist yet although we might introduce it in the future. Please keep using writer.Send() for now. :slight_smile:


#8

Hi Jonas, Thanks for the heads-up!

That’s a bummer :wink: . Total misunderstanding on my side though. I was misled by the code. Seeing all the Dirty flags on the generated IComponentData, I assumed they would be used even in the MonoBehaviour workflow.

public global::Generated.Improbable.Coordinates Coords
{
  get => coords; 
  set
  {
    DirtyBit = true;
    coords = value;
  } 
}

I can’t actually find where the Reader.Data field is implemented :joy: (I find only the interface). However, the fact that it returns IComponentData — which is a struct — means of course it is returned by value. So writes are not propagated to the “original copy”. I could have known this (and would have found out the hard way otherwise :wink: )


Not sure if this is possible :thinking: but I think one could definitely make the case for doing a ref return of the IComponentData struct so it would be directly modifiable. A lot of the boilerplate/repeated code in my project results from having to
a) keep Dirty flags to avoid sending Updates multiple times per frame and
b) making sure I keep my own copy of all component fields to ensure I have the latest value while an Update is processed (this issue)

Then again, I suppose switching to the ECS workflow would fix all of this as well :slight_smile:


#9

Continuing to update my project to use the GDK:


AddAndInvoke is no longer available. Component update events are now real events (using +=), but AddAndInvoke is easy enough to replace with a subscription + a manual call to the event handler:

block.SizeUpdated += OnSizeUpdated;
OnSizeUpdated(block.Data.Size);

548 :x:

The new EntityBuilder

  • Had to import the new classes. Improbable.Worker.Core; Improbable.Gdk.Core;

  • AddPositionComponent is now called AddPosition

  • AddMetadataComponent is now AddMetadata

  • CommonRequirementsSets no longer exists. Part of the effort to get rid of the two fixed worker types UnityWorker / UnityClient I guess? The EntityBuilder now takes a simple string for the worker type. Good stuff. I decided to stick with “UnityWorker” and “UnityClient” for the worker names though.

  • Had to use MyComponent.Component.CreateSchemaComponentData in AddComponent which is a little more verbose but no problems there.


Having a bit of trouble figuring out how to replace CommonRequirementSets.SpecificClientOnly(clientId).
The GDK Example passes the attribute down a long line of method calls ending in clientAttributeSet.First(attribute => attribute != WorkerUtils.UnityClient); which kind of obfuscates where the clientId really comes from

I guess I can still pass in the clientId that I was using previously? It’s a string.

Good stuff :star:️:

  • No longer have to bother with Improbable.Collection.Map and List, can just pass in Dictionary.
  • Similarly Improbable.Collections.Option<t> can now just be nullable<T> / T?
  • EntityBuilder is always an EntityBuilder, (not an IComponentAdder at some point :slight_smile: )

And that concludes the migration of EntityTemplateFactory.cs !

499 :x:


#10

Hey,
Yeah, sorry for the confusion. We are considering to make component updates seamless in the MonoBehaviour API too.

  • Reader.Data is implemented in Improbable.Gdk.GameObjectRepresentation.ReaderWriterBase which is a common parent class for all concrete Readers and Writers.
  • Yes, clientAttributeSet.First(attribute => attribute != WorkerUtils.UnityClient); is a complicated way to pass in the worker id string of your client.

#11

Converting more Component MonoBehaviours to the GDK.

Came across a method that queries for authority before doing some stuff:

if (block.Authority == Authority.Authoritative) {...}

This works as before, just had to import Improbable.Worker.Core.


Creating updates is slightly different. Before:
block.Send(new Block.Update().SetSize(size));
After:
block.Send(new Block.Update(){Size = size});

475 :x:


Sending a command to a different entity:

  1. Is it correct to Inject a CommandSender of a component type that the current entity does not have?
class MyFooBehaviour : MonoBehaviour
{
  [Require] Foo.Requirable.Writer fooWriter;

  // The entity that this MonoBehaviour is added to
  // (with a 'Foo' component) does NOT have a 'Bar' component
  [Require] Bar.Requirable.CommandRequestSender barSender;

  void DoStuff()
  {
    var anotherEntityId = GetSomeEntityWithABarComponent();
    barSender.SendMyCommandRequest( anotherEntityId, new MyCommandRequest(...) )
  }
}
  1. Additionally, you used to have to pass a writer to the SpatialOS.Commands.SendCommand method to ensure the command gets sent from an entity with authority. But I assume that is handled automatically now?
  2. Ah, The Component Command Senders do not supply a Context yet. Makes Callbacks difficult. Then again, I was only using the Callbacks to log the response status, so I’ll just “fire and forget” from now.

A Writer is not a Reader?

I could no longer listen to field update events on writers, so I had to add readers and writers everywhere I was only using a writer before.

[Require] Foo.Requirable.Writer fooWriter;

fooWriter.FieldUpdated += OnFieldUpdated; // NOPE

346 :x:


#12

Re: Sending a command to a different entity

  1. Yes, you can require a CommandRequestSender for any command. Only the receiving entity needs to make sure that it has the component and write authority.
  2. Unfortunately not. Passing a writer when sending a command was a “hack” for essentially getting a mutex. This ensured that you didn’t accidentally send a command multiple times in situations where you intended to only send a single command. (e.g. when creating entities). This was a safeguard of sorts that has been completely removed. A workaround for now would be to manually check for authority in your own code.
  3. Yes, this feature is missing at the moment.

Re: A Writer is not a Reader?
This was not intended haha. We’ll fix this.


#13

Wait a second. Which Component exactly does a worker need Authority over in order to Send a command? I thought Any, which is why you used to pass “Any Writer” to SendCommand?
The Receiver of the command needs Authority on the component that defines the command, yes?

(Actually, let me rephrase that last sentence: The command is automatically sent to the worker that is authoritative over the Component)


#14

Hey, sorry about the confusion.
Here is how commands worked in the UnitySDK:

  • You can send a command from anywhere. Conceptually, commands were not sent by an entity, they were sent by a worker. (SpatialOS.Commands was a static class accessible from anywhere). You also technically don’t need authority over anything to send a command as far as the SpatialOS runtime is concerned.
  • Authority however is required for receiving command requests. When you send out a command, the worker that is authoritative over the component of the target entity would get the request.
  • When sending a command you can optionally pass in a writer. This was not necessary and every command call had a counterpart that didn’t take a writer. E.g. There was a SpatialOS.Commands.CreateEntity(Writer writer, Entity entity) method and a SpatialOS.WorkerCommands.CreateEntity(Entity entity) method (actual names might differ, can’t remember the exact syntax anymore). Both methods would do the same thing.
  • You were able to submit a writer for any component when using the methods that take a writer. The writer could have been completely unrelated to the command you are trying to send. The writer was also technically not necessary in any way for sending the command (hence the variant that doesn’t take a writer at all).
  • The reason we allowed you to submit a writer was to help you avoid writing bugs where you accidentally send a command on multiple workers. Imagine the following situation:
  • You have a MonoBehaviour that creates a player entity in response to something happening in the world (an event on another entity “E”). This monobehaviour is run on all workers.
  • If E is now observed (checked-out) by multiple workers, e.g. worker “A” and “B”, you could run the risk of both A and B simultaneously attempting to create a player entity when they independently observe the event. You would end up with two player entities in that case in a situation where you only wanted one.
  • To mitigate this and to save you the work of rewriting your code to take this caveat into account, we provided the command sending API where you could optionally submit a writer. Any writer is fine here.
  • One characteristic about writers is that only one worker can have one at the same time since any component can only be written to by one worker always. Submitting a writer when sending a command is like getting a lock. You ensure that only one worker can send a command even if the code is running on multiple workers. In case multiple workers attempt to send the same command, only the worker that had authority over some component ended up sending the command.

Does that make sense?


#15

Fixing in this PR: https://github.com/spatialos/UnityGDK/pull/373


#16

Ah, thank you Jonas! That clears things up. Also, interesting point about the ‘lock’, that is indeed something to pay attention to.

And I must say I like the GDK way of calling commands. The call itself is much easier to read. (Compared to all the Commands.Descriptor stuff)


#17

So, I finished all the ‘syntax’ stuff that I mentioned in earlier posts. Most of it was straight forward Find & Replace.

Left with 44 :x: compile errors mostly in the following categories:

LocalEntities

As mentioned before, I don’t think there’s a drop-in replacement for LocalEntities yet, but I use it here and there to look up another entity on the same worker. For example when the player grabs an item from the ground, I need to look up the item by EntityId on the same worker, to validate whether the player is allowed to pick it up.

:white_check_mark: Wrote an ugly hack using FindObjectsOfType, will replace with proper API later

Running behaviours only on [WorkerType(UnityWorker)]

I previously used this attribute to strip MonoBehaviours from Prefabs.

  • Sometimes I could just get rid of it, since the MonoBehaviour [Required] a Writer anyway, so the MB would be disabled on the Client. (Disabled, not stripped, but that’s just a performance thing)

  • The only essential usage was on MonoBehaviours that deal purely with Rendering. They don’t inject any Writer, so they would be enabled on all worker types. Actually, skipping rendering-related tasks on a server is also just a performance thing. So I could just get rid of the attribute and see what happens.

However: I think that it would be better to refactor to an ECS based approach where I run certain Systems only on a certain Worker type. Getting rid of attached MonoBehaviours is a bonus in that case. Conclusion: no more WorkerType() is not a big deal.

:white_check_mark: Checked out the pull request for this feature on GitHub

:interrobang:️ Bootstrap.cs :interrobang:

I still have the old Bootstrap.cs from the SDK in there. The big questionmarks for me are around the new connection workflow and setting up WorkerSystem correctly.

:white_check_mark: Copied over the WorkerConnector classes from the Playground.

PlayerBuildProcess

I was using the new minimal build pipeline, and I have a PlayerBuildProcess.cs that I’m not sure what to do with.

  • If I handle Prefab instantiation myself (With the “Game Object Creation” feature module set to manual creation, do I need to intervene in any way in the build process?

:white_check_mark: just deleted it.

Minor:

  • I have a if (SpatialOS.IsConnected) somewhere. I guess I should just copy the new Heartbeat / ClientConnection workflow.

#18

Monday

WorkerPlatform naming

I renamed my Worker Config to “UnityServer” from “UnityGameLogic”,

06

but I don’t think the SpatialOS menu responds to that…
16

I guess that menu is hard coded, as are the WorkerPlatform.UnityClient and WorkerPlatform.UnityGameLogic strings?

I just reverted to “UnityGameLogic”, but I was hoping to straighten things out because I now have:

  • The worker attribute still named “UnityWorker” (SDK legacy)
  • The scene is named "UnityServer"
  • The WorkerPlatform is named "UnityGameLogic"
  • And then there is : spatialos.unity.worker.build.json & spatialos.UnityWorker.worker.json

Edit: :white_check_mark: I had to go back and rename everything to UnityGameClient

Setting up the Game Object Creation feature module

I can run a server and it connects successfully to the runtime. But no entities are created. I for sure have my Prefabs in the wrong folder, but maybe the Game Object Creation module should complain if it can’t find a prefab? I did enable it:

GameObjectCreationSystemHelper.EnableStandardGameObjectCreation(world);

It is not clear to me how I’m supposed to tell the Game Object Creation module where my prefabs are…

Edit: Okay, I see. It just has to be in any “Resources” folder under the right path (Prefabs/Common)

No way! The Server worker just runs without runtime errors :scream: I think a bunch of stuff is missing but it’s a good start.

SnapshotStream

Okay, PHEW, got some errors at least on the Client. :wink: First I got:

Create player request failed: Component 13000 not found

So I realised I would probably have to regenerate a snapshot because my existing default snapshot was using the old SDK PlayerCreation component (different ID).

But, when I hit Generate Snapshot, I get:

[Exception] ArgumentNullException: The default component Vtable is not set. Set it to PassthroughComponentVtable to treat components without Vtable override as raw schema data.
Parameter name: defaultComponentVtable

Probably related to:

 snapshotStream = new SnapshotOutputStream(Config.DefaultSnapshotPath, 
    new SnapshotParameters());

The only thing I did here when migrating to the GDK was add a new SnapshotParameters() as an argument, no idea what I’m actually supposed to pass in?


Okay, gotcha (looked at the Playground implementation). I guess we’re not supposed to use SnapshotOutputStream at all anymore? It’s now just:

var snapshot = new Snapshot();
snapshot.AddEntity(...)

:question:

Worker Attributes vs Worker Types

I also changed the attribute string values in the progress of migrating to the GDK. I didn’t realise that the actual strings were "physics" and "visual" before. So I had to make sure to update spatialos.*.worker.json with the new attributes that I chose.

Another gotcha: I never really got that there was a difference between Worker Attribute and the Worker Type that WorkerConnectorBase.Connect() expects. So I tried to pass the attribute string into Connect() and got an error.

Connect() wants the actual WorkerType, which I guess should correspond to the spatialos.{WorkerType}.worker.json files

The runtime is pretty helpful there though:

[improbable.bridge.v2.BridgeFactoryImpl] Failed to initiate worker connection because ‘client’ is not a known worker type, available worker types are: [UnityClient, UnityWorker]

Create player at arbitrary position

I kept getting

[Error] Create player request failed: Component 13000 not found

Then I discovered that I was adding the PlayerCreation component from the SDK instead of the PlayerCreator from the Feature Module.

Additionally. The Player Creation module currently automatically sends a PlayerCreate request for creating the player at (0,0,0)

First I thought I would have to disable the SendCreatePlayerRequest system and manually send the request (equivalent to what the SDK did in Bootstrap.cs: )

I actually solved it by putting the positioning logic inside the CreatePlayerEntityTemplate method:

PlayerLifecycleConfig.CreatePlayerEntityTemplate = (List<string> attributeSet, Vector3f dontCareAboutPosition) =>
{
    var clientId = attributeSet.First(attribute => attribute != WorkerAttribute.Client);

    Vector3 pos = DoSomeLogicToGetPosition();

    return EntityTemplateFactory.CreatePlayerTemplate(clientId, pos);
};

(And avoided having to touch the feature module internals)


#19

Tuesday

Removed some of the last SDK left over code (the HandleClientConnection MonoBehaviour that was on the Player)

Now I’m really just getting into good old usage bugs :slight_smile:

Player Lifecycle is broken

For some reason, each time I connect a client, a new player is added, but they aren’t removed when I disconnect. This suggests to me that something about the way I implemented the Player Lifecycle/Heartbeats is broken.

I do not see the PlayerHeartbeat component on the Player entity. Am I supposed to manually add that component in my CreatePlayerEntityTemplate code?

Answer: Yes.

.AddComponent(PlayerHeartbeatClient.Component.CreateSchemaComponentData(), clientId)
.AddComponent(PlayerHeartbeatServer.Component.CreateSchemaComponentData(), WorkerAttribute.Server)

Removing WorkerType introduced some bugs

Before, I was relying on [WorkerType.UnityWorker] to strip certain MonoBehaviours from prefabs. I removed all these attributes, because I figured that MonoBehaviours requiring a Writer would be disabled anyway.

But the behaviour is of course not identical. A stripped MonoBehaviour doesn’t exist, but a disabled MonoBehaviour still runs Awake().

Had to move some procedures to OnEnable() to fix these bugs for now. (And later will rewrite to WorkerType-Only ECS systems)

Logging

Relatively minor changes for logging. Debug.Log isn’t automatically forwarded to the runtime anymore, so have to send manually:

// Debug.LogWarningFormat("Validate failed for [{0}] [{1}] [{2}]", gameObject, itemEntityId, item);
GetComponent<SpatialOSComponent>().Worker.LogDispatcher.HandleLog(LogType.Warning, 
    new LogEvent(string.Format("Validate failed for [{0}] [{1}] [{2}]", gameObject, itemEntityId, item)));

Weird Update Send bug

I had a weird bug in my (user) code. For some reason a component was not getting updated.

I think it might be due to having both a Reader and a Writer for the same component (maybe injection messed up?) I had both because previously the Writer didn’t have any event handlers on it.

Now I only have a Writer, and things are working.

Having No Command Response Callbacks is seriously tedious

First off you have to inject response handlers manually (pretty verbosely):

[Require] WorldCommands.Requirable.WorldCommandRequestSender worldCommandRequestSender;
[Require] WorldCommands.Requirable.WorldCommandResponseHandler worldCommandResponseHandler;

Then you write this fake callback so you can have a closure that includes the Responder:

void HandleDropItem(PlayerStatus.DropItem.RequestResponder responder)
{
    var entityTemplate = CreateTemplateForDroppedItem();

    // This looks like a callback but not really!
    worldCommandRequestSender.CreateEntity(entityTemplate, 
        context: new CommandCallbackContext<WorldCommands.CreateEntity.ReceivedResponse>(
            response =>
            {
                if (response.Op.StatusCode == StatusCode.Success)
                {
                    InventoryRemove(inventory.Count - 1);
                    responder.SendResponse(new StatusResponse(Config.ResponseStatusSuccess));
                }
                else
                {
                    responder.SendResponseFailure("Could not create entity");
                    throw new Exception("Could not create entity for dropped item");
                }
            }
        ));
    }
    else
    {
        responder.SendResponseFailure("No item to drop");
    }
}

And finally you have to not forget to actually CALL the “fake callback” or you’ll be puzzled why your inventory doesn’t change:
🤦

void HandleCreateEntityResponse(WorldCommands.CreateEntity.ReceivedResponse response)
{
    (response.Context as CommandCallbackContext<WorldCommands.CreateEntity.ReceivedResponse>).DoResponse(response);
}

#20

Wednesday

Came in this morning and found out that most behaviour is actually working in the GDK right now! All in all, migration was relatively painless.

:tada::tada:

I would say the biggest shortcomings are currently:

  • Lack of Command Response callbacks in general. And lack of even a Context for Component Commands.
  • Setup that’s magical but not quite magical.
    • Player creation involves setting CreatePlayerEntityTemplate and adding components to player manually. Would be nice if Feature Module setup involved only a single call that makes sure everything is passed in correctly (either through validation or required arguments or something). Would be even nicer if the name/location of this “one call setup” was somewhat standardised between feature modules.
    • Inheriting from WorkerConnectorBase is not that bad actually :slight_smile:
  • Defining EntityTemplates in a clean & extensible way. But this is really hard even just in non-generic user code.
  • Injection can get a little verbose if one behaviour sends many different commands and handles responses.

All in all, however, great work! The GDK so far is a sensible piece of code, and it’s relatively smooth to navigate and learn about independently.


#21

Thursday

Since I migrated from an actual SDK project, I still had the old build commands in there spatialos.unity.worker.build.json & spatialos.unity.client.build.json.

Running spatial worker build --target=deployment caused those to run again, and with it, the SDK code generation, which messed up my project. I had to delete those two files and put a no-op in the worker configs (like the GDK example project)