In terms of network transmission, in a realtime multiplayer game, data format is key. We cover techniques for serializing and deserializing data, including formats and libraries. We answer questions like these: Which data should be transmitted? In which situations? Read for answers. Hello everyone, I'm Dmitrii Ivashchenko, Lead Software Engineer at MY.GAMES. We're continuing our on Unity Realtime Multiplayer in 2023, and this time we'll discuss preparing game data for network transmission. article series What is the preferred format for passing data? We've already taken a deep dive and seen how networks are structured and how they function, including a look at the various limitations and peculiarities we must deal with. However, the exchange of information relies on more than just network infrastructure; data format must also be considered. So, next up, we'll see how to transmit data over a network efficiently — and how to avoid any pitfalls in the process. Serialization and deserialization Serialization and deserialization are critical elements for implementing real-time multiplayer games. These operations are used to convert game data into a format that can be transmitted over the network and back again. is the process of converting an object's state into a byte stream for transmission over a network, for saving to memory, or for sharing between different services or processes. Serialization In the context of multiplayer games, serialized data would include data about the current state of the game, such as the position and status of players, the state of the game world, and so on. is the reverse process where the serialized byte stream is converted back into objects in memory. Deserialization Both play an important role in multiplayer games with constant data exchange between clients and servers, and we need to consider several aspects of this process: : Serialization and deserialization must be as fast and efficient as possible to avoid delays. Choosing an appropriate serialization method is particularly important: some methods (such as JSON or XML) are easily readable and writable by humans, but they are usually less efficient compared to binary serialization. Performance : Serialized data must occupy as little memory as possible to reduce the load on the network and players' devices. Data compression can help reduce the size of transmitted data, but it can also increase the time required for serialization and deserialization. Memory : Serialization and deserialization processes must be protected from manipulation and attacks. (For example, deserializing unreliable data can lead to security vulnerabilities.) Security : Game data may change over time, so it is important to manage version changes in the serialization and deserialization process. Mechanisms for managing version changes can vary depending on the serialization method used. Version compatibility JSON is one of the most popular formats for exchanging data between a client and a server, and it's supported by almost every programming language (including C#). To work with JSON in Unity, you can use the classes from the namespace. JSON JsonUtility UnityEngine Let's see how it's done. First, let's create a data class: [System.Serializable] public class PlayerData { public string playerName; public int score; } Next, we can use the and methods for serialization and deserialization, respectively. ToJson FromJson public void ExampleUsage() { // Create an instance of player data PlayerData player = new PlayerData { playerName = "TestPlayer", score = 5000 }; // Serialize player data into JSON string json = JsonUtility.ToJson(player); Debug.Log(json); // Output: {"playerName":"TestPlayer","score":5000} // Decerialize JSON back to player date PlayerData deserializedPlayer = JsonUtility.FromJson<PlayerData>(json); Debug.Log(deserializedPlayer.playerName); // Output: TestPlayer Debug.Log(deserializedPlayer.score); // Output: 5000 } In this example, we create an instance of the class, serialize it to a JSON string, and then deserialize it back into a object. PlayerData PlayerData Binary Serialization Moving on, is preferable to JSON serialization for several reasons. It is more efficient in terms of performance and resource utilization since data is transformed into a compact binary format. This results in faster processing time and less data volume for storage and transmission over the network. binary serialization Additionally, unlike JSON, which is easily readable and editable and is not suitable for storing and transmitting sensitive data, binary data is more difficult to read and modify, making it more secure for storing sensitive information. In Unity and C#, you can use the and classes from the and namespaces, respectively. Let's take a look at an example: BinaryFormatter MemoryStream System.Runtime.Serialization.Formatters.Binary System.IO using System.IO; using System.Runtime.Serialization.Formatters.Binary; public static byte[] Serialize(object obj) { BinaryFormatter bf = new BinaryFormatter(); using (var ms = new MemoryStream()) { bf.Serialize(ms, obj); return ms.ToArray(); } } public static T Deserialize<T>(byte[] byteArr) where T : class { using (var memStream = new MemoryStream()) { var binForm = new BinaryFormatter(); memStream.Write(byteArr, 0, byteArr.Length); memStream.Seek(0, SeekOrigin.Begin); var obj = (T) binForm.Deserialize(memStream); return obj; } } In this example, takes an object and converts it into an array of bytes. takes an array of bytes and restores it into an object of type . Serialize() Deserialize<T>() T Here are some examples of using these functions: public void ExampleUsage() { PlayerData player = new PlayerData { playerName = "TestPlayer", score = 5000 }; byte[] serializedPlayer = Serialize(player); PlayerData deserializedPlayer = Deserialize<PlayerData>(serializedPlayer); Debug.Log(deserializedPlayer.playerName); Debug.Log(deserializedPlayer.score); } Here, we've created an instance of the class, serialized it, and then deserialized it back into a object. (Note that the class must be marked as . This is important because the can only serialize classes marked with this attribute.) PlayerData PlayerData PlayerData [Serializable] BinaryFormatter ProtoBuf Google's ( ) is a binary serialization format designed to be much more compact, faster, and easier to use compared to standard formats such as JSON or XML. It not only provides efficient data serialization and deserialization, but it also speeds up data exchanges by reducing the amount of data transmitted. Protocol Buffers ProtoBuf ProtoBuf is particularly useful in situations where response time and efficient resource utilization are critical factors, such as inter-server communication or real-time transmission of large amounts of data. FlatBuffers Another widely used solution for data serialization is FlatBuffers. FlatBuffers is an efficient binary serialization format created by that provides fast and easy serialization and deserialization of structured data. The FlatBuffers protocol was designed with the features of real-time multiplayer games and applications in mind and offers several advantages over ProtoBuf. Google One of the key advantages of FlatBuffers over ProtoBuf is that it allows direct access to serialized data without prior deserialization. This means that you don't have to spend processor time deserializing entire blocks of data if you only need to extract certain parts of the data. Unlike ProtoBuf, FlatBuffers supports trees and graphs of structured data, making it a more flexible option for complex data structures. Although ProtoBuf and FlatBuffers have a lot in common, FlatBuffers may be the better choice for projects that require high performance and efficiency when working with large and complex data structures. Transmission of State and Input Developing a multiplayer game involves making many decisions, and one of the most important is choosing between synchronizing the game state and transmitting user input. State Transmission Game state transmission involves the control node (or server) receiving a command from the player processing it, and then transmitting the new game state to all participants. For example, player A sends the command to the server. The server receives the request, processes it, and informs all players that player A is now at point (1, 2). Player A, upon receiving the response from the server, moves to the new position, and the character of player A is also moved to the new position on the screens of other players. MoveTo(1, 2) In this approach, the role of the control node (or server) is important, as it provides centralized management of the game's state and ensures synchronization between all players. Let's consider a code example that demonstrates this approach. The class in this code serves as a global game state manager in the context of a networked game, using Unity's Mirror. It uses the singleton pattern to manage the only instance of : GameStateManager GameStateManager public class GameStateManager : NetworkBehaviour { private static GameStateManager instance; public static GameStateManager Instance { get { return instance; } } [Command] public void CmdMovePlayer(Vector3 position) { RpcMovePlayer(position); } [ClientRpc] private void RpcMovePlayer(Vector3 position) { // Update the player's position on all clients foreach (PlayerController player in players) { player.MoveTo(position); } } } The method is used to send a command to the server to move the player to a new position. The server then broadcasts this information to all clients using , which updates the position of each player on all client machines. This allows all clients to be aware of changes in the position of other players. CmdMovePlayer RpcMovePlayer When a player presses the spacebar, a random new position on the field is selected, and then the command is called to send this new position to the server: CmdMovePlayer public class PlayerController : NetworkBehaviour { private void Update() { if (!isLocalPlayer) return; // Handle player input if (Input.GetKeyDown(KeyCode.Space)) { // Move the player to a new position Vector3 newPosition = new Vector3(Random.Range(-5f, 5f), 0f, Random.Range(-5f, 5f)); CmdMovePlayer(newPosition); } } [Command] private void CmdMovePlayer(Vector3 position) { // Send the player's new position to the server GameStateManager.Instance.CmdMovePlayer(position); } public void MoveTo(Vector3 position) { // Move the player to the specified position transform.position = position; } } The method then passes this information to the game state manager , which updates the player's position on the server. The method is used for the actual movement of the player to the specified position on the client machine. CmdMovePlayer GameStateManager MoveTo Input transmission An alternative approach is to transmit user input. In this case, the player sends their command to all other participants, and each of them independently processes this input and updates the game state. Continuing the example with player A, they send the command to all players. After receiving confirmation from all players, Player A moves to the new position, and the character of Player A also moves to the new position on their screens. MoveTo(1, 2) Unlike the previous approach, this one does not require a control node that would centrally process all commands and update the game state. Instead, each player independently processes input and updates the game state: public class InputManager : NetworkBehaviour { public void SendInput(Vector3 moveDirection) { CmdSendInput(moveDirection); } [Command] private void CmdSendInput(Vector3 moveDirection) { RpcUpdateInput(moveDirection); } [ClientRpc] private void RpcUpdateInput(Vector3 moveDirection) { // Update the player's input on all clients foreach (PlayerController player in players) { player.UpdateInput(moveDirection); } } } This code sends player input to the server using the command , and then the server broadcasts this input to all connected clients through , updating the movement of each player on all client machines: CmdSendInput RpcUpdateInput public class PlayerController : NetworkBehaviour { private Vector3 inputDirection; private void Update() { if (!isLocalPlayer) return; // Handle player input float horizontalInput = Input.GetAxis("Horizontal"); float verticalInput = Input.GetAxis("Vertical"); inputDirection = new Vector3(horizontalInput, 0f, verticalInput).normalized; // Send the input to all other players InputManager.Instance.SendInput(inputDirection); } public void UpdateInput(Vector3 moveDirection) { // Update the player's input inputDirection = moveDirection; } } In each frame, horizontal and vertical input from the local player is read and this movement information is transmitted to all other players through . InputManager.Instance.SendInput(inputDirection) After that, the player's movement direction is updated based on incoming data from other clients using the method. UpdateInput Choosing between State Transfer and Input Transfer The choice between these two approaches largely depends on the requirements of the game. If a high degree of synchronization is required between all players and the possibility of cheating needs to be prevented, then the state transfer approach may be the preferred way. However, this usually demands more network traffic and it can also be more difficult to implement. The approach of transferring input, on the other hand, may be easier to implement and it might require less , but this assumes that the game logic must be deterministic (i.e. it should give the same result with the same input). In some cases, this can be difficult to achieve. network traffic Conclusion We've talked about techniques for serializing and deserializing data for network transmission, the formats, and some available libraries to do this. We've also discussed which data (state or user input) should be transmitted over the network and in which situations. In the next article, we will examine the main network topologies used in real-time multiplayer games!