Liberi Developer Guide: Sync Framework

From EQUIS Lab Wiki

Revision as of 17:33, 27 April 2016 by Clarke (Talk | contribs)
(diff) ← Older revision | Current revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Contents

Overview

The Sync framework is how Liberi synchronizes game objects, states, and messages between one server and all the clients connected to it. This has nothing to do with the overarching network architecture - it refers only to the real-time relationship between peers that are viewing the same contents.

This article assumes you've already read through the Network Architecture article. Knowledge of the Janus framework is strongly recommended. This article is just an introduction to the main concepts in the Sync framework. For a detailed reference, one should consult the API reference, or take a look at the following source files: Scripts/Core/Sync.Static.cs, Scripts/Components/Sync.cs. If you really want to see the nitty gritty internals, check out Scripts/Core/Sync.Static.Internal.cs.

Peers

A server and all the clients connected to it together form a "peer group". Each peer in the group is assigned an integer index which uniquely identifies it to the rest of the group. The server's index is always 0. Peer indices are used to specify object ownership (covered later), and also used internally by the framework for messaging. Using Sync.LocalPeerIndex in code will return the index of the peer on which the code is currently executing.

Objects

The Sync framework revolves around objects. Specifically, spawning, despawning and synchronizing objects. Objects in Liberi fall into a few categories:

  • Synchronized Map Object:
These are objects that are pre-existing parts of the scene, and are also synchronized between peers. For example, each minigame scene has a controller object which synchronizes the minigame state between all the peers.
  • Unsynchronized Map Object:
These are objects which are pre-existing parts of the scene, but don't need to be synchronized. For example, props and terrain are just there for anyone who loads the scene.
  • Synchronized Spawned Object:
These are objects which are spawned during a session, and are synchronized across the peer group. For example, a new monster appears in the desert, and must behave consistently for anyone that sees it.
  • Unsynchronized Spawned Object:
These are objects which are spawned during a session, but don't need to be synchronized. The most common examples are visual effects. For example, you can spawn a cloud of sparkly fairy dust in the same location on all the peers, but unless the cloud moves, or affects other objects somehow, there is no reason why the clouds spawned on all of the peers have to be the same cloud.

An object can only be synchronized if it has a Sync component attached. See the Sync Component section for details.

Ownership

Each synchronized object, whether spawned or pre-existing, have an owner. The owner of an object is responsible for making all decisions regarding that object's logical behaviour, and is the source for all changes to the object's states.

For example, a player avatar would be owned by the client of the corresponding player, because it is desirable to have immediate access to the player's inputs that drive the avatar's behaviour. Since the server constitutes an objective, authoritative picture of the world, it owns all objects that are part of the world. This includes minigames, monsters, projectiles, bots, moving platforms, etc.

For synced spawned objects, ownership is specified when the object is spawned. Synced map objects all belong to the server. For unsynced objects, the concept of ownership doesn't really matter, so each peer essentially considers itself to be the "owner" of all unsynced objects. The Sync class contains various methods and properties for querying object ownership. See the Scripts/Core/Sync.Static.cs source file for details. Currently, the ownership of an object cannot be changed after being set.

Script Roles

Scripts attached to synchronized objects can specify a "role" for itself via the Script attribute. This role is used to determine whether or not the script should be enabled on each peer. There are various script roles defined in the ScriptRole enumeration, but the most common are: Logic and View. Scripts with a "logic" role only execute on the peer that owns the object, whereas scripts with a "view" role will execute on all clients. Essentially, logic scripts define all the states and behaviours of an object, but ignore all the aspects of presenting them. On the other hand, view scripts will not care for when or why an object does what it does, but will be in charge of reflecting all of its behaviours in a visual or auditory manner. By thinking of this dichotomous relationship in terms of "logic vs. presentation", you remove the burden of thinking about less intuitive concepts like "server" and "client" when writing game scripts.

Other script roles constitute some combination of Logic and View. These are used for special cases, and have alternate names which are easier to remember. For example, a script with the ServerOwner role will only execute on the owner peer if it also happens to be the server. The ClientOwner role will only execute on the owner peer if it is also a client. This is useful when youhave different ways of controlling an object depending on if its owned by the server or by a client (e.g., avatars). Instead of creating two different avatar prefabs with different logic scripts attached, you can simply attach all the different kinds of scripts to the same prefab, with different roles specified for each. Then, depending on the ownership of the object during runtime, it will execute only the correct scripts. Since the most common usage for these script roles is for differentiating player controls from bot controls, they have alternate names: HumanBrain and BotBrain. See the Scripts/Core/ScriptRole.cs file for details on all the different possible script roles. See the Creating a Minigame, Creating an Avatar, and Creating a Mob articles for more practical examples of script roles in action.

Spawning

In Liberi game code, the GameObject.Instantiate method is not used. Instead, we use one of the variations of Sync.Spawn to spawn new objects into the scene. Use Sync.SpawnLocal to spawn an unsynced object. All of the variations of these two methods use some or all of the following parameters:

  • prefab / prefabId:
Either the reference to an object prefab, or simply its ID. For the ID to work, the prefab must be located directly under a Resources folder somewhere in your project.
  • position:
The position of the newly spawned object.
  • direction:
The direction of the newly spawned object. See the Physics article for details about how Liberi handles object orientations.
  • ownerPeerIndex:
The peer index of the intended owner of this object. If unspecified, defaults to the peer index of the server.
  • details:
A UJeli object containing any kind of additional information to be passed to the spawned object. See the next section for details about... details. lol.

The spawn details of a spawned object are always accessible for as long as the object exists. You can access them through Sync.GetSpawnDetails. You can also use Sync.GetSpawnInfo to fetch a structure containing all the spawn-time parameters of an object (prefab, position, rotation, owner, etc.), including the spawn details.

In Liberi, the OnSpawn method replaces the Start method. This method serves the same purpose as Start with two exceptions: First, it has an optional argument containing the spawn details of the object, as mentioned in the parameter listing above. Second, its execution is dependent on the script's role. For example, consider the logic script of a monster. If it had a Start method, it would be executed on all peers. This is because the role-based filtering of scripts happens after creating the object, so there is no time to stop Start from executing. On the other hand, OnSpawn would only execute on the server, making it appropriate for initial actions taken by the monster. OnSpawn is also called appropriately for map objects that are not spawned, just for consistency. See any avatar, mob or minigame script for examples of OnSpawn in use.

Despawning

Objects are despawned through the Sync.Despawn method. This method is the same whether the object is synchronized or unsynchronized, spawned or pre-existing. The OnDespawn method can be used to perform actions upon despawning an object.

Synchronization

All synchronization in Liberi is done via Janus timelines attached to synchronized objects. There are three main types of synchronization in Liberi:

  • Continuous States:
These are states that are constantly being updated. They are usually values on a continuum, and thus have the possibility of being interpolated or extrapolated if needed. Examples of continuous states include positions, angles, and health. These by default have a cache size of 3 and hold a maximum of 3 entries.
  • Discrete States:
These are states which are either integral, enumerable, or are strings or references. Discrete states tend not to change as often as continuous states, but any updates that do happen are usually critical. Examples of discrete states include scores and modes (activities).
  • Events:
These do not describe how the object is like at a point in time, but rather describe instantaneous things the object is doing. An example is avatar skill execution. As a general rule, anything which is done by the object instantaneously is a candidate for an event.

The Sync framework provides three utility methods to generate timelines for these three cases: Sync.CreateContinuousState, Sync.CreateDiscreteState, and Sync.CreateEvent. The timelines returned by these methods will be preconfigured in the optimal way for their usage. As a general rule, you declare timelines in logic scripts, and create/configure/subscribe to them in the Awake method. Your logic script should be solely responsible for inserting values into its timelines, while other scripts may be able to query timelines or subscribe to changes. For example, the GekkuLogic script is responsible for updating the current "condition" of the Gekku avatar (burnt, stunned, etc.) by inserting values into the Condition timeline. Meanwhile, the GekkuView script subscribes to changes in the Condition timeline, and reflects the condition with visual effects or sounds (flames, stars, etc.). Values can be inserted into timelines relative to the current time by using the overriden [] operator. For example, the following code changes the Gekku's condition to "stunned" 500 milliseconds in the future:

Condition[0.5f] = GekkuCondition.Stunned;

The same operator can be used to query the value of a state timeline at a time relative to now. For example, the following code fetches the aim of the Gekku 500 milliseconds ago:

Vector3 oldAim = Aim[-0.5f];

When using [] to query discrete state timelines, you will always get the last known value at the time you are querying. When quering continuous state timelines, depending on the interpolation/extrapolation settings of the timeline, you may get an interpolated or extrapolated value. In most cases, you probably just want the most recent value of a state. In that case, you can simply use the Timeline.LastValue property instead:

Vector3 currentAim = Aim.LastValue;

Note that object states are available to players that join the game late, since it is always important to have the most up-to-date states of an object no matter when you join. On the other hand, events are not available to late-joiners, since they are only relevant when they happen, and not after. Exceptions can always be made, however. The timelines returned by the aforementioned methods can be tweaked however you want.

Sync Component

As mentioned previously, the Sync component is necessary for any object to be synchronized. However, it also performs another important function: automatic synchronization of common object properties. These include position and orientation, among other things. It exposes several properties in the editor for you to tweak. For example, the strength of smooth corrections for object positions, and the rate at which values are propagated. The default values should be just fine for most cases, but a deeper understanding of Janus and general networking concepts is recommended if you wish to make tweaks.

Adjusting Sync Values

To make adjustments to the sync framework these changes can be made in the file Sync.Static.cs (or for much lower level changes Sync.Static.Internal.cs). The three types of synchronization used in Liberi by this framework are continuous states, discrete states, and events; each of which relies on a Janus timeline. The default settings for each of these three synchronized are held in methods in Sync.Static.cs and are called CreateConinuousState, CreateDiscreteState, and CreateEvent respectively.

In each method there are 3 variables that modify the attributes of the Janus timeline.

  • Cache Size:
This determines the number of events that get saved in a timeline before previous events are discarded. If the cache size is larger like in continuous states then several values are recorded and can be referenced.
  • Max Entries:
This determines the number of different entries that can be stored in a single timeline. The timeline functions much like an array and this determines the size of the array.
  • Delivery Mode:
This determines how messages will be received by the sync framework. If messages are sent ReliableOrdered then messages will be sent in the guaranteed order and will be guaranteed to be received eventually. ReliableUnordered will guarantee message delivery but they may not be in the correct order. Unreliable functions like standard UDP and messages may or may not arrive. Typically it is recommended to send messages using ReliableOrdered unless the order does not matter.