Multiplayer system with Netcode

Angelina de Best

Introduction

Managing networking in games presents a significant challenge. The goal is to ensure seamless gameplay even under conditions like 100ms ping lag and 10% packet loss. This necessitates addressing issues such as latency, packet loss, and synchronization between different game clients. These issues can significantly impact the gaming experience, making it less enjoyable for players.

In response to this problem, the proposed solution is the development of a Unity asset that streamlines the networking process. This asset includes a rollback system and a network transform that supports client prediction with extrapolation and interpolation. The purpose of these features is to tackle the challenges posed by latency and packet loss, thereby ensuring that the game operates smoothly under these conditions.

The design approach for the solution involves two primary components: a rollback system and a network transform. The rollback system is engineered to manage inconsistencies between game states on different clients due to latency or packet loss. It accomplishes this by temporarily “rolling back” the game state to a previous point, updating it with the correct information, and then “replaying” any actions that occurred since then.

The rollback system is a crucial component of the solution. It is designed to handle the inconsistencies that can arise between game states on different clients due to latency or packet loss. The system works by temporarily “rolling back” the game state to a previous point in time, updating it with the correct information, and then “replaying” any actions that have occurred since that point. This ensures that all clients have a consistent view of the game state, even in the face of network issues.

The network transform, on the other hand, supports client prediction with extrapolation and interpolation. This means that it can predict the future state of the game based on the current state and the known actions of the players. This prediction is then used to update the game state on the client side, allowing for smooth gameplay even under conditions of high latency or packet loss.

Together, these two components form a comprehensive solution to the problem of managing networking in Unity games. By addressing the issues of latency, packet loss, and synchronization, they ensure that players can enjoy a seamless and enjoyable gaming experience, regardless of the network conditions. This represents a significant advancement in the field of game development, and opens up new possibilities for the creation of complex, multiplayer games that can be played across a wide range of network conditions.

In conclusion, the complexity of managing networking in Unity games can be effectively addressed through the development of a Unity asset for Unity Netcode. This asset, which includes a rollback system and a network transform, provides a robust solution to the challenges posed by latency and packet loss. By ensuring smooth gameplay under a variety of network conditions, it enhances the gaming experience for players and represents a significant contribution to the field of game development.



Setup with Unity Netcode

Installation through Unity registry

  1. Open Unity Editor and navigate to Window > Package Manager.
  2. In the Package Manager, go to the Unity registry and import Netcode for GameObjects.

Installation through package manager

  1. Open Unity Editor and go to Window > Package Manager.
  2. In the Package Manager, select Add > Add package by name
  3. Type com.unity.netcode.gameobjects into the package name field, then select Add.

Adding the Client Transform Package

  1. For the client transform, you’ll need the NetworkTransform component which synchronizes the position, rotation, and scale of objects across the network.
  2. You can find this component in the Multiplayer Samples Utilities package, select Add > Add Git url and paste https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop.git?path=/Packages/com.unity.multiplayer.samples.coop#main into the url field, then select Add.

This will add the Client Network Transform and the Client Network Animator. When the installation is done these package should be in your package manager:

Setting Up the Network Manager

  • The NetworkManager is a key component in Unity Netcode that manages the network state of your game.
  • To set it up, create a new GameObject in your scene and add the NetworkManager component to it.
  • In the NetworkManager’s inspector, you will find settings for configuring your network environment, such as the transport protocol and network address.

Important settings:

  • Tick Rate: Set to 50, this determines how often per second the network layer will update. In Unity, the Fixed Update step runs at a default rate of 50 times per second, which is why aligning the tick rate with this ensures that network updates coincide with physics updates, providing a consistent experience across different systems.

  • Player Prefab: This is where you assign the prefab that represents players in the networked game. It’s crucial for instantiating player objects across the network.

  • Network Prefabs List: Here you can add prefabs that should be available for instantiation over the network.

  • Spawning Settings: These include options like ‘Recycle Network Ids’, which allows the reuse of network IDs after a set delay, helping to manage the number of unique identifiers needed for networked objects.

  • Bandwidth Settings: ‘Rpc Hash Size’ is set to ‘Var Int Four Bytes’, which defines the size of the hash used for Remote Procedure Calls (RPCs), affecting how much data is sent over the network for these calls

Player Spawning

  • To handle player spawning, you’ll need to create a Network Prefab for your player object.
  • Attach a NetworkObject component to your player prefab to make it network-aware.
  • Register this prefab with the NetworkManager by adding it to the Network Prefabs List in the NetworkManager’s inspector.
  • Make a simple script for starting the client and host and spawning the network object:

Example:

public class PlayerSpawner : NetworkBehaviour
{
    public GameObject playerPrefab; // Reference to your player prefab

    void Update()
    {
        if (IsOwner && IsServer || IsClient)
        {
            // Example: Spawn on spacebar press
            if (Input.GetKeyDown(KeyCode.Space))
            {
                Vector3 spawnPosition = transform.position; // Set your spawn position
                SpawnPlayer_ServerRpc(spawnPosition);
            }
        }

        if (!IsClient &&!IsHost)
        {
           if (Input.GetKeyDown(KeyCode.C))
            Networkmanager.Singleton.StartClient();
           if (Input.GetKeyDown(KeyCode.H))
            Networkmanager.Singleton.StartHost();
        }
    }

    [ServerRpc]
    private void SpawnPlayer_ServerRpc(Vector3 spawnPosition)
    {
        // Instantiate the player prefab locally (on the server)
        GameObject playerInstance = Instantiate(playerPrefab, spawnPosition, Quaternion.identity);

        // If this instance is the server, spawn it for all clients
        NetworkObject networkObject = playerInstance.GetComponent<NetworkObject>();
        networkObject.Spawn();
    }
}

NetworkTransform 

  • The NetworkTransform component is used to synchronize an object’s position, rotation, and scale across the network.
  • Add a NetworkTransform component to any GameObject that you want to synchronize.
  • Make sure the GameObject also has a NetworkObject component attached to it.

RPC and Network Variables

Remote Procedure Calls (RPCs) are a fundamental concept in Unity’s Netcode. They allow methods to be called on objects that exist in different executables, essentially enabling events to be triggered when needed. When an RPC method is invoked on one machine, it executes on another [5]. This is useful in a client-server model where a client can invoke a server RPC on a Network Object. The RPC is then placed in a local queue and sent to the server, where it is executed on the server version of the same Network Object. Similarly, a server can invoke a client RPC on a Network Object. Unity developers can declare multiple RPCs under a NetworkBehaviour, and these RPC calls will be replicated as part of its replication in a network frame.

Network Variables, on the other hand, provide a way to synchronize a property (or “variable”) between a server and clients without the need for custom messages or RPCs. A NetworkVariable is essentially a wrapper or “container” for the stored value of type T. To access the actual value being synchronized, you must use the NetworkVariable.Value property. When a NetworkVariable value changes, any connected clients that have subscribed to the NetworkVariable.OnValueChanged event will be notified of the change. It’s important to note that a NetworkVariable’s value can only be set when initializing the property or while the associated NetworkObject is spawned.

The choice between NetworkVariables and RPCs is largely determined by the specific requirements of your project. RPCs are generally employed for fleeting events or data that is only relevant at the moment it is received. On the other hand, NetworkVariables are utilized for enduring states or data that will persist for more than a brief period [6].



Unity Lobby and Relay

Unity’s Lobby and Relay services are very useful for developing multiplayer games. The Lobby service allows you to create , join, or list multiplayer game sessions, simplifying player connectivity [10] . In contrast, the Relay service ensures secure, low-latency communication between players, even those behind firewalls or NATs [14].

How to install Lobby and Relay

  1. Access Services: In Unity, open Window > General > Services. This will open the Unity Services window. You can also look at the little cloud icon in the top right corner of in the editor and click on it to open the Services window.
  2. Select Your Project: In the Services window, you’ll see a list of your projects. Click on the project you want to work on. This will open the project’s settings. On the left side, you’ll see a tab labeled Multiplayer. Click on this tab to access the multiplayer settings.
  3. Enable Relay and Lobby: In the Multiplayer settings, you’ll see another tab with Unity Services, namely Relay and Lobby. Click on both sections to expand them. There should be a switch or button labeled Enable or On next to each service. Click on these switches or buttons to enable the services. You might need to wait a few moments for the services to be enabled.
  4. Set Up Relay and Lobby: Once the Relay and Lobby services are enabled, you’ll need to set them up. Each service has its own setup instructions, which you can find in the Unity Cloud Dashboard. To access the dashboard, go to cloud.unity.com in your web browser and sign in with your Unity account. Once you’re signed in, navigate to the Products tab from the sidebar. Under Gaming Services > Multiplayer, go to Lobby and select Launch. When you launch Lobby for the first time, this adds Lobby to the Shortcuts section on the sidebar and opens the Overview page. Follow the instructions provided on the Overview page to set up the Lobby service. Repeat the process for the Relay service [14].
  5. Link Your Lobby Project in the Unity Editor: After setting up the Lobby and Relay services in the Unity Cloud Dashboard, you’ll need to link your project in the Unity Editor to these services. To do this, go back to the Unity Editor and open the Services window again (Window > General > Services). Click on the Multiplayer tab and then on the Link button. This will link your project to the Lobby and Relay services you set up in the Unity Cloud Dashboard [11] .

Creating a lobby

A lobby is a virtual room where players can gather before starting a game. When a player decides to create a new lobby in Unity, they have the ability to set several properties for their lobby. These properties can include the lobby name, its visibility (public or private), the maximum number of players it can accommodate, a password for private lobbies, and any initial custom data for the lobby or the players. An example of creating/joining a lobby [10]:

   async Task CreateNewLobbyAsync(Player player)
    {
        var newLobbyName = "NewLobby" + Guid.NewGuid();
        var maxPlayers = 8;
        var isPrivate = false;

        var newLobby = await LobbyService.Instance.CreateLobbyAsync(
            lobbyName: newLobbyName,
            maxPlayers: maxPlayers,
            options: new CreateLobbyOptions()
            {
                IsPrivate = isPrivate,
                Player = player
            });
    }

The code down below by initializing Unity Services and signing in the player. Then it creates a allocation with a maximum connections to handle the network traffic between the players. The Relay server data, which includes the allocation and the protocol (“dtls”) to be used for communication, is then set in the network transport. The function then starts hosting the lobby with a maximum of 8 connections and returns a join code that can be used by other players to join the lobby. If a join code is obtained, the lobby UI is set up and the join code is cached. A CreateLobbyOptions object is created with the join code and other necessary data. If the join code is not obtained, an error message is logged. This function is designed to handle the process of setting up a lobby with Relay, focusing on allocation and setting the server data, which differentiates it from a standard lobby setup.

public async void HostLobbyWithRelay()
    {
        try
        {
            await UnityServices.InitializeAsync();
            await SignInPlayer();

            // Create a Relay allocation with a maximum number of connections
            Allocation allocation = await RelayService.Instance.CreateAllocationAsync(8);

            // Set Relay server data in the network transport
            NetworkManager.Singleton.GetComponent<UnityTransport>().SetRelayServerData(new RelayServerData(allocation, "dtls"));

            // Start hosting the lobby and get the join code
            string joinCode = await StartHostWithRelay(8);
            Debug.Log($"Join code: {joinCode}");

            // If join code is obtained, set up lobby UI and cache the join code
            if (!string.IsNullOrEmpty(joinCode))
            {

                // Create lobby data object and add the join code
                CreateLobbyOptions lobbyData = new CreateLobbyOptions()
                {
                    IsPrivate = false,
                    Player = new Player(AuthenticationService.Instance.PlayerId, null, data: new Dictionary<string, PlayerDataObject>()),
                    Data = new Dictionary<string, DataObject>(){
                        {"JoinCode", new DataObject(DataObject.VisibilityOptions.Public, joinCode)}
                    }
                };
            }
            else
            {
                Debug.LogError("Failed to start hosting with relay.");
            }
        }

Joining a lobby

Once a lobby has been created, other players can join it. To join a lobby, players need to specify the lobby ID or provide a lobby code. Here’s an example of how you might join a lobby by specifying a lobby ID:

return await LobbyService.Instance.JoinLobbyByIdAsync(
            lobbyId: lobby.Id,
            options: new JoinLobbyByIdOptions()
            {
                Player = player
            });

Joining with relay is possible with a join code which is a small change in the code:

var joinAllocationData = await RelayService.Instance.JoinAllocationAsync(joinCode: joinCode);

Lobby queries

Lobby queries are a way for players to discover public lobbies they might want to join. Players can browse through the lobbies and view their public information. They can also specify one or more filters when using the query API to view only the lobbies that fit their parameters. For example, players can filter by lobby name, maximum lobby size, available player slots, and creation date, or by using custom data such as game type, map, or level requirements. This makes it easier for players to find a lobby that suits their preferences.

   async Task<List<Lobby>> FindOpenLobbiesAsync()
    {
        var queryFilters = new List<QueryFilter>
        {
            new QueryFilter(
                field: QueryFilter.FieldOptions.AvailableSlots,
                op: QueryFilter.OpOptions.GT,
                value: "0")
        };

        var queryOrdering = new List<QueryOrder>
        {
            new QueryOrder(false, QueryOrder.FieldOptions.Created),
        };

        var response = await LobbyService.Instance.QueryLobbiesAsync(new QueryLobbiesOptions()
        {
            Count = 20,
            Filters = queryFilters,
            Order = queryOrdering,
        });

        return response?.Results;
    }


Client authorative movement vs Server authorative movement

Which to use

The choice between client authoritative movement and server authoritative movement in online games depends on whether your game is competitive, with who it is supposed to be played with and what a developers budget is (time and money wise).

Games like Valorant and Counter-Strike: Global Offensive (CS:GO) opt for server authoritative movement to prevent cheating [1] . In these highly competitive games, having the server validate all player actions and positions helps maintain a level playing field for everyone. This is crucial for maintaining the integrity of the game and ensuring that skill, not exploits, determines success. To avoid input lag these games implement client-sided prediction so it moves the client locally and the server checks afterwards if the positions are synced but this can cause rubberbanding on higher latency [2] . To prevent input delay, these games use client-sided prediction. This means your device moves your character immediately based on your input, without waiting for confirmation from the server. Later, the server checks if everything matches up with everyone else’s actions. However, when there’s high latency, this can lead to “rubberbanding,” where your character jumps back and forth as the server catches up with your movements.

On the other hand, games like Minecraft (coop survival) mainly use client authoritative movement, especially in multiplayer survival mode. This simplifies development and reduces server load, which is important for a game focused on creativity and exploration rather than strict competition. By letting the client handle movement and interactions.

Network Transform vs Client Network Transform vs Client Predictive Network Transform

In Unity netcode, a network transform is a tool that helps synchronize a player’s position, rotation, and scale across the network. It’s handy because it saves bandwidth by only sending updates for what’s being used. Since Unity netcode follows a server authoritative model, clients can’t directly move a network transform [15]. However, clients can still benefit from it by receiving updates from the server about other players’ movements, keeping the game consistent for everyone involved. Additionally, Unity provides a feature called the client network transform, which allows clients to have some authority over movement.

When making a game that is server authoritative with client-side prediction, using Unity’s network transforms won’t be of any help. Since the Network Transform doesn’t support local movement (in the sense of networking), I decided to create my own network transform which allows the client to move locally, but it won’t update until the server updates the client’s input. The movement isn’t handled in this script, only the transform updates and the interpolation and extrapolation.

Prediction

This picture below illustrates extrapolation and interpolation [7]:

In networking, prediction involves estimating the future state of a network element or traffic based on historical data. Extrapolation and interpolation are two methods used for prediction:

  • Extrapolation is the process of estimating values beyond the known range of data. In the context of the code snippet down below, it’s used to predict the position of a networked object when there’s a delay in receiving updates. The extrapolationPercentage variable determines how much the prediction should lean towards the last known direction of movement.
  • Interpolation is when you estimate values that are inside the range of your current data. It’s used in networking to make sure that the movement of objects is smooth when updates are received. This avoids any sudden jumps or jerky movements. Codes often use methods like Vector3.Lerp for straight-line movement and Quaternion.Lerp for rotation to smoothly transition from one point to another.

The code down below explained:

  • If the player is local (IsLocalPlayer), it sends its current position and rotation to the server using SendTransformToServerRpc.
  • For non-local players, the script predicts their position and rotation. If useExtrapolation is true and the time since the last update is less than the fixed delta time, it calculates an extrapolated position and applies it.
  • If the time since the last update is longer, it falls back to interpolating between the last known position (lastServerPosition) and the target position (targetPosition) received from the server.
  • The TargetUpdateTransformClientRpc method updates the target position and rotation for all clients, which is then used for interpolation or extrapolation.
if (useExtrapolation && Time.time - lastServerTimestamp < Time.fixedDeltaTime)
            {
                float timeSinceLastUpdate = Time.time - lastServerTimestamp;
                Vector3 extrapolatedPosition = lastServerPosition + (targetPosition - lastServerPosition) * (timeSinceLastUpdate * extrapolationPercentage);

                if (!useInterpolation)
                {
                    transform.position = extrapolatedPosition;
                    transform.rotation = targetRotation;
                }
                else if (useSlerp)
                {
                    transform.position = Vector3.Slerp(transform.position, extrapolatedPosition, lerpRate * Time.fixedDeltaTime);
                    transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationLerpRate * Time.fixedDeltaTime);
                }
                else
                {
                    transform.position = Vector3.Lerp(transform.position, extrapolatedPosition, lerpRate * Time.fixedDeltaTime);
                    transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, rotationLerpRate * Time.fixedDeltaTime);
                }
            }
            else
            {
                 if (!useInterpolation)
                {
                    transform.position = targetPosition;
                    transform.rotation = targetRotation;
                }
                else if (useSlerp)
                {
                    transform.position = Vector3.Slerp(transform.position, targetPosition, lerpRate * Time.fixedDeltaTime);
                    transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotationLerpRate * Time.fixedDeltaTime);
                }
                else
                {
                    transform.position = Vector3.Lerp(transform.position, targetPosition, lerpRate * Time.fixedDeltaTime);
                    transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, rotationLerpRate * Time.fixedDeltaTime);
                }
            }

Summarized (TL:DR)

The client uses prediction to guess its own movements by running the same movement logic as the server. This helps to reduce input lag, which is the delay between pressing a button and seeing the action on screen. For other players, the client uses interpolation and extrapolation. Interpolation smooths out their movements by filling in the gaps between the actual positions reported by the server. Extrapolation is like an educated guess of where they will be next, based on their last known positions and speeds. This way, everyone’s movement looks smoother and more responsive. Here is the before and after.

Before:

After

Deterministic Input

Determinism in gaming means that the game will always produce the same outcome from the same initial conditions. This predictability is vital for multiplayer games to ensure that all players see the same game state, despite any differences in hardware or network conditions. In a deterministic system, every action, like moving a character or firing a weapon, has a predefined result that will occur no matter how many times you repeat it, given that all the initial conditions are the same [13].

Non-determinism, on the other hand, introduces variability. In a non-deterministic system, the same initial conditions can lead to different outcomes. This can be due to various factors like random number generation, complex interactions within the game’s physics engine, or differences in processing speed. Non-determinism can make a game feel more dynamic or realistic, but it can also lead to inconsistencies, which can be problematic in competitive gaming environments [13].

Input message and encryption

EncryptInput function is using AES encryption to secure the InputMessage. This could be particularly useful if the InputMessage contains sensitive data such as control commands in a game or application that you wouldn’t want to be intercepted or tampered with. The encryption process makes it much more difficult for any malicious actor to manipulate the input data, enhancing the security of your system.[12]

public byte[] EncryptInput(InputMessage input)
    {
        byte[] inputBytes = new byte[41];
        Buffer.BlockCopy(BitConverter.GetBytes(input.frameID), 0, inputBytes, 0, 4);
        Buffer.BlockCopy(BitConverter.GetBytes(input.forwardInput), 0, inputBytes, 4, 4);
        Buffer.BlockCopy(BitConverter.GetBytes(input.rightInput), 0, inputBytes, 8, 4);
        Buffer.BlockCopy(BitConverter.GetBytes(input.lookDirectionY), 0, inputBytes, 16, 4);

        Buffer.BlockCopy(BitConverter.GetBytes(input.jumpInput ? 1 : 0), 0, inputBytes, 28, 1);
        Buffer.BlockCopy(BitConverter.GetBytes(input.isRunning ? 1 : 0), 0, inputBytes, 29, 1);
        Buffer.BlockCopy(BitConverter.GetBytes(input.clientId), 0, inputBytes, 30, 8);

        using (Aes aesAlg = Aes.Create())
        {
            aesAlg.Key = encryptionKey;
            aesAlg.Mode = CipherMode.CBC;
            aesAlg.IV = new byte[16]; 

            ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

            using (MemoryStream msEncrypt = new MemoryStream())
            {
                using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
                {
                    csEncrypt.Write(inputBytes, 0, inputBytes.Length);
                    csEncrypt.FlushFinalBlock();
                }
                return msEncrypt.ToArray();
            }
        }
    }
InputMessage inputMessage = new InputMessage
            {
                frameID = NetworkSingleton.Instance.frameID.Value,
                forwardInput = Input.GetAxis("Vertical"),
                rightInput = Input.GetAxis("Horizontal"),
                lookDirectionY = playerCamera.transform.eulerAngles.y,
                jumpInput = Input.GetKey(KeyCode.Space),
                isRunning = Input.GetKey(KeyCode.LeftShift) && Stamina > 0
            };

            byte[] encryptedInput = NetworkSingleton.Instance.EncryptInput(inputMessage);

Rollback System

Rollback systems are used to maintain consistency in multiplayer games, especially games with an competitive egde . Due to communication delays across the network, users may have an inconsistent view of the game world. Rollback systems address this issue by using rollback mechanisms to correct inconsistencies that occur due to the disorder of the arrival of update messages [9].

In this rollback system, local inputs are processed immediately while remote inputs are predicted. When the actual remote inputs arrive over the network, the system checks if anything was mispredicted. If a misprediction is found, time is rewound to the mispredicted frame and then simulated back to the present using the corrected input values and further predicted values. This rollback and correction process all happens in the span of a single frame such that the user only sees a slight pop in the results.[9]

The code down below is the rollback function for the position of the player. It checks if the network objects id is in the dictionary and it searching the state which is on that frame id and then it return its position.

public Vector3 Rollback(ulong netObjId, ulong frameNr, Vector3 originalPosition)
    {
        if (localDictionary.ContainsKey(netObjId) && localDictionary[netObjId].Count > 0)
        {
            List<(ulong frameId, Vector3 position)> states = localDictionary[netObjId];

            // Find the index of the state corresponding to the specified frame ID
            int frameIndex = states.FindIndex(state => state.frameId == frameNr);

            if (frameIndex != -1)
            {
                // Get the position from the state
                Debug.LogFormat("Rollback for Object : {0} on Frame :{1}", netObjId, frameNr);
                Vector3 position = states[frameIndex].position;
                return position;
            }
            else
            {
                Debug.LogErrorFormat("No state found for Object : {0} on Frame :{1}", netObjId, frameNr);
                return originalPosition;
            }
        }
        else
        {
            Debug.LogError($"NetworkObject with ID {netObjId} not found in objectStates dictionary.");
            return originalPosition;
        }
    }

This code defines a method SaveState that is used to save the state of a network object identified by netObjId at a specific frameId with a given position. The state is stored in a local dictionary localDictionary. If the network object does not exist in the dictionary, a new list is created for it. The state is then added to the list. If the number of states for the network object exceeds a maximum limit maxStates, the oldest state is removed. Finally, the state is serialized to a JSON string using the SerializeToJson method and synchronized across the network using the SyncStatesRpc method. The serialization is necessary for Rpc calls.

public void SaveState(ulong netObjId, ulong frameId, Vector3 position)
    {
        // Check if the object already exists in the dictionary
        if (!localDictionary.ContainsKey(netObjId))
        {
            Debug.Log($"Adding list for NetworkObject with ID {netObjId}");
            localDictionary[netObjId] = new List<(ulong frameId, Vector3 position)>();
        }

        // Add the current state to the list of states for the object
        localDictionary[netObjId].Add((frameId, position));

        // Check if the number of states exceeds the maximum allowed
        if (localDictionary[netObjId].Count > maxStates)
        {
            Debug.Log($"Removing oldest state for NetworkObject with ID {netObjId}");
            localDictionary[netObjId].RemoveAt(0);
        }
        string json = SerializeToJson(frameId, position);
        SyncStatesRpc(netObjId, json);
    }

Here is a video of the rollback when a player has 100% packet loss:

Conclusion

The development of a network game can be significantly enhanced with the right implementations. The incorporation of a rollback system ensures both safety and balance within the games, while network transforms, with interpolation and extrapolation, contribute to an improved gaming experience.

Looking back, the project’s scope, especially concerning Research & Development, indicated that adding multiplayer was overly ambitious. What might have taken only a few minutes in a single-player game could translate to hours in a multiplayer setup, highlighting the necessity for a more cautious approach to defining scope in upcoming projects.

Looking ahead, I will be working on the development of the asset and adding more variables that work with Rpcs.

In summary, this project had its difficulties but also taught me a lot. I’ve learned about managing scope and the specific challenges of making multiplayer games. These lessons will help me in future projects. I’ll also apply what I’ve learned to keep developing the asset. Even though it was tough, this project was an important step in my game development journey.

References

  1. Valve Developer Community, “Source Multiplayer Networking,” Valve Corporation, [Online]. Available: https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking#Input_prediction. [Accessed: Apr. 7, 2024].
  2. Riot Games Technology, “Peeking VALORANT’s Netcode,” Riot Games, [Online]. Available: https://technology.riotgames.com/news/peeking-valorants-netcode. [Accessed: Apr. 7, 2024].
  3. G. Gambetta, “Client-Side Prediction and Server Reconciliation,” [Online]. Available: https://gabrielgambetta.com/client-side-prediction-server-reconciliation.html. [Accessed: Apr. 7, 2024].
  4. Coder’s Block, “Client-Side Prediction in Unity 2018,” [Online]. Available: https://www.codersblock.org/blog/client-side-prediction-in-unity-2018. [Accessed: Apr. 7, 2024].
  5. Unity Multiplayer Docs, “Messaging System,” Unity Technologies, [Online]. Available: https://docs-multiplayer.unity3d.com/netcode/current/advanced-topics/messaging-system/. [Accessed: Apr. 7, 2024].
  6. Unity Multiplayer Docs, “RPCs and Network Variables,” Unity Technologies, [Online]. Available: https://docs-multiplayer.unity3d.com/netcode/current/learn/rpcvnetvar/. [Accessed: Apr. 7, 2024].
  7. Statology, “Interpolation vs. Extrapolation,” [Online]. Available: https://www.statology.org/interpolation-vs-extrapolation/. [Accessed: Apr. 7, 2024].
  8. Unity Technologies, “Multiplayer Community Contributions,” GitHub, [Online]. Available: https://github.com/Unity-Technologies/multiplayer-community-contributions/pull/130/commits/65c5ddf8cf221525b142a650b642dbffa491aee9. [Accessed: Apr. 7, 2024].
  9. YouTube, “Netcode Explained,” [Video]. Available: https://www.youtube.com/watch?v=W3aieHjyNvw&t=1371s. [Accessed: Apr. 7, 2024].
  10. Unity Documentation, “Create a Lobby,” Unity Technologies, [Online]. Available: https://docs.unity.com/ugs/en-us/manual/lobby/manual/create-a-lobby. [Accessed: Apr. 7, 2024].
  11. Unity Documentation, “Get Started with the Lobby,” Unity Technologies, [Online]. Available: https://docs.unity.com/ugs/en-us/manual/lobby/manual/get-started. [Accessed: Apr. 7, 2024].
  12. Get Blog You, “How to Decrypt AES Encrypted File in C#,” [Online]. Available: https://getblogyou.blogspot.com/2023/07/how-to-decrypt-aes-encrypted-file-in-csharp.html. [Accessed: Apr. 7, 2024].
  13. YouTube, “Unity Multiplayer Tutorial,” [Video]. Available: https://www.youtube.com/watch?v=lCfouAH_N5w. [Accessed: Apr. 7, 2024].
  14. “Unity relay.” https://docs.unity.com/ugs/manual/relay/manual/introduction.
  15. “Tricks and patterns to deal with latency | Unity Multiplayer Networking,” Jan. 25, 2024. https://docs-multiplayer.unity3d.com/netcode/current/learn/dealing-with-latency/

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *