Working with P2P Networking in Unity, C#, and Facepunch.Steamworks
August 31, 2017
Tagged: Unity Networking
Networking feels like a massive task to take on as an indie and the truthfulness of it is that it becomes harder and harder the later you start trying to implement it. This is to say that, if you plan on having your game be networked, you ideally start implementing network functionality from day one.
The reason for this is that, when you’re working with networked code in a P2P context, you’ll want to be able to use the same code that executes local game logic to be the same code that you call when you need to execute an event that you receive from the network.
Otherwise, you’ll end up having two functions for every call, one that works locally and one that the network calls that then calls the local function. The second method is totally viable but the former allows for much cleaner code. When you receive a call from the network, you just call the same function that you would normally call to move something locally.
To elaborate a bit more, here’s how I’m doing this in my game.
To start, I just write down what the action I went sent over the network. These are usually “big actions” that affect moment to moment play like moving, shooting, spawning, etc. Once you know the action, you need to come up with your own network packet type that your game understands. “Network Packet” sounds both scary and archaic, but it loosely translates to “data structure that your game understands and can be easily sent across a network”.
The goal here is to send the minimum amount of information required to still successfully produce the required action on the side of the person you’re send to. You want your packets to be small so you can ensure that you are using a lot of data over the network due to any sort of interruptions.
Thinking about what this is can be difficult depending on the action you’re implementing, but, if you’re reusing client code of network code it’s pretty easy — it’s likely just the function parameters! So, let’s assume I’m trying to move something, and get started with my move function.
public void moveToTile(List<Tile> tilesToMoveTo, bool sendNetworkAction)
{
(lots of code here I took out for clarity sake)
Events.CanMove.InteractableMoved(Interactable, tilesToMoveTo, sendNetworkAction);
}
When I move something, I do the moving (in the omitted code), then send out an event that states that something has moved. This event is subscribed to by my class P2PHandler, and when it receives this event calls the requisite function that bundles up the network packet to be sent:
public P2PHandler()
{
//subscribe to the move event
Events.CanMove.InteractableMoved += sendP2PMove;
}
void sendP2PMove(Interactable interactable, List<Tile> tilesMovedTo, bool sendNetworkAction)
{
//this is here so the call isn’t infinitely bounced back and forth.
if(!sendNetworkAction){return;}
List<Vector2> tilePositions = new List<Vector2>();
foreach (Tile tile in tilesMovedTo)
{
//NOTE A
tilePositions.Add(new Vector2(tile.GridX, tile.GridY));
}
//NOTE B
int interactableID = interactable.OwningPlayer.InteractablesManager.GetIDForInteractable(interactable);
//build the move packet
P2PMove moveBody = new P2PMove(interactableID, tilePositions);
//build the full packet
P2PMessage message = new P2PMessage(interactable.OwningPlayer.PlayerName, P2PMessageKey.MOVE, moveBody.Serialize());
//send across the network
GameManager.NetworkManager.Client.SendP2PMessage(message);
}
A few notes on this before moving on. At points NOTE A/B, you can see that I’m kind of “casting” my function parameters to different types. I’m converting a list of Tiles to a List of Vector2’s and converting the moved interactable to just be an int. What I’m doing here is serializing my parameters into a format that can be serialized by Unity.
Serialization is a whole topic in and of itself, but the core idea is that Unity cannot remember every detail about any type you declare, and instead only “knows” about a specific set of types outlined here. It is possible to make it so that something like Tile could just be easily sent across the wire as is, but often time optimizing for serialization can lead to a lot of excess code and headaches as you may have to jump through extra hoops to get at simple parameters.
What I prefer doing for bigger types like this is to just send a integer reference that points to the object on the client side, and make sure that those tables mirror each other on initialization.
To actually send this data, I use P2PMessage which is defined like so:
//Unity will only serialize “custom structs with the Serializable attribute”
[System.Serializable]
public struct P2PMessage
{
public string Player;
public P2PMessageKey Key;
public string Body;
public P2PMessage(string player, P2PMessageKey key, string body)
{
Player = player;
Key = key;
Body = body;
}
}
public enum P2PMessageKey
{
MOVE,
SPAWN
}
public interface P2PAction
{
string Serialize();
}
[System.Serializable]
public struct P2PMove : P2PAction
{
public int InteractableID;
public List<Vector2> Locations;
public P2PMove(int interactableID, List<Vector2> locations)
{
InteractableID = interactableID;
Locations = locations;
}
public string Serialize()
{
//serialize this struct to JSON using Unity’s built in serializer
return JsonUtility.ToJson(this);
}
}
So a P2PMessage is basically a key, and then a P2PAction that is serialized in the P2PMessage constructor. P2PMove in this case is the container for the data that is necessary to properly interpret the move action on the client side. So here’s the last bit of code from above that does the packing and sending:
//build the move packet
P2PMove moveBody = new P2PMove(interactableID, tilePositions);
//build the full packet
P2PMessage message = new P2PMessage(interactable.OwningPlayer.PlayerName, P2PMessageKey.MOVE, moveBody.Serialize());
//send across the network
GameManager.NetworkManager.Client.SendP2PMessage(message);
So you package up your parameters and send off the message. I’m using Facepunch.Steamworks to do this, which makes sending P2P data really easy. Here’s the code for that:
public void SendP2PMessage(P2PMessage message)
{
//serialize the whole message
string serializedMessage = JsonUtility.ToJson(message);
//convert to bytes
var data = Encoding.UTF8.GetBytes( serializedMessage );
//loop through all members of the current lobby and send the data
//lobbies in my game act as a sort of server
foreach (ulong id in GetLobbyMemberIDs())
{
// don't send the P2P message to the player who sent the message
if(id == PlayerID){continue;}
//call the Facepunch.Steamworks DLL
Native.Networking.SendP2PPacket( id, data, data.Length );
}
}
Once you send the packet, how do you get it? In the same class that sends the P2PMessage, I’m also subscribing to P2PEvents:
void SubscribeToP2PEvents()
{
Native.Networking.SetListenChannel(0 , true);
Native.Networking.OnP2PData += OnP2PData;
Native.Networking.OnIncomingConnection += OnIncomingConnection;
Native.Networking.OnConnectionFailed += OnConnectionFailed;
}
When I get P2P data, OnP2PData is called:
void OnP2PData(ulong sender, byte[] bytes, int length, int channel)
{
var str = Encoding.UTF8.GetString( bytes, 0, length );
//de-serialize the message
P2PMessage serializedMessage = JsonUtility.FromJson<P2PMessage>(str);
GameManager.NetworkManager.HandleP2PMessage(sender, serializedMessage);
}
And in this class I grab the message, and deserialize it into it’s proper form, a P2PMessage. I then tell the NetworkManager that I got a new P2PMessage. The NetworkManager function looks like this:
public void HandleP2PMessage(ulong sender, P2PMessage msg)
{
P2PHandler.ParseP2PMessage(sender, msg);
}
Which brings us back to the same class that sent the message originally, but on the other side of the wire! For now, the P2PHandler class both sends and parses events, though it may change if things become to bulky. When it recieves a message, it is parsed like this:
public void ParseP2PMessage(ulong senderID, P2PMessage msg)
{
//grab the proper player who the action was done for
Player player = MatchManager.GetPlayer(msg.Player);
//switch on the message key
switch (msg.Key)
{
case P2PMessageKey.MOVE:
//deserialize the message body
P2PMove moveBody = JsonUtility.FromJson<P2PMove>(msg.Body);
//get tiles to move to
List<Tile> tilesToMoveTo = new List<Tile>();
foreach (Vector2 pos in moveBody.Locations)
{
//"deserialize" tile coords to tiles
tilesToMoveTo.Add(MatchManager.CurrentMap.GetTileAtCoords(pos.x, pos.y));
}
CanMove interactable = player.InteractablesManager.ActiveInteractables[moveBody.InteractableID].GetAspect<CanMove>();
//call the move function.
//note the false flag, which instructs the program to NOT also try sending the event again.
interactable.moveToTile(tilesToMoveTo, false);
break;
////other cases
}
}
This code calls the move function for the proper intractable, and now both clients are in sync! I didn’t realize this would be as long-winded when I set out to write about some P2P stuff, but I hope this helps out anyone looking to implement P2P networking in their (Unity) games! I will say that this envato tuts post was a godsend, and most of the ideas here are an outgrowth of that. I highly recommend people who are interested in this read over that, because it provides a great high level overview of P2P networking. Until next time!
Published on August 31, 2017.
Tagged: Unity Networking