Unity Multiplayer Tutorial: Netcode for GameObjects and Join Codes

Illustration of four players connected through a central server

Multiplayer is the feature that most often turns a fun prototype into a six-month detour. The good news is that Unity’s current stack makes the first version much simpler than it used to be. This Unity multiplayer tutorial uses Netcode for GameObjects (NGO) in Unity 6. You’ll build a small game where players join, move, see each other and shoot, then connect over the internet with a join code using Multiplayer Services sessions and Relay, so nobody has to port-forward. Along the way it explains the three ideas everything else builds on: ownership, NetworkVariables and RPCs.

Pick Your Multiplayer Stack

Option Best for
Netcode for GameObjects (Unity) Co-op and small competitive games (2–16 players) built with normal GameObjects. Covered here.
Netcode for Entities (Unity) Large-scale, fast competitive games with client prediction, built on DOTS/ECS. Much steeper learning curve.
Mirror, FishNet, Photon Fusion Third-party libraries with their own strengths, communities and (for Photon) hosted servers.

Decide early whether your game is host-based (one player’s game acts as the server, ideal for co-op and cheap) or needs dedicated servers (competitive games where you don’t trust any player). NGO supports both, and the code is the same. This tutorial uses a host.

Install and Set Up the NetworkManager

  1. In Package Manager, install Netcode for GameObjects (com.unity.netcode.gameobjects). It brings in Unity Transport.
  2. Also install Multiplayer Play Mode for testing, and Multiplayer Services (com.unity.services.multiplayer) for the online section.
  3. Create an empty GameObject called NetworkManager, add the NetworkManager component, and set its Transport to Unity Transport (click Select Transport if it asks).

Then add three temporary buttons so you can start a session. Put this on any object in the scene:

using Unity.Netcode;
using UnityEngine;

public class NetworkButtons : MonoBehaviour
{
    void OnGUI()
    {
        if (NetworkManager.Singleton.IsClient || NetworkManager.Singleton.IsServer) return;

        GUILayout.BeginArea(new Rect(10, 10, 200, 120));
        if (GUILayout.Button("Host"))   NetworkManager.Singleton.StartHost();
        if (GUILayout.Button("Client")) NetworkManager.Singleton.StartClient();
        if (GUILayout.Button("Server")) NetworkManager.Singleton.StartServer();
        GUILayout.EndArea();
    }
}

Host = server plus a local player. Client connects to a host or server (by default at 127.0.0.1:7777, set on Unity Transport). Server runs without a local player.

Make a Networked Player

  1. Create a capsule called Player and add a NetworkObject component. Every object that exists on the network needs one.
  2. Turn it into a prefab and delete it from the scene.
  3. On the NetworkManager, drag the prefab into Player Prefab. NGO spawns one for each player who connects, owned by that player.

Any other prefab you spawn at runtime (bullets, pickups, enemies) must be added to the NetworkManager’s Network Prefabs list, and spawned on the server:

var go = Instantiate(bulletPrefab, pos, rot);
go.GetComponent<NetworkObject>().Spawn();   // server only

The key idea: ownership and authority

The server is in charge of the game state. Each NetworkObject also has an owner (the server by default, or the player whose player object it is). Scripts that derive from NetworkBehaviour get properties to check who they are running as:

  • IsServer: this code is running on the server (or host).
  • IsOwner: this copy of the object belongs to this player.
  • IsLocalPlayer: this is my own player object.

The same script runs on every machine, once per copy of the object. Most multiplayer bugs are code running on copies it shouldn’t, like every client reading the keyboard and moving every player.

Sync Movement with NetworkTransform

Add a NetworkTransform component to the Player prefab. It replicates position, rotation and scale. By default the server has authority, so only the server can move it. For a simple co-op game, let the owner move their own player: set NetworkTransform’s Authority Mode to Owner (in older NGO versions, use the ClientNetworkTransform sample script instead). Then only the owner reads input:

using Unity.Netcode;
using UnityEngine;

public class PlayerMovement : NetworkBehaviour
{
    [SerializeField] float speed = 5f;

    void Update()
    {
        if (!IsOwner) return;   // only move my own player

        var input = new Vector3(Input.GetAxisRaw("Horizontal"), 0, Input.GetAxisRaw("Vertical"));
        transform.position += input.normalized * speed * Time.deltaTime;
    }
}

Owner authority feels instant for the moving player, but it trusts the client, so a cheater could teleport. For competitive games, keep server authority: the owner sends input to the server with an RPC, and the server moves the player.

Sync State with NetworkVariables

A NetworkVariable<T> is a value the server writes and every client receives automatically, including players who join later. Use it for persistent state: health, score, team, a door being open.

using Unity.Netcode;
using UnityEngine;

public class Health : NetworkBehaviour
{
    public NetworkVariable<int> Current = new(100);   // server writes, everyone reads

    public override void OnNetworkSpawn()
    {
        Current.OnValueChanged += (oldValue, newValue) =>
            Debug.Log($"{name} health {oldValue} -> {newValue}");
    }

    public void TakeDamage(int amount)
    {
        if (!IsServer) return;          // only the server changes game state
        Current.Value = Mathf.Max(0, Current.Value - amount);
    }
}

NetworkVariables support primitives, structs that implement INetworkSerializable, and fixed strings like FixedString64Bytes. Do your setup in OnNetworkSpawn, not Start, because network values aren’t ready before the object spawns.

Send Actions with RPCs

RPCs (remote procedure calls) are one-off messages: “I fired”, “play this explosion”. In NGO 2.x you mark a method with [Rpc] and say who receives it. The method name must end in Rpc.

using Unity.Netcode;
using UnityEngine;

public class PlayerShoot : NetworkBehaviour
{
    [SerializeField] NetworkObject bulletPrefab;

    void Update()
    {
        if (IsOwner && Input.GetMouseButtonDown(0))
            FireRpc(transform.position + transform.forward, transform.rotation);
    }

    [Rpc(SendTo.Server)]
    void FireRpc(Vector3 pos, Quaternion rot)
    {
        // runs on the server: validate (cooldown, ammo) before trusting the client
        var bullet = Instantiate(bulletPrefab, pos, rot);
        bullet.Spawn();
        PlayMuzzleFlashRpc();
    }

    [Rpc(SendTo.Everyone)]
    void PlayMuzzleFlashRpc()
    {
        // cosmetic effect on every machine
    }
}

Rule of thumb: state goes in NetworkVariables, events go in RPCs. A late joiner sees the current NetworkVariable values, but never receives RPCs sent before they joined.

🧰 Tools that save time on multiplayer projects

Save systems, input rebinding and editor tooling get harder once networking is involved. We picked the Asset Store tools that are worth paying for as an indie developer.

See the best Unity Asset Store tools →

Test with Multiplayer Play Mode

Building the game every time you want to test two players is painfully slow. Multiplayer Play Mode runs up to four players from one Editor: open Window > Multiplayer > Multiplayer Play Mode, tick one or more virtual players, and press Play. Each virtual player opens in its own window. Click Host in the main Editor and Client in the others.

For latency testing, the Network Simulator component (from the Multiplayer Tools package) adds lag and packet loss, so you find out early how your game feels at 150 ms instead of on launch day.

Play Online: Sessions, Relay and Join Codes

Connecting to 127.0.0.1 only works on one machine. Over the internet, the host is usually behind a router that blocks incoming connections. Relay solves this by routing traffic through Unity’s servers, and sessions (from the Multiplayer Services package) wrap Relay, lobbies and join codes into a few calls.

  1. Link your project to a Unity Cloud project: Edit > Project Settings > Services.
  2. Enable Relay (and optionally Lobby) for the project in the Unity Cloud dashboard. Both have a free tier.
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Multiplayer;
using UnityEngine;

public class OnlineSession : MonoBehaviour
{
    ISession session;

    async void Start()
    {
        await UnityServices.InitializeAsync();
        if (!AuthenticationService.Instance.IsSignedIn)
            await AuthenticationService.Instance.SignInAnonymouslyAsync();
    }

    public async void Host()
    {
        var options = new SessionOptions { MaxPlayers = 4 }.WithRelayNetwork();
        session = await MultiplayerService.Instance.CreateSessionAsync(options);
        Debug.Log("Join code: " + session.Code);   // show this to your friend
    }

    public async void Join(string code)
    {
        session = await MultiplayerService.Instance.JoinSessionByCodeAsync(code);
    }
}

With a NetworkManager in the scene, creating a session starts the host and joining starts the client, so you don’t call StartHost yourself. Replace the OnGUI buttons with a Host button and a code field connected to these methods. Unity also ships ready-made Multiplayer Widgets (Create/Join/Quick Join session UI) if you’d rather not build the menu yourself.

Troubleshooting

Client connects, but other players don’t appear

The prefab isn’t set as the Player Prefab or isn’t in the Network Prefabs list, or it’s missing a NetworkObject component.

“NetworkPrefab hash was not found”

The client and host have different Network Prefabs lists, usually because one is an old build. Rebuild both, and make sure every runtime-spawned prefab is registered.

Every player moves when I press a key

Your input code is missing if (!IsOwner) return;, so it runs on every copy of every player.

The client’s movement snaps back

The NetworkTransform is server-authoritative, so the server keeps overwriting the client’s position. Use Owner authority or send input to the server.

“Client is not allowed to write to this NetworkVariable”

By default only the server writes. Change the value inside a server RPC, or set the variable’s write permission to Owner if the owning client should write it.

Joining by code fails

Check the project is linked under Services, Relay is enabled in the dashboard, both players are signed in, and the code is entered exactly as shown. Log the exception message, which names the failing service.

FAQ

How do I make a multiplayer game in Unity?

Install Netcode for GameObjects, add a NetworkManager with Unity Transport, give your player prefab a NetworkObject, sync it with NetworkTransform, NetworkVariables and RPCs, and use Multiplayer Services sessions with Relay to play online.

Is Unity multiplayer free?

Netcode for GameObjects is free. Relay, Lobby and sessions have free tiers that cover development and small games, then usage-based pricing.

Netcode for GameObjects or Mirror?

Both work well for small games. NGO is Unity’s official solution and integrates with Unity’s services. Mirror has a long history and a large community. Pick one and stick with it.

What is the difference between a NetworkVariable and an RPC?

A NetworkVariable is replicated state that late joiners also receive. An RPC is a one-off message delivered only to whoever is connected at that moment.

Do I need a dedicated server?

Not for co-op or casual games. A player can host, and Relay handles connectivity. Competitive games with cheating concerns benefit from dedicated servers.

How do I test multiplayer without building?

Use the Multiplayer Play Mode package to run up to four players from the Editor.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *