Unity Mirror插件WebGL端多人联机实现
Demo地址https://csdnimg.cn/release/blog_editor_html/release2.3.7/ckeditor/plugins/CsdnLink/icons/icon-default.png?t=O83Ahttps://gitee.com/njiyue/unity-mirror-webgl-test使用Mirror插件及其开源的SimpleWebTransport实现,简单记载下碰到的题目。详细原理就不多先容了哈~ Unity版本:2022.3.48f1c1
1. Unity导入mirror插件、SimpleWebTransport包
https://i-blog.csdnimg.cn/direct/e056206724da47c3825c78aa20e6bab4.png https://i-blog.csdnimg.cn/direct/4865d65ef6a84bf3a1f036076d5dd1d8.png
2. 报错:
导入插件和SimpleWebTransportSimpleWebTransport包后,会报这个错:
https://i-blog.csdnimg.cn/direct/2c2be171e34b498192e955a14c444dcb.png
原因:步伐集关联有题目
解决方法:黑色的谁人SimpleWebTransport脚本文件放到SimpleWeb目次下
https://i-blog.csdnimg.cn/direct/8ade95a441b44fe58e062fcd18325662.png
3. 报错:
https://i-blog.csdnimg.cn/direct/37f2902c76c84583819092f7e757d918.png
原因:由于SimpleWebTransport包太久没被维护了,导致与最新版本的Mirror插件API对不上。
解决方法:
更改SimpleWebTransport 脚本的内容
https://i-blog.csdnimg.cn/direct/1efbeaafd8184e06add553503f67f0d8.png
更改为:
using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Authentication;
using UnityEngine;
using UnityEngine.Serialization;
namespace Mirror.SimpleWeb
{
public class SimpleWebTransport : Transport
{
public const string NormalScheme = "ws";
public const string SecureScheme = "wss";
public ushort port = 7778;
public int maxMessageSize = 16 * 1024;
public int handshakeMaxSize = 3000;
public bool noDelay = true;
public int sendTimeout = 5000;
public int receiveTimeout = 20000;
public int serverMaxMessagesPerTick = 10000;
public int clientMaxMessagesPerTick = 1000;
public bool batchSend = true;
[Tooltip("Waits for 1ms before grouping and sending messages. " +
"This gives time for mirror to finish adding message to queue so that less groups need to be made. " +
"If WaitBeforeSend is true then BatchSend Will also be set to true")]
public bool waitBeforeSend = false;
public bool clientUseWss;
public bool sslEnabled;
public string sslCertJson = "./cert.json";
public SslProtocols sslProtocols = SslProtocols.Tls12;
Log.Levels _logLevels = Log.Levels.none;
/// <summary>
/// <para>Gets _logLevels field</para>
/// <para>Sets _logLevels and Log.level fields</para>
/// </summary>
public Log.Levels LogLevels
{
get => _logLevels;
set
{
_logLevels = value;
Log.level = _logLevels;
}
}
void OnValidate()
{
if (maxMessageSize > ushort.MaxValue)
{
Debug.LogWarning($"max supported value for maxMessageSize is {ushort.MaxValue}");
maxMessageSize = ushort.MaxValue;
}
Log.level = _logLevels;
}
SimpleWebClient client;
SimpleWebServer server;
TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout);
public override bool Available()
{
return true;
}
public override int GetMaxPacketSize(int channelId = 0)
{
return maxMessageSize;
}
void Awake()
{
Log.level = _logLevels;
}
public override void Shutdown()
{
client?.Disconnect();
client = null;
server?.Stop();
server = null;
}
void LateUpdate()
{
ProcessMessages();
}
/// <summary>
/// Processes message in server and client queues
/// <para>Invokes OnData events allowing mirror to handle messages (Cmd/SyncVar/etc)</para>
/// <para>Called within LateUpdate, Can be called by user to process message before important logic</para>
/// </summary>
public void ProcessMessages()
{
server?.ProcessMessageQueue(this);
client?.ProcessMessageQueue(this);
}
#region Client
string GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme;
string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme;
public override bool ClientConnected()
{
// not null and not NotConnected (we want to return true if connecting or disconnecting)
return client != null && client.ConnectionState != ClientState.NotConnected;
}
public override void ClientConnect(string hostname)
{
// connecting or connected
if (ClientConnected())
{
Debug.LogError("Already Connected");
return;
}
UriBuilder builder = new UriBuilder
{
Scheme = GetClientScheme(),
Host = hostname,
Port = port
};
client = SimpleWebClient.Create(maxMessageSize, clientMaxMessagesPerTick, TcpConfig);
if (client == null) { return; }
client.onConnect += OnClientConnected.Invoke;
client.onDisconnect += () =>
{
OnClientDisconnected.Invoke();
// clear client here after disconnect event has been sent
// there should be no more messages after disconnect
client = null;
};
client.onData += (ArraySegment<byte> data) => OnClientDataReceived.Invoke(data, Channels.Reliable);//——————————————————修改
client.onError += (Exception e) =>
{
OnClientError.Invoke(TransportError.Unexpected, e.ToString());//——————————————————修改
ClientDisconnect();
};
client.Connect(builder.Uri);
}
public override void ClientDisconnect()
{
// dont set client null here of messages wont be processed
client?.Disconnect();
}
//#if MIRROR_26_0_OR_NEWER //——————————————————注释掉
public override void ClientSend(ArraySegment<byte> segment, int channelId = Channels.Reliable)//——————————————————修改
{
if (!ClientConnected())
{
Debug.LogError("Not Connected");
return;
}
if (segment.Count > maxMessageSize)
{
Log.Error("Message greater than max size");
return;
}
if (segment.Count == 0)
{
Log.Error("Message count was zero");
return;
}
client.Send(segment);
}
//#else//——————————————————注释掉 Start
//public override bool ClientSend(int channelId, ArraySegment<byte> segment)
//{
// if (!ClientConnected())
// {
// Debug.LogError("Not Connected");
// return false;
// }
// if (segment.Count > maxMessageSize)
// {
// Log.Error("Message greater than max size");
// return false;
// }
// if (segment.Count == 0)
// {
// Log.Error("Message count was zero");
// return false;
// }
// client.Send(segment);
// return true;
//}
//#endif//——————————————————End
#endregion
#region Server
public override bool ServerActive()
{
return server != null && server.Active;
}
public override void ServerStart()
{
if (ServerActive())
{
Debug.LogError("SimpleWebServer Already Started");
}
SslConfig config = SslConfigLoader.Load(this);
server = new SimpleWebServer(serverMaxMessagesPerTick, TcpConfig, maxMessageSize, handshakeMaxSize, config);
server.onConnect += OnServerConnected.Invoke;
server.onDisconnect += OnServerDisconnected.Invoke;
server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable);
server.onError += OnServerError.Invoke;
SendLoopConfig.batchSend = batchSend || waitBeforeSend;
SendLoopConfig.sleepBeforeSend = waitBeforeSend;
server.Start(port);
}
public override void ServerStop()
{
if (!ServerActive())
{
Debug.LogError("SimpleWebServer Not Active");
}
server.Stop();
server = null;
}
public override void ServerDisconnect(int connectionId)//——————————————————bool 修改为 void
{
if (!ServerActive())
{
Debug.LogError("SimpleWebServer Not Active");
//return false;//——————————————————注释掉
}
//return server.KickClient(connectionId);//——————————————————注释掉
}
//#if MIRROR_26_0_OR_NEWER//——————————————————注释掉
public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)//——————————————————修改
{
if (!ServerActive())
{
Debug.LogError("SimpleWebServer Not Active");
return;
}
if (segment.Count > maxMessageSize)
{
Log.Error("Message greater than max size");
return;
}
if (segment.Count == 0)
{
Log.Error("Message count was zero");
return;
}
server.SendOne(connectionId, segment);
return;
}
//#else//——————————————————注释掉 Start
//public override bool ServerSend(System.Collections.Generic.List<int> connectionIds, int channelId, ArraySegment<byte> segment)
//{
// if (!ServerActive())
// {
// Debug.LogError("SimpleWebServer Not Active");
// return false;
// }
// if (segment.Count > maxMessageSize)
// {
// Log.Error("Message greater than max size");
// return false;
// }
// if (segment.Count == 0)
// {
// Log.Error("Message count was zero");
// return false;
// }
// server.SendAll(connectionIds, segment);
// return true;
//}
//#endif//——————————————————End
public override string ServerGetClientAddress(int connectionId)
{
return server.GetClientAddress(connectionId);
}
public override Uri ServerUri()
{
UriBuilder builder = new UriBuilder
{
Scheme = GetServerScheme(),
Host = Dns.GetHostName(),
Port = port
};
return builder.Uri;
}
#endregion
}
}
更改SimpleWebServer脚本内容
https://i-blog.csdnimg.cn/direct/0177ab39a43343298e0e0a84786af00e.png
更改为:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.SimpleWeb
{
public class SimpleWebServer
{
readonly int maxMessagesPerTick;
readonly WebSocketServer server;
readonly BufferPool bufferPool;
public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig)
{
this.maxMessagesPerTick = maxMessagesPerTick;
// use max because bufferpool is used for both messages and handshake
int max = Math.Max(maxMessageSize, handshakeMaxSize);
bufferPool = new BufferPool(5, 20, max);
server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool);
}
public bool Active { get; private set; }
public event Action<int> onConnect;
public event Action<int> onDisconnect;
public event Action<int, ArraySegment<byte>> onData;
public event Action<int, TransportError, string> onError;//——————————————————修改
public void Start(ushort port)
{
server.Listen(port);
Active = true;
}
public void Stop()
{
server.Stop();
Active = false;
}
public void SendAll(List<int> connectionIds, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
buffer.SetReleasesRequired(connectionIds.Count);
// make copy of array before for each, data sent to each client is the same
foreach (int id in connectionIds)
{
server.Send(id, buffer);
}
}
public void SendOne(int connectionId, ArraySegment<byte> source)
{
ArrayBuffer buffer = bufferPool.Take(source.Count);
buffer.CopyFrom(source);
server.Send(connectionId, buffer);
}
public bool KickClient(int connectionId)
{
return server.CloseConnection(connectionId);
}
public string GetClientAddress(int connectionId)
{
return server.GetClientAddress(connectionId);
}
public void ProcessMessageQueue(MonoBehaviour behaviour)
{
int processedCount = 0;
// check enabled every time incase behaviour was disabled after data
while (
behaviour.enabled &&
processedCount < maxMessagesPerTick &&
// Dequeue last
server.receiveQueue.TryDequeue(out Message next)
)
{
processedCount++;
switch (next.type)
{
case EventType.Connected:
onConnect?.Invoke(next.connId);
break;
case EventType.Data:
onData?.Invoke(next.connId, next.data.ToSegment());
next.data.Release();
break;
case EventType.Disconnected:
onDisconnect?.Invoke(next.connId);
break;
case EventType.Error:
onError?.Invoke(next.connId, TransportError.Unexpected, next.exception.ToString());//——————————————————修改
break;
}
}
}
}
}
4. 更改 SimpleWeb.jslib 内容
https://i-blog.csdnimg.cn/direct/ed2bc23c9a5646ef8930e280bd1dff0f.png
更改为:
// this will create a global object
const SimpleWeb = {
webSockets: [],
next: 1,
GetWebSocket: function (index) {
return SimpleWeb.webSockets
},
AddNextSocket: function (webSocket) {
var index = SimpleWeb.next;
SimpleWeb.next++;
SimpleWeb.webSockets = webSocket;
return index;
},
RemoveSocket: function (index) {
SimpleWeb.webSockets = undefined;
},
};
function IsConnected(index) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
return webSocket.readyState === webSocket.OPEN;
}
else {
return false;
}
}
function Connect(addressPtr) {
const address = Pointer_stringify(addressPtr);
console.log("Connecting to " + address);
// Create webSocket connection.
webSocket = new WebSocket(address);
webSocket.binaryType = 'arraybuffer';
const index = SimpleWeb.AddNextSocket(webSocket);
const reObj = 'JSInfoReceiver';//Unity接收信息的游戏对象
// Connection opened
webSocket.addEventListener('open', function (event) {
console.log("Connected to " + address);
SendMessage(reObj,'OpenCallback', index);
//Module.dynCall('vi', openCallbackPtr, );
});
webSocket.addEventListener('close', function (event) {
console.log("Disconnected from " + address);
SendMessage(reObj,'CloseCallBack', index);
//Module.dynCall('vi', closeCallBackPtr, );
});
// Listen for messages
webSocket.addEventListener('message', function (event) {
if (event.data instanceof ArrayBuffer) {
// TODO dont alloc each time
var array = new Uint8Array(event.data);
var arrayLength = array.length;
var bufferPtr = _malloc(arrayLength);
var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength);
dataBuffer.set(array);
SendMessage(reObj,'MessageCallback1', index);
SendMessage(reObj,'MessageCallback2',bufferPtr);
SendMessage(reObj,'MessageCallback3',arrayLength);
//Module.dynCall('viii', messageCallbackPtr, );
_free(bufferPtr);
}
else {
console.error("message type not supported")
}
});
webSocket.addEventListener('error', function (event) {
console.error('Socket Error', event);
SendMessage(reObj,'ErrorCallback', index);
//Module.dynCall('vi', errorCallbackPtr, );
});
return index;
}
function Disconnect(index) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
webSocket.close(1000, "Disconnect Called by Mirror");
}
SimpleWeb.RemoveSocket(index);
}
function Send(index, arrayPtr, offset, length) {
var webSocket = SimpleWeb.GetWebSocket(index);
if (webSocket) {
const start = arrayPtr + offset;
const end = start + length;
const data = HEAPU8.buffer.slice(start, end);
webSocket.send(data);
return true;
}
return false;
}
const SimpleWebLib = {
$SimpleWeb: SimpleWeb,
IsConnected,
Connect,
Disconnect,
Send
};
autoAddDeps(SimpleWebLib, '$SimpleWeb');
mergeInto(LibraryManager.library, SimpleWebLib); 更改原因:jslib中使用了Unity已弃用的API:Runtime.dynCall
5. 更改 “WebSocketClientWebGl” 脚本内容
https://i-blog.csdnimg.cn/direct/800b85c1c33642a284263a3f93ceb879.png
更改为:
using System;
using System.Collections.Generic;
using AOT;
namespace Mirror.SimpleWeb
{
public class WebSocketClientWebGl : SimpleWebClient
{
static readonly Dictionary<int, WebSocketClientWebGl> instances = new Dictionary<int, WebSocketClientWebGl>();
/// <summary>
/// key for instances sent between c# and js
/// </summary>
int index;
internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick)
{
#if !UNITY_WEBGL || UNITY_EDITOR
throw new NotSupportedException();
#endif
}
public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index);
public override void Connect(Uri serverAddress)
{
#if UNITY_WEBGL
index = SimpleWebJSLib.Connect(serverAddress.ToString()/*, OpenCallback, CloseCallBack, MessageCallback, ErrorCallback*/);
#else
index = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback);
#endif
instances.Add(index, this);
state = ClientState.Connecting;
}
public override void Disconnect()
{
state = ClientState.Disconnecting;
// disconnect should cause closeCallback and OnDisconnect to be called
SimpleWebJSLib.Disconnect(index);
}
public override void Send(ArraySegment<byte> segment)
{
if (segment.Count > maxMessageSize)
{
Log.Error($"Cant send message with length {segment.Count} because it is over the max size of {maxMessageSize}");
return;
}
SimpleWebJSLib.Send(index, segment.Array, 0, segment.Count);
}
void onOpen()
{
receiveQueue.Enqueue(new Message(EventType.Connected));
state = ClientState.Connected;
}
void onClose()
{
// this code should be last in this class
receiveQueue.Enqueue(new Message(EventType.Disconnected));
state = ClientState.NotConnected;
instances.Remove(index);
}
void onMessage(IntPtr bufferPtr, int count)
{
try
{
ArrayBuffer buffer = bufferPool.Take(count);
buffer.CopyFrom(bufferPtr, count);
receiveQueue.Enqueue(new Message(buffer));
}
catch (Exception e)
{
Log.Error($"onData {e.GetType()}: {e.Message}\n{e.StackTrace}");
receiveQueue.Enqueue(new Message(e));
}
}
void onErr()
{
receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error")));
Disconnect();
}
public static void OpenCallback(int index) => instances.onOpen();
public static void CloseCallBack(int index) => instances.onClose();
public static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances.onMessage(bufferPtr, count);
public static void ErrorCallback(int index) => instances.onErr();
}
}
更改原因:公开静态方法供接下来的 JSInfoReceiver对象 调用
6. 更改 “SimpleWebJSLib” 脚本内容
https://i-blog.csdnimg.cn/direct/3da88bba1ffb4876b7c42efcd7ed85ac.png
更改为:
using System;
#if UNITY_WEBGL
using System.Runtime.InteropServices;
#endif
namespace Mirror.SimpleWeb
{
internal static class SimpleWebJSLib
{
#if UNITY_WEBGL
internal static extern bool IsConnected(int index);
#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments
#pragma warning restore CA2101 // Specify marshaling for P/Invoke string arguments
internal static extern int Connect(string address/*, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback*/);
internal static extern void Disconnect(int index);
internal static extern bool Send(int index, byte[] array, int offset, int length);
#else
internal static bool IsConnected(int index) => throw new NotSupportedException();
internal static int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback) => throw new NotSupportedException();
internal static void Disconnect(int index) => throw new NotSupportedException();
internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException();
#endif
}
}
7. Unity新建一个脚本,定名为 “JSInfoReceiver”
脚本内容:
using Mirror.SimpleWeb;
using System;
using UnityEngine;
public class JSInfoReceiver : MonoBehaviour
{
private int num1;
private int num2;
public void OpenCallback(int index) => WebSocketClientWebGl.OpenCallback(index);
public void CloseCallBack(int index) => WebSocketClientWebGl.CloseCallBack(index);
public void MessageCallback1(int index) => this.num1 = index;
public void MessageCallback2(int index) => this.num2 = index;
public void MessageCallback3(int index) => WebSocketClientWebGl.MessageCallback(num1, new IntPtr(num2), index);
public void ErrorCallback(int index) => WebSocketClientWebGl.CloseCallBack(index);
}
8. Unity创建一个空物体,定名为 “JSInfoReceiver” ,挂载上JSInfoReceiver脚本组件
https://i-blog.csdnimg.cn/direct/9416884b663d451fa0e16b2a7388c7d0.png
至此,就可以使用Mirror插件举行WebGL端的开发了,Transport改用SimpleWebTransport即可。
https://i-blog.csdnimg.cn/direct/80870ff20b07411388a9afaf8d4bd668.png
简单实现了一个demo(其实就是Mirror插件的案例简单改了一下)链接放文章开头了。开一个Windows步伐做服务端,开两个WebGL做客户端参加,没得题目。
https://i-blog.csdnimg.cn/direct/a25106a8e1e9442389698d6bce40b8f1.png
如果使用https,必要指定一个ssl证书,在步伐根目次下创建一个cert.json文件,写入一个如图所示的Cert对象的json字符串,path和password分别指向ssl证书的路径和密码应该就可以了(暂时没有测试)。
https://i-blog.csdnimg.cn/direct/df031204844545a1b877342c7244777f.png
参考:UnityWebGL使用Mirror举行多人在线碰到的题目
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]