Skip to content

Multiplayer

Valk edited this page Jan 11, 2025 · 24 revisions
Multiplayer.Preview.mp4

(Note that all visual glitch-like artifacts seen in the video have been resolved, I'm just too lazy to make a new video)

The 2D Top Down genre includes a client-authoritative multiplayer setup, demonstrating how player positions update on each other's screens.

Note

Each packet comes with a small overhead—either 1 or 2 bytes, depending on reliability configured—and a one-byte opcode to identify its purpose. Everything else in the packet is strictly the data we need to send.

🌱 First Look at a Client Packet

Below is an example of a client packet. The client uses this packet to inform the server of its position. To actually do something with Position on the server, override the Handle method from ClientPacket.

public class CPacketPlayerInfo : ClientPacket
{
    // The NetSend parameter indicates the order of what gets sent first
    [NetSend(1)]
    public string Username { get; set; }

    [NetSend(2)]
    public Vector2 Position { get; set; }
}

🌿 First Look at a Server Packet

Below is an example of a server packet. The server uses this packet to inform each client about the position updates of all other clients. To actually do something with Positions on the client, override the Handle method from ServerPacket.

public class SPacketPlayerPositions : ServerPacket
{
    [NetSend(1)]
    public Dictionary<uint, Vector2> Positions { get; set; }
}

Note

If you need more control on how data is sent in a packet, read this.

📦 Sending a Packet from the Client

// Player.cs
Net.Client.Send(new CPacketPlayerInfo { Username = playerUsername, Position = playerPosition });

🎁 Sending a Packet from the Server

// GameServer.cs
Send(new SPacketPlayerPositions { Positions = Positions }, peerId);

⛔ Net Exclude Attribute

Lets say you want to [NetSend] this PlayerData but you don't want to send the PrevPosition.

public class PlayerData
{
    public string Username { get; set; }
    public Vector2 Position { get; set; }

    [NetExclude] // [NetSend] will ignore PrevPosition
    public Vector2 PrevPosition { get; set; }
}

Side Notes

  • Do not directly access properties or methods across threads unless they are explicitly marked as thread safe. Not following thread safety will result in random crashes with no errors logged to the console. Things on the client thread should stay on the client thread and things on the server thread should stay on the server thread. If you need to communicate between them use the existing ConcurrentQueues.

  • A common oversight is using one data type for writing and another for reading. For example, if you have an integer playerCount and you write it with writer.Write(playerCount), but then read it as a byte with playerCount = reader.ReadByte(), the data will be malformed because playerCount wasn't converted to a byte prior to writing. To avoid this, ensure you cast your data to the correct type before writing, even if it feels redundant.

You thought there was more? Nope! That's it! Multiplayer has never felt easier.