tsx81429 发表于 2025-3-13 09:30:38

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]
查看完整版本: Unity Mirror插件WebGL端多人联机实现