当前位置: 首页 > news >正文

【Unity网络同步框架 - Nakama研究(三)】

文章目录

  • 【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方法里面进行监听和执行,涉及到CinemachineTimeline,动画状态机等等,就不在这里详细展开了。

下一章讲讲服务器的扩展相关和一些可能遇到的问题

相关文章:

  • PostgreSQL 和 MySQL 区别
  • C#中多态性核心讲解
  • S7-1200 G2移植旧版本S7-1200程序的具体方法示例
  • 16003. orin camera 相机驱动源码 imx477分析记录
  • 直方图(信息学奥赛一本通-1115)
  • 几款可用于绘制工艺原理图的开源框架
  • flutter 专题 八十八 Flutter原生混合开发
  • MySQL 8 主从同步安装与配置
  • C语言实现括号匹配检查及栈的应用详解
  • C#—【在不同的场景该用哪种线程?】
  • 机器狗硬件设计踩坑实录
  • 【愚公系列】《高效使用DeepSeek》004-DeepSeek的产品形态和功能详解
  • 前端UnoCSS面试题及参考答案
  • Opencv之掩码实现图片抠图
  • 金融时间序列分析(Yahoo Finance API实战)
  • 《炎龙骑士团外传风之纹章》秘籍
  • Cadence学习笔记3
  • C++类的基础题(4)
  • MIPI电平标准详解
  • 【Spring Cloud】 核心组件全解析与 2024 【微服务框架】选型指南
  • 外交部:将持续便利中外人员往来,让“中国游”金字招牌更加闪耀
  • 以色列称“将立即允许恢复”人道主义物资进入加沙
  • 河南发布高温橙警:郑州、洛阳等地最高气温将达40℃以上
  • 解读|战国子弹库帛书漂泊海外79年今归国,追索仍将继续
  • 国际博物馆日|在辽宁省博物馆遇见敦煌
  • 《风林火山》千呼万唤始出来,戛纳首映后口碑崩盘?