🔗 Communication
RPC (Remote Procedure Call)⚓️
RPCs (Remote Procedure Calls) are a standard software industry concept that allows methods to be called on objects that are not in the same executable. They enable communication between different processes or systems over a network.
With RPCs, a server can invoke functions on a client, and similarly, a client can invoke functions on a server. This bi-directional communication allows for flexible and dynamic interactions between clients and servers, facilitating various operations such as requesting data, executing commands, and synchronizing states across different parts of a distributed system. RPCs provide a powerful mechanism for enabling remote interactions and enhancing the functionality of networked applications.
RPC Basic Structure
graph LR
A[Ruan<br>___Local Player___] ---> | User Input Rpc | B{Server}
B ---> | Move Rpc | A
B ---> | Move Rpc | C[Junior<br>___Remote Player___]
B ---> | Move Rpc | D[Mike<br>___Remote Player___]
The diagram illustrates the basic flow of an RPC (Remote Procedure Call) in a multiplayer environment.
RPC Flow
-
Local Player (Ruan)
- Sends input to server via RPC
- Receives validated updates back
-
Server
- Validates input
- Broadcasts updates to all players
-
Remote Players
- Receive server-validated updates
- Apply changes to maintain sync
RPC Naming Convention and Base Classes
RPC's are also supported in base classes. If you are using a base class for network functionality, ensure that the base class name includes the Base
prefix.
Naming Convention
Base classes using RPCs must include the Base
suffix:
- ✅
PlayerBase
- ✅
CharacterBase
- ❌
BasePlayer
- ❌
Player
Example
Note
Before proceeding, refer to the Communication Structure and Service Locator Pattern pages for essential background information.
Method Signature⚓️
Remote Procedure Calls (RPCs) in our networking system allow communication between clients and servers. Each RPC method can be configured with different parameters to handle various networking scenarios.
RPC methods can accept up to three parameters:
Parameter | Description | Availability |
---|---|---|
DataBuffer message |
Contains the transmitted data | Client & Server |
NetworkPeer peer |
Information about the calling client | Server Only |
int seqChannel |
Controls message ordering and priority | Client & Server |
Remote Procedure Calls (RPCs) require proper method decoration with either:
[Server]
- Marks a method that executes on the server when called by a client[Client]
- Marks a method that executes on clients when called by the server
Each RPC method must specify a unique numeric ID between 1-230 within its class:
C# | |
---|---|
Here are all valid server RPC signatures, from simplest to most complex:
Server-Side Signatures⚓️
Data + Peer Info
C# | |
---|---|
Valid client RPC signatures with common use cases:
Client-Side Signatures⚓️
RPC ID System
📝 Each RPC method requires a unique numeric identifier (ID) within its class:
- IDs are used for message routing between client and server
- Only needs to be unique within the same class
- Different classes can reuse the same IDs
ID Range Requirements
⚠️ RPC IDs must follow these rules:
- Valid range:
1
to230
- Cannot be zero or negative
- Cannot exceed 230 💥 Runtime Exception will be thrown if these rules are violated
Use Constants for RPC IDs
It's recommended to use constants for RPC IDs to improve code maintainability and prevent duplicate IDs. This makes it easier to manage and refactor RPC calls across your codebase.
Example
C# | |
---|---|
Implementation Examples⚓️
Example 1 (NetworkBehaviour)
Example 2 (ServerBehaviour & ClientBehaviour)
Script ID Configuration
Script IDs act as a bridge between client and server components, ensuring they can communicate properly. Each pair of corresponding client/server scripts must share the same ID in their Unity Inspector.
Key Points:
- The ID links matching client and server components
- Must be unique across your project
- Set in Unity Inspector for both scripts
- Mismatched IDs will break communication
How to Invoke a RPC⚓️
The Client
and Server
properties are part of the public API inherited from NetworkBehaviour
, ClientBehaviour
, ServerBehaviour
, and DualBehaviour
.
They are designed to facilitate communication between client and server in the network environment. Each property enforces specific usage restrictions to ensure proper client-server interactions.
The network components provide two key properties for managing client-server communication:
Client Property
- Purpose: Enables clients to send RPCs to the server
- Access: Client-side only (throws exception if accessed on server)
- Main Method:
Rpc()
with multiple overloads - Example Usage:
Server Property
- Purpose: Enables server to send RPCs to clients
- Access: Server-side only (throws exception if accessed on client)
- Main Method:
Rpc()
with multiple overloads - Example Usage:
Both properties enforce proper client-server architecture by restricting access to the appropriate side. For detailed API information including all overloads, see the API Reference.
Example 1 (NetworkBehaviour) - Send an RPC from the client to the server
C# | |
---|---|
Example 2 (NetworkBehaviour) - Send an RPC from the server to the client
Rpc()
with arguments:
Tip
The Client.Rpc()
and Server.Rpc()
methods has 8 overloads and optional arguments. However, the overloads available can vary depending on the network base class used(i.e. NetworkBehaviour
, ClientBehaviour
, ServerBehaviour
, and DualBehaviour
).
For details on the available overloads, please refer to the API Reference.
Client-Side
Server-Side
Direct Value Transmission
RPCs support direct sending of primitive and unmanaged types without manual DataBuffer
creation.
Supported Types
- Primitives (
int
,float
,bool
, etc) - Unity types (
Vector3
,Quaternion
, etc) - Blittable structs
Examples
Type Restrictions
❌ Not Allowed Without a DataBuffer
:
- Reference types
- Classes
- Arrays
- Strings
✅ Allowed:
- Primitive types
- Unmanaged structs
- Unity value types
Network Variables⚓️
A [NetworkVariable]
is a powerful attribute that automatically synchronizes state between server and clients without manual RPC implementation. When a network variable's value changes on the server, the framework automatically propagates these changes to all connected clients, ensuring state consistency across the network.
Key benefits:
- Automatic synchronization without manual networking code
- Significantly reduces boilerplate compared to RPCs
- Change detection and validation out of the box
This provides an efficient way to maintain synchronized game state with minimal code overhead.
Network Variable Structure
graph LR
Ref{Game Object<br>___Server Side___} --> | Health Change | A{RPC}
A ---> | Health Update | B[Mike<br>___Client Side___]
A ---> | Health Update | C[Ruan<br>___Client Side___]
The diagram illustrates how a Network Variable operates in a multiplayer environment:
- A server-side game object modifies a variable (e.g.,
Health
). - This change is processed by the server, which acts as the authoritative source.
- The server then sends updates to all connected clients (e.g., Mike and Ruan), ensuring that each client reflects the latest value of the variable.
- These updates allow all players to have a synchronized and consistent view of the variable's state, regardless of who initiated the change or their connection latency.
This structure highlights the server's role in maintaining authority and consistency across the network.
Base Class Support⚓️
Network Variables support inheritance through base classes, allowing you to define shared networked state that derived classes can access and modify.
Base Class Naming Convention
When using Network Variables in base classes:
- Base class names must end with the
Base
suffix - The suffix is required for proper code generation
- Incorrect naming will prevent network synchronization
Base Class Implementation
Network Variables defined in base classes are automatically available to all derived classes, maintaining synchronization across the inheritance chain while allowing customization through virtual hooks.
Note
Before proceeding, refer to the Communication Structure and Service Locator Pattern pages for essential background information.
How to Use⚓️
Network Variable Inspector
Network variables are automatically displayed in the Unity Inspector even without the [SerializeField]
attribute. However, without this attribute they are read-only and not serialized.
To make network variables both visible and editable in the Inspector:
- Add both
[NetworkVariable]
and[SerializeField]
attributes - This enables full serialization and editing capabilities
- Without
[SerializeField]
, values reset on scene reload
Network Variable Naming Requirements
Field Naming Convention
Network variable fields must follow these rules:
- Fields must be prefixed with
m_
- First letter after prefix must be capitalized
- Class must be marked as
partial
✅ Valid Examples:
C# | |
---|---|
❌ Invalid Examples:
C# | |
---|---|
Why partial?
The partial
keyword is required because the source generator needs to extend the class with additional generated code for network variable functionality.
Network Variable Source Generation
The Omni Source Generator
automatically generates several elements for each [NetworkVariable]
:
Properties:
- Public property for accessing the variable
- Getter/setter with network synchronization
Hooks:
OnVariableChanged
method for value change detectionpartial void
hooks for custom change handling- Base class override hooks with
protected virtual
methods
Options:
- Variable-specific network options (e.g.,
HealthOptions
) - Customizable delivery modes and target options
- Serialization and synchronization settings
Methods:
- Manual sync methods (e.g.,
SyncHealth()
) - Value validation methods
- Networking utility methods
Example of generated elements for a health variable:
Generated Properties⚓️
Generated properties in Omni are designed to automatically synchronize their values across the server and all connected clients each time the property is modified. This ensures that all instances of the property remain consistent throughout the networked environment, maintaining real-time accuracy.
Warning
Omni does not perform checks to determine if the new value is different from the current value. Each time the property’s setter
is invoked, the value is synchronized across the network, regardless of whether it has changed. This can lead to unnecessary network updates if the property is set to the same value repeatedly, so it is recommended to manage calls to the setter carefully to optimize performance.
Automatically Synchronized
C# | |
---|---|
Tip
You can modify the underlying field directly instead of the property if you don’t want automatic synchronization. To manually synchronize the modified field, simply call:
SyncHealth(DefaultNetworkVariableOptions)
SyncMana(DefaultNetworkVariableOptions)
for immediate network updates.
Warning
If you modify a field immediately after instantiating a networked object or within Awake()
or Start()
, the variable will synchronize correctly. This is because, during object initialization, the server automatically sends updates for network variables to clients. However, if you modify the property instead of the field at these early stages, synchronization may fail. Property changes trigger an update message, but if the object has not yet been instantiated on the client side, the update will not be applied.
Bug
Occasionally, generated code may not be recognized by the IDE’s IntelliSense (e.g., in Visual Studio). If this occurs, a simple restart of the IDE should resolve the issue.
Default Behaviour⚓️
Tip
Use DefaultNetworkVariableSettings
to adjust how network variables are transmitted across the network. This allows for configuring default behaviors for all network variables. For more specific control, you can use individual settings like HealthOptions
and ManaOptions
to customize the transmission behavior of specific variables.
Example
Generated Methods⚓️
The [NetworkVariable]
attribute will generate methods for each network variable, such as:
Health Hook
Mana Hook
void SyncHealth(NetworkVariableOptions options);
Manually synchronizes the m_Health
field, allowing control over when and how this field is updated across the network.
void SyncMana(NetworkVariableOptions options);
Manually synchronizes the m_Mana
field.
RouteX⚓️
RouteX is a simple simulation of Express.js
and is one of the most useful features of the API. It can be easily used to request a route and receive a response from the server. Routes can also send responses to multiple clients beyond the one that originally requested the route.
Registering Routes⚓️
- Import the
RouteX
module withusing static Omni.Core.RouteX;
andOmni
withusing Omni.Core;
- Register the routes on the
Awake
method or on theStart
method, eg:
Note
RouteX
supports both asynchronous and synchronous operations, providing flexibility for various use cases. All functions include asynchronous versions workflows. For additional overloads, detailed explanations, and further information on synchronous and asynchronous versions, consult the API Reference
.
Example
Example
Requesting Routes⚓️
Example
C# | |
---|---|
Info
Omni provides the HttpResponse
and HttpResponse<T>
objects to streamline the process of sending responses. These objects allow you to include a status code, a message, and optionally, a payload (via the generic version). This approach offers a more organized and structured way to handle and send responses in your application.
Example
Example
For more details, refer to the API Reference.
Serialization and Deserialization⚓️
Omni supports serialization of a wide range of data types, including primitives, complex classes, structs, dictionaries, and more, providing unmatched flexibility for networked data structures. Omni offers two serialization methods: JSON-based serialization for readability and compatibility, and binary-based serialization for optimized performance and minimized data size.
With Omni, everything is serializable. All network operations utilize the DataBuffer
object, a dedicated data buffer that efficiently handles data preparation and transmission across the network, ensuring seamless and effective communication.
Info
The DataBuffer
is the core of all Omni operations. It is used universally across RPCs, RouteX, custom messages, and other network features. Understanding how to manage and utilize DataBuffer
is essential for working effectively with Omni.
Danger
As a binary serializer, DataBuffer
requires that the order of reading matches the order of writing precisely. Any discrepancy in the read/write sequence can lead to corrupted or unexpected data. Developers should ensure consistency and adherence to the defined structure when serializing and deserializing data with DataBuffer
.
Info
The DataBuffer
functions similarly to a combination of MemoryStream
and BinaryWriter
and BinaryReader
. It includes comparable properties and features, such as Position
, enabling developers to efficiently manage and navigate the buffer while performing read and write operations.
Primitives⚓️
Omni’s DataBuffer
provides efficient support for primitive types, allowing direct serialization of commonly used data types such as integers, floats, and booleans. This simplifies network data handling by enabling fast read and write operations for foundational data types.
Using these primitives, Omni ensures minimal overhead in data serialization, making it suitable for high-performance networking where lightweight data handling is essential. Primitive types can be written to or read from the DataBuffer
in a straightforward manner, supporting rapid data transmission across client-server boundaries.
Reading Primitives
C# | |
---|---|
Complex Types⚓️
Omni supports the serialization of complex types using Newtonsoft.JSON
or MemoryPack
. For objects and data structures that go beyond primitive types, JSON serialization provides a readable, flexible format ideal for compatibility with third-party systems, while MemoryPack
enables efficient binary serialization for high-performance network transfers.
Using these serialization methods, Omni can seamlessly handle complex data types, such as custom structs
, classes
, dictionaries
, and nested structures
, ensuring that all necessary data is transmitted effectively and accurately across the network.
JSON Serialization
MemoryPack Serialization
C# | |
---|---|
JSON Deserialization
MemoryPack Deserialization
C# | |
---|---|
Note
See the Newtonsoft.JSON
or MemoryPack
documentation for more information about using annotation attributes to customize serialization and deserialization.
Info
When sending a DataBuffer
, you will always receive a DataBuffer
in response; it is not possible to send and receive data in any other way without using a DataBuffer
, as all operations utilize it internally. You must also ensure that the reading and writing occur in the same order.
Compression⚓️
The DataBuffer
object in Omni supports efficient data compression, utilizing the Brotli
and LZ4
algorithms. These algorithms are designed to optimize network performance by reducing data size without significant overhead, ensuring faster transmission and lower bandwidth usage.
- Brotli: A highly efficient compression algorithm ideal for scenarios where maximum compression is needed, offering significant size reduction for complex or large datasets.
- LZ4: Focused on speed,
LZ4
provides fast compression and decompression, making it suitable for real-time applications that prioritize performance over compression ratio.
With these options, Omni allows developers to tailor data compression to their specific needs, balancing speed and efficiency for various network scenarios.
Cryptography⚓️
Omni employs AES
encryption to secure data buffers, ensuring that sensitive information remains protected during network transmission. The cryptographic system is designed with flexibility and security in mind, offering both peer-specific and global encryption keys to handle various scenarios.
Peer-Specific Encryption
Each peer
in the network is assigned its own unique encryption key. When a client (e.g., Client A) sends a message using its key, only that client can decrypt the message. This ensures a high level of security, as no other client can access the encrypted data. Peer-specific encryption is ideal for situations where private communication or data integrity is paramount.
Info
Encryption keys are exchanged between the client and server using RSA
, a robust public-key cryptography algorithm. This ensures that the AES
keys used for data encryption remain secure during transmission, as only the intended recipient can decrypt the exchanged keys. By combining RSA
for key exchange with AES
for data encryption, Omni provides a highly secure and efficient cryptographic system for multiplayer environments.
Global Server Key
In addition to peer-specific keys, Omni provides a global server encryption key. Unlike peer-specific keys, the global key can be used to encrypt and decrypt any data, including messages originating from other clients. This global key is managed by the server and allows for seamless handling of shared data, such as broadcasted messages or server-wide updates. It provides a flexible option for scenarios where universal decryption is required without compromising security.
Key Features
- AES Encryption: Omni uses the
Advanced Encryption Standard (AES)
to ensure robust protection against unauthorized access. - Peer-Specific Keys: Restrict decryption to the originating peer, enhancing data privacy.
- Global Server Key: Enable decryption of any data within the network, facilitating shared communication and server-driven operations.
- Flexibility: The dual-key system allows developers to tailor encryption strategies to the needs of their application, balancing security and convenience.
Omni's cryptography framework ensures that all data transmitted across the network is secure, whether it's private peer-to-peer communication or broadcasted messages. By combining peer-specific encryption with a global server key, Omni provides a powerful and flexible system for managing encrypted data in multiplayer environments.
Encryption
Decryption
See the API Reference
for more information about the DataBuffer
and its usage.
IMessage Interface⚓️
This interface, IMessage
, can be implemented to customize the serialization and deserialization of a type when used within an RPC or NetworkVariable
. By implementing IMessage
, you define how data is written to and read from a DataBuffer
, enabling greater control over data structure and format.
The IMessageWithPeer
interface extends IMessage
to include additional properties, such as SharedPeer
and IsServer
, which are useful for managing encryption and authentication in networked communications. This extension provides enhanced flexibility for handling secure and authenticated messaging between server and client.