关于游戏服务端:游戏启动后提示安装HMS-Core点击取消未再次提示安装HMS-Core初始化失败返回907135003

问题形容咱们国内的华为联运游戏集成华为游戏服务SDK 之后,被审核驳回:在未装置或须要更新华为挪动服务(HMS Core)的手机上,提醒装置华为挪动服务,点击勾销,未再次提醒装置HMS Core。 问题解决我分割了华为技术反对(在此提单),对方让我先复现问题并取日志看报错信息,我复现后看日志,发现这种场景下,初始化失败,会返回错误码907135003。在开发者领导文档上搜寻该错误码,能够间接疏导到游戏初始化章节。 我点进去参考发现文档和示例代码都已阐明该错误码场景的解决方案: 也就是说,初始化失败,返回错误码907135003,此时须要咱们再调用init接口,我依照这样批改了代码从新测试,提醒装置HMS Core,点击勾销后,能够再次弹出HMS Core的更新弹框了。问题失去解决,现已从新提交审核,并审核通过。 文档参考链接: https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/game-start-0000001050123475 PS: HMS Core装置异样问题的复现条件要求: 可用非华为手机测试:当时卸载掉HMS Core; 可用华为手机测试:要先回退HMS Core版本(在手机-设置-利用治理-HMS Core-卸载更新)

July 4, 2022 · 1 min · jiezi

关于游戏服务端:使用-Kubernetes-扩展专用游戏服务器第4部分缩减节点

在前三篇文章中,咱们将游戏服务器托管在 Kubernetes 上,测量并限度它们的资源应用,并依据应用状况扩充集群中的节点。当初咱们须要解决更艰难的问题:当资源不再被应用时,放大集群中的节点,同时确保正在进行的游戏在节点被删除时不会中断。 从外表上看,按比例放大集群中的节点仿佛特地简单。 每个游戏服务器具备以后游戏的内存状态,并且多个游戏客户端连贯到玩游戏的单个游戏服务器。 删除任意节点可能会断开流动玩家的连贯,这会使他们怄气! 因而,只有在节点没有专用游戏服务器的状况下,咱们能力从集群中删除节点。 这意味着,如果您运行在谷歌 Kubernetes Engine (GKE) 或相似的平台上,就不能应用托管的主动缩放零碎。援用 GKE autoscaler 的文档“ Cluster autoscaler 假如所有复制的 pod 都能够在其余节点上重新启动……” — 这在咱们的例子中相对不起作用,因为它能够很容易地删除那些有沉闷玩家的节点。 也就是说,当咱们更认真地钻研这种状况时,咱们会发现咱们能够将其合成为三个独立的策略,当这些策略联合在一起时,咱们就能够将问题缩小成一个可治理的问题,咱们能够本人执行: 将游戏服务器组合在一起,以防止整个集群的碎片化当 CPU 容量超过配置的缓冲区时,封闭节点一旦节点上的所有游戏退出,就从集群中删除被封闭的节点让咱们看一下每个细节。 在集群中将游戏服务器分组在一起咱们想要防止集群中游戏服务器的碎片化,这样咱们就不会在多个节点上运行一个任性的小游戏服务器集,这将避免这些节点被敞开和回收它们的资源。 这意味着咱们不心愿有一个调度模式在整个集群的随机节点上创立游戏服务器 Pod,如下所示:而是咱们想让咱们的游戏服务器pod安顿得尽可能紧凑,像这样:要将咱们的游戏服务器分组在一起,咱们能够利用带有 PreferredDuringSchedulingIgnoredDuringExecution 选项的 Kubernetes Pod PodAffinity 配置。 这使咱们可能通知 Pods 咱们更喜爱按它们以后所在的节点的主机名对它们进行分组,这本质上意味着 Kubernetes 将更喜爱将专用的游戏服务器 Pod 搁置在曾经具备专用游戏服务器的节点上(下面曾经有 Pod 了)。 在现实状况下,咱们心愿在领有最专用游戏服务器 Pod 的节点上调度专用游戏服务器 Pod,只有该节点还有足够的闲暇 CPU 资源。如果咱们想为 Kubernetes 编写本人的自定义调度程序,咱们当然能够这样做,但为了放弃演示简略,咱们将保持应用 PodAffinity 解决方案。也就是说,当咱们思考到咱们的游戏长度很短,并且咱们将很快增加(and explaining)封闭节点时,这种技术组合曾经足够满足咱们的需要,并且打消了咱们编写额定简单代码的须要。 当咱们将 PodAffinity 配置增加到前一篇文章的配置时,咱们失去以下内容,它通知 Kubernetes 在可能的状况下将带有标签 sessions: game 的 pod 搁置在彼此雷同的节点上。 apiVersion: v1kind: Podmetadata: generateName: "game-"spec: hostNetwork: true restartPolicy: Never nodeSelector: role: game-server containers: - name: soccer-server image: gcr.io/soccer/soccer-server:0.1 env: - name: SESSION_NAME valueFrom: fieldRef: fieldPath: metadata.name resources: limits: cpu: "0.1" affinity: podAffinity: # group game server Pods preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchLabels: sessions: game topologyKey: kubernetes.io/hostname封闭节点当初咱们曾经把咱们的游戏服务器很好地打包在一起了,咱们能够探讨“封闭节点”了。“封闭节点”到底是什么意思?很简略,Kubernetes 让咱们可能通知调度器:“嘿,调度器,不要在这个节点上调度任何新货色”。这将确保该节点上不会调度新的 pod。事实上,在 Kubernetes 文档的某些中央,这被简略地称为标记节点不可调度。在上面的代码中,如果您专一于 s.bufferCount < available,您将看到,如果以后领有的 CPU缓冲区的数量大于咱们所须要的数量,咱们将向戒备节点发出请求。 ...

April 9, 2021 · 3 min · jiezi

关于游戏服务端:探索使用-Kubernetes-扩展专用游戏服务器第1部分容器化和部署

你为什么要这样做?只管容器(containers)和 Kubernetes 是很酷的技术,但为什么咱们要在此平台上运行游戏服务器? 游戏服务器的扩大很艰难,并且通常是专有软件的工作 - 软件容器和 Kubernetes 应该使它更容易,并且编码更少。容器为咱们提供了一个可部署的工件,可用于运行游戏服务器。这打消了在部署过程中装置依赖项或配置机器的须要,并且极大地提高了人们对软件在开发和测试中可能像在生产环境中一样运行的信念。通过将软件容器和 Kubernetes 联合应用,咱们能够建设一个松软的根底,从而基本上能够大规模运行任何类型的软件 - 从部署(deployment),运行状况查看(health checking),日志聚合(log aggregation),扩大(scaling)等等,并应用 API 在简直所有级别上管制这些事件。从实质上讲,Kubernetes 实际上只是一个集群治理解决方案,简直可用于任何类型的软件。大规模运行专用游戏须要咱们跨机器集群治理游戏服务器过程 – 因而,咱们能够利用在该畛域曾经实现的工作,并依据本人的特定需要对其进行定制。这两个我的项目都是开源的,并且是踊跃开发的,因而咱们也能够利用将来开发的任何新性能。Paddle Soccer 为了验证我的实践,我创立了一个非常简单的基于 Unity 的游戏,称为 Paddle Soccer,该游戏本质上与形容的齐全一样。这是一款两人在线游戏,其中每个玩家都是 paddle,他们踢足球,试图相互得分。它具备一个 Unity 客户端以及一个 Unity 专用服务器。它利用 Unity High Level Networking API 来在服务器和客户端之间提供游戏状态同步和 UDP 传输协定。 值得注意的是,这是一款 session-based 的游戏; 即:你玩了一段时间,而后游戏完结,你回到大厅再玩,所以咱们将专一于这种扩大,并在决定何时增加或删除服务器实例时应用这种设计。 也就是说,实践上这些技巧也实用于 MMO 类型的游戏,只是须要进行一些调整。 Paddle Soccer 架构Paddle Soccer 应用传统的整体体系结构来进行基于会话的多人游戏: 玩家连贯到 matchmaker 服务,该服务应用 Redis 将它们配对在一起,以帮忙实现此目标。一旦两个玩家退出到一个游戏会话中,matchmaker 会与 game server manager 对话,让它在咱们的机器集群中提供一个游戏服务器。game server manager 创立一个新的游戏服务器实例,该实例在集群中的一台计算机上运行。game server manager 还获取游戏服务器运行所在的IP地址和端口,并将其传递 matchmaker 服务。matchmaker 服务将 IP 和端口传递给玩家的客户端。…最初,玩家间接连贯到游戏服务器,当初能够开始对战了。因为咱们不想本人构建这种类型的集群治理和游戏服务器编排,因而咱们能够依附容器和 Kubernetes 的弱小性能来解决尽可能多的工作。 ...

April 8, 2021 · 3 min · jiezi

关于游戏服务端:HCaaS实战之我的世界Java版

我的世界是很火的游戏,能够本人DIY剧情和倒退脉络,充沛享受发明的乐趣。 接下里就带你一分钟搭建MC私服——v1.7.10版本。 先说后果,自己做好镜像后公开这个镜像仓库,这样HCaaS用户都能够应用这个镜像: mirror.cubepaas.com/github-clz/mc-server:KCauldron1. 创立mc服务端工作负载 登入HCaaS,通过“工作负载”去“部署服务”,在Docker镜像处抉择“镜像地址”,而后填入下面的那个镜像仓库地址(dockerhub下面也有很多镜像能够尝试) 配置好容器规格(低于8X服务会起不来),端口映射,mc私服只须要关上25565端口,而后抉择客户端能够拜访的网络模式——L4层负载平衡 第一步配置完了,间接跳到第三步。在镜像中配置的默认启动参数是“-Xmx4096M -Xms512M”,如果本身有不同的配置参数,能够在“命令”栏通过sed -i命令来替换"/mc/mc-trigger-file.sh"启动脚本中的-XMmx参数和-Xms参数 点击确认,而后就等着工作负载启动和生成SLB拜访地址。 2. 下载v1.7.10的客户端(参考 HCaaS收费搭建Minecraft服务器--东阳君の博客 底部) 关上客户端,在“多人模式”下,连贯工作负载中SLB拜访地址。而后就能够自在徜徉MC的世界里了。 -----------------------------镜像制作分割线---------------------------------- 啰嗦一下制作镜像的过程 ① 筹备软件包,服务端zip包和启动脚本 目前不反对rar格局,须要本人转化为zip格局,不要带最外层文件夹。Minecraft-1.7.10-server.zip 启动脚本: #!/bin/bashjava -Xms512M -Xmx4096M -jar ./KCauldron.jar nogui② 开启镜像仓库(有帖在先这里不赘述了) ③ 疾速部署--抉择模板类型,本例用Jar模板 ④ 制作“业务镜像” 抉择镜像下拉菜单:制作镜像(再次部署就能够抉择已有镜像) 构建环境:本例中应用java8 镜像仓库凭证:第一步镜像仓库配置实现后下拉菜单中就能够抉择对应的镜像库 设置好镜像名称和tag:以自带镜像仓库为例,镜像名称后面跟着我的项目名 上传服务端压缩包和启动文件mc-trigger-file.sh 而后填写启动脚本的门路。 点击“制作镜像”就开始了制作。直到显示success ⑤ 共享镜像 进入镜像仓库,点击“公有”,变成“公开”,而后小伙伴就能够下载仓库里的镜像了 进入刚刚制作的镜像页面,获取镜像下载地址: 这样你就能够把本人做的镜像分享给小伙伴了。HCaaS让私服更自在~

November 21, 2020 · 1 min · jiezi

游戏合服时如何避免主键冲突

Last-Modified: 2019年5月10日15:23:31 背景滚服类型的游戏常见于 手游、网游(包括H5), 滚服类型游戏的特点(与传统大服架构区别): 单服同时在线游戏人数少(eg. 3000人), 达到上限就开新服以下这部分内容来自: https://www.cnblogs.com/youji...滚服模式是游戏类型,技术架构和急功近利的坑钱策略等因素共同决定的,大服游戏包括绝大部分端游,以及类COC这样类型的游戏。 另外,虽然像英雄联盟,王者荣耀这样的游戏也分服架构,但是这个并不是我理解中的“滚服游戏“,首先他们虽然分服,但是每个服的人数上限也是可以高达几十万,他们并不会发生频繁的合服情况。 而滚服游戏更多是通过游戏策略设计,鼓励玩家花钱走捷径透支游戏生命周期,甚至几天即可独霸一个服务器。从而导致其他玩家望尘莫及,即使是花钱追也性价比极低,还不如进入一个新服重新开始。 这就导致了新服一开,玩家即蜂拥而至,争先恐后练级升装备,以求最快速进入排行榜前列,如果努力一番发现落后了,可能就只能坐等下一个新服。这也导致了新服人数火爆,老服慢慢变成人烟凋零的村服,甚至没人的死服。 为了能够节约服务器带宽资源,同时让少数剩余的玩家能够玩得起来,就必须要要进行频繁的合服,把若干个互不相干的服务器玩家,合并到一个服里面;这样又开启一波玩家竞争和收割。 合服处理合服时要特别注意: 防止主键冲突防止唯一(unique)键不冲突 (eg. 用户昵称)清空僵尸数据/无效玩家数据(小心数据残留, 避免数据不一致)insert into 时注意字段顺序不一致问题处理主键冲突的办法主要有2种: 合服前预处理冲突键开服时预分配好可能的冲突键, 合服时则无需额外处理(推荐)防止主键冲突合服时处理冲突如果在一开始没有设计好数据库的话, 合服时很容易遇到的普遍情况就是: 主键冲突 游戏通常有角色表, 道具表, 一般都是用数据库的自增长(AUTO_INCREMENT)特定来创建其主键, 以此保证主键的唯一,以如下表结构为例,id 只能保证在本服中唯一,A服中有个玩家id是1, B服中也有个玩家id是1, 合服前必须解决这个冲突. -- 玩家表CREATE TABLE `users`( `id` int(11) unsigned not null, `name` varchar(50) default null, primary key (`id`)) Engine=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;-- 玩家道具表CREATE TABLE `props`( `id` int(11) unsigned not null, `user_id` int(11) unsigned not null, primary key (`id`)) Engine=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;通常做法是给 B表中的所有 users.id 字段值加上一个基数(max('A.users.id'), 同时还要修改涉及到的其他表, 比如 props.user_id 字段必须相应修改, 否则无法关联到对应玩家,因为修改了 users.id 后一般要修改相应的数十张表中的 user_id, 同时若使用了外键还得额外处理外键的删除和重做. ...

May 10, 2019 · 1 min · jiezi

如何实现手游中的账户系统|游戏后端

作者:崔毅然在这篇文章中,我们使用 LeanCloud 作为后端来实现游戏内的账户系统。这篇文章以 Unity 游戏引擎中的 C# 语言为示例,主要讲解如何实现几种主流的登录方式,包括游客登录、游客账号升级、手机号验证码登录、用户名密码注册及登录。接入 SDK首先要接入 LeanCloud 的 SDK,接入方式可以参考文档。游客登录为了让玩家尽快体验游戏,每一个游戏都会有游客登录的功能。游客登录在 LeanCloud 中可以这样来实现:var user = await AVUser.LogInAnonymouslyAsync();调用上述代码成功后,LeanCloud 会自动生成一个游客用户登录,进入「控制台」 - 「存储」-「_User」表就可以看到表中新增了一条数据。在客户端登录的游客信息会一直被 SDK 存在本地,直到玩家删除游戏或主动退出登录。但就像所有游戏中的游客登录一样,当该游客退出登录后会丢失自己全部的游戏数据,为了保存游戏数据,需要将游客账号升级为正式账号。游客账号升级为了不丢失玩家的数据,我们会在游戏内建议玩家升级账号为正式玩家。例如绑定微信登录、绑定用户名密码及手机号,绑定成功后玩家就能以正式的登录方式获取到自己的游戏数据。升级为微信登录假设我们已经通过某些方法(例如使用 ShareSDK)拿到了微信的 openId、access_token、unionId 等,可以这样在 LeanCloud 中将游客账号关联到微信登录中:var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “openid”, “OPENID” },// openId 是用户在当前微信应用下的唯一 Id}; // unionId 是用户在整个微信内的唯一 Idvar unionId = “ox7NLs06ZGfdxbLiI0e0F1po78qE”; AVUserAuthDataLogInOption options = new AVUserAuthDataLogInOption{ UnionIdPlatform = “weixin”,// 这里指定用微信平台 AsMainAccount = true}; var user = AVUser.CurrentUser;// 绑定微信登录,第二个参数 weixinapp1是自定义的当前微信应用的标识await user.AssociateAuthDataAndUnionIdAsync(authData, “weixinapp1”, unionId, options);关联成功后,玩家以后就可以用微信登录了,登录代码见下文的第三方账户登录。绑定用户名、密码及手机号var currentUser = AVUser.CurrentUser;currentUser.Username = “username”;currentUser.Password = “password”;user.MobilePhoneNumber = “186xxxxxxxx”;await currentUser.SaveAsync();如果保存了手机号,保存成功后 LeanCloud 会自动向该手机号发送一条验证码,用户输入验证码后验证手机号:await AVUser.VerifyMobilePhoneAsync(“6位数字验证码”);手机号码验证成功后,该玩家以后就能以手机号登录了,这样就保证了游戏数据不会丢失。手机号+验证码登录、用户名及密码登录的代码见下文。手机号 + 验证码登录这种登录方式下,如果 _User 表中没有这个手机号,则视为新用户,会自动注册账号并登录;如果 _User 表中某个用户已经有了这个手机号(例如曾使用过该手机号登录,或通过游客账号升级绑定的信息),则直接登录。首先,调用发送登录验证码的接口:await AVCloud.RequestSMSCodeAsync(“18611111111”);然后使用验证码来登录var user = await AVUser.SignUpOrLogInByMobilePhoneAsync(“18611111111”, “6位短信验证码”);用户名 + 密码注册登录这种是最常见的登录方式,稍微有一点麻烦的是,需要玩家记住自己的用户名和密码。注册如果 _User 表中没有相应的用户名密码信息,例如从未注册过,也没有通过游客升级的方式增加用户名密码,需要先注册。var user = new AVUser();user.Username = “Tom”;user.Password = “cat!@#123”;await user.SignUpAsync();Debug.Log(user.Username);登录var user = await AVUser.LogInAsync(“username”, “password”);Debug.Log(user.Username);第三方登录微信或 QQ 登录可以让玩家更便捷的登录游戏。利用 LeanCloud 第三方登录的模块就可以完成这种场景。微信登录假设现在开发者已经通过某些方法(例如使用 ShareSDK)拿到了微信的 openId、access_token、unionId 等,无需注册就可以在 LeanCloud 中直接登录。如果游客已经升级绑定了微信信息,也可以通过这种方式来登录。var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “openid”, “OPENID” },// openId 是用户在当前微信应用下的唯一 Id};// unionId 是用户在整个微信内的唯一 Id var unionId = “ox7NLs06ZGfdxbLiI0e0F1po78qE”; AVUserAuthDataLogInOption options = new AVUserAuthDataLogInOption{ UnionIdPlatform = “weixin”,// 这里指定用微信平台 AsMainAccount = true}; // 绑定微信登录,第二个参数 weixinapp1 是自定义的当前微信应用的标识var user = await AVUser.LogInWithAuthDataAndUnionIdAsync(authData, “weixinapp1”, unionId, options);在 LogInWithAuthDataAndUnionIdAsync 这个方法中,第二个参数是自己定义的微信应用的名字,第三个参数 unionId 是用户在多个微信应用之间互通的唯一 id。如果我们有多个微信应用,就可以通过 unionId 登录来实现多个微信应用之间的账号互通。其他平台如果是其他平台,例如 facebook 是没有 unionId 的,这个时候只需要 access_token、expires_in、uid 三个自定义字段就可以了。var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “uid”, “FACEBOOK_UID” },};var user = await AVUser.LogInWithAuthDataAsync(authData, “facebook”);由于 LeanCloud 默认只支持微信、QQ、新浪微博登录,因此对 Facebook 需要额外去设置一下唯一索引,设置唯一索引的方式非常简单,只需要进入控制台,在 _User 表中选择「其他」-「索引」,将 authData.facebook.uid 建立唯一索引,并且勾选上「允许缺失值」选项,这样 Facebook 登录也完成了。 ...

March 7, 2019 · 2 min · jiezi

如何通过服务端控制游戏逻辑

在如何开发答题对战小游戏中给大家展示了对战开发的基础结构:拥有多个房间类型的游戏,每个房间有两个玩家的情况下,游戏过程中玩家之间的通信均通过 LeanCloud Play 实时对战转发。游戏中,我们还使用了 MasterClient ,作为一个裁判或上帝视角的角色,用于出题及判断每个玩家的分数。Play 实时对战默认房间的创建者为 MasterClient,也就是说,创建房间的 Client 会有两种身份,一个是普通的玩家,另一个是 MasterClient 裁判角色。MasterClient 除了在答题小游戏中出题之外,还可以在卡牌类游戏中洗牌、控制刷怪的时机或等级、判断游戏胜负等等。它掌握着房间内整个游戏逻辑。既然 MasterClient 是个这么重要的角色,那么我们把他放在客户端就会有一个重要问题:安全隐患。例如客户端的代码被破解之后,MasterClient 身份的玩家可以篡改游戏数据,指定本该输掉的人胜利等。为了解决这个问题,我们把控制游戏逻辑的 MasterClient 从客户端移到服务端,这样从客户端就拿不到游戏逻辑代码,进而也无法控制游戏逻辑。我们把每个房间的 MasterClient 托管在一个叫 Client Engine 的后端服务上,MasterClient 在 Client Engine 中通过实时对战后端服务和客户端进行交互。产生了新的架构:这里 Client Engine 和实时对战云都是 LeanCloud 的服务,同在 LeanCloud 的后端内网中。实战开发目标 Demo下面我们感受下如何基于这种架构开发小游戏,在这次分享中我们的目标是开发一个剪刀石头布对战小游戏。你可以用两个浏览器打开这个页面,感受下整个小游戏。在这个小游戏中,两个客户端点击「快速开始」,进入到同一个房间内,游戏开始后进行猜拳,一轮猜拳后判断胜负,游戏结束。游戏逻辑我们把游戏逻辑拆解为以下步骤:1.进入房间:客户端点击「快速开始」时,MasterClient 及玩家客户端 A 和 B 进入同一房间2.双方开始游戏:玩家 A 选择手势玩家 B 界面展示:对方已选择玩家 B 选择手势玩家 A 界面展示:对方已选择玩家 A 及 B 的界面展示结果3.游戏结束,双方离开房间,房间销毁。服务和语言1.服务选择:选择已经搭建好的后端服务 LeanCloud Play,不需要我们再自己去搭建后端整体架构。服务端逻辑控制:Client Engine游戏内通信:实时对战服务2.语言选择:JavaScript(这样我们一个人就能搞定前端和后端的代码)明确服务端及客户端的分工服务端:托管 MasterClient 代码,控制游戏逻辑。客户端:根据情况展示 UI准备项目框架服务端 Client Engine 初始项目客户端项目游戏逻辑开发下面我们进入写代码的模块。进入房间客户端点击「快速开始」时,MasterClient 及玩家客户端 A 和 B 进入同一房间Client Engine 服务端:维护 MasterClient 并创建房间,下发 roomName 给客户端客户端:加入服务端创建的房间中我们先看一下 Client Engine 中的逻辑:Client Engine 负责维护 MasterClient 并创建房间,通过一个名为 /reservation 的自定义 API 接口为客户端提供 roomName,在这个接口中我们实现逻辑「快速开始」。「快速开始」中创建房间的功能是使用 Client Engine SDK 中的 GameManager 来实现的。在 Client Engine 中,我们使用到的 Client Engine SDK 提供以下两个组件。Game:每个房间对应一个 Game 实例,Client Engine 中有 N 个 Game。GameManager:GameManager 负责创建、管理、销毁 Game。我们只需要根据情况组合这两个组件的功能就可以实现自己的需求。接下来我们写「快速开始」的逻辑:随机为客户端找到一个房间,如果没有空房间,就创建一个新房间。import { Game, GameManager, ICreateGameOptions } from “@leancloud/client-engine”; export default class Reception<T extends Game> extends GameManager<T> { public async makeReservation(playerId: string) { let game: T; const availableGames = this.getAvailableGames(); if (availableGames.length > 0) { game = availableGames[0]; this.reserveSeats(game, playerId); } else { game = await this.createGame(playerId); } return game.room.name; } }在这段代码中,我们创建了一个 Reception 类继承自 GameManager 来管理 Game。在这个类中,我们写了一个 public 方法 makeReservation 实现「快速开始」:首先调用 GameManager 自身的 getAvailableGames() 方法查看有没有可用的空房间,如果有,就取第一个空房间,返回 roomName ;如果没有空房间,则使用 GameManager 的 createGame() 方法创建一个新房间,返回新房间的 roomName。从上面的代码中我们还可以看到,Reception 管理了一个 T 类型的 Game 对象,因此我们还需要为 Reception 准备 Game。下面我们继续自定义一个自己的 Game :import { Game } from “@leancloud/client-engine”;import { Event, Play, Room } from “@leancloud/play”;export default class RPSGame extends Game { constructor(room: Room, masterClient: Play) { super(room, masterClient); }}在这段代码中,我们自定义了一个名为 RPSGame 的类继承自 Game,之后会在 RPSGame 中撰写房间内的游戏逻辑,在这里我们先简单的将这个类构造出来。接下来我们把这个类给到 Reception,让 Reception 来管理这个类。import PRSGame from “./rps-game”;const reception = new Reception( PRSGame, APP_ID, APP_KEY, {concurrency: 2});在这段代码中,我们创建了一个 reception 对象,在创建对象的第一个参数中,我们传入了刚才创建的 RPSGame,这样 Reception 就可以管理 RPSGame 了,到现在为止「快速开始」的逻辑就可以跑起来了。下面我们写一个 API 接口来提供「快速开始」功能:app.post("/reservation", async (req, res, next) => { try { const {playerId} = req.body as {playerId: any}; // 调用我们在 Reception 类中准备好的 makeReservation() 方法 const roomName = await reception.makeReservation(playerId); return res.json({roomName}); } catch (error) { next(error); }}到这里,服务端「快速开始」就准备好了,当客户端调用该 /reservation 接口时,服务端会执行快速开始的逻辑,给客户端随便返回一个有空位的房间。客户端调用 /reservation 的示例代码如下:// 向 Client Engine 请求快速开始。// 这里通过 HTTP 调用在 Client Engine 中实现的 /reservation 接口const { roomName } = await (await fetch( ${CLIENT_ENGINE_SERVER}/reservation, { method: “POST”, headers: {“Content-Type”: “application/json”}, body: JSON.stringify({ playerId: play.userId }) } )).json(); // 加入房间return play.joinRoom(roomName);当客户端 A 和 客户端 B 都运行加入房间的代码,进入同一个房间后,就可以开始游戏了,接下来是实现房间内的逻辑。自定义游戏逻辑限定房间人数export default class RPSGame extends Game { public static defaultSeatCount = 2;}在这段代码中,我们给 RPSGame 设定一个静态属性 defaultSeatCount = 2,当房间玩家数量为两个人时,GameManager 会认为房间已满,不再是可用房间;GameManager 管理的 MasterClient 向实时对战服务请求创建新房间时,也会以这里的数量为标准,限定房间最大玩家数量是 2 个人,满 2 个人时不得有新玩家再加入房间。房间人满,广播游戏开始当房间内的玩家数量等于 defaultSeatCount 时,我们可以通过以下代码来监听房间人满事件:@watchRoomFull()export default class RPSGame extends Game { public static defaultSeatCount = 2; constructor(room: Room, masterClient: Play) { super(room, masterClient); // 游戏创建后立刻监听房间人满事件 this.once(AutomaticGameEvent.ROOM_FULL, this.start); } protected start = async () => { // 标记房间不再可加入 this.masterClient.setRoomOpened(false); // 向客户端广播游戏开始事件 this.broadcast(“game-start”); …… }}在这段代码中,@watchRoomFull 装饰器会让 Game 在人满时抛出 ROOM_FULL 事件,我们在 constructor() 方法中监听到这个事件后,调用了自己的 start 方法。在 start 方法中,我们将房间关闭,然后向客户端广播 game-start 事件,客户端收到这个事件后,在界面上展示:游戏开始。双方开始游戏我们再看一下双方游戏的逻辑:玩家 A 选择手势玩家 B 界面展示:对方已选择玩家 B 选择手势玩家 A 界面展示:对方已选择玩家 A 及 B 的界面展示结果将游戏逻辑对应到开发逻辑上,过程如下图所示:从图中可以看到,这里涉及到三方:客户端 A 、客户端 B、处在 Client Engine 中的 MasterClient。当客户端 A 出拳时,发送一个名为 play 的事件给 MasterClient,MasterClient 接收事件后,记录下来客户端 A 的选项,然后抹掉选项数据将事件转发给客户端 B,这样客户端 B 只知道客户端 A 出拳,但是并不知道具体手势是什么。接着客户端 B 出拳发送 play 事件,MasterClient 转发给客户端 A。这时 MasterClient 发现双方都已经出拳了,判定游戏结果,并通过广播 game-over 事件通知双方客户端游戏结束。首先我们看一下客户端 A 的出拳代码:play.sendEvent(“play”, {index}, {receiverGroup: ReceiverGroup.MasterClient});在这段代码中,客户端 A 使用实时对战 SDK 发送了 play 事件,在事件中附带了手势数据 {index},指定这个事件的接收对象为 MasterClient。处在 Client Engine 中的 MasterClient 收到 play 事件后转发事件给客户端 B:this.masterClient.on(Event.CUSTOM_EVENT, event => { const eventId = event.eventId; if (eventId === ‘play’) { this.forwardToTheRests(event, (eventData) => {}); }});在这段代码中,我们使用了 SDK 中 Game 提供的 forwardToTheRests() 方法,这个方法会转发事件给房间内其他人,第一个参数是原始事件 event,在第二个参数中,我们修改了原始 event 中的数据,将 eventData 设置为了空数据,这样客户端 B 收到事件时无法知道具体的手势信息。当客户端 B 收到事件后,就可以在界面上展示:对方已选择。相关代码如下:play.on(Event.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { const eventId = event.eventId; if (eventId === ‘play’) { //这里写客户端 UI 展示的代码 }});接着游戏逻辑是,客户端 B 选择手势,MasterClient 转发手势给客户端 A,这里的逻辑和上面的一样,不再赘述,我们直接跳到判断游戏胜负并广播游戏结束。相关代码如下:this.masterClient.on(Event.CUSTOM_EVENT, event => { const eventId = event.eventId; if (eventId === ‘play’) { …… if (answerArray.length === 2) { const winner = this.getWinner(answerArray); this.broadcast(“game-over”, {winnerId: winner.userId}); } }});在这段代码中可以看到,每次 MasterClient 收到 play 事件时,都会保存玩家的手势,当发现两个玩家都出拳后,根据两个玩家的出拳结果判断胜负,然后广播 game-over 事件,在 game-over 事件中告诉所有人胜负。客户端收到 game-over 事件后,在界面上展示游戏结束。客户端相关代码如下:play.on(Event.CUSTOM_EVENT, ({ eventId, eventData, senderId }) => { const eventId = event.eventId; if (eventId === ‘play’) { …… } if (eventId === ‘game-over’) { //展示游戏结束 }});离开房间当两个客户端都离开房间后,房间会被 GameManager 自动销毁,不需要我们再写额外的代码。总结在本次分享中,我们把负责游戏逻辑的 MasterClient 放在服务端来保证安全性。MasterClient 被托管到 Client Engine 中,通过实时对战后端云与同房间内的客户端传递消息,保证游戏正常运行。参考资料如果你希望有更详细的资料来帮助你一步一步开发猜拳小游戏,或更进一步了解 Client Engine,可以参考以下文档Client Engine 快速入门你的第一个 Client Engine 小游戏上期分享补充增加倒计时可以自己尝试为剪刀石头布游戏增加倒计时功能,例如某个客户端在限定时间内没有做出选择,则输掉本局比赛。RxJS如果希望对事件有更好的代码组织方式,可以学习下 RxJS。Q & A1.Client Engine SDK 和 Play SDK 有什么不一样?Play SDK 指的是实时对战 SDK,玩家客户端和处在 Client Engine 中的 MasterClient 都要使用这个 SDK 与实时对战服务交互,进而互相传递消息。为了方便大家撰写 Client Engine 中的代码,Client Engine SDK 提供了两方面的功能:对 Play SDK 更进一步的封装,提供了作为 MasterClient 便利的方法:广播、转发消息等。额外提供了 GameManager 及 Game,方便对多个房间进行管理。2.使用 Client Engine 开发游戏逻辑,和在客户端开发游戏逻辑相比,各自有什么优缺点。在一开始的时候有讲到,将代码放到 Client Engine 中会更安全,避免客户端被破解,进而篡改游戏逻辑。可能有的同学认为有一个缺点是需要部署并运维服务端,但 Client Engine 的使用方式十分便捷,全部交给 LeanCloud 来部署运维,自己只需要写游戏逻辑就可以,所以不存在自己部署以及运维困难的问题。3.如今都原生支持异步的情况下,还需要学习 RxJS 吗?RxJS 会将异步及事件组合为一个流式操作,在大型项目上逻辑性会更好,对工程师要求的抽象水平更高,代码也会更加简洁。参考资料中《你的第一个 Client Engine 小游戏》使用的是 Play SDK 事件代码,github 的 repo 中使用了 Client Engine 封装的 RxJS 的方法,建议自己亲自动手写一写代码,会感受到其中的不同。 ...

January 10, 2019 · 4 min · jiezi