Multiplayer Networking: Unity Netcode

What is Unity Netcode?

Unity Netcode for GameObjects (NGO) is Unity's official networking solution that makes it easy to create multiplayer games. It handles the complex aspects of networking like synchronization, client-server architecture, and data replication, allowing you to focus on your game logic.

Why Use Unity Netcode?

  • Built-in: Official Unity solution with full support
  • Simple: Easy to set up and use compared to third-party solutions
  • Flexible: Works with both client-server and peer-to-peer architectures
  • Scalable: Handles everything from 2-player games to massive multiplayer experiences
  • Integrated: Works seamlessly with Unity's other systems

Who This Chapter is For

This chapter is for developers who want to add multiplayer functionality to their Unity games. You should have completed the previous Unity chapters and be comfortable with C# scripting and Unity's component system.

Understanding Multiplayer Architecture

Client-Server Model

  • Server: Authoritative game state, handles all game logic
  • Clients: Send input to server, receive game state updates
  • Host: One client that also runs the server (common for smaller games)

Key Concepts

  • NetworkObject: Components that can be synchronized across the network
  • RPCs: Remote Procedure Calls for client-server communication
  • Variables: Network variables that automatically sync when changed
  • Spawn/Despawn: Network objects that can be created and destroyed

Setting Up Unity Netcode

1. Install Netcode Package

  1. Open Window > Package Manager
  2. Switch to Unity Registry
  3. Search for "Netcode for GameObjects"
  4. Click Install

2. Create a Network Manager

using Unity.Netcode;
using UnityEngine;

public class NetworkManager : NetworkBehaviour
{
    public override void OnNetworkSpawn()
    {
        if (IsServer)
        {
            Debug.Log("Server started!");
        }
        else
        {
            Debug.Log("Client connected!");
        }
    }
}

3. Basic Network Setup

using Unity.Netcode;
using UnityEngine;

public class GameManager : NetworkBehaviour
{
    [SerializeField] private GameObject playerPrefab;

    public override void OnNetworkSpawn()
    {
        if (IsServer)
        {
            // Spawn players when they connect
            NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
        }
    }

    private void OnClientConnected(ulong clientId)
    {
        // Spawn player for new client
        GameObject player = Instantiate(playerPrefab);
        player.GetComponent<NetworkObject>().SpawnAsPlayerObject(clientId);
    }
}

Creating Network Objects

1. Make Objects Networkable

using Unity.Netcode;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    [SerializeField] private float moveSpeed = 5f;

    public override void OnNetworkSpawn()
    {
        // Only the owner can control this player
        if (!IsOwner) return;

        // Enable input for this player
        GetComponent<PlayerInput>().enabled = true;
    }

    private void Update()
    {
        if (!IsOwner) return;

        // Handle movement
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        Vector3 movement = new Vector3(horizontal, 0, vertical) * moveSpeed * Time.deltaTime;
        transform.Translate(movement);
    }
}

2. Network Variables

using Unity.Netcode;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    // Network variables automatically sync across clients
    public NetworkVariable<int> Health = new NetworkVariable<int>(100);
    public NetworkVariable<Vector3> Position = new NetworkVariable<Vector3>();

    public override void OnNetworkSpawn()
    {
        // Subscribe to network variable changes
        Health.OnValueChanged += OnHealthChanged;
        Position.OnValueChanged += OnPositionChanged;
    }

    private void OnHealthChanged(int oldValue, int newValue)
    {
        Debug.Log($"Health changed from {oldValue} to {newValue}");
        // Update UI, play effects, etc.
    }

    private void OnPositionChanged(Vector3 oldValue, Vector3 newValue)
    {
        // Smoothly move to new position
        transform.position = Vector3.Lerp(transform.position, newValue, Time.deltaTime * 10f);
    }
}

Remote Procedure Calls (RPCs)

1. Server RPCs

using Unity.Netcode;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    [ServerRpc]
    public void TakeDamageServerRpc(int damage)
    {
        // Only runs on server
        Health.Value -= damage;

        if (Health.Value <= 0)
        {
            DieClientRpc();
        }
    }

    [ClientRpc]
    public void DieClientRpc()
    {
        // Runs on all clients
        Debug.Log("Player died!");
        // Play death animation, effects, etc.
    }
}

2. Client RPCs

using Unity.Netcode;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    [ClientRpc]
    public void ShowMessageClientRpc(string message)
    {
        // Show message on all clients
        Debug.Log(message);
    }

    public void SendMessageToAll(string message)
    {
        if (IsServer)
        {
            ShowMessageClientRpc(message);
        }
    }
}

Player Movement and Input

1. Networked Movement

using Unity.Netcode;
using UnityEngine;

public class NetworkPlayer : NetworkBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float jumpForce = 10f;

    private Rigidbody rb;
    private bool isGrounded;

    public override void OnNetworkSpawn()
    {
        rb = GetComponent<Rigidbody>();

        if (!IsOwner)
        {
            // Disable input for other players
            GetComponent<PlayerInput>().enabled = false;
        }
    }

    private void Update()
    {
        if (!IsOwner) return;

        HandleMovement();
        HandleJump();
    }

    private void HandleMovement()
    {
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");

        Vector3 movement = new Vector3(horizontal, 0, vertical) * moveSpeed;
        rb.velocity = new Vector3(movement.x, rb.velocity.y, movement.z);
    }

    private void HandleJump()
    {
        if (Input.GetKeyDown(KeyCode.Space) && isGrounded)
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = true;
        }
    }

    private void OnCollisionExit(Collision collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isGrounded = false;
        }
    }
}

Game State Synchronization

1. Network Variables for Game State

using Unity.Netcode;
using UnityEngine;

public class NetworkGameState : NetworkBehaviour
{
    public NetworkVariable<int> Score = new NetworkVariable<int>(0);
    public NetworkVariable<float> GameTime = new NetworkVariable<float>(0f);
    public NetworkVariable<bool> GameStarted = new NetworkVariable<bool>(false);

    public override void OnNetworkSpawn()
    {
        if (IsServer)
        {
            // Start game timer
            InvokeRepeating(nameof(UpdateGameTime), 1f, 1f);
        }
    }

    private void UpdateGameTime()
    {
        if (IsServer && GameStarted.Value)
        {
            GameTime.Value += 1f;
        }
    }

    [ServerRpc]
    public void StartGameServerRpc()
    {
        GameStarted.Value = true;
    }

    [ServerRpc]
    public void AddScoreServerRpc(int points)
    {
        Score.Value += points;
    }
}

2. Synchronized Game Objects

using Unity.Netcode;
using UnityEngine;

public class NetworkPickup : NetworkBehaviour
{
    public NetworkVariable<bool> IsCollected = new NetworkVariable<bool>(false);

    public override void OnNetworkSpawn()
    {
        IsCollected.OnValueChanged += OnCollectedChanged;
    }

    private void OnCollectedChanged(bool oldValue, bool newValue)
    {
        if (newValue)
        {
            // Hide pickup when collected
            gameObject.SetActive(false);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (!IsCollected.Value && other.CompareTag("Player"))
        {
            CollectPickupServerRpc();
        }
    }

    [ServerRpc]
    private void CollectPickupServerRpc()
    {
        IsCollected.Value = true;
    }
}

Pro Tips

1. Optimize Network Traffic

  • Use NetworkVariable for frequently changing data
  • Use RPCs for one-time events
  • Consider client-side prediction for smooth movement
  • Use interpolation for smooth object movement

2. Handle Network Issues

  • Implement reconnection logic for dropped connections
  • Use heartbeat systems to detect disconnections
  • Handle lag compensation for competitive games
  • Implement rollback systems for fast-paced games

3. Security Considerations

  • Never trust client data - validate everything on server
  • Use server authority for critical game logic
  • Implement anti-cheat measures for competitive games
  • Use encryption for sensitive data

Common Mistakes to Avoid

1. Client Authority Issues

// ❌ WRONG - Don't let clients control game state
[ClientRpc]
public void UpdateScoreClientRpc(int newScore)
{
    Score.Value = newScore; // This can be exploited!
}

// ✅ CORRECT - Server controls game state
[ServerRpc]
public void UpdateScoreServerRpc(int points)
{
    Score.Value += points; // Server validates and updates
}

2. Network Object Spawning

// ❌ WRONG - Don't spawn without proper setup
Instantiate(playerPrefab); // This won't sync!

// ✅ CORRECT - Use NetworkObject.Spawn()
GameObject player = Instantiate(playerPrefab);
player.GetComponent<NetworkObject>().Spawn();

3. RPC Timing Issues

// ❌ WRONG - Don't call RPCs before network is ready
void Start()
{
    SendMessageClientRpc("Hello!"); // Network might not be ready
}

// ✅ CORRECT - Wait for network spawn
public override void OnNetworkSpawn()
{
    SendMessageClientRpc("Hello!");
}

Troubleshooting

Common Issues and Solutions

Issue: Players can't see each other

  • Solution: Ensure NetworkObjects are properly spawned and have NetworkObject components

Issue: Movement feels laggy

  • Solution: Implement client-side prediction and server reconciliation

Issue: Game state desyncs between clients

  • Solution: Use server authority and validate all game logic on server

Issue: RPCs not working

  • Solution: Check that NetworkBehaviour is properly set up and RPCs are called after OnNetworkSpawn

Next Steps

Now that you understand Unity Netcode basics, you can:

  1. Create your first multiplayer game with simple player movement
  2. Add game mechanics like scoring, power-ups, and objectives
  3. Implement advanced features like lag compensation and anti-cheat
  4. Deploy your game to Unity's Relay service for easy multiplayer hosting

Related Topics

  • Unity Netcode Documentation: Official Unity networking guide
  • Multiplayer Game Design: Learn about multiplayer game mechanics
  • Network Security: Understand multiplayer security best practices
  • Game Servers: Learn about dedicated server hosting

Conclusion

Unity Netcode makes multiplayer game development accessible and powerful. By understanding the client-server model, NetworkObjects, RPCs, and proper game state management, you can create engaging multiplayer experiences that scale from small co-op games to massive multiplayer worlds.

Remember: Server authority is key - always validate game logic on the server to prevent cheating and ensure fair gameplay for all players.