commit 6490c988490a779b3e93bb0112565ded47aeb949 Author: Kevin Li Date: Wed Aug 30 20:26:51 2023 +0800 Working code diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..0fcc54f --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c919269e61d904cf0964b56eb9b1b6ab +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef2cf86 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# GURU Dof kcp Client + +### VERSION 0.0.1 + +## 插件介绍 + +基于upm_guru_kcp库,服务与dof项目的客户端类封装 + + +## 安装和接入 + +### 插件引入 + +- 本插件需要使用公司内部的Gitlab加载对应的repo, 详见 [Castbox内部git服务器Gitea使用指南](https://docs.google.com/document/d/1DiGPDD5Teu2QcYaBhqAcsqMbb3DJ5sdnTQMuvVzJSUk/edit#heading=h.w9tkkzwwyjxf) +- 根据文档部署好本机配置后, 请在Unity内部配置如下参数 + - TBD + - TBD + - TBD + + +## 逻辑实现 + +- TBD + + + + +## 问题答疑 + +- Q +> A + +- Q +> A + +- Q +> A \ No newline at end of file diff --git a/README.md.meta b/README.md.meta new file mode 100644 index 0000000..871bd51 --- /dev/null +++ b/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 177739947c6344e95a62c8671cf2e96c +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..722eb93 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 23b66401566ec4490b51f7d2375e1228 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GameEventMgr.cs b/Runtime/GameEventMgr.cs new file mode 100644 index 0000000..50dcdce --- /dev/null +++ b/Runtime/GameEventMgr.cs @@ -0,0 +1,509 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace Guru +{ + + public class GameEventMgr + { + private static GameEventMgr _instance; + public static GameEventMgr Instance + { + get + { + if (_instance == null) + { + _instance = new GameEventMgr(); + } + return _instance; + } + } + + public static void Init(bool is_debug_mode = false) + { + if (_instance == null) + { + _instance = new GameEventMgr(); + } + + _isDebugMode = is_debug_mode; + + if(_isDebugMode) + { + Debug.Log($"[GameEventMgr]Init: Running in debug mode"); + } + } + + public static bool _isDebugMode = false; + + struct DelegateGameObjectBonding + { + public GameObject gameObject; + public Delegate listener; + + public DelegateGameObjectBonding(GameObject game_object, Delegate del) + { + gameObject = game_object; + listener = del; + } + } + + private readonly Queue> _listenerListQueue = new (); + private readonly Queue> _listenerBondingListQueue = new (); + private readonly Dictionary> _eventTable = new (); + private readonly Dictionary> _eventObjTable = new (); + private readonly Dictionary> _pendingAddTable = new (); + private readonly Dictionary> _pendingObjAddTable = new (); + private readonly Dictionary> _pendingRemoveTable = new (); + private readonly Dictionary> _pendingObjRemoveTable = new (); + + // Using raising event list to prevent infinite loop call or changing delegate list + private readonly List _raisingEventIds = new (); + + private GameEventMgr() { } + + public void AddListener(int event_id, Action new_listener, GameObject life_cycle_obj = null) + { + DoAddListener(event_id, new_listener, life_cycle_obj); + } + + public void AddListener(int event_id, Action new_listener, GameObject life_cycle_obj = null) + { + DoAddListener(event_id, new_listener, life_cycle_obj); + } + + public void AddListener(int event_id, Action new_listener, GameObject life_cycle_obj = null) + { + DoAddListener(event_id, new_listener, life_cycle_obj); + } + + public void AddListener(int event_id, Action new_listener, GameObject life_cycle_obj = null) + { + DoAddListener(event_id, new_listener, life_cycle_obj); + } + + public void RemoveListener(int event_id, Action listener, GameObject life_cycle_obj = null) + { + DoRemoveListener(event_id, listener, life_cycle_obj); + } + + public void RemoveListener(int event_id, Action listener, GameObject life_cycle_obj = null) + { + DoRemoveListener(event_id, listener, life_cycle_obj); + } + + public void RemoveListener(int event_id, Action listener, GameObject life_cycle_obj = null) + { + DoRemoveListener(event_id, listener, life_cycle_obj); + } + + public void RemoveListener(int event_id, Action listener, GameObject life_cycle_obj = null) + { + DoRemoveListener(event_id, listener, life_cycle_obj); + } + + public void Raise(int event_id) + { + DoRaise(event_id); + } + + public void Raise(int event_id, T arg1) + { + DoRaise(event_id, arg1); + } + + public void Raise(int event_id, T1 arg1, T2 arg2) + { + DoRaise(event_id, arg1, arg2); + } + + public void Raise(int event_id, T1 arg1, T2 arg2, T3 arg3) + { + DoRaise(event_id, arg1, arg2, arg3); + } + + public void ClearNullGameObjectListeners() + { + var etor = _eventObjTable.GetEnumerator(); + + while (etor.MoveNext()) + { + var listeners = etor.Current.Value; + + for (int i = 0; i < listeners.Count; i++) + { + if(listeners[i].gameObject == null) + { + listeners.RemoveAt(i--); + } + } + } + } + + private List SpawnDelegateList() + { + if(_listenerListQueue.Count > 0) + { + return _listenerListQueue.Dequeue(); + } + else + { + return new List(); + } + } + + private void DespawnDelegateList(List list) + { + if (list == null) return; + + list.Clear(); + _listenerListQueue.Enqueue(list); + } + + private List SpawnDelegateBondingList() + { + if (_listenerBondingListQueue.Count > 0) + { + return _listenerBondingListQueue.Dequeue(); + } + else + { + return new List(); + } + } + + private void DespawnDelegateBondingList(List list) + { + if (list == null) return; + + list.Clear(); + _listenerBondingListQueue.Enqueue(list); + } + + private bool DoAddListener(int event_id, Delegate new_listener, GameObject life_cycle_obj = null) + { + if (new_listener == null) + { + Debug.LogError($"[GameEventMgr]DoAddListener: Can't add empty listener for event {event_id}!"); + return false; + } + + bool result = true; + + // If event is raising, add it into pending list + if (_raisingEventIds.Contains(event_id)) + { + if (life_cycle_obj != null) + { + if (!_pendingObjAddTable.TryGetValue(event_id, out var pending_listeners)) + { + pending_listeners = SpawnDelegateBondingList(); + _pendingObjAddTable[event_id] = pending_listeners; + } + + pending_listeners.Add(new DelegateGameObjectBonding(life_cycle_obj, new_listener)); + } + else + { + if (!_pendingAddTable.TryGetValue(event_id, out var pending_listeners)) + { + pending_listeners = SpawnDelegateList(); + _pendingAddTable[event_id] = pending_listeners; + } + + pending_listeners.Add(new_listener); + } + } + else + { + if (life_cycle_obj != null) + { + if (!_eventObjTable.TryGetValue(event_id, out var listener_bondings)) + { + listener_bondings = SpawnDelegateBondingList(); + _eventObjTable[event_id] = listener_bondings; + } + + if (listener_bondings.Count == 0 || listener_bondings[0].listener.GetType() == new_listener.GetType()) + { + var bonding = new DelegateGameObjectBonding(life_cycle_obj, new_listener); + + if (_isDebugMode && listener_bondings.Contains(bonding)) + { + Debug.LogError($"[GameEventMgr]DoAddListener: Already in the list, will not add it again [{new_listener.Target}.{new_listener.Method?.Name}]"); + } + else + { + listener_bondings.Add(bonding); + } + } + else if (listener_bondings[0].listener.GetType() != new_listener.GetType()) + { + Debug.LogError($"[GameEventMgr]DoAddListener: Attempting to add listener with inconsistent signature for event {event_id}. Current listeners type({listener_bondings[0].listener.GetType().Name}) != added type({new_listener.GetType().Name})"); + result = false; + } + } + else + { + if (!_eventTable.TryGetValue(event_id, out var listeners)) + { + listeners = SpawnDelegateList(); + _eventTable[event_id] = listeners; + } + + if (listeners.Count == 0 || listeners[0].GetType() == new_listener.GetType()) + { + if (_isDebugMode && listeners.Contains(new_listener)) + { + Debug.LogError($"[GameEventMgr]DoAddListener: Already in the list, will not add it again [{new_listener.Target}.{new_listener.Method?.Name}]"); + } + else + { + listeners.Add(new_listener); + } + } + else if (listeners[0].GetType() != new_listener.GetType()) + { + Debug.LogError($"[GameEventMgr]DoAddListener: Attempting to add listener with inconsistent signature for event {event_id}. Current listeners type({listeners[0].GetType().Name}) != added type({new_listener.GetType().Name})"); + result = false; + } + } + } + + return result; + } + + private bool DoRemoveListener(int event_id, Delegate listener, GameObject life_cycle_obj = null) + { + if (listener == null) + { + Debug.LogError($"[GameEventMgr]DoRemoveListener: Can't remove empty listener for event {event_id}!"); + return false; + } + + bool result = false; + + // If event is raising, add it into pending list + if (_raisingEventIds.Contains(event_id)) + { + if (life_cycle_obj != null) + { + if (!_pendingObjRemoveTable.TryGetValue(event_id, out var pending_listeners)) + { + pending_listeners = SpawnDelegateBondingList(); + _pendingObjRemoveTable[event_id] = pending_listeners; + } + + pending_listeners.Add(new DelegateGameObjectBonding(life_cycle_obj, listener)); + } + else + { + if (!_pendingRemoveTable.TryGetValue(event_id, out var pending_listeners)) + { + pending_listeners = SpawnDelegateList(); + _pendingRemoveTable[event_id] = pending_listeners; + } + + pending_listeners.Add(listener); + } + + result = true; + } + else + { + if (life_cycle_obj != null) + { + if (_eventObjTable.TryGetValue(event_id, out var listeners)) + { + if (listeners.Count > 0) + { + listeners.Remove(new DelegateGameObjectBonding(life_cycle_obj, listener)); + result = true; + } + + if (listeners.Count == 0) + { + DespawnDelegateBondingList(listeners); + _eventObjTable.Remove(event_id); + } + } + } + else + { + if (_eventTable.TryGetValue(event_id, out var listeners)) + { + if (listeners.Count > 0) + { + listeners.Remove(listener); + result = true; + } + + if (listeners.Count == 0) + { + DespawnDelegateList(listeners); + _eventTable.Remove(event_id); + } + } + } + } + + return result; + } + + private void LogException(Exception ex, string event_tag, string method_name) + { + string msg = ex.Message; + string stack_trace = ex.StackTrace; + + if (ex.InnerException != null) + { + msg = ex.InnerException.Message; + stack_trace = ex.InnerException.StackTrace; + } + + Debug.LogError($"[GameEventMgr]{method_name}: Event({event_tag}) {msg}\nStack Trace: {stack_trace}"); + } + + private bool DoRaise(int event_id, params object[] args) + { + if (_raisingEventIds.Contains(event_id)) + { + Debug.LogError($"[GameEventMgr]DoRaise: Can't raise event({event_id}) again inside the same event raising process!"); + return false; + } + + bool result = false; + _raisingEventIds.Add(event_id); + + if (_eventObjTable.TryGetValue(event_id, out var bond_listeners)) + { + for (int i = 0; i < bond_listeners.Count; i++) + { + var bond_listener = bond_listeners[i]; + + if (bond_listener.gameObject != null) + { + try + { + bond_listener.listener.Method.Invoke(bond_listener.listener.Target, args); + result = true; + } + catch (Exception ex) + { + bond_listeners.RemoveAt(i--); + LogException(ex, $"{event_id}_Obj[{bond_listener.gameObject.name}]", "DoRaise"); + } + } + else + { + bond_listeners.RemoveAt(i--); + Debug.Log($"[GameEventMgr]DoRaise: Remove null GameObject listener ({event_id})!"); + continue; + } + } + } + + if (_eventTable.TryGetValue(event_id, out var listeners)) + { + for (int i = 0; i < listeners.Count; i++) + { + var listener = listeners[i]; + + if(listener != null) + { + try + { + listener.Method.Invoke(listener.Target, args); + result = true; + } + catch (Exception ex) + { + listeners.RemoveAt(i--); + LogException(ex, event_id.ToString(), "DoRaise"); + } + } + } + } + + _raisingEventIds.Remove(event_id); + + if (_pendingObjAddTable.TryGetValue(event_id, out var add_bond_listeners)) + { + if (add_bond_listeners != null && add_bond_listeners.Count > 0) + { + bond_listeners.AddRange(add_bond_listeners); + DespawnDelegateBondingList(add_bond_listeners); + } + + _pendingObjAddTable.Remove(event_id); + } + + if (_pendingObjRemoveTable.TryGetValue(event_id, out var remove_bond_listeners)) + { + if (remove_bond_listeners != null && remove_bond_listeners.Count > 0) + { + for (int i = 0; i < remove_bond_listeners.Count; i++) + { + bond_listeners.Remove(remove_bond_listeners[i]); + } + + DespawnDelegateBondingList(remove_bond_listeners); + } + + _pendingObjRemoveTable.Remove(event_id); + } + + if (bond_listeners?.Count == 0) + { + DespawnDelegateBondingList(bond_listeners); + _eventObjTable.Remove(event_id); + } + + if (_pendingAddTable.TryGetValue(event_id, out var add_listeners)) + { + if (add_listeners != null && add_listeners.Count > 0) + { + listeners.AddRange(add_listeners); + DespawnDelegateList(add_listeners); + } + + _pendingAddTable.Remove(event_id); + } + + if (_pendingRemoveTable.TryGetValue(event_id, out var remove_listeners)) + { + if (remove_listeners != null && remove_listeners.Count > 0) + { + for (int i = 0; i < remove_listeners.Count; i++) + { + listeners.Remove(remove_listeners[i]); + } + + DespawnDelegateList(remove_listeners); + } + + _pendingRemoveTable.Remove(event_id); + } + + if (listeners?.Count == 0) + { + DespawnDelegateList(listeners); + _eventTable.Remove(event_id); + } + + return result; + } + + public void Clear() + { + _eventTable.Clear(); + _eventObjTable.Clear(); + _pendingAddTable.Clear(); + _pendingObjAddTable.Clear(); + _pendingRemoveTable.Clear(); + _pendingObjRemoveTable.Clear(); + _raisingEventIds.Clear(); + } + } +} diff --git a/Runtime/GameEventMgr.cs.meta b/Runtime/GameEventMgr.cs.meta new file mode 100644 index 0000000..117a2ae --- /dev/null +++ b/Runtime/GameEventMgr.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8912da6e3d9994390b3795e3e9b065d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GameKcpClient.cs b/Runtime/GameKcpClient.cs new file mode 100644 index 0000000..3ef81ac --- /dev/null +++ b/Runtime/GameKcpClient.cs @@ -0,0 +1,168 @@ +using System; +using System.Net; +using System.IO; +using dotNetty_kcp; +using DotNetty.Buffers; +using UnityEngine; +using Guru; +using Dof; + +namespace DofLibrary +{ + + public class GameKcpClient : KcpListener + { + public Action OnConnected; + + private KcpClient _kcpClient; + public IMessageSender sender { get; private set; } = new MessageSender(); + private IMessageReceiver _receiver; + private bool _running; + + public GameKcpClient(DotNetty.Unity.Level level = DotNetty.Unity.Level.ALL) + { + // 指定DotNetty日志的显示级别 + DotNetty.Unity.UnityLoggerFactory.Default.Level = level; + } + + /// + /// 当前 Client 是否处于连接状态 + /// + public bool Running => _running; + + /// + /// 创建一个到 Dof GameServer 的连接 + /// + /// Dof GameServer 的地址,形如 xxx.xxx.xxx.xxx + /// 房间ID + /// 事件回调接口 + public async void Connect(string serverAddress, int port, IMessageReceiver receiver) + { + _receiver = receiver; + _receiver.MessageSender = sender; + _running = true; + + var channelConfig = new ChannelConfig(); + channelConfig.initNodelay(true, 40, 2, true); + channelConfig.Sndwnd = 512; + channelConfig.Rcvwnd = 512; + channelConfig.Mtu = 512; + channelConfig.FecDataShardCount = 10; + channelConfig.FecParityShardCount = 3; + channelConfig.AckNoDelay = true; + channelConfig.Crc32Check = false; + channelConfig.UseConvChannel = true; + //channelConfig.Conv = UnityEngine.Random.Range(1, int.MaxValue); + channelConfig.TimeoutMillis = 10000; + + _kcpClient = new KcpClient(); + _kcpClient.init(channelConfig); + + var remoteAddress = new IPEndPoint(IPAddress.Parse(serverAddress), port); + var task = _kcpClient.BindLocal(); + await task; + var channel = task.Result; + _kcpClient.connect(channel, remoteAddress, channelConfig, this); + } + + private void ProcessMessage(ServerMessage serverMessage) + { + if (serverMessage.PlayerEntered != null) + { + var cid = serverMessage.PlayerEntered.Cid; + sender.Cid = cid; + _receiver.OnPlayerEntered(cid); + } + else if(serverMessage.GameStart != null) + { + _receiver.OnGameStart(serverMessage.GameStart); + } + else if (serverMessage.LevelStart != null) + { + _receiver.OnLevelStart(serverMessage.LevelStart); + } + else if (serverMessage.PointFound != null) + { + _receiver.OnPointFound(serverMessage.PointFound); + } + else if (serverMessage.GameFinish != null) + { + _receiver.OnGameFinish(serverMessage.GameFinish); + } + } + + /// + /// kcp 连接后的回调函数 + /// + /// kcp连接 + public void onConnected(Ukcp ukcp) + { + Debug.Log($"[GameKcpClient]onConnected: conv = {ukcp.getConv()}"); + sender.Client = ukcp; + + if (OnConnected != null) + { + Loom.QueueOnMainThread(() => + { + OnConnected.Invoke(); + }); + } + } + + /// + /// kcp 接收到消息的回调函数 + /// + /// 接收到的消息buffer + /// kcp连接 + public void handleReceive(IByteBuffer byteBuf, Ukcp ukcp) + { + Debug.Log("[GameKcpClient]handleReceive"); + + var ms = new MemoryStream(1024 * 1024 * 1); + var data_len = byteBuf.ReadableBytes; + byteBuf.ReadBytes(ms, data_len); + var msg = ProtobufHelper.FromBytes(typeof(ServerMessage), ms.GetBuffer(), 0, data_len); + + if (msg is ServerMessage server_msg) + { + Loom.QueueOnMainThread(() => + { + ProcessMessage(server_msg); + }); + } + else + { + Debug.LogError("[GameKcpClient]handleReceive: Fail to Deserialize ServerMessage"); + } + } + + /// + /// kcp 连接或者接收消息出错的回调函数 + /// + /// 异常 + /// kcp连接 + public void handleException(Exception ex, Ukcp ukcp) + { + Debug.LogError($"[GameKcpClient]handleException: {ex}"); + } + + /// + /// kcp 连接关闭时的回调函数 + /// + /// kcp连接 + public void handleClose(Ukcp ukcp) + { + Debug.Log("[GameKcpClient]: Connection closed"); + } + + /// + /// 关闭 Client,应该在游戏结束时调用 + /// + public void Close() + { + _kcpClient?.stop(); + _running = false; + } + } + +} \ No newline at end of file diff --git a/Runtime/GameKcpClient.cs.meta b/Runtime/GameKcpClient.cs.meta new file mode 100644 index 0000000..5582f7a --- /dev/null +++ b/Runtime/GameKcpClient.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 71aa1db1be3544524bb58107e80708ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/GuruDofLib.Runtime.asmdef b/Runtime/GuruDofLib.Runtime.asmdef new file mode 100644 index 0000000..6e45568 --- /dev/null +++ b/Runtime/GuruDofLib.Runtime.asmdef @@ -0,0 +1,16 @@ +{ + "name": "GuruDofLib.Runtime", + "rootNamespace": "", + "references": [ + "GUID:832e7ae06a4304a17a11ca2f7b21373d" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Runtime/GuruDofLib.Runtime.asmdef.meta b/Runtime/GuruDofLib.Runtime.asmdef.meta new file mode 100644 index 0000000..f2dd175 --- /dev/null +++ b/Runtime/GuruDofLib.Runtime.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e2e9b8b39d6d74d1094c47c89de72e2e +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/IMessageReceiver.cs b/Runtime/IMessageReceiver.cs new file mode 100644 index 0000000..ad195ae --- /dev/null +++ b/Runtime/IMessageReceiver.cs @@ -0,0 +1,45 @@ +using Dof; + +namespace DofLibrary +{ + public interface IMessageReceiver + { + /// + /// Set MessageSender for sending messages + /// + IMessageSender? MessageSender + { + set; + } + + /// + /// 当玩家进入房间成功时,服务端发送此事件 + /// + /// 当前房间里的玩家编号,为0|1 + void OnPlayerEntered(long cid); + + /// + /// 当房间里已经进入两个玩家时,服务端发送此事件 + /// + /// + void OnGameStart(GameStart gameStart); + + /// + /// 当两个玩家都准备好了某个关卡时,服务端发送此事件 + /// + /// 当前开始的关卡ID + void OnLevelStart(LevelStart levelStart); + + /// + /// 当另一个玩家找到了当前关卡的某个点时,服务端发送此事件 + /// + /// 另一个玩家找到的点 + void OnPointFound(PointFound pointFound); + + /// + /// 当两个玩家都完成了所有关卡时,服务端发送此事件 + /// + /// 两个玩家的得分 + void OnGameFinish(GameFinish gameFinish); + } +} \ No newline at end of file diff --git a/Runtime/IMessageReceiver.cs.meta b/Runtime/IMessageReceiver.cs.meta new file mode 100644 index 0000000..c1e74fd --- /dev/null +++ b/Runtime/IMessageReceiver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2d2e7c96d274a436895dfb34fdac9d8d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/IMessageSender.cs b/Runtime/IMessageSender.cs new file mode 100644 index 0000000..60dd7d4 --- /dev/null +++ b/Runtime/IMessageSender.cs @@ -0,0 +1,59 @@ +using dotNetty_kcp; + +namespace DofLibrary +{ + /// + /// 游戏消息发送接口 + /// + public interface IMessageSender + { + /// + /// 设置 kcp 客户端,用于发送消息 + /// + Ukcp? Client + { + set; + } + + /// + /// 当收到服务端发来的 PlayerEntered 事件时,设置房间中的玩家编号 + /// + long Cid + { + set; + } + + /// + /// 发送 PlayerEnter 进入房间 消息 + /// + /// 房间ID + /// 玩家ID + /// 玩家昵称 + /// 玩家国家 + void PlayerEnter(string roomId, string uid, string nickName, string country); + + /// + /// 发送 LevelPrepared 关卡准备完毕 消息 + /// + /// 关卡ID + void LevelPrepared(string levelId); + + /// + /// 发送 PointFound 找到点位 消息 + /// + /// 当前关卡ID + /// 点位编号 + void PointFound(string levelId, int pointId); + + /// + /// 发送 LevelEnd 关卡结束 消息 + /// + /// 关卡ID + void LevelEnd(string levelId); + + /// + /// 发送 AllLevelEnd 所有关卡结束 消息 + /// + void AllLevelEnd(); + } +} \ No newline at end of file diff --git a/Runtime/IMessageSender.cs.meta b/Runtime/IMessageSender.cs.meta new file mode 100644 index 0000000..d614a9c --- /dev/null +++ b/Runtime/IMessageSender.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 29452c4084f2f45399bb395cedd91ebd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Loom.cs b/Runtime/Loom.cs new file mode 100644 index 0000000..c06abe2 --- /dev/null +++ b/Runtime/Loom.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using UnityEngine; + +namespace Guru +{ + // Loom lets you easily run code on another thread and have that other thread run code on the main game thread when it needs to. + public class Loom : MonoBehaviour + { + public static int maxThreads = 10; + + private static int numThreads; + private static Loom _instance; + + public static Loom Instance + { + get + { + Initialize(); + return _instance; + } + } + + void Awake() + { + _instance = this; + initialized = true; + } + + static bool initialized; + + public static void Initialize() + { + if (!initialized) + { + if (!Application.isPlaying) return; + + initialized = true; + var g = new GameObject("Loom"); + DontDestroyOnLoad(g); + _instance = g.AddComponent(); + } + } + + public struct DelayedQueueItem + { + public float time; + public Action action; + } + + private List _actions = new List(); + private List _delayed = new List(); + private List _currentDelayed = new List(); + + // Runs a set of statements on the main thread + public static void QueueOnMainThread(Action action) + { + QueueOnMainThread(action, 0f); + } + + // Runs a set of statements on the main thread (with an optional delay). + public static void QueueOnMainThread(Action action, float time) + { + if (time != 0) + { + lock (_instance._delayed) + { + _instance._delayed.Add(new DelayedQueueItem { time = Time.unscaledTime + time, action = action }); + } + } + else + { + lock (_instance._actions) + { + _instance._actions.Add(action); + } + } + } + + // Runs a set of statements on another thread + public static Thread RunAsync(Action a) + { + Initialize(); + + while (numThreads >= maxThreads) + { + Thread.Sleep(1); + } + + Interlocked.Increment(ref numThreads); + ThreadPool.QueueUserWorkItem(RunAction, a); + + return null; + } + + private static void RunAction(object action) + { + try + { + ((Action)action)(); + } + catch + { + } + finally + { + Interlocked.Decrement(ref numThreads); + } + } + + public static void Reset() + { + if (_instance == null) return; + + lock (_instance._delayed) + { + _instance._delayed.Clear(); + _instance._currentDelayed.Clear(); + } + + lock (_instance._actions) + { + _instance._actions.Clear(); + } + } + + public bool IsThreadsMax() + { + return numThreads >= maxThreads; + } + + void OnDisable() + { + if (_instance == this) + { + _instance = null; + } + } + + List _currentActions = new List(); + + void Update() + { + if (_actions.Count > 0) + { + lock (_actions) + { + _currentActions.Clear(); + _currentActions.AddRange(_actions); + _actions.Clear(); + } + + for (int i = 0; i < _currentActions.Count; i++) + { + try + { + _currentActions[i].Invoke(); + } + catch(Exception ex) + { + LogException(ex); + } + } + + if (_delayed.Count > 0) + { + lock (_delayed) + { + _currentDelayed.Clear(); + + for (int i = 0; i < _delayed.Count; i++) + { + var d = _delayed[i]; + + if (d.time <= Time.unscaledTime) + { + _currentDelayed.Add(d); + } + } + + for (int i = 0; i < _currentDelayed.Count; i++) + { + var item = _currentDelayed[i]; + _delayed.Remove(item); + } + } + + for (int i = 0; i < _currentDelayed.Count; i++) + { + try + { + _currentDelayed[i].action.Invoke(); + } + catch (Exception ex) + { + LogException(ex); + } + } + } + } + } + + void LogException(Exception ex) + { + string msg = ex.Message; + string stack_trace = ex.StackTrace; + + if (ex.InnerException != null) + { + msg = ex.InnerException.Message; + stack_trace = ex.InnerException.StackTrace; + } + + Debug.LogError($"[Loom]InvokeMainThreadAction: {msg}\nStack Trace: {stack_trace}"); + } + } +} diff --git a/Runtime/Loom.cs.meta b/Runtime/Loom.cs.meta new file mode 100644 index 0000000..212c95b --- /dev/null +++ b/Runtime/Loom.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8d56f3a7c2d1a48c78f742f63a075fb7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/MessageSender.cs b/Runtime/MessageSender.cs new file mode 100644 index 0000000..f8765fc --- /dev/null +++ b/Runtime/MessageSender.cs @@ -0,0 +1,122 @@ +using Dof; +using dotNetty_kcp; +using DotNetty.Buffers; +using UnityEngine; +using System.IO; +using base_kcp; +using Guru; + +namespace DofLibrary +{ + + /// + /// 游戏消息发送接口实现类 + /// + internal class MessageSender : IMessageSender + { + private long _cid; + private Ukcp _client; + private MemoryStream _localSendMs = new(1024 * 1024 * 1); + + /// + /// 设置 kcp 客户端,用于发送消息 + /// + public Ukcp Client + { + set => _client = value; + } + + /// + /// 当收到服务端发来的 PlayerEntered 事件时,设置房间中的玩家编号 + /// + public long Cid + { + set => _cid = value; + } + + public void PlayerEnter(string roomId, string uid, string nickName, string country) + { + var playerEnter = new ClientMessage + { + PlayerEnter = new PlayerEnter + { + RoomId = roomId, + Uid = uid, + NickName = nickName, + Country = country + } + }; + Send(playerEnter); + Debug.Log("PlayerEnter message sent"); + } + + public void LevelPrepared(string levelId) + { + var message = new ClientMessage + { + LevelPrepared = new LevelPrepared + { + Cid = _cid, + Level = levelId + } + }; + Send(message); + Debug.Log("LevelPrepared message sent"); + } + + public void PointFound(string levelId, int pointId) + { + var message = new ClientMessage + { + PointFound = new PointFound + { + Cid = _cid, + Level = levelId, + PointId = pointId + } + }; + Send(message); + Debug.Log("PointFound message sent"); + } + + public void LevelEnd(string levelId) + { + var message = new ClientMessage + { + LevelEnd = new LevelEnd + { + Cid = _cid, + Level = levelId + } + }; + Send(message); + Debug.Log("LevelEnd message sent"); + } + + public void AllLevelEnd() + { + var message = new ClientMessage + { + AllLevelEnd = new AllLevelEnd + { + Cid = _cid + } + }; + Send(message); + Debug.Log("AllLevelEnd message sent"); + } + + private void Send(ClientMessage message) + { + _localSendMs.SetLength(0); + _localSendMs.Position = 0; + + ProtobufHelper.ToStream(message, _localSendMs); + + var dataBuf = Unpooled.DirectBuffer(1024 * 1024 * 1); + dataBuf.WriteBytes(_localSendMs.GetBuffer(), 0, (int)_localSendMs.Length); + _client.write(dataBuf); + dataBuf.Release(); + } + } +} \ No newline at end of file diff --git a/Runtime/MessageSender.cs.meta b/Runtime/MessageSender.cs.meta new file mode 100644 index 0000000..fa820a8 --- /dev/null +++ b/Runtime/MessageSender.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83018a8afd2ab4af499e831caa714f77 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkGen.meta b/Runtime/NetworkGen.meta new file mode 100644 index 0000000..19960ae --- /dev/null +++ b/Runtime/NetworkGen.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 92dc6b0ee5121a94bb34348448a983a1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NetworkGen/Messages.cs b/Runtime/NetworkGen/Messages.cs new file mode 100644 index 0000000..a904b49 --- /dev/null +++ b/Runtime/NetworkGen/Messages.cs @@ -0,0 +1,263 @@ +// This file was generated by a tool; you should avoid making direct changes. +// Consider using 'partial classes' to extend these types +// Input: messages.proto + +#pragma warning disable CS1591, CS0612, CS3021 + +namespace Dof +{ + + [global::ProtoBuf.ProtoContract()] + public partial class ClientMessage + { + [global::ProtoBuf.ProtoMember(1, Name = @"player_enter")] + public PlayerEnter PlayerEnter + { + get { return __pbn__actual.Is(1) ? ((PlayerEnter)__pbn__actual.Object) : default(PlayerEnter); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(1, value); } + } + public bool ShouldSerializePlayerEnter() => __pbn__actual.Is(1); + public void ResetPlayerEnter() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 1); + + private global::ProtoBuf.DiscriminatedUnionObject __pbn__actual; + + [global::ProtoBuf.ProtoMember(2, Name = @"level_prepared")] + public LevelPrepared LevelPrepared + { + get { return __pbn__actual.Is(2) ? ((LevelPrepared)__pbn__actual.Object) : default(LevelPrepared); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(2, value); } + } + public bool ShouldSerializeLevelPrepared() => __pbn__actual.Is(2); + public void ResetLevelPrepared() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 2); + + [global::ProtoBuf.ProtoMember(3, Name = @"point_found")] + public PointFound PointFound + { + get { return __pbn__actual.Is(3) ? ((PointFound)__pbn__actual.Object) : default(PointFound); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(3, value); } + } + public bool ShouldSerializePointFound() => __pbn__actual.Is(3); + public void ResetPointFound() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 3); + + [global::ProtoBuf.ProtoMember(4, Name = @"level_end")] + public LevelEnd LevelEnd + { + get { return __pbn__actual.Is(4) ? ((LevelEnd)__pbn__actual.Object) : default(LevelEnd); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(4, value); } + } + public bool ShouldSerializeLevelEnd() => __pbn__actual.Is(4); + public void ResetLevelEnd() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 4); + + [global::ProtoBuf.ProtoMember(5, Name = @"all_level_end")] + public AllLevelEnd AllLevelEnd + { + get { return __pbn__actual.Is(5) ? ((AllLevelEnd)__pbn__actual.Object) : default(AllLevelEnd); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(5, value); } + } + public bool ShouldSerializeAllLevelEnd() => __pbn__actual.Is(5); + public void ResetAllLevelEnd() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 5); + + [global::ProtoBuf.ProtoMember(6, Name = @"player_leave")] + public PlayerLeave PlayerLeave + { + get { return __pbn__actual.Is(6) ? ((PlayerLeave)__pbn__actual.Object) : default(PlayerLeave); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(6, value); } + } + public bool ShouldSerializePlayerLeave() => __pbn__actual.Is(6); + public void ResetPlayerLeave() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 6); + + } + + [global::ProtoBuf.ProtoContract()] + public partial class ServerMessage + { + [global::ProtoBuf.ProtoMember(1, Name = @"player_entered")] + public PlayerEntered PlayerEntered + { + get { return __pbn__actual.Is(1) ? ((PlayerEntered)__pbn__actual.Object) : default(PlayerEntered); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(1, value); } + } + public bool ShouldSerializePlayerEntered() => __pbn__actual.Is(1); + public void ResetPlayerEntered() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 1); + + private global::ProtoBuf.DiscriminatedUnionObject __pbn__actual; + + [global::ProtoBuf.ProtoMember(2, Name = @"game_start")] + public GameStart GameStart + { + get { return __pbn__actual.Is(2) ? ((GameStart)__pbn__actual.Object) : default(GameStart); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(2, value); } + } + public bool ShouldSerializeGameStart() => __pbn__actual.Is(2); + public void ResetGameStart() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 2); + + [global::ProtoBuf.ProtoMember(3, Name = @"level_start")] + public LevelStart LevelStart + { + get { return __pbn__actual.Is(3) ? ((LevelStart)__pbn__actual.Object) : default(LevelStart); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(3, value); } + } + public bool ShouldSerializeLevelStart() => __pbn__actual.Is(3); + public void ResetLevelStart() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 3); + + [global::ProtoBuf.ProtoMember(4, Name = @"point_found")] + public PointFound PointFound + { + get { return __pbn__actual.Is(4) ? ((PointFound)__pbn__actual.Object) : default(PointFound); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(4, value); } + } + public bool ShouldSerializePointFound() => __pbn__actual.Is(4); + public void ResetPointFound() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 4); + + [global::ProtoBuf.ProtoMember(5, Name = @"game_finish")] + public GameFinish GameFinish + { + get { return __pbn__actual.Is(5) ? ((GameFinish)__pbn__actual.Object) : default(GameFinish); } + set { __pbn__actual = new global::ProtoBuf.DiscriminatedUnionObject(5, value); } + } + public bool ShouldSerializeGameFinish() => __pbn__actual.Is(5); + public void ResetGameFinish() => global::ProtoBuf.DiscriminatedUnionObject.Reset(ref __pbn__actual, 5); + + } + + [global::ProtoBuf.ProtoContract()] + public partial class PlayerEnter + { + [global::ProtoBuf.ProtoMember(1, Name = @"room_id")] + [global::System.ComponentModel.DefaultValue("")] + public string RoomId { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(2, Name = @"uid")] + [global::System.ComponentModel.DefaultValue("")] + public string Uid { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(3, Name = @"nick_name")] + [global::System.ComponentModel.DefaultValue("")] + public string NickName { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(4, Name = @"country")] + [global::System.ComponentModel.DefaultValue("")] + public string Country { get; set; } = ""; + + } + + [global::ProtoBuf.ProtoContract()] + public partial class LevelResource + { + [global::ProtoBuf.ProtoMember(1, Name = @"level_id")] + [global::System.ComponentModel.DefaultValue("")] + public string LevelId { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(2, Name = @"android_generation")] + [global::System.ComponentModel.DefaultValue("")] + public string AndroidGeneration { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(3, Name = @"ios_generation")] + [global::System.ComponentModel.DefaultValue("")] + public string IosGeneration { get; set; } = ""; + + } + + [global::ProtoBuf.ProtoContract()] + public partial class PlayerEntered + { + [global::ProtoBuf.ProtoMember(1, Name = @"cid")] + public long Cid { get; set; } + + } + + [global::ProtoBuf.ProtoContract()] + public partial class GameStart + { + [global::ProtoBuf.ProtoMember(1, Name = @"level_resource")] + public global::System.Collections.Generic.List LevelResources { get; } = new global::System.Collections.Generic.List(); + + } + + [global::ProtoBuf.ProtoContract()] + public partial class LevelPrepared + { + [global::ProtoBuf.ProtoMember(1, Name = @"cid")] + public long Cid { get; set; } + + [global::ProtoBuf.ProtoMember(2, Name = @"level")] + [global::System.ComponentModel.DefaultValue("")] + public string Level { get; set; } = ""; + + } + + [global::ProtoBuf.ProtoContract()] + public partial class LevelStart + { + [global::ProtoBuf.ProtoMember(1, Name = @"level")] + [global::System.ComponentModel.DefaultValue("")] + public string Level { get; set; } = ""; + + } + + [global::ProtoBuf.ProtoContract()] + public partial class PointFound + { + [global::ProtoBuf.ProtoMember(1, Name = @"cid")] + public long Cid { get; set; } + + [global::ProtoBuf.ProtoMember(2, Name = @"level")] + [global::System.ComponentModel.DefaultValue("")] + public string Level { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(3, Name = @"point_id")] + public int PointId { get; set; } + + } + + [global::ProtoBuf.ProtoContract()] + public partial class LevelEnd + { + [global::ProtoBuf.ProtoMember(1, Name = @"cid")] + public long Cid { get; set; } + + [global::ProtoBuf.ProtoMember(2, Name = @"level")] + [global::System.ComponentModel.DefaultValue("")] + public string Level { get; set; } = ""; + + } + + [global::ProtoBuf.ProtoContract()] + public partial class AllLevelEnd + { + [global::ProtoBuf.ProtoMember(1, Name = @"cid")] + public long Cid { get; set; } + + } + + [global::ProtoBuf.ProtoContract()] + public partial class GameScore + { + [global::ProtoBuf.ProtoMember(1, Name = @"uid")] + [global::System.ComponentModel.DefaultValue("")] + public string Uid { get; set; } = ""; + + [global::ProtoBuf.ProtoMember(2, Name = @"score")] + public int Score { get; set; } + + } + + [global::ProtoBuf.ProtoContract()] + public partial class GameFinish + { + [global::ProtoBuf.ProtoMember(1, Name = @"scores")] + public global::System.Collections.Generic.List Scores { get; } = new global::System.Collections.Generic.List(); + + } + + [global::ProtoBuf.ProtoContract()] + public partial class PlayerLeave + { + [global::ProtoBuf.ProtoMember(1, Name = @"cid")] + public long Cid { get; set; } + + } + +} + +#pragma warning restore CS1591, CS0612, CS3021 diff --git a/Runtime/NetworkGen/Messages.cs.meta b/Runtime/NetworkGen/Messages.cs.meta new file mode 100644 index 0000000..7adbd75 --- /dev/null +++ b/Runtime/NetworkGen/Messages.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d2ca874f5de9f40e0bf69a03d2921c90 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ProtobufHelper.cs b/Runtime/ProtobufHelper.cs new file mode 100644 index 0000000..62a8f40 --- /dev/null +++ b/Runtime/ProtobufHelper.cs @@ -0,0 +1,61 @@ +using System; +using System.IO; +using ProtoBuf.Meta; + +namespace Guru +{ + public interface ISupportInitialize + { + void BeginInit(); + void EndInit(); + } + + public interface IDisposable + { + void Dispose(); + } + + public static class ProtobufHelper + { + public static void Init() + { + } + + public static object FromBytes(Type type, byte[] bytes, int index, int count) + { + using (MemoryStream stream = new MemoryStream(bytes, index, count)) + { + object o = RuntimeTypeModel.Default.Deserialize(stream, null, type); + if (o is ISupportInitialize supportInitialize) + { + supportInitialize.EndInit(); + } + return o; + } + } + + public static byte[] ToBytes(object message) + { + using (MemoryStream stream = new MemoryStream()) + { + ProtoBuf.Serializer.Serialize(stream, message); + return stream.ToArray(); + } + } + + public static void ToStream(object message, MemoryStream stream) + { + ProtoBuf.Serializer.Serialize(stream, message); + } + + public static object FromStream(Type type, MemoryStream stream) + { + object o = RuntimeTypeModel.Default.Deserialize(stream, null, type); + if (o is ISupportInitialize supportInitialize) + { + supportInitialize.EndInit(); + } + return o; + } + } +} \ No newline at end of file diff --git a/Runtime/ProtobufHelper.cs.meta b/Runtime/ProtobufHelper.cs.meta new file mode 100644 index 0000000..c935ae9 --- /dev/null +++ b/Runtime/ProtobufHelper.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87fa43cd32c964f6a9353a6400490b67 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/package.json b/package.json new file mode 100644 index 0000000..fffcf87 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "com.guru.unity.gurudoflib", + "displayName": "GuruDofLib", + "version": "0.0.1", + "description": "基于Guru kcp库实现的客户端接口封装", + "unity": "2020.3", + "license": "MIT", + "category": "Game tool", + "dependencies": { + } +} \ No newline at end of file diff --git a/package.json.meta b/package.json.meta new file mode 100644 index 0000000..0dc0244 --- /dev/null +++ b/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 41df6effece9c454c85e2908e1b00150 +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: