Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Erik Svedäng, the winner of IGF 2009, explains the high-level concepts behind networking games.
This article will try to explain a handful of fundamental concepts that all are central to how networked games work. It does not contain any code examples and tries to not delve into minor details. Instead, its goal is to prepare someone new to the field for thinking about networking from a high-level perspective; what problems can arise and how they are commonly solved. The information in here is very useful for understanding the coherence SDK, but it should also be general enough to be applicable to any other similar networking library.
When a game runs on your local computer, it contains a lot of data which is used to model the game. This includes things like animation state, the position and orientation of various game objects, AI calculations, physical forces, among with any gameplay-specific variables. Colloquially we refer to all of this data as state. Efficiently updating state is a hard problem, even for a game that is only running locally.
To create the illusion that you're playing together in the same game world, a networked multiplayer game has to transmit enough of its state to the other players. Since computer networks have limited bandwidth it is absolutely necessary to restrict the amount of data being sent.
Generally speaking, there are two main ways to synchronize state; we can either send inputs, or the updated data itself. It is also possible to mix these approaches in various ways. We will now discuss each of the options briefly.
It is usually possible to enumerate a number of predefined inputs that the players of the game are allowed to perform (e.g. "jump", "run", "activate"). When an input is applied to the local game state, we can also make sure it is simultaneously sent to every other player in the session. If we make sure that each player starts the game in exactly the same state, and make sure that everyone applies exactly the same inputs as everyone else, the game state will appear in sync for each player. For certain types of games, this can save a lot of data from having to be transferred.
A good example might be an RTS game with hundreds of units, where it might be enough to send the coordinates of mouse clicks instead of the location of each unit. This of course requires completely deterministic game logic, which is a challenge in itself.
Another problem is that if there's even the slightest mismatch in inputs, the local game states of the players will begin to diverge. To learn more about this approach (and how to work around some of the problems) see our documentation on GGPO.
It is noteworthy that sending inputs doesn't necessarily require a server; thus it is a great model to be used in a peer-to-peer setting.
A second approach is to send the updated data itself. This can often be more costly in terms if data transfer (a single player action can change a lot of local data, which in turn has to be transmitted to the other players). It leads to some nice benefits though; most importantly that game states are allowed to diverge slightly, as long as they have a chance to catch up.
This concept is usually referred to as eventual consistency. Not having a single "initial state" also makes it easier to support features like letting players join late, or backing up the state of the game world.
Since it's the clients that run the simulation locally and then send the updated game state to the server, this setup can be referred to as client-authoritative.
A third option is a combination of the two solutions above, where clients send inputs but receive updated world data. This requires a central Simulator that is be able to run the game logic. The Simulator is a program trusted by the game developer and it knows how the inputs sent by the players are supposed to affect the game state.
This is a server-authoritative setup; players won't be in charge of the simulation and can't affect the game state directly. This has multiple implications, for example it shifts some of the burden of computation from user devices onto the server. To read more about this approach, see Server-authoritative setup.
It is also worth noting that you can combine client-authoritative simulation with inputs in interesting and useful ways. For example, it is possible to let players simulate some less-critical parts of the game state locally, while still sending inputs for their characters to a central server to be processed.
As stated before, a game contains a lot of data and it is not feasible to send all of it over the network in a real-time fashion. While using inputs is often the most lightweight choice in terms of data usage, it is common to have to send updates to the game state - both from the client to the server, and vice versa. In both those cases we have to use some optimizations. Here are the most important ones.
By keeping track of what the other players know about the state of your game, it is often possible to avoid a lot of data transfer. For example, a player might drop some game object on the ground and send the new location of it to each other participant. Unless that object moves, it is unnecessary to keep sending the same position over and over. This simple idea is used pervasively in coherence (and other similar networking solutions) to great effect.
It's important to acknowledge that a game sometimes generates many changes in a short timeframe. In such a situation, it is useful to prioritize changes based on how important they are for the particular game in question, while also factoring in how long it has been on hold. This means that an "old" change that doesn't get sent will build up importance and relative priority compared to other changes, eventually getting sent.
Finally, a major way of limiting data usage is to filter out uninteresting information and only send the most important parts based on the needs of each participant, also known as Area of Interest. Most commonly this takes the form of a position-based query. The query will make sure that a specific player only gets updates from objects in its vicinity. Anything far away will simply be ignored, and no data has to be sent. It is also possible to send some (but less detailed) data depending on distance. To learn more about these techniques, take a look at the coherence documentation for Queries and Level of Detail.
A game can have many users, and to facilitate the optimizations mentioned in the previous section it is necessary to track what each participant knows about the game state (and what they are interested in knowing). Instead of putting this burden on each game client, which entails an additional performance cost and can be hard to coordinate, it is better to make this part of a central server. For coherence, this is named the Replication Server.
In the case of using an input-based setup, there also has to be a central arbiter in charge of handling the received inputs, applying them to the game state, and sending the new game state to each client. In a coherence setup, the simulation of the game (which requires game-specific knowledge) is handled by a Simulator which communicates with the Replication Server.
This modular approach where various tasks are performed by different programs, potentially on different machines or from different physical locations, can help with the scaling of a game if it has many users.
Most people who play computer games versus other people online want it to be fair, with equal conditions for each player.
If your game is client-authoritative, with clients sending updates of the game state to the server, we can't verify the validity of such an update and it becomes a problem. It would be quite feasible for a savvy player to modify their game and remove certain limitations put there by the game developer.
As an example, a game client could send an update that sets the health of each enemy to 0. To prevent such blatant cheating, it is useful to introduce the concept of authority (also often called "ownership"). This means that the Replication Server keeps track of which client has the rights to update each entity in the game. If an unauthorized update is sent to the server, it is rejected and will not get sent to any other participant.
For an input-based game, the cheating problem is slightly different. Since inputs will have to be applied in the right situation to have any effect, it is much harder to simply set the game state to illegal values. The role of authority in this case is to make sure that no player sends inputs for a game object they shouldn't be able to control.
In many cases it is useful to allow for the transfer of authority. For example, there could be a magical potion that you can drink from in the game. If a player has authority over the potion, she can move it around and drink from it, or refill it. If she then gives the potion to another player, they would get authority over it and the original player would no longer be able to update it.
For certain game objects where we don't trust the players with updating them (or don't want potentially expensive logic to run on their devices) it is also possible to have dedicated machines that have authority over those objects and update them (see Simulators).
There are multiple ways of sending data over a network. These are called protocols. When speed is not the single most important factor, TCP is often used. It has mechanisms for checking that the correct information was sent and it will try to resend the information if it was lost along the way to it recipient.
This design does not work well for fast-paced games, since their simulations run at many frames per second. By the time a lost network message has been resent and finally made it to its final address, the information in it will have a high chance of already being outdated.
So instead of TCP, games often use UDP. This protocol is unreliable by design, but coherence adds a reliability layer on top of it. If turns out that an update didn't make it to its recipient, that update will be re-sent, but only after checking if any more recent changes to its data exist. This way, it is more likely that each player gets a consistent and up-to-date view of the shared game state.
Sending data from one computer to another takes time, and there's no way around that. As a programmer of a networked game, it is important to embrace this fact and recognize that it changes how you must think about your game logic. When programming a single-player game (especially if it only runs on a single processor thread) we can assume that any change to the game state is immediate. In a networked game, this is not true.
This means that each player of a networked game is playing in their own "parallel universe", which affect each other at a distance. Updates to data that you don't have authority over will appear in an irregular and unpredictable way. Because of this it is beneficial to use a defensive coding style that tries to correct for out-of-order updates, and other unexpected circumstances.
One example of such a coding technique (which is already built into coherence) is interpolation. It uses a selection of algorithms to predict what a value will be, based on previous values. This "smooths out" the values over time, which often looks better than using the raw versions. The best example of this is probably interpolation of position - if an object is moving in a straight line at a certain speed and then the update with its new position is somehow lost, it is better to assume that the object will keep moving instead of stopping it.
If the concepts in this article were new to you, we hope that you now feel more confident thinking about the challenges of networked game. While networking surely can be tricky at times, it's also immensely cool and fun when it works - hopefully coherence will make you reach that point in no time! Our docs contain lots of information on how to proceed from here. Perhaps you should start by following a tutorial?
Using the same scene as in the previous lesson, we now take a look at another way to make Clients communicate: Network Commands. Network Commands are commonly referred to as "RPCs" (Remote Procedure Calls) in other networking frameworks. You can think of them as sending messages to objects, instead of syncing the value of a variable.
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
Spacebar or Joypad button down: Jump
Q or D-pad up: Wave
Building on top of previous examples, let's now focus on two key player actions. Press Space to jump, or Q to greet other players. For both of these actions to play their animation, we need to send a command over the network to invoke Animator.SetTrigger()
on the other Client.
Like before, select the player Prefab located in the /Prefabs/Characters
folder, and browse its Hierarchy until you find the child GameObject called Workman.
Open the coherence Configure window on the third tab, Methods:
You can see how the method Animator.SetTrigger(string)
has been marked as a Network Command. With this done, it is now possible to invoke it over the network using code.
You can find the code doing so in the Wave
class (located in /Scripts/Player/Wave.cs
):
Analysing this line of code, we can recognize 5 key parts:
First, notice how the command is invoked on a specific CoherenceSync
(that sync
property).
We want to invoke this command on a component that is an Animator
.
We invoke a method called "Animator.SetTrigger".
With MessageTarget.Other
, we are asking to send this message only to network entities other than the one that has the CoherenceSync
we chose to use.
We pass the string "Wave"
as the first parameter of the method to invoke.
Because we don't invoke this on the one with authority, you will notice that just before invoking the Network Command, we also call SetTrigger
locally in the usual way:
An alternative to this would have been to call CoherenceSync.SendCommand()
with MessageTarget.All
.
In this example we used Network Commands to trigger a transition in an animation state machine, but they can be used to call any instantaneous behavior that has to be replicated over the network. As an example of this, it is also used in the Persistence lesson to change a number in a UI element across all Clients.
The basics of coherence
The First Steps project contains a series of small sample scenes, each one demonstrating one or more features of coherence.
If you're a first time user, we suggest to go through the scenes in the established order. They will guide you through some key coherence and networking concepts:
Remember that playing the scenes on your own only shows part of the picture. To fully experience the networked aspects, you have to play in one or more built instances alongside the Unity Editor, and even better - with other people.
The Unity project can be downloaded from its Github repo. The Releases page contains pre-packaged .zip files.
To quickly try a pre-built version of the game, head to this link and either play the WebGL build directly in the browser, or download one of the available desktop versions.
Share the link with friends and colleagues, and have them join you!
Once you open the project in the Unity Editor, you can build scenes via File > Build Settings, as per usual.
If you want to try all the scenes in one go, keep them all in the build and place SceneSelector as the first one in the list.
If you're working on an individual scene instead, bring that one to the top and deselect the others. The build will be faster.
To be able to connect, you need to also run a local Replication Server, that can be started via coherence > Local Replication Server > Run for Worlds.
You can try running multiple Clients rather than just two, and see how replication works for each of them. You can also have one Client just be the Unity Editor. This allows you to inspect GameObjects while the game runs.
Since you might be building frequently, we recommend making native builds (macOS or Windows) as they are created much faster than WebGL.
You can also upload a build to the cloud and share a link with friends. To do that, follow these steps or watch this quick video to learn how to host builds on the coherence Cloud.
In this sample we look at how to network simple physics simulated directly on the Clients, and the implications of this setup.
If we were making a game that relied on precise physics at play between the players (like a sports match, for instance), we would probably go with a setup where the Clients connect to a that runs the physics and prevents cheating.
However, that makes running the game much more expensive for the developer, since a Simulator has to be always on.
Physics | | Uniqueness |
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
Spacebar or Joypad button down: Jump
E or Joypad button left: Pick up / throw objects
This scene features a few crates that the players can pick up and throw around. Who runs the physics simulation here? You could say that everyone runs their part.
Let's take a closer look at the setup.
Select one of the crates in the scene. You can see that they have normal Box Collider
and Rigidbody
components. Up until a player is connected, they are being simulated locally. In fact if you press Play, they will fall down and settle.
The crates also have a CoherenceSync
component. The first player to connect gets authority over them, and begins simulating the physics for them.
That Client now syncs 5 values over the network, including the most important ones that will drive the crate's motion: Transform.position
and Transform.rotation
.
On other Clients however (the ones that connect after the first one) these crates will become "remote". Their Rigidbody will become kinematic, so that now their movement is controlled by the authority (i.e. the first Client).
At this point, the first Client to connect is simulating all the crates. However, if we were to leave things like this, interacting with physical objects that are simulated by another Client would be quite unpleasant due to the lag.
To make it better, other Clients steal authority over crates, whenever they either:
Touch/collide with a crate directly
Pick a crate up
In code, this authority switch is a trivial operation, done in a single line. You can find the code in the Grabbable
class. Essentially, it boils down to this:
As you can see, it's good practice to ask first if the requesting script already has authority over an object, to avoid wasted work.
If the request succeeds, the instance of the crate on the requesting Client becomes authoritative, and the Client starts simulating its physics. On the other Client (the previous owner) the object becomes remote (and its Rigidbody kinematic), and is now just receiving position and rotation over the network.
Careful! Since authority request is a network operation, you can't run follow-up code right away after having requested it. It's good practice to set a listener to the events that are available on the Coherence Sync component, like this:
This way, as soon as the reply comes back, we can perform the rest of the code.
Also note that while it's totally possible to configure an object so that Clients can just steal authority from each other, we configured the crates here to require an authority request.
When they want authority, Clients have to request it and most importantly, wait for an answer.
We implemented this request / answer mechanism to avoid problems of concurrency, where two players are requesting authority on a crate at the same time, and end up with a broken state because the game code assumes that they both got it.
So who is running the physics, after all? We can now say that it's everyone at the same time, as roles change all the time.
As mentioned before, pressing Tab (or clicking the Joystick) switches to an authority view. It's very interesting to see how crates switch sides when a player interacts with them.
For more on authority, take a look inside the Grabbable
class. It has more code regarding authority events, all commented.
There is one important thing to note in this setup. Since the objects are already in the scene at the start, by default every time a Client connects it would try to sync those instances to the network. This is very similar to what we have seen with character instantiation so far: each Clients brings their own copy.
However, in this case this would effectively duplicate the crates, once online. One extra copy for each connected player! We don't want that.
For this reason, the CoherenceSync
is configured so that these crates have No Duplicates. This is generally the correct way of configuring networked Prefab instances that have been manually placed in the scene.
In addition to a unique identifier (the Manual Unique ID), coherence will auto-assign an additional identifier (the Prefab Instance Unique ID) whenever the crate is instantiated in the scene at edit time.
With these parameters in mind, the way the crates behave is as follows:
At the start, none of the entities exist on the Replication Server (yet).
Client A connects. They sync the crates onto the network. Being unique, the Replication Server takes note of their ID.
Client B connects. They try to bring the same crates onto the network, but because it is set to be No Duplicates and coherence finds there is already a network entity with the same ID, it doesn't create a new network entity but recognises that crate as the one on the server, and just makes it non-authoritative for Client B.
If Client A disconnects, the crates are not destroyed because their Lifetime is set to Persistent. They briefly become orphaned (no one has authority on them) but immediately the authority is passed to Client B due to the option Auto-adopt Orphan being on.
If everyone disconnects, the crates remain on the Replication Server as network entities that are orphaned. They keep whatever position/rotation they had, since nobody is simulating them anymore.
At this point, nobody is connected. The Replication Server is not doing any work.
When a new Client reconnects and tries to bring the crates online again, the same thing happens again: the crates in the scene are associated with the orphaned entities and are adopted by the new client, who assumes authority on them.
They will also most probably see the crates snap to the last seen position/translation that was stored on the Replication Server, which is synced just before they assume full control over the crates.
At this point, they start simulating their physics locally, like normal.
Getting updates about every entity in the whole scene is unfeasible for big-world games, like MMOs. For this, coherence has a flexible system for creating areas of interest, and getting updates only about the entities that each Client cares about, using a tool called Live Query.
|
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
Spacebar or Joypad button down: Jump
This scene contains two cubes that represent areas of interest. Every connected Client can only see other players if they are standing inside one of these cubes.
Select one of the two GameObjects named LiveQuery. You will see they have a Coherence Live Query component:
This component defines an area of interest, in this case a 10x10x10 cube (5 is the Radius). This is telling the Replication Server that this Clients is only interested in network entities that are physically present within this volume.
If a Client has to know about the whole world, it's just enough to set the Live Query Radius to 0, to make it capture all updates.
In addition, Live Queries can be moved in space. They can be parented to the camera, to the player, or to other moving elements that denote an area of interest - depending on the type of game.
It is also possible, like in this scene, to have more than one Live Query. They will act as additive, requesting updates from entities that are within at least one of the volumes.
Notice that at least one Live Query is needed: a Client with no Live Query in the scene will receive no updates at all.
If you explored previous scenes you might have noticed that GameObjects with a Live Query component were actually there, but in this scene we gave them a special visual representation, just for demo purposes.
Try moving in and out of volumes. You will notice that network-instantiation takes care of destroying the GameObject representing a remote entity that exits a Live Query, and reinstantiates it when it enters one again.
Also, notice that the player belonging to the local Client doesn't disappear. coherence will stop sending updates about this instance to other Clients, but the instance is not destroyed locally, as long as the Client retains authority on it.
If a GameObject can be in a state that needs to be computed somehow, it might not appear correctly in the instant it gets recreated.
Using the same scene as in the , let's see how to easily sync animation over the network.
| Bindings
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
Spacebar or Joypad button down: Jump
We haven't mentioned it before, but the character Prefab does a lot more than just syncing its position and rotation.
When you move around, you will notice that animation is also replicated across Clients. This is done via synced Animator parameters (and Network Commands, but we cover these in the ).
Very much like in the example about position and rotation, just sending these across the network allows us to keep animation states in sync, making it look like network-instantiated Prefabs on other Clients are performing the same actions.
Open the player Prefab located in the /Prefabs/Characters
folder. Browse its Hierarchy until you find the child GameObject called Workman. You will notice it has an Animator
component.
Select this GameObject and open the Animator window.
As is usually the case, animation is controlled by a few Animator parameters of different types (int, bool, float, etc.).
Make sure to keep the GameObject with the Animator component selected, and open the coherence Configure window:
You will see that a group of animation parameters are being synced. It's that simple: just checking them will start sending the values across, once the game starts, just like other regular public properties.
There is only one piece missing: animation Triggers. We use one to trigger the transition to the Jump state.
As we mentioned in the intro - in a simple game where precise physics are non-crucial this might be enough, and it will definitely keep the costs of running the game down, since no has to run in order to make the game playable.
For more information on persistence, there's about it.
Now it's clear why Transform.position
cannot be excluded from synchronization, as we saw in . coherence needs to know where network entities are in space at all times, to detect if they fall within a Live Query or not.
For instance, an animation state machine might not be in the correct animation state if it had previously reached that state via a trigger parameter. You would have to ensure that the trigger is called again when the instance gets network-instantiated (via a ) or switch your state machine to use other type of animation parameters, which would be automatically synced as soon as the entity gets reinstantiated.
Did you notice that we are able to configure bindings even if this particular GameObject doesn't have a CoherenceSync
component on it? This is done via the one attached to the root of the player Prefab. These parameters on child GameObjects are what we call deep bindings. Learn more in the lesson, or on .
Since Triggers are not a variable holding a value that changes over time, but rather an action that happens instantaneously, we can't just enable in the Config window like with other animator parameters. We will see how to sync them in the , using Network Commands.
Every now and then it makes sense to parent network entities to each other, for instance when creating vehicles or an elevator. In this sample scene we'll see what are the implications of that, and how coherence uses this to optimize network traffic.
Moving platforms | Local positions | Parenting at runtime | Optimization
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
Spacebar or Joypad button down: Jump
This wintery setting contains 2 moving platforms running along splines. Players can jump on them and they will receive the platform's movement and rotation, while still being able to move relative to the platform itself.
One important note: this sample describes parenting at runtime. For more information on edit-time parenting, see the page about Nesting Prefabs at Edit time.
This scene doesn't require anything special in terms of network setup to work.
Direct parenting of network entities in coherence happens exactly like usual, with a simple transform.SetParent()
. The player's Move
script is set to recognize the moving platforms when it lands on them, and it just parents itself to it.
As for the platforms, they are just moving themselves as kinematic rigid bodies, following the path of their spline (see the FloatingPlatform
script). Their position and rotation is synced on the network, and the first Client to connect assumes authority over them.
Once directly parented, coherence automatically switches to sync the child's position and rotation as local, rather than in world space. This means that when child entities don't move within their parent, no data about them is being sent across the network.
Imagine for instance a situation where 3 players are riding one of the platforms and not moving, only the coordinates of the platform are being synced every frame.
You might have noticed we always mentioned "direct" parenting. One limitation of this simple setup is that the parented network entity has to be a first-level child of the parent one. This doesn't exclude that the parent can have other child GameObjects (and other networked entities!), but networked entities have to be a direct child.
A hierarchy could look like this:
Platform
Player
Character graphics
Bones
...
Platform's graphics
...
(In bold is the root of each Prefab, which has a CoherenceSync
component)
You can even parent multiple network entities to each other. For example, a networked character holding a networked crate, riding a networked elevator, on a networked spaceship. In that case:
Spaceship
Elevator1
Elevator graphics
Elevator2
Player
Crate
Character graphics
Elevator graphics
Spaceship graphics
...
For cases like these, coherence takes care of them automatically. More complex hierarchies require a different handling, and we cover them in another lesson.
When parenting entities, it is important that the child's position, rotation, and local scale are replicated so that all Clients see the relative state of the child when connected to a parent. If these properties are not replicated on the child, it is possible that different Clients will see different states of the child relative to the parent.
Before we dive into the networking-specific topics, in this introductory page we'll quickly go over how the whole gameplay is structured and set up. We'll cover it both from a point of view of Prefabs and of code so you know where to look for what.
WASD: Move | Shift: Sprint | Spacebar: Jump | E: Pick up/throw, Chop trees, Sit/stand | C: Random appearance | 1: Wave emote | 2: Dance | 3: Yes emote | 4: No emote | Enter: Show chat/send message | Esc: Cancel chat
Left stick: Move | Left trigger: Sprint | Button south: Jump | Button west: Pick up/throw, Chop trees, Sit/stand | Button east: Random appearance | D-pad up: Wave emote | D-pad down: Dance | D-pad left: Yes emote | D-pad right: No emote | Select button: Show/hide chat | Start button: Send chat
You'll find the Player Prefab in Prefabs/Characters
.
When connecting, an instance of the Player is instantiated in the scene by the PlayerHandler
script, which listens to the corresponding event fired by CoherenceBridge
.
The player character is a Rigidbody-driven kinematic capsule that is hovering above the ground slightly, and detecting the ground via a raycast. Movement values are provided by the Move
script on its root, which is in turn informed by the PlayerInput
component. When instantiated over the network both these components are disabled, and the Rigidbody is set to be kinematic.
Besides movement, other actions are controlled by scripts on three child GameObjects: Interactions, Emotes, and Chat.
When pressing the interaction key, the right action will be carried on by one of the scripts ChopAction
, SitAction
, and GrabAction
, depending on the type of the object highlighted (a ChoppableTree
, a Chair
, or a Grabbable
).
The chat system is described here. Other actions are described below.
The Player Prefab builds on the structure and functionality of the one used in the First Steps tutorial project, adding more actions. If you find it complex to dive into, try exploring that version first.
The trees have an Interactable
script that indicates which mesh gets highlighted.
They have an amount of energy that determines the number of times they need to be chopped to be cut down. When they run out, they transition to a chopped state and spawn a tree log. A coroutine makes them spring out again after a certain amount of time.
Read more about how characters interact with remote trees in this page about dealing with a non-authority object.
The campfire is at the center of this demo. Players can burn anything they can pick up by simply throwing the object into it. The campfire exists only in one instance and is pre-placed in the scene, and marked as unique on the network by setting the Uniqueness property of its CoherenceSync
to No Duplicates.
Most of the logic of the campfire is in the Campfire
component. This handles a lot of the networking flow, and can be run by a Client but, if a Simulator connects, they will take over.
In addition to calculating which fire effect to display, it's also in charge of replicating the sound of burning an object on all Clients (read more about effects here).
Learn more about the campfire's logic on this page.
They are all Prefab Variants of a base Prefab called Base_BurnableObject, which you can inspect to get a sense of the common functionality.
The objects have several scripts: Grabbable
provides the ability for them to be picked up, carried and thrown, while Burnable
grants the ability to be burnt on the campfire.
They have a collider at the root which determines collisions, but a child GameObject named Interaction (and its Interactable
script) has the trigger collider that makes it interactive, and allows to pick the object up. The Interactable
script also holds a reference to the objects to highlight when the player's interaction trigger intersects the object.
The logs that are spawned when chopping down trees are not unique, and they are set to Allow Duplicates. Check this page for more info on the logs and how they are recycled using an object pool.
Instead, the other burnable objects are pre-placed in the scene, and set to be unique (No Duplicates): the banjo, the cooler, the bins, the mushrooms, and more. More details on the lifetime of these pre-placed objects in the section below.
The Keeper Robot is an NPC designed to be run by a Simulator (aka, the "server"), to restore the campsite to its initial state even when no-one is connected.
Its script will cycle through all unique campfire objects every X seconds. If an object has been destroyed, it will recreate it and put it in its place. If it has been moved, it will just chase it down and put it back into its place.
The way the robot knows about destroyed objects is because the objects, when created the first time, spawn an invisible marker (that we call an "object anchor") which the robot can inspect to know which object has disappeared, and where it was originally placed. The page about custom instantiation has more info on these objects and their anchors.
Read more about this server-side NPC works on its dedicated page.
Sitting is one of the three actions that can be performed by interacting with objects. It doesn't have networking effects, so it's not covered in this tutorial pages.
Once you follow the instructions to install the coherence SDK package for Unity, you will be able to explore the package Samples with no additional download. You can either:
Go to: Coherence > Explore Samples
Open Unity's Package Manager (Window > Package Manager) and navigate to the package samples
Note that the Samples are meant to be self-explanatory, so they come with no documentation.
If, once imported the samples, the scenes show up magenta/pink, it's because the samples are made for the built-in pipeline and your project is using either URP or HDRP.
To fix this in URP, go to: Window > Rendering > Render Pipeline Converter
Click on the checkboxes to choose what to convert (Materials is necessary), then click the Initialize and Convert button. After a brief loading, you should see the example scenes displayed correctly.
For more information, refer to Unity's guides for URP or for HDRP.
We have seen a lot of examples where objects belonging to a Client would disappear with them when they disconnect. We call these objects session-based entities.
But coherence also has a built-in system to make objects survive the disconnection of a Client, and be ready to be adopted by another Client or a Simulator. We call these objects persistent. Persistent objects stay on the Replication Server even if no Client is connected, creating the feeling that the game world is alive beyond an individual player session.
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
P or Right shoulder button: Plant a flower (hold to preview placement)
Players can plant flowers in this little valley. Each flower has 3 phases: starts as a bud, blooms into a full flower, and then withers after some time.
Creating a flower generates a new, persistent network entity. Even if the Client disconnects, the flower will persist on the server. When they reconnect, they will see the flower at their correct stage of growth (this is a little trick we explain later).
Planting too many flowers starts erasing older flowers. A button in the UI allows clearing all flowers (belonging to any player) at any time.
When using the plant action, any connected player instantiates a copy of the Flower Prefab (located in the /Prefabs/Nature
folder).
By selecting the Prefab asset, we can see its CoherenceSync
component is set up like this:
In particular, notice how the Lifetime property is set to Persistent. This means that when the Client who plants a flower disconnects, the network entity won't be automatically destroyed. Auto-adopt Orphan set to on makes it so the next player who sees the flower instantly adopts it, and keeps simulating its growth.
Opening coherence's Configuration window, you will see that we sync position, rotation, and a variable called timePlanted
:
When it gets instantiated, the flower writes the current UNIX timestamp into the timePlanted
variable. This variable never changes after this, and is used to reconstruct the phase in which the flower is in (see below). Similarly, as the flower is not moving, position and rotation are only synced at the time of planting.
Once a flower has spawned, all of its logic runs locally (no coherence involved). An internal timer calculates what phase it should be in by looking at the timePlanted
property and doing the math, and playing the appropriate animations and particles as a result.
coherence supports the ability to have an instance of the game active in the cloud, running some logic all the time (we call this a Simulator). However, this might be an expensive setup, and it's good advice to think things through differently to keep the cost of running your game lower.
To achieve this, the flowers of this scene store the Flower.timePlanted
value on the Replication Server. A Replication Server with no connected Clients is dormant, and has a very low cost to run. So when nobody's connected the flowers are not actually simulating, they are just waiting.
When a new Client comes online and this value is synced to them, they immediately fast-forward the phase of the flower to the correct value, and then they start simulating locally as normal.
This gives the players the perception that things are still running even when they are not connected.
This setup is not bulletproof, and could be easily cheated if a player comes online with a modified Client, changing the algorithm calculating the flowers' phase.
But for a game in which this calculation is not critical, especially if it doesn't affect other player's experience of the game, this can be a nice setup to cut some costs.
Every Client can, at any time, remove all flowers from the scene by clicking a button in the UI.
It's important to remember that you shouldn't call Destroy()
on a network entity on which the Client doesn't have authority on. To achieve this, we first request authority on remote flowers and listen for a reply. Once obtained it, we destroy them.
Check the code at the end of the Flower
script:
As we discussed in the Physics lesson, switching authority is a network operation that is asynchronous, so we need to wait for the reply from the player who currently has authority.
This scene demonstrates the simplest networking scenario possible with coherence. Characters sync their position and rotation, which immediately creates a feeling of presence. Someone else is connected!
CoherenceSync | Bindings | Component behaviours | Authority
WASD or Left stick: Move character
Hold Shift or Shoulder button left: Run
Spacebar or Joypad button down: Jump
Upon connecting, a script instantiates a character for you. Now you can move and jump around, and you will see other characters move too.
To be able to connect, you need to also run a local Replication Server, that can be started via coherence > Local Replication Server > Run for Worlds.
coherence takes care of keeping network entities in sync on all Clients. When another Client connects, an instance of your character is instantiated in their scene, and an instance of their character is instantiated into yours. We refer to this as network instantiation.
When you click Connect in the sample UI, the CoherenceBridge
opens a connection. The PlayerHandler GameObject on the root of the hierarchy controls character instantiation by responding to that connection event.
Its PlayerHandler
script implements something like this:
On connection, a character is created. On disconnection, the same script destroys the character's instance. Note how instantiating and removing a network entity is done just with regular Unity Instantiate
and Destroy
.
Now let's take a look at the Prefab that is being instantiated. You can find it in the /Prefabs/Characters
folder.
By opening coherence's Configuration window (either by clicking on the Configure button on the CoherenceSync
component, or by going to coherence > GameObject Setup > Configure), you can see what is synced over the network.
When this window opens on the Variables tab you will notice that, at the very top, Transform.position
and Transform.rotation
are checked:
This is the data being transferred over the network for this object. Each Client sends the position and rotation of the character that they have authority over to every other connected Client, every time there is a change to it that is significant enough. We call these bindings.
Each connected Client receives these values and applies them to the Transform
component of their own instance of the remote player character.
In First Steps, all the variables are set to public by default. The network code that coherence automatically generates can only access public variables and methods, without them being public syncing would not work.
In your own projects, keep it in mind to always set synced variables to public!
To ensure that Clients don't modify the properties of entities they don't have authority over, we need to make sure that they are not running on the character instances that are non-authoritative.
coherence offers a rapid way to make this happen. If you open the Components tab of the Configuration window, you will see that 3 components are configured to do something special:
In particular:
The PlayerInput
and KinematicMove
scripts get disabled.
The Rigidbody
component is made kinematic.
While in Play Mode, try selecting a remote player character. You will notice that some of its script have been disabled by coherence:
You can learn more about Component Actions here.
One important concept to get familiar with is the fact that every networked entity exists as a GameObject on every Client currently connected. However, only one of them has what we call authority over the network entity, and can control its synced variables.
For instance, if we play this scene with two Clients connected, each one will have 2 player instances in their respective worlds:
This is something to keep in mind as you decide which components have to keep running or be disabled on remote instances, in order to not have the same code running unnecessarily on various Clients. This could create a conflict or put the two GameObjects in a very different state, generating unwanted results.
In the Unity Editor, when connected, the name of a GameObject and the icon next to it informs you about its current authority state (see image above).
There are two types of authority in coherence: State and Input. For the sake of simplicity, in this project we often refer just to a generic "authority", and what we mean is State authority. Go here for more info on authority.
If you want to see which entities are currently local and which ones are remote, we included a debug visualisation in the project. Hit the Tab key (or click the Joystick) to switch to a view that shows authority. You can keep playing the game while in this view, and see how things change (try the Physics scene!).
Advanced networking concepts
Once you have learned the basics using the tutorial project, Campfire is the natural follow-up to get acquainted with more advanced and practical topics.
As with First Steps, you can download the whole Campfire Unity project and explore it at your own pace. Instead of being a series of independent scenes, Campfire is one big scene that presents multiple concepts working together at the same time. We recommend using the pages on this section as guidance on the individual topics, starting with getting acquainted with the .
The Unity project can be downloaded from its . The Readme will tell you the minimum Unity version to use.
To quickly try out the game, we shared a WebGL build on the . You can play it directly in the browser, or download one of the available desktop versions. Share the link with friends and colleagues, and try it together!
To play as a regular Client, make sure that the GameObject called Simulator is disabled in the scene Main:
Without it, the game will behave as a pure Client and spawn a player character on connection.
If you want to make a game build, simply having that object off will produce a Client build. You can run many Client builds to experience multiplayer gameplay.
First, enable the Simulator GameObject in the scene.
Now press Play and connect.
The robot will start acting, exactly like it would do if it were running on a Simulator (minus, of course, the network delay). This allows you to see what would be happening on the server, with the full debugging power of the Unity Editor.
You can even use this Editor instance running alongside one or more Client builds.
To create a Simulator build, you have two ways to go about it, as usual:
building a Simulator to launch locally on your machine
building one to upload on the coherence Cloud
In both cases, make sure that the Simulator GameObject is enabled in the scene.
Don't change the Keeper Robot's Simulate In property like described in the previous section, since to run this behavior on the Simulator we want it to stay Server Side.
| |
Network entities need to be created and removed all the time. This can be due to entities getting in and out of a LiveQuery, or simply because gameplay requires so. If that is the case, we can leverage coherence's object pooling system in order to avoid costly calls to Instantiate
and Destroy
, which are famously expensive operations in Unity.
In this project we use pooling for one very clear use case: the tree logs that get spawned when chopping down a tree.
This was a natural choice as players will be chopping trees all the time, but we can also assume that they will burn the logs on the fire almost as often. So by pre-allocating a pool of around 10 logs, we should be covered in most cases.
To set up the log to behave like this, all we did was to set that option on the log's own CoherenceSync
inspector.
A pool configured like this means that coherence will pre-spawn 10 instances of the Prefab at the beginning of the game.
However if we were to need more, we could request more instances and they would be created and added to the pool. The game can even go above 20. If that were to happen, any instance released beyond 20 wouldn't just be returned to the pool, but would be destroyed.
In other words, 10 and 20 represent the lower and upper limit for the amount of memory we are reserving for the logs alone in our game. We are considering anything above 20 as a temporary exception.
When we press Play, coherence instantiates these 10 logs, deactivate them, and put the pool in the DontDestroyOnLoad scene:
Because they are inactive, their CoherenceSync
components are not syncing any value.
To spawn a new log we only need to call one line of code. However, we don't provide a reference to a regular Prefab like we would with Instantiate
. We instead leverage the CoherenceSyncConfig
object that represents the log.
This CoherenceSyncConfig
contains all the info that coherence needs to handle this particular Prefab over the network. If we inspect it, we will notice that it contains in fact how the object is loaded (Load via) and how it's instantiated (Instantiate via).
You can notice how this is the same info we saw while configuring the CoherenceSync
before.
Now that we have a reference to it, we can spawn the log with one line of code. In the ChoppableTree
script, we do something like:
This line looks remarkably similar to Unity's own Instantiate
in its syntax. The difference is that it gives us back a reference to the CoherenceSync
attached to the log instance that will be enabled. From this, we can do all sorts of setup operations by just fetching other components with GetComponent
, to prepare the instance.
When we are done with it (in this case, when it's thrown into the campfire), we can dispose of it:
(this line is in the Burnable.cs
class, inside the GetBurned()
method)
The instance is then automatically returned into the pool, and disabled.
When taking an instance out of the pool or when returning it, coherence doesn't automatically do any particular clean up to its state.
As such, when we reuse a pool instance, it is good practice to think of what values should be reset that might have been messed up by previous usage. We should think about what happens during gameplay, and use OnEnable
/ OnDisable
as needed to ensure that disabled instances are put in a state that makes them ready to be used again.
For this project, since an object can be burned while being carried, we do some cleaning in the OnDisable
of the Grabbable.cs
class to prepare the wood logs for another round, like so:
| |
It's often the case that in addition to objects being fully owned by players, like their characters, there is often the need to have objects that exist only in one copy in the world and that need to store a complex state that needs to be reflected in the same way on each Client. And the state might not be a simple int
or bool
that can be just automatically synced over the network whenever it changes, but something more complex that requires to be elaborated.
This is often the case for more invisible objects like a leaderboard, a spawn point, a score counter or a match timer; but can also be the case for objects that have graphics.
An example of such an object, that also happens to be very central to this demo, is the campfire. As the players pick up objects and throw them on the fire, the campfire needs to perform a calculation based on a timer and the type of the object burned to decide which fire effect to play.
Timing is key here! If two players throw in two objects, one right after the other, they activate a special effect that makes the campfire burn bigger and brighter. But the two objects need to get on the fire within 2.5 seconds from each other (it's the teamEffortLength
variable in the Campfire.cs
script).
Because this calculation depends on the timer value that is managed by the Authority, we can't just independently calculate a result on each Client, as they would almost certainly end up with different results. We need to inform the Authority that the action is taking place, let it figure out the final state, and only then propagate the resulting state and actions to all Clients.
(1) Action happens on a Client -> (2) Authority campfire is notified, processes result -> (3) Authority campfire sends result to all others -> (4) Non-authority campfire objects execute local effects
We do have an extra challenge here though. Ultimately we want the Authority to inform everyone to play specific visual and sound effects depending on the object burned. But we can't send Network Commands with a reference to audio assets or particle systems. So we need to change this information to something we can send, and then on the receiving end, "unpack it" and transform it into the info we actually need (i.e., which sound).
If you look into the Campfire.cs
script, you will find this sequence of actions as exemplified by the flow below:
(1) The player throws an object on the fire. BurnObjectLocal()
is invoked by the Burnable
that collided with the Campfire
. The script checks if Authority is already on this Client:
The method invoked in both cases is BurnObject()
, but it's invoked differently depending on whether it is local (direct invocation) or remote (using SendCommand
via the CoherenceSync
).
We use the ID of the CoherenceSyncConfig
of the object that burned as a parameter. The ID is a string, so it's something we can send over the network.
(2) The logic for which fire effect to play is then calculated in BurnObject()
.
The campfire uses the CoherenceSyncConfig
ID as a key to look into the CoherenceSyncConfigRegistry
, and find the right object archetype to play the right effect.
(3) ChangeFireState()
is invoked locally on the Authority. Here the Authority updates its own property activeFireEffect
which, being a synced property, gets sent to the other Clients.
But updating that int wouldn't be enough to tell which sound to play, so we send a command to invoke the FireStateChanged()
method, passing the CoherenceSyncConfig
ID which the non-authoritative campfire instances can use to trace down the object that burned in the CoherenceSyncConfigRegistry
.
(4) The non-authoritative clients execute FireStateChanged()
, which turn on/off the appropriate fire particles, and play a specific sound.
If the Client (or a Simulator) detaining the authority on the campfire disconnects, we need to make sure that whoever gets assigned authority next can pick up the job exactly where it was left off, and continue simulating the campfire logic without interruption.
That's why in the Campfire.cs
class we make sure to sync three values:
activeFireEffect
is an index (expressed as an integer) of which fire effect should be playing right now.
fireTimer
and bigFireTimer
are two countdowns that indicate how much time the fire will still burn normally or, when in "big fire mode", brighter.
However, there's an opportunity to be smart here. fireTimer
and bigFireTimer
are variables that are updated every Update on the Authority, but they are only useful in case the Authority gets transferred. So what we can do using the Optimization panel is to reduce the frequency they are sent to other Clients to a much more manageable value of once every second.
This might not be very precise and would have been unacceptable in the case of a visible timer, but here it doesn't matter. To the players this is going to be invisible, but we avoid a lot of network traffic.
As mentioned before, this mini state-machine behavior can run perfectly on one of the connected Clients. There is one catch though: this way, if no one is connected, the fire will stop updating because no one is simulating it, and thus it will never burn out.
Try this: connect, throw an object on the fire, disconnect, and reconnect after some time. The value of fireTimer
will still be the same and so the fire will still be burning no matter how much time has passed.
Using an Authority transfer, it is trivial to let this behaviour run on a Simulator if there is one connected. Look into the Campfire
class, within OnLiveQuerySynced
:
With this simple code, whenever a Simulator connects and sees the persistent campfire network entity, it will take Authority over it. If it were ever to go offline and a client is connected, that Client would take back Authority. If the Simulator comes back online, it would steal it again. And so on.
While this is not a cheat-proof solution, it can be useful for various scenarios.
Having a behavior set up this way allows the Prefab and its logic to be used in an offline mode without modification (because the offline player would act as the owner Client). This can be useful to create a free demo version; a tutorial mode; or even to showcase the game in conditions of limited connectivity.
You could launch the game with no Simulators to run a game preview while keeping costs down, like during an Early Access or a Steam festival. Later on when it goes live, the game could be switched to use a Simulator, and no change to the code would be required.
| 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.
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.
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.
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).
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
:
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.
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:
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.
Finally, you might have noticed how we not only compile this code for Simulator builds, but also when in the 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.
Game characters and other networked entities are often made of very deep hierarchies of nested GameObjects, needing to sync specific properties along these chains. In addition, a common use case is to parent a networked object to the tip of a chain of GameObjects.
Let's see how to handle these cases.
|
A/D or Left/right joypad triggers: Rotate crane base
W/S or Left joystick up/down: Raise/lower crane head
Q/E or Left joystick left/right: Move crane head forward/back
P/Space/Enter or Joypad button left: Pick up and release crate
This scene features a robotic arm that can be controlled by one player at a time. In the scene, a small crate can be picked up and released.
The first player to connect takes control of the arm, and other players can request it via a UI button.
To demonstrate complex hierarchies we choose to sync the movement of a robot arm, made of several GameObjects. In addition to syncing several positions and rotations, we also sync animation variables and other script parameters, present on child objects.
To sync the whole arm we use a coherence feature called deep bindings, that is bindings that are located not on the root object, but deeper in the transform hierarchy.
Select the RobotArm Prefab asset located in /Prefabs/Characters
, and open it for editing. You will immediately notice a host of little coherence icons to the right of several GameObjects in the Hierarchy window:
These icons are telling us that these GameObjects have one or more binding currently configured (a variable, a method, or a component action).
Now open the coherence Configuration window, and click through those objects to discover what's being synced:
In addition to position and rotation, we also choose to sync the animation parameter ClawsOpen, and enable Animator.SetTrigger()
as a Network Command. Finally we disable the Robot Arm script when losing authority (to disallow input).
This is the base of the robot arm, for which we only sync rotation:
We don't sync the rotation of every object in the chain, since the arm is equipped with an IK solver, which allows us to just sync the target (Two-Bone IK_target) and work out the rotation of the limb (robotarm_bottomarm and robotarm_toparm) on each Client:
By syncing all of these properties, we can have the robotic arm move in sync on all Clients, simply by translating the tip of the IK, and rotating the base of the crane. All of the bindings in this hierarchy are synced through the Coherence Sync component present on the Prefab's root object RobotArm.
As you can see, using deep bindings doesn't require any special setup: they are enabled in exactly the same way as a binding, a Network Command, or a Component action is enabled on the root GameObject.
The Path property displays the location in the hierarchy where this object will be inserted. It gets automatically updated by coherence every time the object is parented. Each number represents a child in the root object (and it's 0-based).
Once we have this component set up, parenting the object only requires calling Transform.SetParent()
like any usual parenting operation, and setting its Rigidbody
component to be kinematic.
When we do this, coherence takes care of propagating the parenting to other Clients, so that the crate becomes a child GameObject on every connected Client.
This code is in the RobotArmHand
class, a component attached to the tip of our hierarchy chain: GrabPoint. In OnTriggerEnter
we detect when the crate is in range, storing a reference to it in a variable of type Transform
named grabbableObject
.
This reference is set to sync:
When the player presses the key P (or the Left Gamepad face button), the referenced crate is parented to the GrabPoint GameObject.
Note that coherence natively supports syncing references to CoherenceSync
and Transform
components, and to GameObjects.
Even if the Robot Arm Hand script is disabled on non-authoritative Clients, it references the correct grabbed crate in the grabbableObject
variable due to it being synced over the network. So when its authority disconnects, other Clients will already have the correct reference to the crate network entity.
This allows us to gracefully handle a case where, for instance, a Client picks up the crate and disconnects. Because both the crate and the robot arm have Auto-adopt Orphan set to "on", authority is passed onto another Client and they immediately have all the data needed to keep handling the crate.
To move authority between Clients, we can use the UI in the bottom left corner. The button is connected to the Robot Arm Authority script on the ArmAuthoritySwapper GameObject, and it transfers authority on both the robot arm and the crate. This script takes care also of what happens as a result of the transfer, including setting the crate to be kinematic or not.
Is Kinematic is set as follows:
The code is in the RobotArmAuthority
class. To detect whether it's currently being held, it's as simple as checking whether its Transform.parent
is null
:
Remember you can use Tab/click the Gamepad stick to use the authority visualization mode. Try requesting authority from another Client while in this mode.
| |
In a networked game, an object's logic is always run by one node on the network, whether it's a Client or a Server (which we call a in coherence). We say that the node "has authority" on the network entity.
There are cases where it makes sense to transfer authority, like it happens in this project with objects that can be picked up. When the player grabs an object, the Client performing it requests authority over the network entity. Once it gets authority it starts running its scripts and has full control over it. This is a very good way to go when only one player can interact with a certain object at a given time.
For more info, check in the First Steps project.
However, there are cases when we don't want to change who has authority on an entity. In the case of an object that many players can interact with at the same time, it wouldn't make sense to continuously move authority between nodes.
The interaction with such remote entities then needs to happen entirely through .
In this project, it is the case of the chairs placed in the scene. The first Client or Simulator to connect will take authority over them, and it will keep it until they disconnect.
When a player wants to sit down on a chair, they inform the Authority that they are doing so. The client holding authority will then set the chair as busy, which prevents other players from sitting on it next time they try.
However, for the sake of simplicity and to illustrate the point, we intentionally left this interaction a bit flaky. Can you guess why? What could go wrong with this setup?
The action originates in SitAction.cs
:
SitAction
checks if the isBusy
property of the chair is set to true
(by the authority, of course). If so, it means someone else is already sat on the chair. If false
, we can sit. So it invokes Chair.Occupy()
.
And further down, the essence of the interaction:
So both when occupying a chair (Occupy()
) or standing up (Free()
), the player executing the action invokes the ChangeState
method, either directly or as a Network Command - depending if they are the one with authority.
So one way or the other, ChangeState
gets executed on the authority, who sets the isBusy
property to its new value. On the next coherence update, the property will be sent to the other Clients.
The answer: Clients are using the isBusy
property as a check for whether they can sit or not. It is possible that two players will approach a chair at the same time, check if isBusy
is false (and yes, it will be false), at which point they will inform the authority that want to sit down on it.
The authority performs no additional checks, so you will see both players successfully sitting on the chair, overlapping on each other.
Thankfully we also coded the rest of the interaction so that this doesn't break the game. So while this incidence and the consequences for this interaction are low-risk, if you're looking to create a more robust system it could make sense to implement a check on the authority, and have the Client wait for an answer before they sit down.
When approaching an object that can be interacted with, the InteractionInput
script does the work of detecting objects that have an Interactable
script, and highlights them by changing their layer. This makes them render with an additional outline, as per one of the passes in the URP Renderer Renderer_WorldUI, contained in Settings
.
The prefab for the interactive tree is in Prefabs/Interactive
. The log that is spawned by it is in Prefabs/Interactive/Burnables
.
The campfire Prefab is in Prefabs/Interactive
.
All non-static interactive objects are in Prefabs/Interactive/Burnables
.
You'll find the robot Prefab in Prefabs/Characters
.
You will find chairs in Prefabs/Interactive/Chairs
.
In this project, there is an NPC that is supposed to be controlled by the Simulator (the ). Though this is intended to be a server-side behavior, you can actually make it run locally and play as a player at the same time without modifications to the code.
Secondly, open the KeeperRobot prefab contained in Prefabs/Characters
. On the CoherenceSync
component, change its Simulate In property to Client Side.
For more information, refer to the .
Check the Log prefab in Prefabs/Interactive/Burnable/
:
This Sync Config can be found in the coherence/
folder, and is a sub-object of another ScriptableObject: the CoherenceSyncConfigRegistry
.
This is in a way similar to . The event flow is very similar:
Right now, we are looking at things in the context of a setup where Authority on the campfire is on one of the Clients. It is totally possible to give the Authority to a Server (and in fact we do in this project, see of this page), but the actual logical process doesn't change at all.
For more info on CoherenceSyncConfig
check out .
For more info on CoherenceSyncConfigRegistry
check out .
So, as long as a Simulator is connected, the campfire will keep burning
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 ), it will run.
The code for the robot is all contained in the KeeperRobot.cs
class, in Scripts/Robot
.
One important note: this sample describes deep parenting at runtime. For more information on edit-time deep parenting, see the page about .
As mentioned in the lesson about , parenting a network entity to a GameObject that belongs to a chain requires some setup. To be able to pick up the crate with the crane, we equip it with a CoherenceNode
component:
Similarly to the crates in the , we don't just want the crate to automatically become non-kinematic when we have authority on it. We want the crate to stay kinematic when authority changes while it's being held by the arm.
On the authority Client | On non-authoritative Clients |
---|
You will find the code of chairs in Chair.cs
, located in Scripts/Objects
. Looking into it, we find the property used as a gate:
We do this in other parts of the demo, like when chopping a tree or when picking up an object. Check the following to explore this similar but more complex use case.
Is being held | true | true |
Has been released | false | true |
Racing games involve multiple vehicles racing a number of laps. They can be realistic or arcade-y, but the end goal is always the same: crossing the finish line first.
In multiplayer racing games it is vital to have as precise information about the position of other clients as possible. The server and the players both need to know details like if its possible to overtake in a curve, if players bumped into each other, and even more importantly - who crossed the finish line first. We need to have server authority, but each Client also needs reliable predictions about where the other players are.
Quick exploration and recommendations for different game genres
This section introduces you to coherence features and terminology by using well-known genres and game types as examples. Each example will come with a list of considerations and how we propose to use coherence to achieve a similar result. As you well know, game creation is a complex process, so the list is far from exhaustive, but aims to highlight pitfalls, suggest solutions and generally just provide you with a starting point when trying to create a multiplayer game with coherence in the context of a game type you are working on.
This section is a beginner-friendly exploration into familiarizing with coherence's terminology and networking mindset, and by no means is representative of a production-ready architecture proposal.
First-Person Shooters (FPS) are games where multiple players join opposing teams and shoot each other. You will often win by either eliminating the opposite team, exploding a bomb, or running out the timer.
Good communication between players is often essential in winning. Serious players will have voice communication, but its also good to have in-game comms to easily communicate tactics. In coherence there is the concept of Client Connections which you can use to easily send messages between players. It can also be used to communicate game state changes e.g., "The bomb has been planted".
When building your level there may be certain objects that should be duplicated across Clients. You want to have a duplicate of each player on every Client, but for something like doors which can be opened or closed, you only want one "shared" door across all Clients. For this to happen you need to understand Uniqueness and Lifetime. Using those concepts, you can make sure that a given objects is persistent on the scene, and that only one exists.
Similar to doors, the bomb is also unique. The difference is that the bomb is spawned and only 1 bomb can exist in the game at any time. Doors are also unique, but multiple instances of a door asset can exist, just not at the same place. To understand more, read about Setting up a global counter. This is the same principle of having a Prefab that is uniquely identified.
Online competitive multiplayer games are tricky to get exactly right, and there is no "right" solution. It's a constant tradeoff between cheat protection, latency, client prediction, etc. You need to do further research to decide on the solution that best suits you. One thing you definitely need to learn about is client vs server authority.
In most MMOs you control a character, interact with other players and group up with other players to clear dungeons. Here's a few networking considerations for anyone creating something similar.
In a MMO the world needs to be persistent. Any given user can join and leave a world at any given time and we want their changes to be persistent. Which NPCs was killed, which treasures were looted, which items are available on the action house, etc. In order to achieve this, you need to run a World Replication Server in the coherence Cloud which will make sure the world state is saved even if no players are logged in.
Given that there can be a large amount of players distributed over a very large area, we don't really care about the ones in all the different areas. By not sending information about players far from the player itself, we can significantly limit the amount of data sent over the network. When using coherence you can use the LiveQuery Component to set a bounding box in which we replicate data from networked entities - anything outside of it is ignored.
Even within a LiveQuery bounding box there might be further room to optimize. When having a large amount of networked entities, you might want to prioritize those that are closer. Our solution to this is called Level Of Detail (LOD). Using this you are able to control values such compression, value range and sample rate in order to hit the optimization sweet spot.
To make sure that all users experience an identical game world at any given time, we need a Simulator to be responsible for taking decisions for the AI, triggering events, etc. In coherence we support launching your game with any number of Simulators taking responsibility for the various parts of your game.
A common part of all MMO's are instanced areas where a smaller group of players enters together and completes a set of tasks. It does not make much sense to run these instances as a part of the World Replication Server. The idea is that it can be possible to spin up a separate server for each group who enters such an instance. In coherence we offer this through the Room Replication Server. This allows you to have a shared world, as well as any number of instances for a specific area for a subset of players.
Fighting games come in many shapes and forms. Usually they involve 2 or more players fighting each other. The players can kick, punch, block, grab and trigger intricate combos for extra damage. Often this type of game relies on quick reactions to the opponent's movement.
For a fighting game, we need a reliable game state regardless of user ping. It needs to be deterministic. For example, if two players press the kick button a few milliseconds apart, the Simulator needs to be able to figure out which player is the one doing the kicking, and which is the one getting kicked. Our solution is called input queues and the setup is described in great detail here. The key idea is that only the input is being processed by the Client, and the Simulator is responsible for deciding the outcome. The Simulator stores a queue of inputs, which is then used to decide on the correct order of actions.
If you want to synchronize more than just the root of the Game Object, e.g. if you want to have precise replication of ragdoll on all Clients, you need to create bindings to more that just the root transform. We support deep bindings which allow you to select any object in the hierarchy and synchronize whatever is needed.
For turn-based games, the requirements for networking can be quite different from other, more fast-paced games. You are only interested in changes to the game state, and don't really need more granularity than that. Let's take chess as an example.
You don't really need a player character so in order to process input you don't really need a CoherenceSync object. You could use a Client Connection Prefab if you want to have an easy way to implement chat.
You might want to have a Simulator to process everything if you want cheat protection, but for a game such as this it could also be viable to simply opt for client-side simulation and then have one of the Clients have authority over the game controller. That is the default setting when adding a CoherenceSync Component. Each client can then talk to the game controller using Commands.
Networked audio | Networked particles | Animation Events
Usually, visual feedback can be expressed via syncing variables like Animator parameters, positions, and rotations. But sometimes we have the need to play sounds and particles, which are not types that can be automatically set to sync, or that we can send as arguments of Network Commands. So how to do it?
This project has a lot of moments where particles and sounds need to play, and we used different strategies for different cases, depending on how fast, repeated, or slow the action is.
The most straightforward solution to play a sound is to use a Network Command. Using Commands, you can remotely invoke methods on AudioSource
or ParticleSystem
components.
To do that, you could simply open the coherence Configuration panel (from the CoherenceSync
), and check the methods you're interested in.
While this is a perfectly fine way of doing things, it requires you to call multiple Network Commands in case you wanted to play a sound and particles at the same time. This could lead to desynchronisation between sound and visuals.
As such, in this project we preferred compacting these calls into methods on their own that are invoked as one Network Command, often without parameters to minimize the data being sent across.
Connected to the above, let's see how to create our own Network Commands to play sounds (or particles) as a result of an event that happened remotely.
For instance, the Keeper Robot has a series of voices that play whenever it is performing an action. The robots is always controlled by the Simulator, so we need to play sounds on the Clients' devices.
For these sounds, we isolated the sound-playing behavior into Commands of their own. At the end of the KeeperRobot.cs
class, we have:
(soundHandler
is a script attached to the same gameObject)
Each of these methods is invoked as a Network Command, like so:
You can see how we don't play the sound over the network, that would be bandwidth-consuming for no reason, but we just communicate the intention to play it.
Because we only have 4 sounds, we sort of "brute-forced" this, and created an individual Network Command for each sound. This is not a bad idea from the point of view of network traffic: sending a Network Command with no parameter produces less traffic than sending one with.
But it could be unwieldy if we had - say - 100 different sounds to play.
This solution also requires us to bake and produce a new schema if we add or remove one of these Commands. So for a more flexible solution, it could be nice to index the sounds and maybe create a generic Command like:
In this case though, it was ok to go for individual Commands.
There are actions that are really quick or short, and asking to play a sound via a command might result in a mismatch between the visuals (an animation) and the sound, due to network delay.
For instance, it wouldn't make sense to send a Command to inform other Clients to play the sound of a footstep. Chances are, by the time they receive the Command, another two-three footsteps have happened.
So for footsteps, jump, landing, and more; we used a slightly different strategy. Audio and particles are all played locally as part of the animation, using Unity's own Animation Events.
A script called PlayAnimationEvents.cs
(remember to add it to the same object as the Animator
!) listens to these events. An example from it:
This ensures an immediate playback, in sync with the animation. Plus, it produces zero network traffic.
So yes, fun fact: to "network" sounds and particles often you can do without networking anything at all!
One more trick! If you have a state machine blending several clips, you might hear multiple overlapping sounds when a transition happens. One less known trick is to measure the weight of each clip while executing Animation Events, like we do below:
Object lifecycle | Custom instantiators | Runtime Unique IDs
In many cases, creating and destroying GameObjects like usual will be enough. Just call Instantiate()
or Destroy()
, and coherence takes care of instantiating and destroying the appropriate Prefab instance on each connected Client.
However, there are moments when it makes sense to customize how exactly coherence does this. To take full control over the lifetime of the object, or to attach custom behavior to these events.
coherence provides different object instantiators by default (and we use the Pool Instantiator one too), but for ultimate control we also have the ability to create new, completely custom ones.
The campsite in this demo has a few pre-placed unique objects in the scene, that can be picked up, moved, and burned on the campfire.
Until the Keeper Robot comes in and recreates them, they will not be replaced.
When we burn them, we could in theory just destroy the instance. However the burn code is deeply nested in the Burnable.cs
class which is used not only by these unique objects, but also by the pooled and non-unique wood logs.
In this method we do this:
A simple ReleaseInstance()
does the trick for the logs which are non-unique objects. They just go back into the object pool.
However, by default unique network entities also get disabled, not destroyed. This doesn't work for our special objects!
We could potentially add an if
statement in the GetBurned()
above, detect if the object being destroyed is a log or not, and act differently based on that. Or subclass the Burnable
and implement overrides for GetBurned
...
... or we can just create a custom instantiator, and take full control of the object's lifecycle. Let's see the code.
Creating a custom instantiator is trivial. We just need a class to implement the interface INetworkObjectInstantiator
, like so:
The key parts of this script being that on network entity creation a simple Object.Instantiate()
is performed, and on release Object.Destroy()
. The other methods (omitted here) are actually empty.
We also want to prepend the class with the DisplayName
attribute so it shows up in the dropdown when we configure a CoherenceSync
. Now the UniqueBurnableObjects instantiator appears alongside the others in the Instantiate via dropdown:
That's it, the instantiator is ready to use.
When we call ReleaseInstance()
now, it will act differently depending on which instantiator the Prefab is configured to use: the wood logs get disabled, but the unique campfire objects get destroyed.
This was a very simple use case for customization, but it illustrates how easy it can be to get in control of the lifetime of Prefab instances associated to network entities.
API Reference for INetworkObjectInstantiator
can be found here.
The first time these special unique objects come online, they spawn a persistent invisible object we call "object anchor". This object holds the original position and rotation of the object, so that the Keeper Robot can come in at a later time and put the recreated object back into its place. You could think of these objects as placeholders.
One interesting thing we do with anchors is that they are themselves unique objects, but because they are spawned at runtime, they need to get their unique ID dynamically at runtime.
The code is in the PersistentObject
class:
We take the ManualUniqueId
from the object spawning it (i.e., "Boombox"), and we combine with the string "-anchor" to create a new unique ID, "Boombox-anchor". We register this ID to the UniquenessManager
of the CoherenceBridge
to inform it that the next spawned network entity will have that ID. And then we simply call Instantiate()
.
Because they are set to be Persistent, even though a player has burned something and disconnected, the anchors stay on the Replication Server. When a Simulator connects it will find these placeholders and, thanks to the synced properties, will know exactly what to recreate and where to put it.
The check code is in KeeperRobot.cs
, under CheckAnchors()
and ActOnAnchor()
.
First, each anchor's isObjectPresent
property is used for a quick scan. This property is synced.
If the object is still present, the robot needs to get a reference to it. It calls GetLinkedObject()
on the anchor, which does this:
Once again using the UUID of the object this anchor is a placeholder for (holdingForUUID
) as a key, we can now ask the UniquenessManager
to retrieve an object that has that UUID.
With a reference to this, the robot can now put it back into place using the anchor's position and rotation as a reference.
And if the object has been destroyed (isObjectPresent
is false), the robot proceeds to recreate it.
Using the anchor's syncConfigId
as a key, it looks in the CoherenceSyncConfigRegistry
and finds the archetype to recreate. This is similar to how we used the registry as a catalogue when dealing with the campfire object.
After that, like we saw before, the robot registers the newly recreated object with the UniquenessManager
so that it has the same UUID that it had before being burned.
The object is reinstated, and to a new Client connecting, it will look exactly the same as if it never got removed.
Authority | Authority transfer | Network Commands
We saw in the previous section about sitting on chairs how sometimes it makes sense not to move authority around between Clients. At this point, Network Commands are the way to interact with a remote object.
Now let's take a look at another case of remote object, where the interactions with it need to be validated by the one holding authority, to avoid nasty cases of concurrency.
In this project, it is the case of the trees that are placed in the scene. The first Client or Simulator to connect will take authority over them, and it will keep it until they disconnect.
When a player wants to chop a tree, they request the Authority to subtract 1 unit of energy. When the energy runs out, it's the Authority that spawns a new Log instance.
This centralization, as opposed to passing authority around, allows multiple players to chop the same tree at the same time and prevents many race conditions, because the important action (destroying the tree and spawning the log) is all resolved on the Client with Authority.
Conceptually, we can imagine the event flow to go like this:
(1) Chop action happens on a Client -> (2) Authority is notified, elaborates new state -> (3) Authority sends result to all others -> (4) All other Clients play out animation and effects
You can find this flow in practice in the ChoppableTree.cs
script. In this script, only one variable is synchronized, the energy of the tree:
The flow goes like this:
(1) A player presses the button to chop down the tree.
It locally invokes the method TryChop()
, which checks if the tree hasn't been already chopped down, subtracts energy locally, and also invokes the Chop()
method, locally or remotely depending if authority on this tree is here or not.
(2) On the Authority, the Chop()
method is called, and checks if the tree needs to be effectively cut down based on its energy:
(3) If so, CutDown()
is invoked locally, spawning the log and informing all other clients to play the animation of the tree disappearing:
(4) Finally, other Clients play animation, particles and sound locally in ChangeState()
. They will also see the log spawn thanks to the automatic network-instantiation.
Why do we subtract energy from a synced variable in TryChop()
when we are not the Authority?
Ultimately, the final word on whether the tree has been chopped down completely is always on the Authority's side, of course. But by subtracting energy locally and immediately, we can deal with cases where the player manages to produce two or more chop inputs before the Network Command has travelled to the Authority (and back) with a result.
Imagine: the tree has 1 energy. If we didn't subtract energy locally, the player would be able to chop several times because until the Authority tells them that the tree is down, they still think it has 1 energy.
In fact, it would send several Chop()
Network Commands for no reason, which the Authority would have do discard on arrival.
Instead, if we immediately change the value on the variable and we use it as an indication of whether we can chop or not this will stop the chopping after one hit, as it should be.
Soon, the Authority will have elaborated on its side that the tree has gone down, and will inform our Client (with ChangeState()
). Because energy
is a synced variable, it will be overwritten again with the value computed on the Authority - which of course will be 0 at this point, so it will match.
So nothing is lost and no state is compromised, but with this little trick we get immediate feedback and we avoid some unneeded network traffic.
Chat | Lobbies
Communication is an inherent part of online games and a chat, however simple, is a great way to enhance the range of expression for the players.
We wanted to implement a very simple chat system. By pressing Enter, a small screen-space UI opens up and allows the player to compose a message. When they press Enter again, a balloon on top of their character displays the message to them, and to all connected Clients.
This is done in three parts.
The Chat
script on the player reads the input, requests ChatComposerUI
to display the chat composer that is part of the screen-space scene UI.
When the player sends a chat message, Chat
is informed by an event sent by ChatComposerUI
, and sends a Network Command SendChatMessage
to all other clients.
Finally, the received message is displayed in world-space over the player's head the script ChatVisualiserUI
present in the Player Prefab.
By default, coherence's Network Commands have a limit in the length that can be sent in one command. This is limited by the length of a UDP packet. While this limitation might be removed in the future, for now it means that chat messages can't be longer than a certain amount.
This amount, however, is quite different depending if you use a parameter of type string
or of type byte[]
(byte array). If you send a string
, you will be able to pass on around 50 characters. This is really not much for a chat system.
If you use byte[]
though, the number of characters goes up to (around) 500. Now we're talking!
So what we do in this demo is that first we convert the string
that the player has typed in the UI into a byte array, and we send that via Network Command:
Then, on the receiving side, we reconvert it back into a string
:
This simple trick allows us to send longer messages, or to send the same message generating less traffic.
Because we are sending the chat messages on the CoherenceSync
that is on the Player Prefab, it means that if that particular player instance is not visible to a Client because it's outside of their LiveQuery, they won't receive the Network Command and thus the chat message. This is maybe desirable in this demo, where the chat is visualised on top of the player.
But if chat messages are shown in a UI panel and players should receive them all regardless, then it might make more sense to rely on a special type of CoherenceSync
: Client Connections. By sending the Network Command on that, it would ensure that the Command is sent and received regardless of LiveQuery ranges.
Read the Client Connections page for more info.
This page talked about a simple chat system to use during gameplay, but keep in mind that coherence also has a solution for long-form chats as part of Lobby rooms. Players can be in a lobby before but also during gameplay.
For more information about Lobbies, read the specific page.
If you're curious about this code, you can check out the file in the coherence package folder in io.coherence.sdk/Coherence.Toolkit/CoherenceSyncConfigs/ObjectInstantiators
and open DefaultInstantiator.cs