Skip to content

Commit

Permalink
Change to use Unity.WebRTC for multiplayer transport
Browse files Browse the repository at this point in the history
  • Loading branch information
kiyohome committed Jul 9, 2023
1 parent 76c1745 commit 67806d3
Show file tree
Hide file tree
Showing 85 changed files with 1,508 additions and 132 deletions.
File renamed without changes.
File renamed without changes.
24 changes: 24 additions & 0 deletions Assets/Extreal/NGO/Extreal.NGO.Dev.asmdef
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "Extreal.NGO.Dev",
"rootNamespace": "",
"references": [
"Unity.WebRTC",
"Unity.Netcode.Runtime",
"Unity.Netcode.Components",
"UniTask",
"UniRx",
"Extreal.Core.Logging",
"Extreal.Core.Common",
"Extreal.Integration.Multiplay.NGO",
"Extreal.P2P.Dev"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
7 changes: 7 additions & 0 deletions Assets/Extreal/NGO/Extreal.NGO.Dev.asmdef.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;

namespace Extreal.SampleApp.Holiday.App.Extreal.NGO
namespace Extreal.NGO.Dev
{
public class NgoHost : NgoServer
{
Expand Down
File renamed without changes.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions Assets/Extreal/NGO/WebRTC/IdMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;

namespace Extreal.NGO.WebRTC.Dev
{
public class IdMapper
{
private readonly Dictionary<string, ulong> strToLongMapping = new Dictionary<string, ulong>();
private readonly Dictionary<ulong, string> ulongToStrMapping = new Dictionary<ulong, string>();

public void Add(string id)
{
var ulongId = Generate();
strToLongMapping.Add(id, ulongId);
ulongToStrMapping.Add(ulongId, id);
}

private ulong Generate()
{
var now = DateTimeOffset.UtcNow;
var id = now.ToUnixTimeMilliseconds() + strToLongMapping.Count;
return (ulong)id;
}

public bool Has(string id) => strToLongMapping.ContainsKey(id);

public ulong Get(string id) => strToLongMapping[id];

public bool Has(ulong id) => ulongToStrMapping.ContainsKey(id);

public string Get(ulong id) => ulongToStrMapping[id];

public void Remove(string id)
{
if (!Has(id))
{
return;
}
var ulongId = strToLongMapping[id];
strToLongMapping.Remove(id);
ulongToStrMapping.Remove(ulongId);
}

public void Clear()
{
strToLongMapping.Clear();
ulongToStrMapping.Clear();
}
}
}
3 changes: 3 additions & 0 deletions Assets/Extreal/NGO/WebRTC/IdMapper.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

210 changes: 210 additions & 0 deletions Assets/Extreal/NGO/WebRTC/NativeWebRtcClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Extreal.Core.Logging;
using Extreal.P2P.Dev;
using Unity.Netcode;
using Unity.WebRTC;

namespace Extreal.NGO.WebRTC.Dev
{
public class NativeWebRtcClient : WebRtcClient
{
private static readonly ELogger Logger = LoggingManager.GetLogger(nameof(NativeWebRtcClient));

private readonly Dictionary<string, List<RTCDataChannel>> dcDict;
private readonly IdMapper idMapper;
private readonly Queue<WebRtcEvent> events;
private readonly PeerClient peerClient;

public NativeWebRtcClient(NativePeerClient peerClient)
{
dcDict = new Dictionary<string, List<RTCDataChannel>>();
idMapper = new IdMapper();
events = new Queue<WebRtcEvent>();
this.peerClient = peerClient;
peerClient.AddPcCreateHook(CreatePc);
peerClient.AddPcCloseHook(ClosePc);
}

private void CreatePc(string id, bool isOffer, RTCPeerConnection pc)
{
if (peerClient.Role == PeerRole.Client && id != peerClient.HostId)
{
return;
}

if (isOffer)
{
var dc = pc.CreateDataChannel("multiplay");
HandleDc(id, true, dc);
}
else
{
pc.OnDataChannel = (dc) => HandleDc(id, false, dc);
}
}

private void HandleDc(string id, bool isOffer, RTCDataChannel dc)
{
if (!dcDict.ContainsKey(id))
{
dcDict.Add(id, new List<RTCDataChannel>());
idMapper.Add(id);
}

dcDict[id].Add(dc);
var clientId = idMapper.Get(id);

if (isOffer) // Host only
{
dc.OnOpen = () =>
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{nameof(dc.OnOpen)}: clientId={clientId}");
}
events.Enqueue(new WebRtcEvent(NetworkEvent.Connect, clientId));
};
}

// Both Host and Client
dc.OnMessage = message =>
{
if (dc.ReadyState != RTCDataChannelState.Open)
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{nameof(dc.OnMessage)}: DataChannel is not open. clientId={clientId}");
}
return;
}
events.Enqueue(new WebRtcEvent(NetworkEvent.Data, clientId, ToByte(message)));
};
dc.OnClose = () =>
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{nameof(dc.OnClose)}: clientId={clientId}");
}
events.Enqueue(new WebRtcEvent(NetworkEvent.Disconnect, clientId));
};
}

private static byte[] ToByte(byte[] message)
{
var strBuf = System.Text.Encoding.ASCII.GetString(message);
var str2Array = strBuf.Split('-');
var byteBuf = new byte[str2Array.Length];
for(var i = 0; i < str2Array.Length; i++){
byteBuf[i] = Convert.ToByte(str2Array[i], 16);
}
return byteBuf;
}

private void ClosePc(string id)
{
if (!dcDict.ContainsKey(id))
{
return;
}
dcDict[id].ForEach(dc => dc.Close());
dcDict.Remove(id);
idMapper.Remove(id);
}

public override void Connect(WebRtcRole role)
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{nameof(Connect)}: role={role}");
}
Role = role;
if (Role == WebRtcRole.Client)
{
var hostId = GetHostId();
if (hostId != HostIdNotFound)
{
events.Enqueue(new WebRtcEvent(NetworkEvent.Connect, hostId));
}
}
}

private static readonly ulong HostIdNotFound = 0;

private ulong GetHostId()
{
var hostId = peerClient.HostId;
if (hostId is null)
{
return HostIdNotFound;
}
return idMapper.Has(hostId) ? idMapper.Get(hostId) : HostIdNotFound;
}

public override void Send(ulong clientId, ArraySegment<byte> payload)
{
if (clientId == NetworkManager.ServerClientId)
{
clientId = idMapper.Get(peerClient.HostId);
}

if (!idMapper.Has(clientId))
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{nameof(Send)}: clientId not found. clientId={clientId}");
}
return;
}

var id = idMapper.Get(clientId);
dcDict[id].ForEach((dc) =>
{
if (dc.ReadyState != RTCDataChannelState.Open)
{
if (Logger.IsDebug())
{
Logger.LogDebug($"{nameof(Send)}: DataChannel is not open. clientId={clientId}");
}
return;
}
dc.Send(ToStr(payload));
});
}

private static string ToStr(ArraySegment<byte> payload)
{
if (0 < payload.Offset || payload.Count < payload.Array.Length)
{
var buf = new byte[payload.Count];
Buffer.BlockCopy(payload.Array!, payload.Offset, buf, 0, payload.Count);
return BitConverter.ToString(buf);
}

return BitConverter.ToString(payload.Array);
}

public override WebRtcEvent PollEvent() => events.Count > 0 ? events.Dequeue() : WebRtcEvent.Nothing;

public override void Disconnect()
{
if (Role == WebRtcRole.Client)
{
var hostId = GetHostId();
if (hostId != HostIdNotFound)
{
events.Enqueue(new WebRtcEvent(NetworkEvent.Disconnect, hostId));
}
}
}

public override void Shutdown()
{
dcDict.Keys.ToList().ForEach(ClosePc);
dcDict.Clear();
idMapper.Clear();
events.Clear();
}
}
}
3 changes: 3 additions & 0 deletions Assets/Extreal/NGO/WebRTC/NativeWebRtcClient.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions Assets/Extreal/NGO/WebRTC/WebRtcClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using Extreal.Core.Common.System;

namespace Extreal.NGO.WebRTC.Dev
{
public abstract class WebRtcClient : DisposableBase
{
protected WebRtcRole Role { get; set; }
public abstract void Connect(WebRtcRole role);
public abstract void Send(ulong clientId, ArraySegment<byte> payload);
public abstract WebRtcEvent PollEvent();
public abstract void Disconnect();
public abstract void Shutdown();
}
}
3 changes: 3 additions & 0 deletions Assets/Extreal/NGO/WebRTC/WebRtcClient.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions Assets/Extreal/NGO/WebRTC/WebRtcClientProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Diagnostics.CodeAnalysis;
using Extreal.P2P.Dev;

namespace Extreal.NGO.WebRTC.Dev
{
public static class WebRtcClientProvider
{
[SuppressMessage("Style", "CC0038")]
public static WebRtcClient Provide(PeerClient peerClient)
{
#if !UNITY_WEBGL || UNITY_EDITOR
return new NativeWebRtcClient(peerClient as NativePeerClient);
#endif
#if UNITY_WEBGL && !UNITY_EDITOR
return null;
#endif
}
}
}
3 changes: 3 additions & 0 deletions Assets/Extreal/NGO/WebRTC/WebRtcClientProvider.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions Assets/Extreal/NGO/WebRTC/WebRtcEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Unity.Netcode;

namespace Extreal.NGO.WebRTC.Dev
{
public class WebRtcEvent
{
public static readonly WebRtcEvent Nothing = new WebRtcEvent();

public NetworkEvent Type { get; private set; }
public ulong ClientId { get; private set; }
public byte[] Payload { get; private set; }

private WebRtcEvent() : this(NetworkEvent.Nothing, ulong.MinValue)
{
}

public WebRtcEvent(NetworkEvent type, ulong clientId, byte[] payload = null)
{
Type = type;
ClientId = clientId;
Payload = payload;
}

public override string ToString() => $"{nameof(Type)}: {Type}, {nameof(ClientId)}: {ClientId}";
}
}
3 changes: 3 additions & 0 deletions Assets/Extreal/NGO/WebRTC/WebRtcEvent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 67806d3

Please sign in to comment.