04 基于Nakama和Unity开发网络多人游戏:Part 3(完)

您好!在我的博客当中,我将持续挑选一些优质的国外技术文章进行翻译,如果文章内容翻译有误,欢迎在评论区指正,感谢:)

02 基于Nakama和Unity开发网络多人游戏:Part 1 - 掘金 (juejin.cn)

以下是原文链接:Tutorial: Making a Multiplayer Game with Nakama and Unity: Part 3/3 -- The Knights of Unity

4.1 实现基础功能

在本小节当中,我们将讲解一些 Nakama 提供的基础功能。我将为你展示一些演示功能的示例代码,并提供省略的代码的相应文档。

4.1.1 多认证源与设备迁移

概念介绍

为了让用户可以轻松的登录到他们的账号,Nakama 提供了多种账号的身份认证方式,本小节将会讲解这些认证的技术。我们同样可以连接多个外部的账号,比如将 Google、Steam 或者 Facebook 登录为 Nakama 的单个账号,它允许我们使用不同的凭证进行登录。比如,我们可以使用账号密码在 PC 上登录,但也可以使用 Google 在手机上登录 - 这两种方式都可以登录到同一个 Nakama 的账号。

Link、UnLink 不同账号

在 Nakama 中,使用不同的方式来进行登录、退出是一件很简单的事情:

C# 复制代码
await Client.LinkDeviceAsync(Session, id);
await Client.LinkEmailAsync(Session, email, password);
await Client.LinkFacebookAsync(Session, facebookToken);
await Client.LinkGoogleAsync(Session, googleToken);
await Client.LinkSteamAsync(Session, steamToken);
 
await Client.UnlinkDeviceAsync(Session, id);
await Client.UnlinkEmailAsync(Session, email, password);
await Client.UnlinkFacebookAsync(Session, facebookToken);
await Client.UnlinkGoogleAsync(Session, googleToken);
await Client.UnlinkSteamAsync(Session, steamToken);

有一些规则用于连接和断开帐户:

  1. 一个账号必须至少要有一种验证方法来连接到账号 - 当我们创建了一个账号时,它可以使用许多种被允许的方式来登录到 Nakama 账号,它暂时是我们唯一的登录此账号的方式。在我们连接到另一个帐户或设备前,禁止解除此方法的连接。
  2. 单个设备或外部账号一次只可以被连接到一个 Nakama 账号 - 我们不能将设备与另一个 Nakama 账户关联,除非我们将其与前一个账户解除关联。同样的,对于 Facebook、Google、Game Center、Steam 以及其他传统的邮箱以及密码方式的账号认证。这可以确保,在当前会话进行身份验证时不会产生歧义。

所有这些规则都用于确保,总是有一种方式可以登录到账号。然而,现在的玩家在游戏时更多的是希望能够尽快体验到纯粹的游戏玩法:当他们不得不经历所有与游戏玩法无关的过程(如创建账户和认证)时,他们会感到不耐烦以至于恼火。这也是为什么开发者更倾向于把以上动作放在后台进行,让玩家可以更好的体验游戏。当玩家游玩了一段时间,再向他们询问更多的信息,比如用户名或者 Facebook 连接(大多数情况下,这种设计会导致某些游戏的功能被锁定,直到我们完成注册,或者我们因此获得某种奖励)。这便是为什么当账号第一次在当前设备登录(它的 ID 与任何现有的 Nakama 账户都没有关联)时,同时完成创建账号这一过程是一个好想法。

不同设备间 Link、Unlink 问题

不幸的是,这种方法可能会导致一些问题。看看现在这个例子:

  1. 在设备 A 上开始游戏,并创建了一个新帐户。游玩了几个关卡并获得了一些喜欢的奖励;
  2. 将我们现有的帐户连接到 Facebook -连接 Facebook 可以让我们分享游戏内的高分给我们的朋友,并给予一些小小的提升奖励;
  3. 在设备 B 上开始游戏:因为我们当前在一个新的手机设备上,该程序的第一次运行将直接创建一个没有奖励或高分的新帐户。 在这种情况下,我们无法将老账号连接到新设备,因为它意味着设备 B 将同时连接两个账号,这违背了第二条规则。我们也不能取消设备 B 与新创建的帐户的连接,因为没有任何的身份验证方法与该帐户连接,这违反了第一条规则。

有两种方法可以解决这个问题:

  1. 删除新帐户,删除其所有进度,并将设备 B 的 ID 链接到旧 ID;
  2. 创建一个"虚拟"设备,将其连接到新帐户,断开设备 B 的连接,并将其重新连接到旧的 Nakama 帐户; 这种方法有其优缺点。删除现有帐户这一动作可确保从数据库中删除未使用的帐户,从而限制数据库的使用量。但与此同时,我们也将因此失去所有的此账号的游戏进度,意味着如何我们想要返回此账号时,在数据库当中没有任何的数据可以帮助我们进行恢复。同时如果我们错误地合并了我们的账户,就没有办法恢复我们的进度; 在《Jolly Rogers》演示中使用的第二种方法可能需要更多的数据库存储空间,因为游戏的第一次运行总是会创建一个新帐户,即使我们在开始游戏时已连接到我们的旧帐户。与第一种方法相反,根据我们如何创建"虚拟"设备,有可能恢复我们的旧帐户。
C# 复制代码
//NakamaSessionManager.cs
 
/// <summary>
/// Transfers this Device Id to an already existing user account linked with Facebook.
/// This will leave current account floating, with no real device linked to it.
/// </summary>
public async Task<bool> MigrateDeviceIdAsync(string facebookToken)
{
    try
    {
        Debug.Log("Starting account migration");
        string dummyGuid = _deviceId + "-";
        
        await Client.LinkDeviceAsync(Session, dummyGuid);
        Debug.Log("Dummy id linked");
        
        ISession activatedSession = await Client.AuthenticateFacebookAsync(facebookToken, null, false);
        Debug.Log("Facebook authenticated");
        
        await Client.UnlinkDeviceAsync(Session, _deviceId);
        Debug.Log("Local id unlinked");
        
        await Client.LinkDeviceAsync(activatedSession, _deviceId);
        Debug.Log("Local id linked. Migration successfull");
        
        Session = activatedSession;
        StoreSessionToken();
        
        ...
        
        return true;
    }
    catch (Exception e)
    {
        Debug.LogWarning("An error has occured while linking dummy guid to local account: " + e);
        return false;
    }
}

在上面显示的示例代码中,通过在设备 ID 的末尾添加一个破折号"-"符号来创建虚拟设备。设备 id 必须由字母和数字组成,长度范围为10 ~ 60字节。Unity 允许我们通过调用 SystemInfo.deviceUniqueIdentifier 来检索我们唯一的 ID,在最后添加破折号后将创建一个全新的 ID。然后,我们可以将这样的 id 链接到新帐户,并解除与实际设备的链接。

4.1.2 Groups and Clans 功能

简介

多人游戏基本都有一些社交方面的功能。Nakama 为开发者提供了这块的一些支持,可以用于创建 Groups 以及 Clans,它允许用户去创建一个团队,进而可以进行共同的 Boss 战、与队友聊天或者加入一个更大的组织。 Group 管理相关的功能在 Nakama 十分容易实现,你可以使用以下几种有用的操作来处理 Groups:

  1. 创建/删除
  2. 加入/退出
  3. 设置基础信息:名字,描述,头像以及是否小组可以被公开查找
  4. 邀请/删除 Group 中用户
  5. 提升 Gruop 成员的职位
  6. 获得小组成员的清单

小组内成员权限

一旦进入小组(创建或者加入一个已存在的小组),你可以分配或被分配以下的权限:

  1. 超级管理员(Superadmin)
  2. 管理员(Admin)
  3. 成员(Member)
  4. 加入请求中(Join Request) 其中,最高权限的是拥有 Group 的超级管理员。一个小组当中至少需要有一个超级管理员,并且它们拥有删除小组的权限。它们可以踢掉某个小组的成员,并可以给小组成员提升权限。 下一个权限等级是管理员,它拥有几乎和超级管理员一样的权限,但是它不能删除当前小组、踢掉超级管理员或者将其他人的权限提升为超级管理员。所有其他的属于当前 clan 的用户都会默认被分配为成员(Member)权限,它们不可以授权、踢人或者管理小组。

小组访问权限

如果一个小组的访问权限设置为了公开,那么用户无需任何要求便可自由的加入小组。所有人都可以创建一个公开的群组,其他人随时可以自由的加入和退出。 这种自由的小组加入与退出机制可能会让一些不受欢迎的用户也加入到小组当中,它们可能会干扰到其他的成员。在这种情况下,一个私人的小组可能是一个更好的选项。当一个用户想要加入小组时,会先进入到加入请求中(Join Request))的状态。当管理员或者超级管理员同意加入请求和,用户才可以加入到小组当中。 游戏通常会具有以上两种小组的类型,然而,出于展示的目的,当前演示的 Demo 只用于公开类型的小组。所有用于管理以及展示小组功能的代码可以在以下文件夹中被找到:/Assets/DemoGame/Scripts/Clans/ folder 下面您可以看到项目中使用的 clan 创建示例:

C# 复制代码
//ClanManager.cs
 
/// <summary>
/// 在Nakama服务器中创建clan,名字为 <paramref name="name"/>.
/// 当名字已经被使用时创建失败
/// Returns <see cref="IApiGroup"/> on success.
/// </summary>
public static async Task<IApiGroup> CreateClanAsync(Client client, ISession session, string name, string avatarUrl)
{
    try
    {
        IApiGroup group = await client.CreateGroupAsync(session, name, "", avatarUrl);
        return group;
    }
    catch (ApiResponseException e)
    {
        if (e.StatusCode == System.Net.HttpStatusCode.InternalServerError)
        {
            Debug.LogWarning("Clan name \"" + name + "\" already in use");
            return null;
        }
        else
        {
            Debug.LogWarning("An exception has occured when creating clan with code " + e.StatusCode + ": " + e);
            return null;
        }
    }
    catch (Exception e)
    {
        Debug.LogWarning("An internal exception has occured when creating clan: " + e);
        return null;
    }
}

4.2 匹配机制

匹配机制介绍

虽然并不适用于所有类型的多人游戏,但 Nakama 提供的匹配功能可以帮助开发者大大减少创建和管理匹配所需的开发工作。它的灵活性和可扩展性使得玩家可以轻松的根据给定的参数来匹配玩家,并进行对抗。 比如,和等级类似或者国家相同的玩家进行对战匹配(可以减少 Ping 延迟)。当匹配的游戏准备好时,它会通知所有匹配的用户,等待所有用户连接完成后开始游戏。 用户可以加入任意数量的匹配池,每当他们接收到一个匹配成功的信息,它们可以决定是否参加匹配到的比赛或为你找到的对手。当用户在某一个池当中时,可以绑定自己的个人信息,进而让服务器为我们寻找最佳匹配。 当匹配玩家成功时,系统将发送游戏即将开始的通知。出于演示目的,以下我们只使用一个配对的队列。 更多关于使用到的匹配机制的参数信息可以在这个链接找到: heroiclabs.com/docs/gamepl... 。 以下演示代码可以在此文件夹中找到:/Assets/DemoGame/Scripts/Matchmaking/folder

4.2.1 主机选择与实时多人游戏

两种主机选择的类型

在大部分的多人游戏当中,我们选择将所有的逻辑计算都移到一个设备上,这个设备可以被称为主机(Host)或者服务器(Server)。这一设计可以帮助我们进行数据同步,因为当所有重要的计算都发生在一台机器上时,所有的输入以及输出都将通过服务器和客户端之间进行传输。 Nakama 允许两种类型的实时多人游戏:

  1. 从客户端中选择主机(Client-authoritative):在客户端中选择一个客户端作为主机,负责所有玩家的游戏逻辑;
  2. 服务器授权(Server-authoritative):在一台与客户端无关的专门负责服务器计算的主服务器机器上进行其逻辑计算; 对于第一种类型的多人游戏,其游戏逻辑由其中一个客户端处理。而对于第二种类型的服务器授权多人游戏,其游戏逻辑的计算由 Nakama 服务器自己负责。当我们拥有强大的服务器设备时,我们应当使用后一种方法,它可以更好的处理相同类型的多个匹配。这种主机类型的多人游戏需要大量的服务器逻辑处理代码。但另一方面,从客户端中选择主机的多人模式也有其优点,它不需要太多的计算能力,因为它的主机选自玩家的设备。这种模式的代码是更容易编写,因为它不需要针对服务器与客户端编写不同的逻辑,而是将它们混合在一起进行处理。

基于客户端中选择主机的游戏 Demo

处于演示的目的,当前 Jolly Rogers Demo 使用第一种:从客户端选择主机的模式来进行实时多人游戏的开发。它只需要非常少的服务器定制逻辑便可以让游戏可以运行。 当匹配到一句游戏后,此时便需要选择主机、选择哪个用户去处理玩家的输入、输出的发送与传输。对于如何选择主机,有以下两种方式可以进行参考:

  1. 随机主机选择:服务器生成一些随机数,从一至玩家数量中,根据索引生成的数字,将玩家选为主机;
  2. 固定主机选择:先获取每个玩家的 Session ID,按顺序排序 (比如按字母顺序排序),排序列表中排在第一位的玩家将会是主机。 第一种的随机主机选择在开发时更加常用,它使用随机数从列表中随机选择一个对象。这一功能需要一些定制的服务器处理逻辑。第二种方法允许我们在不与服务器或其他用户通信的情况下确定谁是主机,因为在加入匹配时,我们会得到所有加入的用户的列表。 Jolly Rogers Demo 使用第二种方法来选择主机,以下是演示代码:
C# 复制代码
//MatchCommunicationManager.cs
 
/// <summary>
/// 使用固定主机选择模式,选择主机
/// </summary>
private void ChooseHost(IMatchmakerMatched matched)
{
    // Add the session id of all users connected to the match
    List<string> userSessionIds = new List<string>();
    foreach (IMatchmakerUser user in matched.Users)
    {
        userSessionIds.Add(user.Presence.SessionId);
    }
    
    // Perform a lexicographical sort on list of user session ids
    userSessionIds.Sort();
    
    // First user from the sorted list will be the host of current match
    string hostSessionId = userSessionIds.First();
    
    // Get the user id from session id
    IMatchmakerUser hostUser = matched.Users.First(x => x.Presence.SessionId == hostSessionId);
    HostId = hostUser.Presence.UserId;
}

4.3 游戏 Demo 中的实时多人游戏

4.3.1 客户端发送 Message

关于 Message

在玩家们都成功匹配并加入游戏、完成主机选择,场景被成功加载后,接下来就可以开始游玩游戏了! 为了和服务器进行通信,客户端需要建立和 Nakama 的 Socket 连接,进而通过它来发送状态 Message 。每一条 Message 都包含了接收者处理时,所必备的信息,比如以下内容:

  1. 用户(User Presence):告知我们是谁发送了此消息;
  2. Match ID:消息来源的 Match ID;
  3. State:Message 的数据;
  4. OpCode:一串整数,用于帮助我们确定如何处理接收到的消息; 用户信息以及 Match ID 的重要性不言而喻,它们用于清晰的定义当前消息的发送者是谁。OpCode 用于决定当前 Message 要被如何处理。State 是一个字节数组,大多数时候是一个已经序列化后的 JSON 字符串,其中包含了根据 OpCode 决定的处理方法所需的所有数据。 有了这个系统,我们可以轻松地处理客户端的通信,而不需要服务器来处理数据。因为消息被发送到主机,由主机处理并从主机发回,服务器仅用于将所有客户端连接在一起。 以下是 Message 处理的演示代码:
C# 复制代码
//MatchCommunicationManager.cs
 
/// <summary>
/// Decodes match state message json from byte form of matchState.State and then sends it
/// to ReceiveMatchStateHandle for further reading and handling
/// </summary>
private void ReceiveMatchStateMessage(IMatchState matchState)
{
    string messageJson = System.Text.Encoding.UTF8.GetString(matchState.State);
    
    if (string.IsNullOrEmpty(messageJson))
    {
        return;
    }
    
    ReceiveMatchStateHandle(matchState.OpCode, messageJson);
}

. . .

/// <summary>
/// Reads match messages sent by other players, and fires locally events basing on opCode.
/// </summary>
public void ReceiveMatchStateHandle(long opCode, string messageJson)
{
    if (GameStarted == false)
    {
        _incommingMessages.Enqueue(new IncommingMessageState(opCode, messageJson));
        return;
    }
    
    //Choosing which event should be invoked basing on opcode
    //then parsing json to MatchMessage class and firing event
    switch ((MatchMessageType)opCode)
    {
        //GAME
        case MatchMessageType.MatchEnded:
            MatchMessageGameEnded matchMessageGameEnded = MatchMessageGameEnded.Parse(messageJson);
            OnGameEnded?.Invoke(matchMessageGameEnded);
            break;
            
        //UNITS
        case MatchMessageType.UnitSpawned:
            MatchMessageUnitSpawned matchMessageUnitSpawned = MatchMessageUnitSpawned.Parse(messageJson);
            OnUnitSpawned?.Invoke(matchMessageUnitSpawned);
            break;
        . . .
    }
}

发送消息的代码和上面的方法类似:将发送的消息先序列化为 JSON 字符串,并将带有特定 OpCode 的 Message 一起发送给其他玩家;

4.3.2 服务器处理逻辑

自定义服务器功能介绍

虽然 Nakama 已经涵盖了很多平时开发中最常用的功能,但有时我们可能会遇到其他具体的问题,这可能只发生在特定的情况下。对于当前的游戏 Demo 来说,当前游戏具有一个被称之为"卡片管理"的功能。当前游戏 Demo 基于用户购买的金币构建用户自己的牌组,所以我们必须设计专门的服务器逻辑,来管理用户的钱包以及它们的卡集;

Nakama 没有在客户端提供和资金/金币相关的客户端处理逻辑,因为如果位于客户端的用户可以自己更改钱包内容,他们中的一些人肯定会利用一些机制获得代码来进行作弊。在这种情况下,我们需要编写自己的服务器逻辑,它将用于处理卡片的购买以及管理。

自定义的 Nakama 服务器脚本使用 Lua 语言进行编写,所有的 .lua 文件都放在 bound 文件夹中,在服务器启动后进行处理。Nakama 会启动 listeners 来获得相关的事件。然后每当事件发生时,Nakama 调用 listeners。我们可以订阅一些事件(称为 hooks),在这里找到它们的列表。下面是一个示例代码,展示了如何向用户的牌组中添加一张随机的卡牌:

Lua 复制代码
//initializer.lua
 
...
nk.register_rpc(db.debug_add_random_card, "debug_add_random_card")
...
 
 
//deck_building.lua
 
function db.debug_add_random_card(context, payload)
    if w.update_wallet(context.user_id, -db.buy_cost, metadata, true) == false then
        return nk.json_encode({ response = false, message = "insufficient funds" })
    end
    local random_type = math.random(db.card_type_first, db.card_type_count - 1)
    local unused_cards = get_cards(context.user_id, "unused_cards")
    add_card(unused_cards, tostring(random_type), tostring(1))
    store_deck(context.user_id, "unused_cards", unused_cards)
    local metadata =
    {
        source = "random_card_bought"
    }
    return nk.json_encode({ response = true, message = "" })
end

4.4 总结

如果您正在为游戏寻找一个开源的服务器,基于此服务器可以编写自己的自定义逻辑,快速简单的实现一个实时多人游戏,Nakama 是一个非常好的选择。它的内容简单,十分易用,可以用来处理丰富的多人游戏相关功能,如聊天或配对。

相关推荐
追风林3 小时前
mac 本地docker-mysql主从复制部署
mysql·macos·docker
城南vision5 小时前
Docker学习—Docker核心概念总结
java·学习·docker
wclass-zhengge5 小时前
Docker篇(Docker Compose)
运维·docker·容器
梦魇梦狸º9 小时前
腾讯轻量云服务器docker拉取不到镜像的问题:拉取超时
docker·容器·github
鬼才血脉9 小时前
docker+mysql配置
mysql·adb·docker
詹姆斯爱研究Java9 小时前
Docker 的基本概念和优势
docker
南猿北者11 小时前
docker镜像仓库常用命令
运维·docker·容器
杨浦老苏11 小时前
Docker可视化管理面板DPanel的安装
运维·docker·群晖
航月13 小时前
FTP、ISCSI、CHRONY、DNS、NFS、DOCKER、MARIADB、NGINX、PHP、CA各服务开启方法
nginx·docker·mariadb
微刻时光13 小时前
Docker镜像分成
java·运维·开发语言·docker·容器·镜像