文章目錄
- 【Unity網絡同步框架 - Nakama研究(三)】
- 準備工作
- 前言
- Unity部分
- 連接服務器
- 創建并進入房間
- 創建人物
- 人物移動和同步
【Unity網絡同步框架 - Nakama研究(三)】
以下部分需要有一定的
Unity
基礎,在官方的案例Pirate Panic
基礎上進行修改而成。如果沒有下載并熟悉過官方案例,最好先下載對應的工程查看。工程地址為:https://github.com/heroiclabs/unity-sampleproject,對應的案例文檔為:https://heroiclabs.com/docs/zh/nakama/tutorials/unity/pirate-panic/,以下運行的Unity
版本為2022LTS,一般關系不大
準備工作
Unity
2022或者隨便一個LTS版本VS
2022Nakama Unity SDK
(官網或者Unity
商店都有,實在找不到把上面的案例的程序集偷出來)
前言
Nakama
是一個網絡同步庫,兼容很多游戲引擎,名字取自于日語伙伴,底層由Go
開發,所以性能上有保證(可以對比其他流行的網絡框架)。并且擁有大量已經開發好而且經過檢驗的功能(聊天,排行榜,群組,房間,身份驗證,存儲,好友等等),但是之前在網絡上,甚至官網上找到的博客或者文章要么是性質雷同,要么就是空談。- 以下的改變主要是用于網上找不到,
AI
提供不準確,論壇全英文,翻找資料麻煩的基礎上提供的。
Unity部分
連接服務器
- 我喜歡盡量把邏輯精簡,讓程序能跑起來,再去研究里面的細節,就像鋼鐵俠里面的臺詞“有時候你得先跑起來,再學會走路”
[SerializeField] private GameConnection _connection;public static string DeviceIdKey => "nakama.deviceId" + UserData.Id;public static string AuthTokenKey => "nakama.authToken" + UserData.Id;public static string RefreshTokenKey => "nakama.refreshToken" + UserData.Id;private Client client;private ISocket socket;private const string ServerIp = "xxx.xxx.xx.xx"; // 你的ip地址public async void RequireEnterRoom(){if (_connection.Session == null){string deviceId = GetDeviceId();if (!string.IsNullOrEmpty(deviceId)){PlayerPrefs.SetString(DeviceIdKey, deviceId);}await InitializeGame(deviceId);}}private async Task InitializeGame(string deviceId){client = new Client("http", ServerIp, 7350, "defaultkey", UnityWebRequestAdapter.Instance);client.Timeout = 5;socket = client.NewSocket(useMainThread: true);string authToken = PlayerPrefs.GetString(AuthTokenKey, null);bool isAuthToken = !string.IsNullOrEmpty(authToken);string refreshToken = PlayerPrefs.GetString(RefreshTokenKey, null);ISession session = null;// refresh token can be null/empty for initial migration of client to using refresh tokens.if (isAuthToken){session = Session.Restore(authToken, refreshToken);// Check whether a session is close to expiry.if (session.HasExpired(DateTime.UtcNow.AddDays(1))){try{// get a new access tokensession = await client.SessionRefreshAsync(session);}catch (ApiResponseException){// get a new refresh tokensession = await client.AuthenticateDeviceAsync(deviceId);PlayerPrefs.SetString(RefreshTokenKey, session.RefreshToken);}PlayerPrefs.SetString(AuthTokenKey, session.AuthToken);}}else{session = await client.AuthenticateDeviceAsync(deviceId);PlayerPrefs.SetString(AuthTokenKey, session.AuthToken);PlayerPrefs.SetString(RefreshTokenKey, session.RefreshToken);}Connect(socket, session);IApiAccount account = null;try{account = await client.GetAccountAsync(session);}catch (ApiResponseException e){Debug.LogError("Error getting user account: " + e.Message);}_connection.Init(client, socket, account, session);}private async void Connect(ISocket socket, ISession session){try{if (!socket.IsConnected){await socket.ConnectAsync(session);}}catch (Exception e){Debug.LogWarning("Error connecting socket: " + e.Message);}}private string GetDeviceId(){string deviceId = "";deviceId = PlayerPrefs.GetString(DeviceIdKey);if (string.IsNullOrWhiteSpace(deviceId)){deviceId = Guid.NewGuid().ToString();}return deviceId;}
上面的這部分就是連接的函數部分,其中的結構GameConnection
如下:
using Nakama;
using UnityEngine;public class GameConnection : ScriptableObject
{private IClient _client;public IClient Client => _client;public ISession Session { get; set; }public IApiAccount Account { get; set; }private ISocket _socket;public ISocket Socket => _socket;private IChannel _channel;public IChannel Channel => _channel;public string MatchID { get; set; }public void Init(IClient client, ISocket socket, IApiAccount account, ISession session){_client = client;_socket = socket;Account = account;Session = session;}
}
上面大部分的代碼都能在案例中找到,有些小修改。需要注意的是,如果要在電腦上實現多開(非編輯器模式下,處于打完包的exe狀態),需要修改DeviceIdKey
等參數,不然服務器接收到的時候,這倆會識別成同一個帳號(因為傳入的參數deviceId
一致),會給后續操作帶來麻煩。
創建并進入房間
- 這一步開始就跟案例中的不一樣了,案例使用的是
AddMatchmakerAsync
,這個方法在文檔中說明是不會創建房間的,只是簡單的匹配機制,所以如果這個時候你寫了如下代碼:
private async void ListMatchesAndJoin(){var minPlayers = 0;var maxPlayers = 10;var limit = 10;var authoritative = true;var label = "";var query = "*";try{var result = await client.ListMatchesAsync(_connection.Session, minPlayers, maxPlayers, limit, authoritative, null, null);// 添加新的列表項foreach (var match in result.Matches){Debug.LogFormat("{0}: {1}/{2} players", match.MatchId, match.Size, maxPlayers);JoinMatch(match.MatchId);break;}}catch (System.Exception e){Debug.LogError("Error listing matches: " + e.Message);}}
到時候你就會發現怎么都拿不到房間信息,一直返回空,這里根據需求分為兩步,一是你自己創建的房間(如果人數為0,會被銷毀,而且走的是官方設定好的邏輯,叫非權威比賽),二是服務器創建的權威比賽,這個比賽即使房間內人數為0也不會解散(關服務器還是會解散的)
// 這是非權威比賽(權威比賽會在上述代碼中直接返回對應的matchid)
var matchid = await _connection.Socket.CreateMatchAsync();// 通過返回的matchid加入
var match = await socket.JoinMatchAsync(matchId);
至于如何創建服務器的權威比賽,留到下次講服務器擴展再說。
- 走到這一步,其實我們已經在房間里了,看服務器的日志,
第一條是連接socket
,第二條是加入房間。
創建人物
- 進入了房間,接下去做的一般是創建你所加入房間的那個擺設,或者新的場景,然后給服務器發送創建人物的信息,涉及到操作信息在房間內的傳遞。
- 創建新的場景這一點,
Unity
自己就能做到 - 發送操作信息要分開,因為
Nakama
有很多種渠道可以發送消息,這里采用正規一點的房間消息,需要注意的是,如果這個房間是非權威房間,那么房間信息Nakama
給你寫好了,如果是服務器自己創建的非權威房間,那么需要你自己寫。
public static async Task SpawnPlayer(){var matchMessagePlayerCreate = new MatchMessagePlayerCreate(BattleSceneController.Instance.Connection.Session.Username,BattleSceneController.Instance.Connection.Session.UserId,randomPos.x,randomPos.y,randomPos.z,0, 0, 0,selectCharacterId,selectCharacterData);BattleSceneController.Instance.StateManager.SendMatchStateMessage(MatchMessageType.UnitSpawned, matchMessagePlayerCreate);BattleSceneController.Instance.StateManager.SendMatchStateMessageSelf(MatchMessageType.UnitSpawned, matchMessagePlayerCreate);}
創建人物信息的方式跟案例里面的差不多,注意一下時序問題即可。然后在監聽對應事件的地方GameStateManager
處理服務器發送過來的消息即可。
private GameConnection _connection;_connection.Socket.ReceivedMatchState += ReceiveMatchStateMessage;private void ReceiveMatchStateMessage(IMatchState matchState){string messageJson = System.Text.Encoding.UTF8.GetString(matchState.State);if (string.IsNullOrEmpty(messageJson))return;ReceiveMatchStateHandle(matchState.OpCode, messageJson);}public void SendMatchStateMessageSelf<T>(MatchMessageType opCode, T message)where T : MatchMessage<T>{switch (opCode){case MatchMessageType.UnitSpawned:OnPlayerCreate?.Invoke(message as MatchMessagePlayerCreate);break;default:break;}}
public void ReceiveMatchStateHandle(long opCode, string messageJson){switch ((MatchMessageType)opCode){case MatchMessageType.UnitSpawned:MatchMessagePlayerCreate matchMessagePlayerCreate = MatchMessagePlayerCreate.Parse(messageJson);OnPlayerCreate?.Invoke(matchMessagePlayerCreate);break;default:break;}}
有一點需要注意的是,Nakama
傳遞的消息結構字段是json
,而且是Base64
轉義之后的,如果你在服務器的日志中看到錯誤信息,記得先轉回正常的字符串。
- 然后你的人物就能出現在場景中了。
人物移動和同步
- 再往后面就是正常的人物之間的同步信息,比如人物的旋轉,移動,動畫等等,都可以在上面
ReceiveMatchStateHandle
方法里面進行監聽和執行,涉及到Cinemachine
,Timeline
,動畫狀態機等等,就不在這里詳細展開了。
下一章講講服務器的擴展相關和一些可能遇到的問題