文章目录
- 【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,一般关系不大
准备工作
- Unity2022或者随便一个LTS版本
- VS2022
- Nakama 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 token
session = await client.SessionRefreshAsync(session);
}
catch (ApiResponseException)
{
// get a new refresh token
session = 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,动画状态机等等,就不在这里详细展开了。
下一章讲讲服务器的扩展相关和一些可能遇到的问题
评论前必须登录!
注册