1.位置同步和状态同步:需要同步的节点上挂载脚本:
gameObject.AddComponent<SyncTransform>(); ; //同步
gameObject.GetComponent<SyncTransform>().syncId = SyncUtilFunc.GetRoleSyncId(PlayerData.Instance.PlayerId); //同步
gameObject.GetComponent<SyncTransform>().ownerState = OwnerState.Mine;
gameObject.GetComponent<SyncTransform>().isRole = true;
gameObject.AddComponent<SyncState>(); //同步
gameObject.GetComponent<SyncState>().syncId = SyncUtilFunc.GetRoleSyncId(PlayerData.Instance.PlayerId); //同步
gameObject.GetComponent<SyncState>().ownerState = OwnerState.Mine;
(1)用于多人同步的基类:SyncBase.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum OwnerState {
Null,//无人拥有
Mine,//拥有者是我
Ohter,//拥有者是其他玩家
}
public class SyncBase : MonoBehaviour
{
/// <summary>
/// 只有拥有者才允许发数据
/// </summary>
public OwnerState ownerState;
public string syncId;
public SyncWrapData dataBase = new SyncWrapData();
protected string dataName;
public void SendNetWork<T>(T data, string stName) {
if (dataName != stName) {
Debug.LogError("error:传输数据出错请检查" + dataName + " :" + stName);
return;
}
dataBase.syncId = syncId;
dataBase.name = dataName;
dataBase.obj = JsonUtility.ToJson(data);
dataBase.playerId = PlayerData.Instance.PlayerId.ToString();
NetManager.Instance.SendSyncData(dataBase);
}
public void AddNetWorkListner<T>(Action<T> callback,string stName) {
if (NetManager.Instance.isConnected() == false) {
return;
}
dataName = stName;
NetManager.Instance.AddSyncHandler(nameof(T), (SyncWrapData syncData) => {
if (syncData.syncId == syncId) { //只关心自己同步ID的事情
if (stName == syncData.name) {//类型结构需要和传递进来的结构一致
var dstData = JsonUtility.FromJson<T>(syncData.obj); //ID一样自然可以转
callback(dstData);
}
}
});
}
}
引入库:LitJson
引用类:
public class SyncWrapData {
public string syncId;
public string playerId; //拥有者的ID
public bool isLocation;
public string name;
public string obj;
}
模型层:SyncModel.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public static class SyncUtilFunc {
public static string GetRoleSyncId(long id) {
return $"{ConstTag.Player}{id}";
}
}
public class SyncWrapData {
public string syncId;
public string playerId; //拥有者的ID
public bool isLocation;
public string name;
public string obj;
}
/// <summary>
/// 数据要乘以10转成分米
/// </summary>
public class SyncTransfomData {
public int[] pos = new int[3];
public int[] roate = new int[3];
public int[] scale = new int[3];
}
public static class SyncTransfomUtil
{
public static SyncTransfomData FromTransform(Transform t,ref SyncTransfomData data) {
data.pos[0] = (int)(t.position.x * 10f);
data.pos[1] = (int)(t.position.y * 10f);
data.pos[2] = (int)(t.position.z * 10f);
data.roate[0] = (int)(t.eulerAngles.x * 10f);
data.roate[1] = (int)(t.eulerAngles.y * 10f);
data.roate[2] = (int)(t.eulerAngles.z * 10f);
data.scale[0] = (int)(t.localScale.x * 10f);
data.scale[1] = (int)(t.localScale.y * 10f);
data.scale[2] = (int)(t.localScale.z * 10f);
return data;
}
public static SyncTransfomData FromTankLocation(TankLocation location, ref SyncTransfomData data) {
data.pos[0] = (int)(location.x*10f);
data.pos[1] = (int)(location.y*10f);
data.pos[2] = (int)(location.z*10f);
data.roate[0] = (int)(location.dx * 10f);
data.roate[1] = (int)(location.dy * 10f);
data.roate[2] = (int)(location.dz * 10f);
return data;
}
public static void ToTransform(Transform t, SyncTransfomData data) {
t.position = new Vector3((float)data.pos[0]/10f, (float)data.pos[1] / 10f, (float)data.pos[2] / 10f);
t.eulerAngles = new Vector3((float)data.roate[0]/10f, (float)data.roate[1] / 10f, (float)data.roate[2] / 10f);
}
public static TankLocation ToTankLocation(ref TankLocation location, SyncTransfomData data) {
location.x = ((float)data.pos[0]) / 10f;
location.y = ((float)data.pos[1]) / 10f;
location.z = ((float)data.pos[2]) / 10f;
location.dx = ((float)data.roate[0]) / 10f;
location.dx = ((float)data.roate[1]) / 10f;
location.dx = ((float)data.roate[2]) / 10f;
return location;
}
/// <summary>
/// 分米内相同
/// </summary>
/// <param name="v1"></param>
/// <param name="v2"></param>
/// <returns></returns>
public static bool V3Equal(Vector3 v1 ,Vector3 v2) {
return (Math.Abs(v1.x - v2.x) < 0.1f) && (Math.Abs(v1.y - v2.y) < 0.1f) && (Math.Abs(v1.z - v2.z) < 0.1f);
}
}
public class SyncStateData {
public string stateName;
}
public class SyncOperationData {
public string operation;
}
public class SyncShareData{
public string ownerId;
public float[] pos = new float[3];
public float[] roate = new float[3];
public float[] scale = new float[3];
}
public class SyncWorldData {
public Dictionary<string, SyncShareData> dir_shareDatas = new Dictionary<string, SyncShareData>();
public Dictionary<string, SyncStateData> dir_stateDatas = new Dictionary<string, SyncStateData>();
public Dictionary<string, SyncTransfomData> dir_transformDatas = new Dictionary<string, SyncTransfomData>();
}
public class SyncWorldModel:SingletonBase<SyncWorldModel> {
SyncWorldData m_worldData = new();
public void SetWorldData(SyncWorldData data) {
m_worldData = data;
}
public SyncShareData GetShareData(string syncId) {
if (m_worldData.dir_shareDatas.ContainsKey(syncId)) {
return m_worldData.dir_shareDatas[syncId];
}
return null;
}
public SyncStateData GetStateData(string syncId) {
if (m_worldData.dir_stateDatas.ContainsKey(syncId)) {
return m_worldData.dir_stateDatas[syncId];
}
return null;
}
public SyncTransfomData GetTransfomData(string syncId) {
if (m_worldData.dir_transformDatas.ContainsKey(syncId)) {
return m_worldData.dir_transformDatas[syncId];
}
return null;
}
public void SetShareData(string syncId, SyncShareData data) {
if (m_worldData.dir_shareDatas.ContainsKey(syncId)) {
m_worldData.dir_shareDatas[syncId] = data;
}
else {
m_worldData.dir_shareDatas.Add(syncId,data);
}
}
public void SetStateData(string syncId, SyncStateData data) {
if (m_worldData.dir_stateDatas.ContainsKey(syncId)) {
m_worldData.dir_stateDatas[syncId] = data;
}
else {
m_worldData.dir_stateDatas.Add(syncId, data);
}
}
public void SetTransfomData(string syncId, SyncTransfomData data) {
if (m_worldData.dir_transformDatas.ContainsKey(syncId)) {
m_worldData.dir_transformDatas[syncId] = data;
}
else {
m_worldData.dir_transformDatas.Add(syncId, data);
}
}
public void SyncWorldData() {
NetManager.Instance.SendSyncWorldData(m_worldData);
}
}
网络层脚本1:NetManager.cs
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using LitJson;
using UnityEngine.Networking;
using UTNET;
using Unity.Burst.Intrinsics;
using System.Runtime.InteropServices;
public class NetMessage<T> {
public uint mergeid;
delegate void Message(object sender, T msg);
}
public class NetManager : MonoBehaviour {
private NetManager() { }
public static NetManager Instance;
public string token, version;
public Client client;
public string HaiMetaSpaceUrl = "https://test-haimeta-1314523153.cos-website.ap-shanghai.myqcloud.com/space";
//192.144.138.221 /game.icewhale.net
Dictionary<string, List<Action<ExternalMessage>>> actions = new Dictionary<string, List<Action<ExternalMessage>>>();
private Coroutine rec;
[DllImport("__Internal")]
private static extern string GetUA();
private void Awake() {
Instance = this;
#if !UNITY_EDITOR && UNITY_WEBGL
string a = GetUA();
if (a == "1")
{
//PC端
Debug.Log("当前运行环境在PC端");
PlayerData.Instance.isRunningPC = true;
}
if (a == "2")
{
//移动端
Debug.Log("当前运行环境在移动端");
PlayerData.Instance.isRunningPC = false;
}
#endif
EventManager.Instance.AddListener(EventName.OnSceneLoaded,OnSceneLoaded);
}
private void OnSceneLoaded(object sender, EventArgs e) {
var arg = e as SceneLoadedArg;
if (arg.state == AppConst.ViewMode) {
CreateConeccetionBtn();
}
}
async void Start() {
DontDestroyOnLoad(this);
Screen.sleepTimeout = SleepTimeout.NeverSleep;
pushBtn();
}
public void webOpenHaiMetaUrl(string param = null) {
//string url = this.HaiMetaSpaceUrl+param;
//UnityEngine.Application.ExternalEval("window.location.href = \"" + url + "\";");
}
public void webOpenNewSpace(string spaceUrl,string spaceId)
{
string url = spaceUrl + "?param={\"spaceId\":"+ spaceId + ",\"userName\":"+PlayerData.Instance.Name+",\"templateId\":"+ PlayerData.Instance.TemplateId + "}" + "&token=" + PlayerData.Instance.ltk + "&type=4"+ "&state=1";
UnityEngine.Application.ExternalEval("window.location.href = \"" + url + "\";");
}
private void Update() {
//if (client == null && newClient != null) {
// client = getNewClient();
//}
}
public async void CreateConeccetionBtn()
{
#if !UNITY_EDITOR
await HttpHelper.Instance.GetServerConfig();
#endif
//string host = string.Format("wss://game.icewhale.net/login");
Debug.Log("CreateConeccetionBtn" +Host.gameServer);
client = new Client(Host.gameServer);
client.OnDisconnect = OnDisconnect;
client.OnReconected = OnReconected;
client.OnError = OnError;
client.OnConnected = OnConnected;
bool isConeccet = await client.ConnectAsync();
if (isConeccet)
{
Debug.Log("网络链接成功");
}
}
//public void resetClient() {
// setNewClient(client);
//}
private void OnConnected() {
Debug.Log("OnConnected");
if (rec != null)
StopCoroutine(rec);
this.TriggerEvent(EventName.SocketOpen, null);
}
private void OnError(string msg) {
Debug.LogError(string.Format("err msg:{0}", msg));
}
private void OnReconected() {
Debug.Log("OnReconect");
}
private void OnDisconnect() {
Debug.Log("OnDisconnect");
//client = null;
WarningPanel.ShowTimeTips("重连中....",10,null,null);
rec = StartCoroutine(client.ReconectScoektAsync()); //断线重连逻辑
}
public void pushBtn() {
LoadNetCofig();
}
void LoadNetCofig() {
var configPath = Application.dataPath;
#if UNITY_EDITOR
var filepath = Path.Combine(Application.dataPath.Replace("Assets", ""), "config.txt");
#else
var filepath = Path.Combine(Application.dataPath, "config.txt");
#endif
Debug.Log("configPath" + filepath);
filepath = filepath.Replace("\\", "/");
LoadFileSetNetwork(filepath);
}
async void LoadFileSetNetwork(string filepath) {
UnityWebRequest www = UnityWebRequest.Get(filepath);
await www.SendWebRequest();
if (www.result == UnityWebRequest.Result.ConnectionError || www.result == UnityWebRequest.Result.ProtocolError) {
Debug.LogError(www.error);
}
else {
string json = www.downloadHandler.text;
var data = LitJson.JsonMapper.ToObject(json);
#if UNITY_EDITOR
var serverMode = AppConst.serverMode;
#else
var serverMode = (ServerMode)int.Parse(HttpHelper.GetUrlParam("severAPI"));
#endif
AppConst.serverMode = serverMode;
switch (serverMode) {
case ServerMode.preview:
Host.ApiHost = (string)data["preview"];
break;
case ServerMode.dev:
Host.ApiHost = (string)data["dev"];
break;
case ServerMode.overseas:
Host.ApiHost = (string)data["overseas"];
break;
}
switch (serverMode)
{
case ServerMode.overseas:
Host.AssetBundleIP = (string)data["OverseasAssetBundleIP"];
break;
case ServerMode.preview:
Host.AssetBundleIP = (string)data["PreviewAssetBundleIP"];
break;
case ServerMode.dev:
if ((string)data["AssetBundleIP"] != string.Empty)
{
Host.AssetBundleIP = (string)data["AssetBundleIP"];
}
break;
}
#if !UNITY_EDITOR
var paramUrl = HttpHelper.GetUrlParam("param");
JsonData paramdata = JsonMapper.ToObject(UtilsFunc.UnicodeToString(paramUrl));
try {
string spaceId = paramdata["spaceId"].ToJson();
PlayerData.Instance.SpaceId = spaceId;
PlayerData.Instance.ltk = HttpHelper.GetUrlParam("token");
}
catch (Exception e) {
Debug.LogError(e.Message + '\n' + e.StackTrace);
}
#endif
Debug.Log("LoadFileSetNetwork SpaceId" + PlayerData.Instance.SpaceId);
//如果在编辑器下面,并且不填sapceId,那么则进入发布模式
if (string.IsNullOrEmpty(PlayerData.Instance.SpaceId) == false) {
await HttpHelper.Instance.GetSceneConfig();
}
#if !UNITY_EDITOR
if (paramdata.ContainsKey("templateId")) {
if (paramdata["templateId"] != null) {
string tempStr = (string)paramdata["templateId"];
bool isExist = int.TryParse(tempStr,out var tempId);
if (isExist) PlayerData.Instance.TemplateId = tempId;
}
}
string stateUrl = HttpHelper.GetUrlParam("state");
#else
string stateUrl = string.IsNullOrEmpty(PlayerData.Instance.SpaceId) == true ? AppConst.PublicMode : AppConst.ViewMode;
#endif
var arg = new SceneLoadActionArgs();
arg.state = stateUrl;
if (stateUrl == AppConst.PublicMode) {
PlayerData.Instance.IsEditorMode = true;
PlayerData.Instance.IsPublicMode = true;
EventManager.Instance.TriggerEvent(EventName.OnPublishEnter, arg);
HttpHelper.Instance.GetDefaultSpaceImg();
return;
}
#if UNITY_EDITOR
Host.gameServer = (string)data["local"];
#endif
EventManager.Instance.TriggerEvent(EventName.LoadSceneAction, arg);
}
}
private async UniTaskVoid CreateConeccetion() {
Debug.Log("开始...");
bool isConeccet = await client.ConnectAsync();
if (isConeccet) {
Debug.Log("网络链接成功");
// this.Register<RegisterInfo>("test", (int code) =>
// {
// Debug.Log("网络事件" +code);
// });
if (client != null) client.isConnect = true;
//this.SendAPI();
}
else
Debug.Log("多次链接仍让未成功");
}
public void AddSyncHandler(string name, Action<SyncWrapData> call) {
NetManager.Instance.Register<GameFrame>("gameFrame", (ExternalMessage pack) => {
GameFrame info = PackageProtocol.DecodeInfo<GameFrame>(pack.data);
//if (info.location.playerId == PlayerData.Instance.PlayerId) {
// return; //自己不用给自己同步
//}
var opc = info.opc;
SyncWrapData dataBase = null;
try {
dataBase = JsonUtility.FromJson<SyncWrapData>(opc);
if (dataBase.playerId != PlayerData.Instance.PlayerId.ToString()) { //只接受不同playerId的数据
call.Invoke(dataBase);
}
}
catch (Exception e) {
//Debug.LogError(e);
}
});
}
public async void SendSyncData(SyncWrapData syncBase) {
string jsonData = JsonUtility.ToJson(syncBase);
if (NetManager.Instance.client == null) {
Debug.LogError("NetManager.Instance.client == null");
return;
}
var gameFrame = default (GameFrame);
gameFrame.opc = jsonData;
await NetManager.Instance.client.RequestAsync<GameFrame, GameFrame>("gameFrame", gameFrame);
}
public async void SendSyncWorldData(SyncWorldData worldData) {
string jsonData = LitJson.JsonMapper.ToJson(worldData);
if (NetManager.Instance.client == null) {
Debug.LogError("NetManager.Instance.client == null");
return;
}
//Debug.Log("SendSyncWorldData worldData:" + jsonData);
var gameFrame = default(GameFrame);
gameFrame.world = jsonData;
await NetManager.Instance.client.RequestAsync<GameFrame, GameFrame>("gameFrame", gameFrame);
}
public async void SendFrameLocationData(SyncTransfomData syncPos,SyncWrapData wrap) {
if (NetManager.Instance.client == null) {
Debug.LogError("NetManager.Instance.client == null");
return;
}
var gameFrame = new GameFrame();
gameFrame.opc = JsonUtility.ToJson(wrap);
gameFrame.location = new TankLocation();
SyncTransfomUtil.ToTankLocation(ref gameFrame.location, syncPos);
gameFrame.location.playerId = PlayerData.Instance.PlayerId;
await NetManager.Instance.client.RequestAsync<GameFrame, GameFrame>("gameFrame", gameFrame);
}
public void Register<T>(string name, Action<ExternalMessage> ac) {
//#if UNITY_WEBGL && !UNITY_EDITOR //只在WebGl下生效
if (PlayerData.Instance.IsEditorMode) return;
//#endif
//加入事件列表
Proto pb = ProtoMaps.Instance.Name2Pb(name);
List<Action<ExternalMessage>> list = null;
if (actions.ContainsKey(name)) {
list = actions[name];
}
else {
list = new List<Action<ExternalMessage>>();
actions.Add(name, list);
}
list.Add(ac);
client.OnNetEx(name, (ExternalMessage pack) => {
T info = PackageProtocol.DecodeInfo<T>(pack.data);
//Debug.Log("网络事件" + JsonUtility.ToJson(info));
//遍历事件列表依次回调
foreach (Action<ExternalMessage> cb in list) {
cb?.Invoke(pack);
}
});
}
public async void SendAPI() {
//请求注册
//RegisterInfo register = new RegisterInfo();
//register.account = "abc";
//register.phoneNum = "abc";
//register.pwd = "abc";
//RegisterInfo a = await client.RequestAsync<RegisterInfo, RegisterInfo>("test", register);
//Debug.Log(a.phoneNum);
client.OnNetEx("login", (ExternalMessage pack) => {
Debug.LogError("回调返回");
//T info = PackageProtocol.DecodeInfo<T>(pack.data);
Debug.Log("网络事件" + JsonUtility.ToJson(info));
遍历事件列表依次回调
//foreach (Action<ExternalMessage> cb in list)
//{
// cb?.Invoke(pack);
//}
});
LoginVerify loginVerify = new LoginVerify();
loginVerify.account = "18060974935";
loginVerify.pwd = "123456ab";
loginVerify.loginBizCode = 1;
UserInfo ret = await NetManager.Instance.client.RequestAsync<LoginVerify, UserInfo>("login", loginVerify);
Debug.Log(ret.nickName);
TankEnterRoom myEnterRoom = new TankEnterRoom();
myEnterRoom.roomId = 1002;
myEnterRoom.team = 1;
TankEnterRoom ret2 = await NetManager.Instance.client.RequestAsync<TankEnterRoom, TankEnterRoom>("enterRoom", myEnterRoom);
Debug.Log(ret2.time);
//int code = await client.RequestAsync<String, int>("gameObjUsed", "ok");
}
public bool isConnected() {
if (client == null) {
return false;
}
return client.isConnect;
}
private void OnDestroy() {
if (client != null) {
client.Cancel();
}
}
}
网络层脚本2:Client.cs
using System;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System.Threading;
using System.Collections;
using UnityWebSocket;
namespace UTNET
{
public enum NetWorkState
{
CONNECTING,
CONNECTED,
DISCONNECTED,
TIMEOUT,
ERROR,
KICK
}
public class Client
{
private static int RqID = 0;
//public NetWorkState state;
public Action OnReconected, OnDisconnect, OnConnected;
public Action<string> OnError;
public uint retry;
public bool isConnect = false;
Protocol protocol;
WebSocket socket;
UniTaskCompletionSource<bool> utcs;
private bool isForce;
private bool isHeardbeat = false;
private string host;
public Client(string host)
{
Debug.Log("Client" + host);
this.host = host;
this.createSocket();
}
private void createSocket()
{
if (socket != null) {
}
Debug.Log("createSocket host=" + this.host);
utcs = new UniTaskCompletionSource<bool>();
if (socket != null) {
socket.CloseAsync();
socket.OnClose += ReCreateSocket;
}
else {
socket = new WebSocket(this.host);
socket.OnOpen += OnOpen;
socket.OnMessage += OnMessage;
socket.OnClose += OnClose;
socket.OnError += OnErr;
}
}
private void ReCreateSocket(object sender, CloseEventArgs e) {
socket = new WebSocket(this.host);
socket.OnOpen += OnOpen;
socket.OnMessage += OnMessage;
socket.OnClose += OnClose;
socket.OnError += OnErr;
}
//断线重连逻辑
public IEnumerator ReconectScoektAsync()
{
while (true)
{
yield return new WaitForSeconds(2);
Debug.Log(" client : reconectScoekt");
if (PlayerData.Instance.isAnotherLogin) break;
if (socket.ReadyState == WebSocketState.Connecting || socket.ReadyState == WebSocketState.Open) break;
socket.ConnectAsync();
}
}
//开始心跳逻辑
public IEnumerator StartHeardBeat()
{
isHeardbeat = true;
while (true)
{
Debug.Log("开始发送心跳");
if (isHeardbeat == false) continue;
yield return new WaitForSeconds(2);
//发送心跳
this.sendHeardbeat();
}
}
private async void sendHeardbeat()
{
int u1 = await this.RequestAsync<int, int>("heartbeat", 21);
}
public UniTask<bool> ConnectAsync()
{
socket.ConnectAsync();
return utcs.Task;
}
private void OnOpen(object sender, OpenEventArgs e)
{
if (protocol == null)
protocol = new Protocol();
protocol.SetSocket(socket);
protocol.OnReconected = OnReconected;
protocol.OnError = OnError;
//bool isOK = await protocol.HandsharkAsync(this.token);
Debug.Log("open:" + e);
isConnect = true;
utcs.TrySetResult(true);
OnConnected?.Invoke();
}
private async void OnClose(object sender, CloseEventArgs e)
{
if (socket.ReadyState == UnityWebSocket.WebSocketState.Connecting || socket.ReadyState == UnityWebSocket.WebSocketState.Open) return;
await UniTask.SwitchToMainThread();
isConnect = false;
Cancel();
if (!isForce)
{
await UniTask.Delay(1000);
//socket.Open();
}
OnDisconnect?.Invoke();
}
public void OnErr(object sender, ErrorEventArgs e)
{
utcs.TrySetResult(false);
isConnect = false;
//Debug.LogError(e.Exception.Message);
}
private void OnMessage(object sender, MessageEventArgs e)
{
protocol.OnReceive(e.RawData);
isConnect = true;
}
public void OnNet(uint mid, uint sid, Action<ExternalMessage> cb)
{
uint merge = PackageProtocol.getMergeCmd(mid, sid);
protocol.SetOnNet(merge, cb);
}
public void OnNetEx(string name, Action<ExternalMessage> cb)
{
Proto pb = ProtoMaps.Instance.protos[name] as Proto;
uint merge = PackageProtocol.getMergeCmd(pb.mid, pb.sid);
protocol.SetOnNet(merge, cb);
}
public void Notify<T>(string route, T info = default)
{
//uint rqID = (uint)Interlocked.Increment(ref RqID);
try
{
#if SOCKET_DEBUG
Debug.Log(string.Format("[Notify] -->> [{0}] {1}", route, JsonUtility.ToJson(info)));
#endif
protocol.Notify<T>(route, info);
}
catch (Exception e)
{
Debug.Log(string.Format("[Notify Exception]{0}", e.Message));
throw e;
}
}
public async UniTask<S> RequestAsyncEx<T, S>(uint mid, uint sid, T info = default, string modelName = null)
{
uint cmdMerge = PackageProtocol.getMergeCmd(mid, sid);
uint rqID = (uint)Interlocked.Increment(ref RqID);
try
{
#if SOCKET_DEBUG
Debug.Log(string.Format("[{0}][Request] -->> [{1}] {2}", rqID, route, JsonUtility.ToJson(info)));
#endif
ExternalMessage pack = await protocol.RequestAsync<T>(rqID, cmdMerge, info, modelName);
S msg = PackageProtocol.DecodeInfo<S>(pack.data);
#if SOCKET_DEBUG
Debug.Log(string.Format("[{0}][Request] <<-- [{1}] {2} {3}", rqID, route, JsonUtility.ToJson(msg), JsonUtility.ToJson(msg.info)));
#endif
return msg;
}
catch (Exception e)
{
//Debug.Log(string.Format("[{0}][RequestAsync Exception]{1}", rqID, e.Message));
throw e;
}
}
public async UniTask<S> RequestAsync<T, S>(string name, T info = default, string modelName = null)
{
// #if UNITY_WEBGL && !UNITY_EDITOR //只在WebGl下生效
if (PlayerData.Instance.IsEditorMode) {
S msg = PackageProtocol.DecodeInfo<S>(null);
return msg;
}
//#endif
//Debug.Log($"sendMsg {name}");
Proto pb = ProtoMaps.Instance.Name2Pb(name);
uint cmdMerge = PackageProtocol.getMergeCmd(pb.mid, pb.sid);
uint rqID = (uint)Interlocked.Increment(ref RqID);
//Debug.LogError("调试消息id" + rqID);
try
{
#if SOCKET_DEBUG
Debug.Log(string.Format("[{0}][Request] -->> [{1}] {2}", rqID, route, JsonUtility.ToJson(info)));
#endif
ExternalMessage pack = await protocol.RequestAsync<T>(rqID, cmdMerge, info, modelName);
S msg = PackageProtocol.DecodeInfo<S>(pack.data);
#if SOCKET_DEBUG
Debug.Log(string.Format("[{0}][Request] <<-- [{1}] {2} {3}", rqID, route, JsonUtility.ToJson(msg), JsonUtility.ToJson(msg.info)));
#endif
return msg;
}
catch (Exception e)
{
//Debug.Log(string.Format("[{0}][RequestAsync Exception]{1}", rqID, e.Message));
//Debug.Log("房间网络连接断开.......");
S msg = PackageProtocol.DecodeInfo<S>(null);
return msg;
throw e;
}
}
public void Cancel(bool isForce = false)
{
this.isForce = isForce;
utcs.TrySetCanceled();
if (socket.ReadyState != UnityWebSocket.WebSocketState.Closed)
{
socket.CloseAsync();
}
if (protocol != null)
{
protocol.StopHeartbeat();
protocol.CanceledAllUTcs();
}
}
public UnityWebSocket.WebSocketState getSocketState() {
if (socket != null) {
return socket.ReadyState;
}
return 0;
}
public void Close() {
Debug.Log("UnityWebSocket......" + socket);
Debug.Log("UnityWebSocket状态......"+ socket.ReadyState);
if (socket!=null && socket.ReadyState == UnityWebSocket.WebSocketState.Open) {
socket.CloseAsync();
Debug.Log("强制玩家关闭连接......");
}
}
void OnDestroy()
{
socket.CloseAsync();
}
}
}
网络层脚步3:PackageProtocol.cs
using ProtoBuf;
using System;
using System.IO;
using System.Text;
using UnityEngine.XR;
namespace UTNET
{
public enum PackageType
{
HEARTBEAT = 1,
REQUEST = 2,
PUSH = 3,
KICK = 4,
RESPONSE = 5,
HANDSHAKE = 6,
ERROR = 7,
NOTIFY = 8
}
public class PackageProtocol
{
//获取cmd
public static uint getCmd(uint merge)
{
return merge >> 16;
}
//获取subCmd
public static uint getSubCmd(uint merge)
{
return merge & 0xFFFF;
}
//获取mergeCmd
public static uint getMergeCmd(uint cmd, uint subCmd)
{
return (cmd << 16) + subCmd;
}
public static byte[] Encode(PackageType type)
{
Package sr = new Package()
{
packageType = (uint)type
};
return Serialize(sr);
}
public static byte[] Encode<T>(PackageType type, uint packID, string route, T info, string modelName = null)
{
Package sr = new Package()
{
packageType = (uint)type,
packID = packID,
route = route,
buff = Serialize<T>(info),
modelName = modelName
};
return Serialize(sr);
}
public static byte[] EncodeEx<T>(uint packID, uint cmdMerge, T info)
{
//Package sr = new Package()
//{
// packageType = (uint)type,
// packID = packID,
// route = route,
// buff = Encoding.Default.GetBytes(SerializeEx<T>(info)),
//};
//return SerializeEx(sr);
ExternalMessage sr = new ExternalMessage()
{
cmdCode = 100,
protocolSwitch = 1,
cmdMerge = cmdMerge,
responseStatus = 0,
validMsg = "",
data = Encoding.UTF8.GetBytes(SerializeEx<T>(info)),
};
return Serialize(sr);
}
public static byte[] Encode<T>(uint packID, uint cmdMerge, T info)
{
ExternalMessage sr = new ExternalMessage()
{
cmdCode = 100,
protocolSwitch = 1,
cmdMerge = cmdMerge,
responseStatus = 0,
validMsg = "",
data = Serialize<T>(info),
msgId = (int)packID,
};
return Serialize(sr);
}
public static string SerializeEx<T>(T t)
{
using (MemoryStream ms = new MemoryStream())
{
Serializer.Serialize<T>(ms, t);
return Encoding.UTF8.GetString(ms.ToArray());
}
}
public static byte[] Serialize<T>(T info)
{
if (typeof(T) == typeof(int))
{
int value = (int)Convert.ChangeType(info, typeof(int));
value = (value << 1);
info = (T)Convert.ChangeType(info, typeof(T));
}
MemoryStream ms = new MemoryStream();
Serializer.Serialize(ms, info);
byte[] buff = ms.ToArray();
ms.Close();
return buff;
//try
//{
// //涉及格式转换,需要用到流,将二进制序列化到流中
// using (MemoryStream ms = new MemoryStream())
// {
// //使用ProtoBuf工具的序列化方法
// Serializer.Serialize<T>(ms, info);
// //定义二级制数组,保存序列化后的结果
// byte[] result = new byte[ms.Length];
// //将流的位置设为0,起始点
// ms.Position = 0;
// //将流中的内容读取到二进制数组中
// ms.Read(result, 0, result.Length);
// return result;
// }
//}
//catch (Exception ex)
//{
// return null;
//}
}
public static ExternalMessage Decode(byte[] buff)
{
//protobuf反序列化
MemoryStream mem = new MemoryStream(buff);
ExternalMessage rs = Serializer.Deserialize<ExternalMessage>(mem);
mem.Close();
return rs;
}
public static T DecodeInfo<T>(byte[] buff)
{
if (buff == null) return default;
T rs;
if (typeof(T) == typeof(int))
{
int value;
using (var stream = new MemoryStream(buff))
{
value = Serializer.Deserialize<int>(stream);
}//转zig zag
value = (value >> 1) ^ -(value & 1);
rs = (T)Convert.ChangeType(value, typeof(T));
}
else
{
MemoryStream mem = new MemoryStream(buff);
rs = Serializer.Deserialize<T>(mem);
mem.Close();
}
return rs;
}
//public static T MyMethod<T>(byte[] buff) where T : Object
//{
// return null;
//}
}
}
网络层脚本4:ProtoMaps.cs
using System;
using System.Collections.Generic;
namespace UTNET
{
public interface IProto
{
}
//R ��������, S ��������
//public class Proto<R, S> : IProto
public class Proto : IProto
{
public string name; //������
public uint mid { get; set; } //��id
public uint sid { get; set; } //��id
public uint mergeid;
public int typ; //������
//public R r;
//public S s;
public Proto(string name, uint mid, uint sid, int typ)
{
this.name = name;
this.mid = mid;
this.sid = sid;
this.typ = typ;
}
}
public class ProtoMaps
{
private static readonly ProtoMaps instance = new ProtoMaps();
public static ProtoMaps Instance
{
get { return instance; }
}
//public Dictionary<string, IProto> protos = new Dictionary<string, IProto>();
public Dictionary<string, Proto> protos = new Dictionary<string, Proto>();
public Dictionary<uint, Proto> id2proto = new Dictionary<uint, Proto>();
public void Add(string name, Proto pb)
{
protos.Add(name, pb);
}
public void SortById()
{
foreach (var item in protos)
{
var pb = item.Value;
uint mergeid = PackageProtocol.getMergeCmd(pb.mid, pb.sid);
pb.mergeid = mergeid;
id2proto.Add(mergeid, pb);
}
}
public ProtoMaps()
{
this.Add("login", new Proto("login", 1, 2, 1));
this.Add("registerInfo", new Proto("registerInfo", 1, 1, 1));
this.Add("enterRoom", new Proto("enterRoom", 1, 13, 3));
this.Add("roleMove", new Proto("roleMove", 4, 3, 3));
//this.Add("onNewRoleEnter", new Proto("onNewRoleEnter", 7, 1, 2));
//服务器主动广播的角色退出
this.Add("roleExitRoom", new Proto("roleExitRoom", 7, 2, 2));
this.Add("talkMrg", new Proto("talkMrg", 1, 9, 3));
//玩家点击的角色退出
this.Add("playerExitRoom", new Proto("playerExitRoom", 1, 5, 3));
this.Add("gameFrame", new Proto("gameFrame", 4, 2, 1));
this.Add("gameObjUsed", new Proto("gameObjUsed", 4, 4, 3));
this.Add("getOtherInfo", new Proto("getOtherInfo", 1, 23, 1));
this.Add("heatBeat", new Proto("heatBeat", 1, 120, 1));
SortById();
}
public Proto Name2Pb(string name)
{
//Proto pb = ProtoMaps.Instance.protos[name];
if (protos.ContainsKey(name))
{
return protos[name];
}
return null;
}
internal Proto GetByMergeId(uint cmdMerge)
{
if (id2proto.ContainsKey(cmdMerge))
{
return id2proto[cmdMerge];
}
return null;
}
}
}
网络层脚步5:ExternalMessage.cs
using ProtoBuf;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace UTNET
{
[ProtoContract]
public class ExternalMessage
{
[ProtoMember(1)]
// 请求命令类型: 0 心跳,1 业务
public int cmdCode;
// 协议开关,用于一些协议级别的开关控制,比如 安全加密校验等。 : 0 不校验
[ProtoMember(2)]
public int protocolSwitch;
// 业务路由(高16为主, 低16为子)
[ProtoMember(3)]
public uint cmdMerge;
// 响应码: 0:成功, 其他为有错误
[ProtoMember(4,DataFormat = DataFormat.ZigZag)]
public int responseStatus;
// 验证信息(错误消息、异常消息),通常情况下 responseStatus == -1001 时, 会有值
[ProtoMember(5)]
public string validMsg;
// 业务请求数据
[ProtoMember(6)]
public byte[] data;
// 消息标记号;由前端请求时设置,服务器响应时会携带上;
[ProtoMember(7, DataFormat = DataFormat.ZigZag)]
public int msgId;
}
[ProtoContract]
public class Package
{
[ProtoMember(1)]
public uint packageType;
[ProtoMember(2)]
public string route = null;
[ProtoMember(3)]
public uint packID = 0;
[ProtoMember(4)]
public byte[] buff = null;
[ProtoMember(5)]
public string modelName = null;
}
//[ProtoContract]
//public class Message<T>
//{
// [ProtoMember(1)]
// public uint err;
// [ProtoMember(2)]
// public string errMsg = default;
// [ProtoMember(3)]
// public T data = default;
// public Message() { }
// public Message(uint err, string errMsg, T info)
// {
// this.err = err;
// this.errMsg = errMsg;
// this.data = info;
// }
//}
[ProtoContract]
public class HandShake
{
[ProtoMember(1)]
public string token;
}
[ProtoContract]
public class Heartbeat
{
[ProtoMember(1)]
public uint heartbeat;
}
}
网络层脚本6:TestProto.cs
using ProtoBuf;
using ProtoBuf.Meta;
using System.Collections.Generic;
[ProtoContract]
public class RegisterInfo
{
[ProtoMember(1)]
public string phoneNum;
[ProtoMember(2)]
public string account;
[ProtoMember(3)]
public string pwd;
}
[ProtoContract]
public class LoginVerify
{
[ProtoMember(1)]
public string account;
[ProtoMember(2)]
public string pwd;
[ProtoMember(3, DataFormat = DataFormat.ZigZag)]
public int loginBizCode;
[ProtoMember(4, DataFormat = DataFormat.ZigZag)]
public int spaceId;
}
[ProtoContract]
public class LobbyTalkMsg
{
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public long id;
[ProtoMember(2, DataFormat = DataFormat.ZigZag)]
public long usrId;
[ProtoMember(3)]
public string nickName;
[ProtoMember(4, DataFormat = DataFormat.ZigZag)]
public int to_usrId;
[ProtoMember(5)]
public string msg;
[ProtoMember(6, DataFormat = DataFormat.ZigZag)]
public int msg_type;
}
[ProtoContract]
public class RequestSuccess
{
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public int success;
}
[ProtoContract]
public struct GameFrame {
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public long frameId;
[ProtoMember(2)]
public string opc;
[ProtoMember(3)]
public string world;
[ProtoMember(4)]
public TankLocation location;
}
[ProtoContract]
public class UserInfo {
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public long id;
[ProtoMember(2)]
public string nickName;
[ProtoMember(3)]
public string account;
[ProtoMember(4)]
public string gender;
[ProtoMember(5)]
public string headImgUrl;
[ProtoMember(6, DataFormat = DataFormat.ZigZag)]
public long avatarId;
[ProtoMember(7)]
public string avatar;
[ProtoMember(8)]
public string hold;
[ProtoMember(9)]
public string mtk;
[ProtoMember(10)]
public string ltk;
[ProtoMember(11, DataFormat = DataFormat.ZigZag)]
public long roomId;
[ProtoMember(12, DataFormat = DataFormat.ZigZag)]
public long team;
[ProtoMember(13)]
public TankLocation tankLocation;
[ProtoMember(14, DataFormat = DataFormat.ZigZag)]
public int speed;
}
[ProtoContract]
public class TankLocation {
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public float x;
[ProtoMember(2, DataFormat = DataFormat.ZigZag)]
public float y;
[ProtoMember(3, DataFormat = DataFormat.ZigZag)]
public float z;
[ProtoMember(4, DataFormat = DataFormat.ZigZag)]
public float dx;
[ProtoMember(5, DataFormat = DataFormat.ZigZag)]
public float dy;
[ProtoMember(6, DataFormat = DataFormat.ZigZag)]
public float dz;
[ProtoMember(7, DataFormat = DataFormat.ZigZag)]
public long playerId;
[ProtoMember(8, DataFormat = DataFormat.ZigZag)]
public long time;
}
[ProtoContract]
public class TankPlayer {
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public long id;
[ProtoMember(2, DataFormat = DataFormat.ZigZag)]
public int speed;
[ProtoMember(3)]
public TankLocation tankLocation;
[ProtoMember(4)]
public string nickname;
[ProtoMember(5)]
public string hold;
// 其他属性: key: 子弹 id 1 玩具弹, 2 雪弹; value : 数量
[ProtoMember(6)]
public Dictionary<int, int> tankBulletMap;
}
[ProtoContract]
public class TankEnterRoom {
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public long roomId;
[ProtoMember(2, DataFormat = DataFormat.ZigZag)]
public long time;
[ProtoMember(3, DataFormat = DataFormat.ZigZag)]
public long team;
[ProtoMember(4, DataFormat = DataFormat.ZigZag)]
public long playerId;
[ProtoMember(5)]
public string sharetoken;
[ProtoMember(6)]
public string usertoken;
[ProtoMember(7)]
public string world;
[ProtoMember(8)]
public List<UserInfo> tankPlayerList;
}
[ProtoContract]
public class UserIds {
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public long[] userIds;
}
[ProtoContract]
public class UserInfos
{
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public UserInfo[] users;
}
[ProtoContract]
public class HeartBeat
{
[ProtoMember(1, DataFormat = DataFormat.ZigZag)]
public int beat;
}
(2)用于多人同步的具体实现类:SyncTransform.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UTNET;
[DisallowMultipleComponent]
public class SyncTransform : SyncBase
{
public SyncTransfomData data = new SyncTransfomData();
public bool isRole;//判断是否是角色的位置同步
bool isFirst = true; //第一次同步不用插值
private void Awake() {
SyncTransfomUtil.FromTransform(transform,ref data);
EventManager.Instance.AddListener(EventName.OnEnterRoom, OnEnterRoom);
}
private void OnEnterRoom(object sender, EventArgs e) {
AddNetWorkListner<SyncTransfomData>(OnSyncNet, nameof(SyncTransfomData));
OnSyncWorldData();
}
// Start is called before the first frame update
void Start()
{
if (syncId == string.Empty) {
Debug.LogError("syncId null");
return;
}
AddNetWorkListner<SyncTransfomData>(OnSyncNet, nameof(SyncTransfomData));
//OnSyncWorldData();
}
void OnSyncWorldData() {
var syncdata = SyncWorldModel.Instance.GetTransfomData(syncId);
if (syncdata != null) {
OnSyncNet(syncdata);
SyncTransfomUtil.ToTransform(transform, syncdata);
}
}
public void InitLocation(SyncTransfomData syncdata) {
data = syncdata;
SyncTransfomUtil.ToTransform(transform, data);
}
public void SetPosAndSync(SyncTransfomData syncdata) {
data = syncdata;
SyncTransfomUtil.ToTransform(transform, data);
SendNetWork<SyncTransfomData>(data, nameof(SyncTransfomData));
SyncWorldModel.Instance.SetTransfomData(syncId, data);
SyncWorldModel.Instance.SyncWorldData();
}
private void OnSyncNet(SyncTransfomData syData) {
data = syData;
}
//private void OnMouseDrag() {
// var dstRay = Camera.main.ScreenPointToRay(Input.mousePosition);
// transform.forward = -Camera.main.transform.forward;
// transform.position = Camera.main.transform.position + Vector3.Normalize(dstRay.direction) * 5f;
// transform.eulerAngles = new Vector3(0, transform.eulerAngles.y, 0);
//}
void SendPlayerLocation(SyncTransfomData data, string stName) {
if (dataName != stName) {
Debug.LogError("error:传输数据出错请检查");
return;
}
dataBase.syncId = syncId;
dataBase.name = dataName;
dataBase.obj = JsonUtility.ToJson(data);
dataBase.isLocation = true;
dataBase.playerId = PlayerData.Instance.PlayerId.ToString();
NetManager.Instance.SendFrameLocationData(data, dataBase);
}
//bool TransformEqual(int[] arryInt) {
// transform.position
//}
// Update is called once per frame
void Update()
{
var posV3 = new Vector3((float)data.pos[0] / 10f, (float)data.pos[1] / 10f, (float)data.pos[2] / 10f);
var rotateV3 = new Vector3((float)data.roate[0] / 10f, (float)data.roate[1] / 10f, (float)data.roate[2] / 10f);
if(ownerState == OwnerState.Ohter) {
if (SyncTransfomUtil.V3Equal(transform.position, posV3) == false)
{
if (isFirst)
{
transform.position = posV3;
}
else if (Vector3.Distance(transform.position, posV3) < 0.5f)
{
transform.position = posV3;
}
else
{
transform.position = Vector3.Lerp(transform.position, posV3, 5f * Time.deltaTime);
}
isFirst = false;
}
if (SyncTransfomUtil.V3Equal(transform.eulerAngles, rotateV3) == false) {
transform.eulerAngles = rotateV3;
}
}
else if (ownerState == OwnerState.Mine){
var scaleV3 = new Vector3((float)data.scale[0] / 10f, (float)data.scale[1] / 10f, (float)data.scale[2] / 10f);
if (SyncTransfomUtil.V3Equal(posV3, transform.position) &&
SyncTransfomUtil.V3Equal(rotateV3, transform.eulerAngles) &&
SyncTransfomUtil.V3Equal(scaleV3, transform.localScale)) {
return;
}
SyncTransfomUtil.FromTransform(transform, ref data);
if (isRole == false) { //如果不是角色就正常同步
base.SendNetWork<SyncTransfomData>(data, nameof(SyncTransfomData));
SyncWorldModel.Instance.SetTransfomData(syncId, data);
SyncWorldModel.Instance.SyncWorldData();
}
else {
SendPlayerLocation(data, nameof(SyncTransfomData));
}
}
}
}
(3)用于状态同步的具体实现类:SyncState.cs
using System;
using UnityEngine;
using UnityEngine.Events;
[DisallowMultipleComponent]
public class SyncState : SyncBase {
public SyncStateData data = new SyncStateData();
public UnityEvent<SyncStateData> onNetSyncEven = new UnityEvent<SyncStateData>();
public UnityEvent<SyncStateData> onLoginSyncEven = new UnityEvent<SyncStateData>();
bool isInitOk;
private void Awake() {
EventManager.Instance.AddListener(EventName.OnEnterRoom,OnEnterRoom);
}
// Start is called before the first frame update
void Start() {
if (syncId == string.Empty) {
Debug.LogError("syncId null");
return;
}
AddNetWorkListner<SyncStateData>(OnSyncNet, nameof(SyncStateData));
}
private void OnEnterRoom(object sender, EventArgs e) {
AddNetWorkListner<SyncStateData>(OnSyncNet, nameof(SyncStateData));
OnSyncWorldData();//如果是创建出来的物体,不是在场景中的,那么就需要在start的时候去处理
}
void OnSyncWorldData() {
if (isInitOk == true) {
return;
}
var syncdata = SyncWorldModel.Instance.GetStateData(syncId);
if (syncdata != null) {
Debug.Log($"OnSyncWorldData {syncId} {syncdata.stateName}");
data = syncdata;
onLoginSyncEven?.Invoke(syncdata);
isInitOk = true;
}
}
private void OnSyncNet(SyncStateData syData) {
onNetSyncEven?.Invoke(syData);
}
public void SendState(string stateName) {
if (ownerState == OwnerState.Ohter) {
return;
}
data.stateName = stateName;
SendNetWork<SyncStateData>(data, nameof(SyncStateData));
SyncWorldModel.Instance.SetStateData(syncId,data);
SyncWorldModel.Instance.SyncWorldData();
}
//float timeadd = 0f;
//private void Update() {
// timeadd += Time.deltaTime;
// if (timeadd >3) {
// timeadd = 0f;
// //data.stateName = "Jump";
// //onNetSyncEven?.Invoke(data);
// GetComponentInChildren<Animator>().SetTrigger("Jump");
// }
//}
}
(4)状态同步的做法:本人
(5)其他人:
private void OnNetSync(SyncStateData data)
{
var splits = data.stateName.Split(';');
var protoName = splits[0];
var userId = long.Parse(splits[1]);
if (protoName == "openVioce")
{
if (userId == this.roleId)
{
this.isShowRoleUIVoice(true);
}
}
if (protoName == "closeVioce")
{
if (userId == this.roleId)
{
this.isShowRoleUIVoice(false);
}
}
}