Running a server-side NPC

Topics covered

Simulators | Flexible authority

Even when creating a game that is mainly client-driven, we can still run some of the code on a Simulator. This is very useful to create, for instance, an NPC that operates even when all Clients (players) are disconnected, to give a semblance of a living world.

Our use case

In this project we used this pattern for the little yellow robot that sits beside the camp. If players move one of the camp's key objects out of place, the robot will tidy up after them. It can even recreate burned objects out of thin air!

Because this behavior is run by a Simulator, even if no-one is connected, given enough time all objects will be back in their place.

Prefab setup

To setup the robot Prefab to be run by a Simulator couldn't be simpler. The only thing we need to do is to set the Simulate In property of the CoherenceSync to Server Side.

By just doing so, when you start the game as a Client, the robot GameObject will be deactivated. But if starting as a Simulator (instructions are here), it will run.

We also set both the KeeperRobot script and the NavMeshAgent to disable on remote instances from the coherence Configuration panel, so they automatically turns themselves off on Client machines.

Note that the GameObject named "Simulator" is disabled by default in the demo scene. When creating a Simulator build, you need to enable it before building, or the robot won't appear in the Simulator (and hence, on Clients).

The code

The code for the robot is all contained in the KeeperRobot.cs class, in 📁Scripts/Robot.

Besides the simple state machine code that runs it, only one thing is worth noting here.

The exact moment when the robot starts acting is not in Start like usual. We imagined this behavior for an always-on world, so that it could start acting even long after other Clients disconnected. To ensure this, we hook into the onLiveQuerySynced event of the CoherenceBridge:

private void Awake()
{
    _sync = GetComponent<CoherenceSync>();
    _sync.CoherenceBridge.onLiveQuerySynced.AddListener(OnLiveQuerySynced);
}

This way, the Simulator has the time to sync up with whatever happened to the campfire objects on the Replication Server, before even beginning to act.

This means that while gameplay can benefit from the presence of this NPC, it's not dependent on it. The Simulator can be always online, or connect and disconnect at times, or to be online only at certain times of the day, and so on.

Coding behaviors like this can open up many creative possibilities in the game's design.

Hiding code from the Clients

One typical pattern here is to wrap any server-side logic in the conditional compilation directive #if COHERENCE_SIMULATOR. This is a great idea especially if the code needs to be obfuscated to normal Client builds, because by doing so, it won't be compiled in the Client at all.

We did it, but we were careful to leave some things out:

public class KeeperRobot : MonoBehaviour
{
    // Properties
    // ...

#if COHERENCE_SIMULATOR || UNITY_EDITOR

    // Server-side behaviour
    // Awake, Start, state machine...
    // ...

#endif

    // Network commands to play on non-authoritative instances:
    [Command] public void PlayHumSound() => soundHandler.Play(humLoop);
    [Command] public void PlayVoiceSound() => soundHandler.Play(voices);
    [Command] public void PlayConjure() => soundHandler.Play(objectConjure);
    [Command] public void PlayAppear() => soundHandler.Play(objectAppear);
}

As you can see, we left out the 4 Network Commands used to play sounds, and the properties they need to do it. The idea here is that the authoritative instance of the robot, which is running the logic on the Simulator, instructs the non-authoritative instances to play sounds when needed.

Remember that disabling a script only prevents Unity functions to be called (Awake, Start, Update...), but it doesn't prevent invoking its methods.

Besides the above, wrapping synced variables or Network Commands inside a pre-compiler directive would hide them from coherence schema baking, effectively creating a different schema for the Simulator, which would then not be able to connect to the RS.

Make sure you keep all data of this type out of the #if, so that both Client and Simulator bake the same schema.

Testing server behavior on a Client in the Editor

Finally, you might have noticed how we not only compile this code for Simulator builds, but also when in the Unity editor:

#if COHERENCE_SIMULATOR || UNITY_EDITOR

This allows us to quickly test the behavior of this robot without adding and removing compilation directives. By simply changing the Simulate In property of the CoherenceSync to Client Side, we can hit the Play button and see the robot move, as if a Simulator was connected.

This is a great way to speed up development and one of the advantages of coherence's flexible authority model: you don't need to code a behavior in a special way to change it from Client to Server side and vice versa.

It is good practice though to switch the robot to Server Side again at regular intervals, and test the game by making an actual Simulator build, in order to create the whole network scenario with all its actors.

This will help locate bugs that have to do with timing, connection speed, authority transfers, etc.

Last updated