-
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
ValksGodotTools/Template
ensures that only the bare minimum data is transmitted, without any unnecessary details like function names. Each packet comes with a small overhead—either 1 or 2 bytes, depending on reliability configured—and a compact 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. The Handle(...)
method is executed on the server thread, so only elements accessible on that thread should be accessed.
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.
public class CPacketPosition : ClientPacket
{
[NetSend(1)]
public Vector2 Position { get; set; }
public override void Handle(ENetServer s, Peer client)
{
// The packet handled server-side (ENet Server thread)
}
}
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. The Handle(...)
method is executed on the client thread, so only elements accessible on that thread should be accessed.
public class SPacketPlayerPositions : ServerPacket
{
[NetSend(1)]
public Dictionary<uint, Vector2> Positions { get; set; }
public override void Handle(ENetClient client)
{
// The packet handled client-side (Godot thread)
}
}
This client packet sends the username then the position in this order.
public class CPacketJoin : ClientPacket
{
[NetSend(1)]
public string Username { get; set; }
[NetSend(2)]
public Vector2 Position { get; set; }
public override void Handle(ENetClient client)
{
// The packet handled client-side (Godot thread)
}
}
Do not use the NetSend attribute if you need to use conditional logic.
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.
public class SPacketPlayerJoinLeave : ServerPacket
{
public uint Id { get; set; }
public string Username { get; set; }
public Vector2 Position { get; set; }
public bool Joined { get; set; }
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();
}
}
public override void Handle(ENetClient client)
{
// The packet handled client-side (Godot thread)
}
}
// Player.cs
Game.Net.Client.Send(new CPacketPosition
{
Position = Position
});
Send(new SPacketPlayerPositions
{
Positions = GetOtherPlayers(pair.Key).ToDictionary(x => x.Key, x => x.Value.Position)
}, Peers[pair.Key]);
Using the [NetExclude]
attribute will exclude properties from being written or read in the network.
public class PlayerData
{
public string Username { get; set; }
public Vector2 Position { get; set; }
[NetExclude]
public Vector2 PrevPosition { get; set; }
}