-
Notifications
You must be signed in to change notification settings - Fork 9
Multiplayer
The 2D Top Down genre includes a client-authoritative multiplayer setup, demonstrating how player positions update on each other's screens. This netcode is the culmination of numerous iterations on multiplayer projects. I've lost count of how many times I've done this.
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.
Multiplayer.Preview.mp4
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
{
// NetSend attribute param indicates the order of which this gets sent
[NetSend(1)]
public string Username { get; set; }
[NetSend(2)]
public Vector2 Position { get; set; }
}
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; }
}
public class SPacketPlayerJoinLeave : ServerPacket
{
public uint Id { get; set; }
public string Username { get; set; }
public Vector2 Position { get; set; }
public bool Joined { get; set; }
// Since we need to use if conditions we actually have to type out the Write and Read functions
public override void Write(PacketWriter writer)
{
writer.Write((uint)Id);
writer.Write((bool)Joined);
if (Joined)
{
writer.Write((string)Username);
writer.Write((Vector2)Position);
}
}
public override void Read(PacketReader reader)
{
Id = reader.ReadUInt();
Joined = reader.ReadBool();
if (Joined)
{
Username = reader.ReadString();
Position = reader.ReadVector2();
}
}
}
// Player.cs
Net.Client.Send(new CPacketPlayerInfo { Username = playerUsername, Position = playerPosition });
// GameServer.cs
Send(new SPacketPlayerPositions { Positions = Positions }, peerId);
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; }
}
Important
Do not directly access properties or methods across threads unless they are explicity 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.
Important
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.