Working code

main
Kevin Li 2023-08-30 20:26:51 +08:00
commit 6490c98849
26 changed files with 1643 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.DS_Store

8
Editor.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c919269e61d904cf0964b56eb9b1b6ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

37
README.md Normal file
View File

@ -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

7
README.md.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 177739947c6344e95a62c8671cf2e96c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Runtime.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23b66401566ec4490b51f7d2375e1228
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

509
Runtime/GameEventMgr.cs Normal file
View File

@ -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<List<Delegate>> _listenerListQueue = new ();
private readonly Queue<List<DelegateGameObjectBonding>> _listenerBondingListQueue = new ();
private readonly Dictionary<int, List<Delegate>> _eventTable = new ();
private readonly Dictionary<int, List<DelegateGameObjectBonding>> _eventObjTable = new ();
private readonly Dictionary<int, List<Delegate>> _pendingAddTable = new ();
private readonly Dictionary<int, List<DelegateGameObjectBonding>> _pendingObjAddTable = new ();
private readonly Dictionary<int, List<Delegate>> _pendingRemoveTable = new ();
private readonly Dictionary<int, List<DelegateGameObjectBonding>> _pendingObjRemoveTable = new ();
// Using raising event list to prevent infinite loop call or changing delegate list
private readonly List<int> _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<T>(int event_id, Action<T> new_listener, GameObject life_cycle_obj = null)
{
DoAddListener(event_id, new_listener, life_cycle_obj);
}
public void AddListener<T1, T2>(int event_id, Action<T1, T2> new_listener, GameObject life_cycle_obj = null)
{
DoAddListener(event_id, new_listener, life_cycle_obj);
}
public void AddListener<T1, T2, T3>(int event_id, Action<T1, T2, T3> 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<T>(int event_id, Action<T> listener, GameObject life_cycle_obj = null)
{
DoRemoveListener(event_id, listener, life_cycle_obj);
}
public void RemoveListener<T1, T2>(int event_id, Action<T1, T2> listener, GameObject life_cycle_obj = null)
{
DoRemoveListener(event_id, listener, life_cycle_obj);
}
public void RemoveListener<T1, T2, T3>(int event_id, Action<T1, T2, T3> 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<T>(int event_id, T arg1)
{
DoRaise(event_id, arg1);
}
public void Raise<T1, T2>(int event_id, T1 arg1, T2 arg2)
{
DoRaise(event_id, arg1, arg2);
}
public void Raise<T1, T2, T3>(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<Delegate> SpawnDelegateList()
{
if(_listenerListQueue.Count > 0)
{
return _listenerListQueue.Dequeue();
}
else
{
return new List<Delegate>();
}
}
private void DespawnDelegateList(List<Delegate> list)
{
if (list == null) return;
list.Clear();
_listenerListQueue.Enqueue(list);
}
private List<DelegateGameObjectBonding> SpawnDelegateBondingList()
{
if (_listenerBondingListQueue.Count > 0)
{
return _listenerBondingListQueue.Dequeue();
}
else
{
return new List<DelegateGameObjectBonding>();
}
}
private void DespawnDelegateBondingList(List<DelegateGameObjectBonding> 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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8912da6e3d9994390b3795e3e9b065d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

168
Runtime/GameKcpClient.cs Normal file
View File

@ -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;
}
/// <summary>
/// 当前 Client 是否处于连接状态
/// </summary>
public bool Running => _running;
/// <summary>
/// 创建一个到 Dof GameServer 的连接
/// </summary>
/// <param name="serverAddress">Dof GameServer 的地址,形如 xxx.xxx.xxx.xxx</param>
/// <param name="port">房间ID</param>
/// <param name="receiver">事件回调接口</param>
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);
}
}
/// <summary>
/// kcp 连接后的回调函数
/// </summary>
/// <param name="ukcp">kcp连接</param>
public void onConnected(Ukcp ukcp)
{
Debug.Log($"[GameKcpClient]onConnected: conv = {ukcp.getConv()}");
sender.Client = ukcp;
if (OnConnected != null)
{
Loom.QueueOnMainThread(() =>
{
OnConnected.Invoke();
});
}
}
/// <summary>
/// kcp 接收到消息的回调函数
/// </summary>
/// <param name="byteBuf">接收到的消息buffer</param>
/// <param name="ukcp">kcp连接</param>
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");
}
}
/// <summary>
/// kcp 连接或者接收消息出错的回调函数
/// </summary>
/// <param name="ex">异常</param>
/// <param name="ukcp">kcp连接</param>
public void handleException(Exception ex, Ukcp ukcp)
{
Debug.LogError($"[GameKcpClient]handleException: {ex}");
}
/// <summary>
/// kcp 连接关闭时的回调函数
/// </summary>
/// <param name="ukcp">kcp连接</param>
public void handleClose(Ukcp ukcp)
{
Debug.Log("[GameKcpClient]: Connection closed");
}
/// <summary>
/// 关闭 Client应该在游戏结束时调用
/// </summary>
public void Close()
{
_kcpClient?.stop();
_running = false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71aa1db1be3544524bb58107e80708ca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
{
"name": "GuruDofLib.Runtime",
"rootNamespace": "",
"references": [
"GUID:832e7ae06a4304a17a11ca2f7b21373d"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e2e9b8b39d6d74d1094c47c89de72e2e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,45 @@
using Dof;
namespace DofLibrary
{
public interface IMessageReceiver
{
/// <summary>
/// Set MessageSender for sending messages
/// </summary>
IMessageSender? MessageSender
{
set;
}
/// <summary>
/// 当玩家进入房间成功时,服务端发送此事件
/// </summary>
/// <param name="cid">当前房间里的玩家编号为0|1</param>
void OnPlayerEntered(long cid);
/// <summary>
/// 当房间里已经进入两个玩家时,服务端发送此事件
/// </summary>
/// <param name="gameStart"></param>
void OnGameStart(GameStart gameStart);
/// <summary>
/// 当两个玩家都准备好了某个关卡时,服务端发送此事件
/// </summary>
/// <param name="levelStart">当前开始的关卡ID</param>
void OnLevelStart(LevelStart levelStart);
/// <summary>
/// 当另一个玩家找到了当前关卡的某个点时,服务端发送此事件
/// </summary>
/// <param name="pointFound">另一个玩家找到的点</param>
void OnPointFound(PointFound pointFound);
/// <summary>
/// 当两个玩家都完成了所有关卡时,服务端发送此事件
/// </summary>
/// <param name="gameFinish">两个玩家的得分</param>
void OnGameFinish(GameFinish gameFinish);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2d2e7c96d274a436895dfb34fdac9d8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

59
Runtime/IMessageSender.cs Normal file
View File

@ -0,0 +1,59 @@
using dotNetty_kcp;
namespace DofLibrary
{
/// <summary>
/// 游戏消息发送接口
/// </summary>
public interface IMessageSender
{
/// <summary>
/// 设置 kcp 客户端,用于发送消息
/// </summary>
Ukcp? Client
{
set;
}
/// <summary>
/// 当收到服务端发来的 PlayerEntered 事件时,设置房间中的玩家编号
/// </summary>
long Cid
{
set;
}
/// <summary>
/// 发送 PlayerEnter 进入房间 消息
/// </summary>
/// <param name="roomId">房间ID</param>
/// <param name="uid">玩家ID</param>
/// <param name="nickName">玩家昵称</param>
/// <param name="country">玩家国家</param>
void PlayerEnter(string roomId, string uid, string nickName, string country);
/// <summary>
/// 发送 LevelPrepared 关卡准备完毕 消息
/// </summary>
/// <param name="levelId">关卡ID</param>
void LevelPrepared(string levelId);
/// <summary>
/// 发送 PointFound 找到点位 消息
/// </summary>
/// <param name="levelId">当前关卡ID</param>
/// <param name="pointId">点位编号</param>
void PointFound(string levelId, int pointId);
/// <summary>
/// 发送 LevelEnd 关卡结束 消息
/// </summary>
/// <param name="levelId">关卡ID</param>
void LevelEnd(string levelId);
/// <summary>
/// 发送 AllLevelEnd 所有关卡结束 消息
/// </summary>
void AllLevelEnd();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 29452c4084f2f45399bb395cedd91ebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

218
Runtime/Loom.cs Normal file
View File

@ -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<Loom>();
}
}
public struct DelayedQueueItem
{
public float time;
public Action action;
}
private List<Action> _actions = new List<Action>();
private List<DelayedQueueItem> _delayed = new List<DelayedQueueItem>();
private List<DelayedQueueItem> _currentDelayed = new List<DelayedQueueItem>();
// 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<Action> _currentActions = new List<Action>();
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}");
}
}
}

11
Runtime/Loom.cs.meta Normal file
View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d56f3a7c2d1a48c78f742f63a075fb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

122
Runtime/MessageSender.cs Normal file
View File

@ -0,0 +1,122 @@
using Dof;
using dotNetty_kcp;
using DotNetty.Buffers;
using UnityEngine;
using System.IO;
using base_kcp;
using Guru;
namespace DofLibrary
{
/// <summary>
/// 游戏消息发送接口实现类
/// </summary>
internal class MessageSender : IMessageSender
{
private long _cid;
private Ukcp _client;
private MemoryStream _localSendMs = new(1024 * 1024 * 1);
/// <summary>
/// 设置 kcp 客户端,用于发送消息
/// </summary>
public Ukcp Client
{
set => _client = value;
}
/// <summary>
/// 当收到服务端发来的 PlayerEntered 事件时,设置房间中的玩家编号
/// </summary>
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();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 83018a8afd2ab4af499e831caa714f77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

8
Runtime/NetworkGen.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 92dc6b0ee5121a94bb34348448a983a1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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<LevelResource> LevelResources { get; } = new global::System.Collections.Generic.List<LevelResource>();
}
[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<GameScore> Scores { get; } = new global::System.Collections.Generic.List<GameScore>();
}
[global::ProtoBuf.ProtoContract()]
public partial class PlayerLeave
{
[global::ProtoBuf.ProtoMember(1, Name = @"cid")]
public long Cid { get; set; }
}
}
#pragma warning restore CS1591, CS0612, CS3021

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d2ca874f5de9f40e0bf69a03d2921c90
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

61
Runtime/ProtobufHelper.cs Normal file
View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 87fa43cd32c964f6a9353a6400490b67
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

11
package.json Normal file
View File

@ -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": {
}
}

7
package.json.meta Normal file
View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 41df6effece9c454c85e2908e1b00150
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: