Ubernet is a networking library for Unity that is built as an abstraction on top of other libraries, implemented as providers. Several providers are available to use and it’s easy to add custom providers.
A high level abstraction called the Network Entity Manager can be used with built-in and custom providers. The Network Entity Manager can synchronize variables and send Remote Procedure Calls (RPCs) on custom objects and Unity MonoBehaviours. Ubernet uses UniRx for all asynchronous operations.
The libraries UniRx, LiteNetLib and the Photon Realtime Unity SDK are included.
Ubernet leverages modern C# features, so the scripting runtime version must be set to “.NET 4.x Equivalent” in Player Settings and enabling the Incremental Compiler in Package Manager is required.
Compared to other networking libraries like Photon's PUN, Ubernet was designed to work with an unlimited number of interchangeable providers. This prevents vendor lock-in since the implementations for all providers share the same interface. There are also several practical benefits like a more elegant API designed around UniRx and easier local testing (no connection to Photon is required when testing with several clients on the same machine or LAN).
At this time, Ubernet has been tested with on Mac and Windows with the Mono scripting backend. IL2CPP platforms have not been tested yet but will be supported in the future.
- Photon
- Mock provider (offline mode)
More providers will be supported in the future.
This guide will show how to connect to Photon. Please refer to the Photon documentation for more information.
You can use the following snippet to get started with Photon. It creates a matchmaker, connects to a game and initializes a Network Entity Manager for the connection. This workflow will be explained in detail below.
public async Task Connect()
{
try
{
PhotonMatchmaker matchmaker = await PhotonMatchmaker.NewContext()
.WithAppId(appId) // Your Photon App ID
.WithRegion(PhotonRegions.Europe)
.WithAppVersion("0.1")
.ConnectToCloud(); // Connect to the Photon cloud
IGame game = await matchmaker.CreateGame(PhotonCreateGameOptions.FromRoomName("myRoom")); // Create the game
IConnection connection = await game.ConnectWithPhoton(); // Connect with Photon
connection.AutoUpdate(); // Automatically update 20 times/second
connection.RegisterUnityDefaultTypes(); // Enable serialization for Vector3 etc.
NetworkEntityManager entityManager = await _connection.CreateEntityManager()
.SetLocalPlayer(new DefaultPlayer("foo"))
.SetAsDefaultEntityManager()
.Initialize(); // Create an Entity Manager
}
catch (UbernetException e)
{
// Handle the exception
}
}
First, you need to register a free Photon account and obtain your App ID from the dashboard. The App ID will be used to connect to Photon.
Since connecting to Photon is asynchronous, a UniRx Observable is returned. You need to subscribe to the observable to obtain the matchmaker.
// Imports
using Skaillz.Ubernet.Providers.Photon;
using UniRx;
// Create a new Matchmaker
PhotonMatchmaker.NewContext()
.WithAppId(appId) // Your Photon App ID
.WithRegion(PhotonRegions.Europe) //US, Japan, Australia and more are available. See the PhotonRegions class for more options.
.WithAppVersion("0.1") // Only players with the same version can connect to the same game
.WithTickRate(20) // The number of times per second the matchmaker sends and receives updates over the network (20 by default)
.ConnectToCloud() // Create the Matchmaker asyncronously
.Subscribe(matchmaker => {
// We're connected to the matchmaker
},
err => {
// An error occured.
Debug.LogException(err);
});
All asynchronous operations return UniRx Observables. You can subscribe to them as shown above, but you can also use it in other ways.
using UniRx;
public IEnumerator Connect()
{
var matchmakerYield = PhotonMatchmaker.NewContext()
...
.ConnectToCloud()
.ToYieldInstruction(); // Convert to yield instruction
yield return matchmakerYield;
if (matchmakerYield.HasResult)
{
// Obtain the matchmaker
PhotonMatchmaker matchmaker = matchmakerYield.Result;
}
else if (matchmakerYield.HasError)
{
// Handle error
Debug.LogException(matchmakerYield.Error);
}
}
using UniRx;
public async Task Connect()
{
try
{
// Observables are awaitable
PhotonMatchmaker matchmaker = await PhotonMatchmaker.NewContext()
...
.ConnectToCloud();
}
catch (UbernetException e)
{
// Handle error
Debug.LogException(e);
}
}
Most of the examples in this guide use the Subscribe()
method to subscribe to Observables.
You can use the Matchmaker to create or join games.
Use matchmaker.CreateGame()
to find a create a new game. You can pass ICreateGameOptions
for additional configuration. If using Photon, passing PhotonCreateGameOptions
is mandatory.
// Create a new game.
matchmaker.CreateGame(new PhotonCreateGameOptions {
RoomName = "MyRoom" // Specify a room name. Leave empty to auto-generate.
})
.Subscribe(game => {
// Do something
});
Use matchmaker.FindRandomGame()
to find a random game. You can pass IJoinRandomGameOptions
for additional configuration. If using Photon, passing PhotonJoinRandomGameOptions
is mandatory.
// Find a random game.
matchmaker.FindRandomGame(new PhotonJoinRandomGameOptions())
.Subscribe(game => {
// Do something
});
Use matchmaker.FindGame()
to find a given game. You must pass IGameQuery
to specify the game you want to find. If using Photon, you should pass a query of type PhotonGameQuery
.
// Find a game with the given room name.
matchmaker.FindGame(new PhotonGameQuery {
RoomName = "MyRoom" // The name of the Photon room you want to join
})
.Subscribe(game => {
// Do something
});
Use Disconnect()
to disconnect from the Matchmaker.
matchmaker.Disconnect()
.Subscribe(_ => {
// Disconnected
});
After you’ve found a game with a matchmaker, you can connect to it. When connecting, every client will be assigned a unique Client ID. Use the following code to connect with Photon:
// Join a random game.
matchmaker.JoinRandomGame(new PhotonJoinRandomGameOptions())
.Subscribe(game => {
// The game has been created. Connect with Photon.
game.ConnectWithPhoton().Subscribe(connection => {
// Connected to the game
});
});
Connections only send and receive events when their Update()
method is called. Make sure to call it periodically.
public IEnumerator UpdateCoroutine()
{
connection.Update();
yield return new WaitForSeconds(0.1f);
}
StartCoroutine(UpdateCoroutine());
You can also use the helper method AutoUpdate
, which will automatically update the connection until it disconnects.
connection.AutoUpdate(20); // Update 20 times/second
Use Disconnect()
to dispose the connection. Be vary that ending events after disconnecting causes errors.
connection.Disconnect()
.Subscribe(_ => {
// Disconnected
connection = null; // Make sure that we can't use the connection anymore
});
When you’ve connected to a game, you can send and receive events.
using UnityEngine;
using Skaillz.Ubernet;
using UniRx;
int eventCode = 20; // Choose a number between 0 and 239
// Send an event with code 20 and content "Hello, world!" to all other clients
connection.SendEvent(eventCode, "Hello, world!", MessageTarget.Others);
// Receive all events with code 20
connection.OnEvent(eventCode).Subscribe(evt => {
// We received an event. Read its data.
Debug.Log((string) evt.Data);
});
See Serialization for more information about what types of values you can send.
You can pass an IMessageTarget
to SendEvent()
to specify the clients the message should be sent to (or leave it empty to send it to all other clients).
The following message targets are available:
MessageTarget.Others
: Send to all remote clients (default)MessageTarget.All
: Send to all clients, including the local clientMessageTarget.Server
: Send to the server (or master client depending on the provider)IPlayer
orIClient
objects (or any other object that implementsIClientIdResolvable
)- A
ClientList
created withClientList.FromClients(player1, player2, client1, ...)
Subscribe to OnClientJoin
or OnClientLeave
to get notified when a client joins or leaves on a Connection. The Clients
list contains all clients that are currently connected.
The Network Entity Manager is a high-level abstraction that can be used with all providers. The Network Entity Manager manages a number of Network Entities. Each Network Entity consists of multiple Network Components.
Usually, you may want to use GameObjects with a GameObjectNetworkEntity
, but any C# class that implements INetworkEntity
can also be used. Analogously, a Network Component maps to a Unity component that derives from MonoNetworkComponent
(or C# classes that implement INetworkComponent
).
Instead of using raw events, variables can be synchronised and RPCs (Remote Procedure Calls) can be sent on Network Entities.
The Matchmaker UI is a Unity component that can be used for development purposes. It provides menus for connecting to matchmakers, joining games and gathering in a lobby.
The Game Scene is loaded after one of the players in the lobby clicks the “Start Game” button. The Lobby Scene will be loaded after the player disconnects. The “Auto Create Entity Manager” checkbox should be checked if the Network Entity Manager is used. If disabled, a button for initializing it is displayed in the lobby.
A Network Entity Manager can be created for an existing connection (see Connecting to games).
_connection.CreateEntityManager()
.SetLocalPlayer(new DefaultPlayer("foo"))
.SetAsDefaultEntityManager()
.Initialize()
.Subscribe(entityManager => {
// Entity Manager created.
});
Calling SetLocalPlayer
is mandatory before initializing the entity manager. The only argument is the IPlayer
object that should be used. You can use DefaultPlayer
for prototyping or use your own implementation. Refer to DefaultPlayer
as a reference when implementing a custom player type. SyncedValues are supported.
MonoBehaviours in your Game Scene must be able to register to the Entity Manager, so the reference should be saved somewhere. There are several approaches:
SetAsDefaultEntityManager
will register the Entity Manager as a singleton. It must be called before any GameObjectNetworkEntity
objects are loaded so that they can retrieve the singleton and register themselves.
This approach is recommended for beginners and smaller projects, but less flexible since only one Entity Manager can be used at the same time.
TBD
A Unity component called Game Object Network Entity
is available. Attach it to a GameObject to register it with the default Entity Manager (created with SetAsDefaultEntityManager()
) automatically.
Network Entities must have a unique ID that is used to synchronise it over the network. This ID is allocated automatically when adding an entity to a scene or instantiating it from a prefab. Components managed by an entity are identified by IDs that are unique per entity.
Game Object Network Entities that are placed in a scene are treated as scene entities. Scene entities are always owned by the server or “master player”.
When you first create a prefab of a Network Entity, you will notice that it asks you to add it to the prefab cache. This is required so that it can be created on all clients automatically. Click the “Add to Cache” button to proceed.
Instead of instantiating prefabs with Instantiate
, use entityManager.InstantiateFromPrefab()
. The passed prefab must be registered in the cache. This instantiates the prefab locally and allocates an entity ID.
using Skaillz.Ubernet.NetworkEntities;
using Skaillz.Ubernet.NetworkEntities.Unity;
entityManager.InstantiateFromPrefab(prefab); // Instantiates at position (0, 0, 0) with no rotation
entityManager.InstantiateFromPrefab(prefab, position, rotation); // Instantiates at the given position and rotation
Alternatively, entityManager.InstantiateFromResourcePrefab(path)
can be used to instantiate a prefab from the Resources folder.
Network Components are C# classes that implement INetworkComponent
. When using MonoBehaviours, it is recommended to derive from MonoNetworkComponent
.
The most basic way to synchronize a component over the network is deriving from MonoNetworkComponent
and using the Serialize
and Deserialize
callbacks.
The number of times these callbacks are called depends on the SerializationRate
of the entity manager. You can change the default value of 20 times per second by passing it as the second argument to CreateEntityManager
, e.g. _connection.CreateEntityManager(60)
to update 60 times per second.
Serialize()
is used to convert the component into bytes that can be sent over the network. Deserialize()
converts the byte representation back to the object presentation.
using Skaillz.Ubernet.NetworkEntities.Unity;
public class MyComponent : MonoNetworkComponent
{
public byte Data;
public override void Serialize(Stream stream)
{
stream.WriteByte(Data);
}
public override void Deserialize(Stream stream)
{
Data = (byte) stream.ReadByte();
}
}
SerializationHelper
is a utility class that contains helper methods to convert more complex types:
using Skaillz.Ubernet.NetworkEntities.Unity;
public class MyComponent : MonoNetworkComponent
{
public string Data;
public override void Serialize(Stream stream)
{
SerializationHelper.SerializeString(Data, stream);
}
public override void Deserialize(Stream stream)
{
Data = SerializationHelper.DeserializeString(stream);
}
}
You can check if the entity the component belongs to is owned by the local client with IsLocal()
.
// Override the Update method in MonoNetworkComponents
protected override void Update()
{
// Make sure to call base.Update()!
base.Update();
if (Entity.IsLocal())
{
// The component can be controlled
}
else
{
// The component is owned by someone else
}
}
Alternatively, you can override OnLocalUpdate()
, which is called each update if owned by the local client or OnRemoteUpdate()
, which is called if it’s owned by another client.
// Calling the base method is not neccesary here.
protected override void OnLocalUpdate()
{
// Called if the entity is local
}
protected override void OnRemoteUpdate()
{
// Called if the entity belongs to someone else
}
RPCs (Remote Procedure Calls) can be used to invoke component methods on other clients. Derive from MonoNetworkComponent.Rpc
to use RPCs.
RPCs can only be used on methods with the [NetworkRpc]
attribute. Currently, overloads are not supported.
RPCs are sent with SendRpc(string methodName, MessageTarget target, params object[] parameters)
. If the message target is omitted, it will be sent to all other players. Instead of hardcoding the method name as a string, using the nameof()
operator is highly recommended to avoid breaking your code after refactoring a method name.
using Skaillz.Ubernet.NetworkEntities.Unity;
public class PlayerHealth : MonoNetworkComponent.Rpc
{
public int Health = 10;
[NetworkRpc]
private void TakeDamage(int amount)
{
Health -= amount;
}
public void SendTakeDamage(int amount)
{
// Invoke the TakeDamage method on all other clients
SendRpc(nameof(TakeDamage), MessageTarget.Others, amount);
// Call the method on the local client (it does not receive the RPC since we use MessageTarget.Others)
TakeDamage(amount);
}
}
SyncedValues are variables that are synchronized over the network. Changes to SyncedValues are only sent if they have been changed by the entity’s owner.
Derive from MonoNetworkComponent.Synced
to use SyncedValues. To use both SyncedValues and RPCs, inherit from MonoNetworkComponent.SyncedRpc
.
SyncedValues are created by passing generic types. All serializable types are supported (see Serialization).
// Initialize with default value
SyncedValue<int> val = new SyncedValue<int>();
// Initialize with custom value
SyncedValue<int> val = new SyncedValue<int>(1);
Shorter versions are available for common types. The following types can also be assigned from the inspector:
SyncedBool
for booleansSyncedInt
for integersSyncedByte
for bytesSyncedShort
for shortsSyncedFloat
for floatsSyncedString
for stringsSyncedVector2
,SyncedVector3
,SyncedQuaternion
,SyncedColor
for commonly used Unity types
You can read or change the current value with the Value
property. SyncedValues are IObservables
, so you can subscribe to changes.
Only the Value
property of SyncedValues should be reassigned, not the SyncedValue object itself, so it’s highly recommended to declare them as readonly
. SyncedValues are cached when the component is registered, so their references should not be changed after registration.
using Skaillz.Ubernet.NetworkEntities;
using Skaillz.Ubernet.NetworkEntities.Unity;
using UniRx;
public class PlayerHealth : MonoNetworkComponent.Synced
{
// SyncedInt is equivalent to SyncedValue<int>.
private readonly SyncedInt _health = new SyncedInt(10);
private void Start()
{
// Subscribe to changes and print them in the console
_health.Subscribe(h => Debug.Log("Health: " + h));
}
public void TakeDamage(int amount)
{
// Check if the entity is local before modifying the value
if (Entity.IsLocal())
{
_health.Value -= amount;
}
}
}
The OnRegister
and OnRemove
callbacks are called when a component is added or removed from an entity. Override them to add your own event handling, but make sure to call base.OnRegister()
and base.OnRemove()
at the beginning.
Serialization is the process of converting an object or a value into a byte representation that can be sent over the network. The reverse process is called deserialization. Whether a value can be serialized depends on its type.
The following types can be serialized by default:
null
- byte
- bool
- short
- int
- long
- float
- double
- string
- byte[]
- object[]
- Typed arrays of any other supported type (e.g.
string[]
,long[]
, etc.)
Ubernet is able to serialize some of the most used Unity types like Vector3
and Quaternion
.
The following types are supported right now:
- Vector2
- Vector3
- Quaternion
- Color
Call RegisterUnityDefaultTypes()
to register them.
using Skaillz.Ubernet.DefaultSerializers.Unity;
// If the same serializer is used, all samples lead to the same results
serializer.RegisterDefaultUnityTypes(); // Register on an ISerializer
connection.RegisterDefaultUnityTypes(); // Register on an IConnection
networkEntityManager.RegisterDefaultUnityTypes(); // Register on a NetworkEntityManager
TBD