Writing A Custom Network Integration
Dissonance is built to be completely decoupled from the underlying networking system, this allows Dissonance to run on top of various different Unity networking assets (e.g. UNet, Forge, Photon etc) just by swapping which Dissonance network component is used. If none of the existing integrations work for your application then you may need to build a custom network integration.
Getting Started⚓︎
Dissonance includes a set of base classes which implement most of the networking logic for you:
BaseCommsNetwork
- This is the main comms network component which you place into your scene. It manages the networking, starting and stopping Dissonance networking in response to network events.BaseServer
- This is a class created by the comms network component on one of the peers in the session. It manages the session as other peers join and leave. You will extend this class to implement your server logic.BaseClient
- This is a class created by the comms network component on all the peers in the session. It manages sending and receiving voice. You will extend this class to implement your client logic.
Create the CustomCommsNetwork
class which extends BaseCommsNetwork
:
public class CustomCommsNetwork
: BaseCommsNetwork<
CustomServer, // A class which implements BaseServer
CustomClient, // A class which implements BaseClient
CustomPeer, // A struct which represents a network connection
Unit, // Nothing
Unit // Nothing
>
{
}
As you can see BaseCommsNetwork
requires 5 type parameters, which specify all the parts of your custom network integration:
CustomServer
- This is a class you will create which extendsBaseServer
CustomClient
- This is a class you will create which extendsBaseClient
CustomPeer
- This is a struct you will create which represents another peer in the network session.CustomClientParam
- This is a struct you will create which contains the data necessary create a network connection (e.g. an IP address). If your network does not need this (e.g. it is already running before Dissonance is started) then just passUnit
.CustomServerParam
- This is a struct you will create which contains the data necessary host a network session (e.g. a port number). If your network does not need this (e.g. it is already running before Dissonance is started) then just passUnit
.
To create all these types define two new classes:
class CustomCommsNetwork : BaseCommsNetwork {}
class CustomClient : BaseClient {}
class CustomServer : BaseServer {}
And three new structs:
struct CustomPeer : IEquatable<CustomPeer> {}
struct CustomServerParam {}
struct CustomClientParam {}
Once you have done this you will have a large number of build errors like "abstract member [...] not implemented" - these are the things you must implement before the network integration can work.
CustomCommsNetwork : BaseCommsNetwork
⚓︎
In your custom comms network class you will need to create your custom client and custom server objects. Dissonance will call these methods when a server or client needs to be created. You shouldn't connect to the network in this method, simply create the objects.
protected override CustomServer CreateServer(CustomServerParam details)
{
return new CustomServer(this, details);
}
protected override CustomClient CreateClient(CustomClientParam details)
{
return new CustomClient(this, details);
}
If you need to do any other setup work for your network system you can override the Initialize
method.
protected override void Initialize()
{
Network.DoSomethingImportant();
// Don't forget to call base.Initialize!
base.Initialize();
}
Finally you need to start the network and tell it to connect, there are two main techniques for this. Some integrations (e.g. Mirror/Photon) have a network system which is already connected and Dissonance can use that, other integrations (e.g. WebRTC) host a network session specifically for voice chat.
If you are using the first technique then you need to monitor the external network system and make sure that Dissonance is running in the same way as the network system by calling RunAsHost
, RunAsDedicatedServer
, RunAsClient
or Stop
. Here is how this is implemented in the HLAPI network integration:
// Check every frame
protected override void Update()
{
// Check if Dissonance is ready
if (IsInitialized)
{
// Check if the HLAPI is ready
var networkActive = NetworkManager.singleton.isNetworkActive && (NetworkServer.active || NetworkClient.active);
if (networkActive)
{
// Check what mode the HLAPI is in
var server = NetworkServer.active;
var client = NetworkClient.active;
// Check what mode Dissonance is in and if
// they're different then call the correct method
if (Mode.IsServerEnabled() != server || Mode.IsClientEnabled() != client)
{
// HLAPI is server and client, so run as a non-dedicated
// host (passing in the correct parameters)
if (server && client)
RunAsHost(Unit.None, Unit.None);
// HLAPI is just a server, so run as a dedicated host
else if (server)
RunAsDedicatedServer(Unit.None);
// HLAPI is just a client, so run as a client
else if (client)
RunAsClient(Unit.None);
}
}
else if (Mode != NetworkMode.None)
{
//Network is not active, make sure Dissonance is not active
Stop();
}
}
base.Update();
}
If you are using the second technique then you will need to decide when to call RunAsHost
, RunAsDedicatedServer
, RunAsClient
or Stop
at the appropriate times.
CustomClient : BaseClient
⚓︎
This class handles all of the client side logic of Dissonance, one of these will be created on every single peer in the session (including the host). There will be two build errors to fix in this class.
Base class [...] doesn't contain a parameterless constructor
. To fix this simply add a constructor which passes a CustomCommsNetwork
to the base class:
public CustomClient(CustomCommsNetwork network, CustomClientParam details)
: base(network)
{
}
abstract member [...] not implemented
. There will be four of these errors to fix:
public override void Connect()⚓︎
This will be called when you need to connect to the session. You should start connecting to the network when this is called, once you have finished connecting (which may take a long time) you must call base.Connected()
. In systems where there is already a network connection setup you may just immediately call base.Connected()
.
protected override void ReadMessages()⚓︎
This will be called periodically to poll messages from the network system. Any packets you receive must be passed to base.NetworkPacketReceived
.
protected override void SendReliable(ArraySegment packet)⚓︎
This method sends a packet to the server using a reliable and in order channel (e.g. TCP). Packets sent with this method are not latency sensitive but MUST arrive in order. If you detect that a reliable packet has been lost you should immediately stop the Dissonance network session.
protected override void SendUnreliable(ArraySegment packet)⚓︎
This methods sends a packet to the server using an unreliable and unordered channel (e.g. UDP). Packets sent with this method are extremely latency sensitive and must arrive as soon as possible or not at all. It is expected that some packets sent using this method may be lost or arrive out of order.
CustomServer : BaseServer
⚓︎
This class handles all the server side logic of Dissonance, one of these will be created on a single peer in the session and handles managing the session. In the basic configuration all voice data is relayed via this peer (see P2P section for details on how to avoid this). There will be five "abstract member [...] not implemented" errors to fix:
public override void Connect()
⚓︎
This will be called when you need to host a new network session (e.g. open a socket).
public override void Disconnect()
⚓︎
This will be called when you need to stop hosting a session (e.g. close the socket).
protected override void ReadMessages()
⚓︎
This will be called periodically to poll messages from the network system. Any packets you receive should be to base.NetworkPacketReceived
. The base.NetworkPacketReceived
method on the server requires an instance of your CustomPeer
type which indicates who sent the message.
protected override void SendReliable(CustomPeer destination, ArraySegment<byte> packet)
⚓︎
This method sends a reliable packet to another peer using a reliable and in order channel (e.g. TCP). Packets sent with this method are not latency sensitive but MUST arrive in order. If you detect that a reliable packet has been lost you should immediately stop the Dissonance network session.
If you need some extra information about who the packet is being sent to, you should add it to the CustomPeer
struct. Remember to go to the ReadMessages
method and add that information to the CustomPeer
struct you passed in to NetworkPacketReceived
.
protected override void SendUnreliable(CustomPeer destination, ArraySegment<byte> packet)
⚓︎
This methods sends a packet to the server using an unreliable and unordered channel (e.g. UDP). Packets sent with this method are extremely latency sensitive and must arrive as soon as possible or not at all. It is expected that some packets sent using this method may be lost or arrive out of order.
ClientDisconnected
⚓︎
When a peer disconnects from the server you must call ClientDisconnected
to notify the server.
Editor Inspector⚓︎
Finally you should create an inspector for your CustomCommsNetwork. Doing this is very simple, extend the BaseDissonanceCommsNetworkEditor
class and pass the same 5 generic types you defined above. Attach the CustomEditor
attribute to the class.
[CustomEditor(typeof(CustomCommsNetwork))]
public class CustomCommsNetworkEditor
: BaseDissonnanceCommsNetworkEditor<
CustomCommsNetwork,
CustomServer,
CustomClient,
CustomConn,
CustomClientParam,
CustomServerParam
>
{
}
This will set up a basic inspector for you.
Testing⚓︎
At this point you should have a basic voice chat system functioning with your custom network. You should set up a test scene to test it. While the test scene is running check these things:
- Look at the inspector for your
CustomCommsNetwork
component.- Once the network session is started the
Mode
should shows "Server & Client", "Client" or "Server" depending on the mode this peer is running in. - Once the network session has connected the
Connection Status
should show "Connected"
- Once the network session is started the
- Try sending a text chat message.
- Create a broadcast and receipt trigger and speak.
- Look at the inspector for the
DissonanceComms
component. It shows a list of client in the session, disconnect a client and make sure they disappear.
Extensions: Loopback⚓︎
The Dissonance networking system create a CustomClient
and a CustomServer
on the host machine (unless running a dedicated server). The server must be able to send and receive message to this local peer the same as any other peer. This can cause complications with some network systems which do not handle this kind of "loopback" correctly. You must also be careful to make sure you can distinguish messages from other peers to the host - make sure that they don't get processed by the host client object.
To handle this many of the Dissonance integrations have a special check for loopback. For example in the HLAPI integration there is a HlapiCommsNetwork:PreprocessPacketToClient
method which is given all packets sent from the server to the client, it checks if the packet is a loopback packet and if so it passes it directly to the client and HLAPI itself never has to deal with this packet.
internal bool PreprocessPacketToClient(ArraySegment<byte> packet,
HlapiConn destination)
{
// No client means this can't be loopback
if (Client == null)
return false;
// HLAPI way to check if this is loopback.
if (NetworkManager.singleton.client.connection != destination.Connection)
return false;
// This is loopback!
// check that we have a valid local client,
// in cases of startup or in-progress shutdowns
if (Client != null)
{
// Don't immediately deliver the packet, add it to a queue and
// deliver it next frame. This prevents the local client from
// executing "within" the local server which can cause
// confusing stack traces.
_loopbackQueue.Add(packet.CopyTo(_loopbackBuffers.Get()));
}
return true;
}
Extensions: Peer To Peer⚓︎
Currently the network integration you have built sends all packets to the server, which then relays them to other clients. If possible you may want to implement peer to peer voice communications. However, you should consider the bandwidth of your game before implementing peer to peer as it is not always beneficial to use it.
In a non P2P setup voice follows a path like:
Speaker -> Server -> Listener #1
-> Listener #2
-> Listener #3
In this case the bandwidth used by the speaker is 1 voice stream ~20 kilobits/second
. The bandwidth used by each listener is 1 voice stream ~20 kilobits/second
. The bandwidth used by the server is (Speakers + Listeners) * Bandwidth = (1 + 3) * ~20 = ~80 kilobits/second
. In this setup the bandwidth of each client (speaker or listener) is the minimum possible. If your game uses client devices with tight bandwidth limits this may be the best setup.
In a P2P setup the voice follows a different path:
Speaker -> Listener #1
-> Listener #2
-> Listener #3
The bandwidth on the server has been reduced (to zero). However, the total bandwidth for the speaker client is now Listeners * Bandwidth = 3 * ~20 = ~60 kilobits/second
.
Implementing P2P⚓︎
If you have decided to use peer to peer you need to modify your CustomClient
class. Wherever you call NetworkReceivePacket
you should modify it to capture the return value of the method call, if the value is not call ReceiveHandshakeP2P
with it and a CustomPeer
object for the sender of the message. For example in the Photon Unity Networking (PUN) integration the receiving code is implemented like this:
// This event is called by PUN when a packet arrives
public void PacketDelivered(byte eventcode, ArraySegment<byte> data,
int senderid)
{
// Skip events we don't care about
if (eventcode != _network.EventCodeToClient)
return;
// Receive the packet, capture return value
var id = NetworkReceivedPacket(data);
// If the value is not null
// pass to handshake method with the `senderid` of this packet
if (id.HasValue)
ReceiveHandshakeP2P(id.Value, senderid);
}
You now need to implement two more methods for sending packets:
SendReliableP2P(List<ClientInfo<TPeer?>> destinations, ArraySegment<byte> packet)
⚓︎
SendUnreliableP2P(List<ClientInfo<TPeer?>> destinations, ArraySegment<byte> packet)
⚓︎
These methods send a packet to a list of destinations. You should send the packet to as many of these destinations as possible and remove them from the list. Once you are done call the base method with the remaining items in the list, they will be sent via the server as usual. For example the PUN implementation of this is:
private void SendUnreliableP2P(IList<ClientInfo<int?>> destinations,
ArraySegment<byte> packet)
{
// Build a list of destinations we know how to send to
// i.e. have a non-null Connection object
var dests = new List<int>();
foreach (var item in destinations)
if (item.Connection.HasValue)
dests.Add(item.Connection);
// Remove all the ones we can send to from the input list
destinations.RemoveAll(dests);
// Send the packets to the list of destinations through PUN
_network.Send(packet, dests, reliable: false);
// Call base to do server relay for all the peers we don't
// know how to contact
base.SendUnreliableP2P(destinations, packet);
}
Because there is a fall-back mechanism you can mix P2P and non-P2P packets as necessary. For example you start by sending everything via the server, establish a p2p connection between clients and if it fails (e.g. due to firewall or NAT settings) you can simply keep on sending via relay for that specific pair of clients. Alternatively you could monitor client bandwidth and send via P2P if there is spare bandwidth - falling back to server relay if the client is close to reaching it's bandwidth limit.
Finally you need to start establishing p2p connections. Override the OnServerAssignedSessionId
method, when this is called you should send a "handshake" packet to every peer you know how to contact directly. This will tell those peers that you are available for p2p communication. For example in the PUN integration this is implemented as:
protected override void OnServerAssignedSessionId(uint session, ushort id)
{
base.OnServerAssignedSessionId(session, id);
// Create the handshake packet to send
var packet = new ArraySegment<byte>(WriteHandshakeP2P(session, id));
// Send this to everyone else in the session through PUN
_network.Send(packet, _network.EventCodeToClient, new RaiseEventOptions {
Receivers = ReceiverGroup.Others,
}, true);
}