So Unity seems to have wrapped WebRTC in a neat package. This looks like good news, since they deprecated UNET without placing a counterbalance first. Whatever.
I now just so happen to have to implement multiplayer for some games, and since my company doesn't want to invest without having a first impression of how it will be received by gamers, I have to make do without a server to handle connections. So I stumbled on WebRTC, of which DataChannels seem to be perfect for my use case, since I will have to transmit a few bytes representing the game state (which is in lockstep, so no problem there).
However, for the life of me I can't understand how this thing works.
It looks like it exchanges addresses and other data via a google STUN server, does some offer\answer shenanigans, and thus the data channel is established. However I can't understand how it knows that 2 devices are the ones that need to be connected, and I can't understand why my code doesn't work. I made a class that connects local and remote peers, so they should be able to exhange data, right?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.WebRTC;
namespace REPLAYSRL.Forza4 {
public class P2PManager : MonoBehaviour {
//---------------------------------------------- VARIABILI PRIVATE
private RTCPeerConnection localPeer, remotePeer; //Le istanze delle due rispettive sessioni peer 2 peer
private RTCDataChannel localDataChannel, remoteDataChannel; //I data channel per il peer locale e quello remoto per una comunicazione fullduplex
private RTCOfferOptions OfferOptions = new RTCOfferOptions {
iceRestart = false,
offerToReceiveAudio = true,
offerToReceiveVideo = false
};
private RTCAnswerOptions AnswerOptions = new RTCAnswerOptions {
iceRestart = false,
};
//---------------------------------------------- VARIABILI PUBBLICHE
/// <summary>
/// Se è stata stabilita una connessione P2P.
/// </summary>
public bool Is_Connected { get; private set; } = false;
/// <summary>
/// Istanza singleton della classe
/// </summary>
public static P2PManager Instance;
//---------------------------------------------- FUNZIONI PRIVATE
private void OnIceConnectionChange(RTCPeerConnection peer, RTCIceConnectionState status) {
Debug.Log(((peer == localPeer) ? "localPeer" : "remotePeer") + " Status: " + status);
}
private void OnIceCandidate(RTCPeerConnection peer, RTCIceCandidate candidate) {
((peer == localPeer) ? remotePeer : localPeer).AddIceCandidate(ref candidate);
Debug.Log("Aggiunto candidato a " + ((peer == localPeer) ? "LocalPeer" : "RemotePeer"));
}
private RTCPeerConnection GetPairedPeer(RTCPeerConnection peer) {
return (peer == localPeer) ? remotePeer : localPeer;
}
private string GetName(RTCPeerConnection peer) {
return (peer == localPeer) ? "localPeer" : "remotePeer";
}
private IEnumerator P2PConnection(DataReceivedBehaviour drb) {
RTCConfiguration peerCfg = default;
peerCfg.iceServers = new RTCIceServer[]
{
new RTCIceServer { urls = new string[] { "stun:stun.l.google.com:19302" } }
};
localPeer = new RTCPeerConnection(ref peerCfg);
localPeer.OnIceCandidate = (candidate => { OnIceCandidate(localPeer, candidate); });
localPeer.OnIceConnectionChange = (state => { OnIceConnectionChange(localPeer, state); });
/*
localPeer.OnDataChannel = (channel => {
//OnChannelCreate
Debug.Log("Canale locale creato.");
localDataChannel = channel;
localDataChannel.OnMessage = (bytes => {
drb(bytes);
});
});
*/
remotePeer = new RTCPeerConnection(ref peerCfg);
remotePeer.OnIceCandidate = (candidate => { OnIceCandidate(localPeer, candidate); });
remotePeer.OnIceConnectionChange = (state => { OnIceConnectionChange(remotePeer, state); });
remotePeer.OnDataChannel = (channel => {
//OnChannelCreate
Debug.Log("Canale remoto creato");
remoteDataChannel = channel;
remoteDataChannel.OnMessage = (bytes => {
drb(bytes);
});
});
RTCDataChannelInit dataChannelCfg = new RTCDataChannelInit(true);
dataChannelCfg.id = 0;
dataChannelCfg.reliable = true;
localDataChannel = localPeer.CreateDataChannel("data", ref dataChannelCfg);
localDataChannel.OnOpen = (() => {
//Inizializzazioni
Is_Connected = true;
});
localDataChannel.OnClose = (() => {
//Distruzioni
Is_Connected = false;
});
//Connessione
var localOfferResult = localPeer.CreateOffer(ref OfferOptions);
yield return localOfferResult;
if (!localOfferResult.IsError) {
var desc = localOfferResult.Desc;
var localDescriptionResult = localPeer.SetLocalDescription(ref desc);
yield return localDescriptionResult;
var op2 = remotePeer.SetRemoteDescription(ref desc);
yield return op2;
var op3 = remotePeer.CreateAnswer(ref AnswerOptions);
yield return op3;
if (!op3.IsError) {
desc = op3.Desc;
var op4 = remotePeer.SetLocalDescription(ref desc);
yield return op4;
var op5 = localPeer.SetRemoteDescription(ref desc);
yield return op5;
}
}
}
//---------------------------------------------- FUNZIONI PUBBLICHE
public delegate void DataReceivedBehaviour(byte[] bytes);
public void Initialize(DataReceivedBehaviour dataReceivedBehaviour) {
WebRTC.Initialize();
StartCoroutine(P2PConnection(dataReceivedBehaviour));
}
public void SendToLocal(byte[] Value) {
localDataChannel.Send(Value);
}
public void SendToLocal(string Value) {
localDataChannel.Send(Value);
}
public void SendToRemote(byte[] Value) {
remoteDataChannel.Send(Value);
}
public void SendToRemote(string Value) {
remoteDataChannel.Send(Value);
}
public void Dispose() {
if (localPeer != null)
localPeer.Close();
if (remotePeer != null)
remotePeer.Close();
WebRTC.Dispose();
}
//---------------------------------------------- FUNZIONI DI UNITY
private void Awake() {
if (Instance == null) {
Instance = this;
} else if (Instance != this) {
Destroy(this);
}
}
}
}
Apologies for the comments in italian.
I don't know, maybe I'm getting something wrong. If you happen to have any advice on other, better alternatives, I'm all ears.