TS 也能开发多人实时对战?
大厂技术 高级前端 Node进阶
点击上方 程序员成长指北,关注公众号
回复1,加入高级Node交流群
❓ 帧同步和状态同步可以并用?
❓ 200ms 毫秒延迟也能实现单机游戏般的丝滑流畅?
❓ 有延迟的情况下怎么实现技能判定?
先来看看 2 天时间做的 Demo
(由 TSRPC + Cocos Creator 开发)
写在前面
多人实时对战一直是游戏开发领域的一块硬骨头。听起来不简单,做好了更难。但时代在进步,技术在发展。就像 Cocos Creator 让游戏开发变得更简单了一样, 基于最新的技术栈和理解,多人实时对战的门槛也一直在降低。
2021 年 12 月 4 日,有幸作为嘉宾参加了深圳站的 Cocos Star Meeting 线下交流会, 就 “多人实时对战” 这个领域结合历经 5 年沉淀的开源框架 TSRPC 进行了一些分享。
以下是本次分享内容的文字实录。
自我介绍
大家好,首先简单自我介绍一下。我是 King 王忠阳,Github ID 是 k8w。
曾经是腾讯互娱的一名老鹅,也是一枚老全栈。2016 年时发现了 TypeScript,开始用它进行全栈开发,之后就真香得一发不可收拾。2017 年,TSRPC 1.0 第一次出现在了 Github 上。历经 5 年的沉淀发展,如今已进入 3.x 版本。
现在我的主要时间精力投入在开源项目 TSRPC 的开发和维护上,也提供一些技术咨询服务。
目录
那么接下来进入今天的主题 —— TSRPC + Cocos,多人实时对战 So Easy!
我将主要分为 3 个部分介绍:
同步策略
介绍在有网络延迟的情况下,优化实时对战体验的方法
网络通信
细数网络通信的痛点,并介绍终极解决方案 —— TSRPC
项目实战
从零开始实现一个疯狂打群架多人版,看看是不是 So Easy
额外内容
补充一些在线下分享时没有提到的一些实用内容
同步策略
说起 “多人实时对战” 首先就会想到 “同步”,那么说起 “同步” 你又最先想到什么呢?
帧同步 vs 状态同步?
没错,很多人会想到 帧同步 和 状态同步。有关它们,你可以搜索到大量的介绍,比如:
但首先,我想更正大家一个常见的误区。
很多时候我们都在讨论 “该用帧同步还是状态同步”,似乎这已经变成了一个非此即彼、二选一的问题。但事实上,帧同步和状态同步不但 不是 二选一的关系,甚至可以 相互替代 和 混合使用。
因为,帧同步和状态同步最终都是同步状态。
假设我们要实现一堆人在一个房间里跑,无论你用状态同步 —— 直接发送你的坐标,或是帧同步 —— 发送移动操作再由客户端计算出最终坐标,对于表现层组件而言,需要的都是你的最终状态 —— 位置坐标。所以实际上帧同步和状态同步更多讨论的是,网络传输什么,状态在哪里计算 —— 看起来更像一个成本问题。
只需确保你的状态计算逻辑在前后端都可运行,那么帧同步或状态同步就是可以并用或随时切换的。
同步状态
帧同步和状态同步都是同步状态。
首先来看看最简单、没有任何优化策略下,状态是怎么被同步的。
本地按下按键,发出操作指令。
指令发送给服务器,服务器得到最新状态,并广播给所有人。
帧同步广播操作指令,状态同步广播结果
前端收到服务器的发送来的新状态后,刷新界面显示。
效果如图。(上面是自己的视角,中间是服务器,下面是其它玩家视角)
很明显,有几个问题:
操作延迟
由于网络延迟,按下左/右按键后,总要过一会才能收到服务器的返回,导致操作延迟,体验很差
卡顿
由于服务器同步的逻辑帧率(每秒 3 次)慢于显示帧率(每秒 60 帧),导致位移一卡一卡的,不流畅
但网络延迟一定客观存在,同步帧率和显示帧率也未必一致。所以我们需要一种办法,能够在有延迟、有卡顿的情况下,还能实现感觉不到延迟、并且丝滑流畅的体验。这就是同步的魔术 —— 同步策略。
同步策略
网络延迟是客观存在的,所以同步策略本质上是一种魔术,要在有延迟的情况下实现没有延迟的错觉。根据项目,主要有几种类型。
首先根据同步节奏分为快慢两种。
慢节奏
同步间隔在 1~2 秒甚至更久,例如打牌、下棋等。这种情况非常简单,只需在表现层面优化,做到 即时反馈 即可。例如五子棋,当玩家点下鼠标按钮时,无需等到服务器返回才显示落子,而是立即显示落子,并发出 “啪” 的一声清脆的回响,给玩家一个即时的操作反馈。可能实际上服务器延迟了 1 秒,但玩家是毫无感知的。
快节奏
同步间隔较短,例如要实现咱们在一屋子里乱跑。那么这个也分情况,无冲突和有冲突的。
如果我们都是幽灵,那么就是无冲突的。因为我们的身体是虚无的,可以互相穿透。你的位置只受你自己控制,任何其它因素都影响不了,这个就叫无冲突。那这个实现方案就非常简单 —— 你自己和其它人区别对待。你自己就实现为一个单机游戏,无论做了任何移动操作,都立即应用到表现层,只是将这些信息同步发送给服务端。其它人呢,就是收到服务端的信息,然后把它们的变化当动画一样播放出来就可以。当然,因为网络延迟的关系,你看到的可能是其它人 5 秒、10 秒前的状态了。可是那有什么关系呢?在幽灵这个无冲突的设定下,不会有任何影响,反而所有人都获得了单机游戏般丝滑流畅无延迟的体验,大家都很开心。
但另一种情况则不同,如果我们都是血肉之躯,会发生实际的物理碰撞,我站在这里你就不可能再站在相同的位置。这种情况就称为有冲突。快节奏有冲突 的同步策略会相对复杂一些,接下来着重介绍。
快节奏有冲突的同步策略
解决快节奏有冲突的同步策略,核心就是 3 个关键词:预测、和解、插值 。理解了这 3 个概念,任何情况的同步对你来说应该都是游刃有余。不过在此之前,让我们先看看逻辑与表现分离的架构。
逻辑与表现分离
多人实时游戏,通常会划分为表现层和逻辑层。表现层指游戏画面的显示和用户输入的获取;逻辑层指渲染无关的、只关注状态变化和计算的玩法逻辑。
逻辑层和表现层最终是面向数据的,例如玩家的位置、生命值等,我们把这些数据统称为 状态。我们把所有能影响状态变化的因素称为 输入,例如玩家操作(如移动)、系统事件(如天上打雷了)、时间流逝等等。
逻辑层就是定义所有状态和输入,然后实现状态变更的算法:
新状态 = 老状态 + 输入1 + ... + 输入N
重要
逻辑层本质上就是一个状态机。
在实现逻辑层的过程中,有几个重要的点需要关注:
无输入,不变化:状态变更仅发生在输入时刻,没有输入时状态不会改变
无外部依赖:状态计算应该没有任何外部依赖,例如
Date.now()
、Math.random()
等,所有这些都应该显式成为输入的一部分结果的一致性:在相同的状态和输入下,得到的新状态应该是一致的
Tips
像随机数这类场景,可以通过伪随机数生成器实现,在相同的种子输入下,随机结果应该是一致的。
在逻辑层的状态计算基础之上,预测、和解、插值就更容易理解了。
预测
预测就是将玩家的输入立即应用到本地状态,而无需等待服务端返回。
如果玩家的每一次操作如果都要等到服务端确认后才能生效,那么延迟将是不可避免的。解决方案就是:玩家做出任何操作后,立刻将输入应用到本地状态,并刷新表现层显示。例如按下了 “右”,那么就立即向右移动,而无需等待服务端返回,效果如图。
现在,操作的延迟消失了。你按下 “左” 或者 “右” 都可以得到立刻的反馈。
但问题似乎并没有完全解决,在移动过程中,你总是能感到来回的 “拉扯” 或者位置抖动。这是因为你在执行本地预测的时候,也在接收来自服务端的同步,而服务端发来的状态总是滞后的。
例如:
你的坐标是
(0,0)
你发出了 2 个
右移
指令(每次向右移动 1 个单位),服务器尚未返回,执行本地预测后,坐标变为(2,0)
你又发出了 2 个
右移
指令,服务器尚未返回,执行本地预测后,坐标变为(4,0)
服务端发回了你的前 2 个右移指令:从
(0,0)
执行 2 次右移,坐标变为(2,0)
,被拉回之前的位置
由于延迟的存在,服务端的同步总是滞后的,所以你总是被拉回之前的位置。如此往复,就是你在图中看到的抖动和拉扯。
归根到底,是服务端同步过来的状态与本地预测的状态不一致,所以我们需要 “和解” 它们。
和解
和解就是一个公式:预测状态 = 权威状态 + 预测输入
重要
和解的概念最难理解,但也是实现无延迟感体验最重要的一步。你可以先简单记住上面的公式,应用到项目中试试看。
权威和预测
一般我们认为服务器总是权威的,从服务端接收到的输入称为 权威输入,经权威输入计算出来的状态称为 权威状态。同样的,当我们发出一个输入,但尚未得到服务端的返回确认时,这个输入称为非权威输入,也叫 预测输入。
在网络畅通的情况下,预测输入迟早会按发送顺序变成权威输入。我们需要知道发出去的输入,哪些已经变成了权威输入,哪些还是预测输入。在可靠的传输协议下(例如 WebSocket)你无需关注丢包和包序问题,所以只需简单地对比消息序号即可做到。
和解过程
在前述预测的基础上,和解就是我们处理服务端同步的状态的方式。如果使用的是状态同步,那么这个过程是:
收到服务端同步来的 权威状态
将本地状态立即设为此权威状态
在权威状态的基础上,应用当前所有 预测输入
如果使用的是帧同步,那么这个过程是:
收到服务端同步来的权威输入
将本地状态立即 回滚 至 上一次的权威状态
将权威输入应用到当前状态,得到此次的 权威状态
在权威状态的基础上,应用当前所有 预测输入
由此可见,状态同步和帧同步只是网络传输的内容不同,但它们是完全可以相互替代的 —— 最终目的都是为了同步权威状态。
例子
这有用吗?我们回看一下上面预测的例子,有了和解之后,会变成怎样:
你的坐标是
(0,0)
你发出了 2 个
右移
指令(每次向右移动 1 个单位),服务器尚未返回
权威状态:
(0,0)
预测输入:
右移#1
右移#2
预测状态:
(2,0)
(权威状态 + 预测输入)
你又发出了 2 个 右移
指令,服务器尚未返回
权威状态:
(0,0)
(未收到服务端同步,不变)预测输入:
右移#1
右移#2
右移#3
右移#4
预测状态:
(4,0)
(权威状态 + 预测输入)
服务端发回了你的前 2 个右移指令 (帧同步)
上一次的权威状态:
(0,0)
权威输入:
右移#1
右移#2
权威状态:
(2,0)
(上一次的权威状态 + 权威输入)预测输入:
右移#3
右移#4
(#1
、#2
变成了权威输入)预测状态:
(4,0)
(权威状态 + 预测输入,之前的拉扯不见了)
看!虽然服务端同步来的权威状态是 “过去” 的,但有了和解之后,拉扯问题解决了,效果如图:
预测 + 和解处理本地输入是非常通用的方式。你会发现,在没有冲突时,网络延迟可以完全不影响操作延迟,就跟单机游戏一样!例如上面移动的例子,如果不发生冲突(例如与它人碰撞),即便网络延迟有 10 秒,你也可以毫无延迟并且平滑的移动。这就是在有延迟的情况下,还能实现无延迟体验的魔术。
冲突
那么冲突的情况会怎样呢?比如上面的例子,你发送了 4 次移动指令,但在服务端,第 2 次移动指令之后,服务端插入了一个新输入 —— “你被人一板砖拍晕了”。这意味着,你的后两次右移指令将不会生效(因为你晕了)。那么该过程会变成这样:
你的坐标是
(0,0)
你发出了 2 个
右移
指令(每次向右移动 1 个单位),服务器尚未返回
权威状态:
(0,0)
预测输入:
右移#1
右移#2
预测状态:
(2,0)
你又发出了 2 个 右移
指令,服务器尚未返回
权威状态:
(0,0)
预测输入:
右移#1
右移#2
右移#3
右移#4
预测状态:
(4,0)
服务端发回了你的前 2 个右移指令
权威状态:
(2,0)
预测输入:
右移#3
右移#4
(#1
、#2
变成了权威输入)预测状态:
(4,0)
服务端发回了与预期冲突的新输入
上一次的权威状态:
(2,0)
权威输入:
你被拍晕了
右移#3
右移#4
权威状态:
(2,0)
(因为先被拍晕了,所以后两个右移指令无效)预测输入:无 (所有预测输入都已变为权威输入)
预测状态:
(2,0)
此时,之前的预测状态 (4,0)
与最新的预测状态 (2,0)
发生了冲突,客户端当然是以最新状态为主,所以你的位置被拉回了 (2,0)
并表现为晕眩。这就是网络延迟的代价 —— 冲突概率。
插值
插值指在表现层更新 “其它人” 的状态变化时使用插值动画去平滑过渡。
到目前为止,我们已经获得了自己在本地无延迟的丝滑体验。但在其它玩家的眼中,我们依旧是卡顿的。这是由于同步帧率和显示帧率不一致导致的,所以我们在更新其它人的状态时,并非一步到位的更新,而是通过插值动画来平滑过渡。
重要
预测+和解是解决 自己 的问题,发生在 逻辑层;插值是解决 其它人 的问题,发生在 表现层 。
例如上面的例子,显示帧率是 30fps,服务端的同步帧率是 3 fps。收到服务端同步的其它玩家的状态后,不是立即设置 node.position
,而是通过 Tween
使其在一个短暂的时间内从当前位置平滑移动到新位置。如此,在其它玩家眼中,我们看起来也是平滑的了:
解决快节奏有冲突的同步,就是 预测、和解、插值 这 3 个核心思想,掌握了它们你应该就能举一反三,轻松应对各种场景。
网络通信
有了思想,我们现在要开始动手写一个多人实时对战的项目了。在动手层面挡在我们面前的第一个问题是什么呢 —— 网络通信。在网络通信这个领域其实我们一直是很难受的,因为一直有很多痛点,但是我们可能已经习惯了。
定义协议之痛
要通信首先要定义协议,就是指在服务端和客户端之间你要发送的是什么,常见的有几种方式。
1. 通过文档定义
很多项目组通过文档来定义协议,问题显而易见。由于文档没有强类型保证,拼写错误、字段类型错误等低级错误频发。协议变更时,文档和代码不一致的情况更是时有发生。不得不花费大量时间联调,但解决的只是这些低级错误,风险大,效率低。
2. 使用 Protobuf
Protobuf 是游戏行业常用的工具,使用它能完成运行时类型检测和二进制序列化。缺点就是它的类型是通过一门独立的语言来定义的,会增加不少额外的学习成本。由于语言不同,Protobuf 也无法完全发挥 TypeScript 的类型特性,例如 A & (B | C)
这样常见的高级类型特性就无法使用了。
3. 使用 TypeScript
直接使用 TypeScript 的类型来定义协议,不但方便,还能在前后端共享,利于代码提示。但 TypeScript 的类型系统仅在编译时刻生效,并无运行时支持,对于不可靠的用户输入这将有很大安全风险。并且也无法像 Protobuf 那样完成二进制序列化。
多种通讯模型
在一个多人实时游戏的网络通信中,我们会以多种方式处理网络请求。
例如调用 API 接口,这是基于 请求/响应 模型的用法。例如登录、注册等接口,在 Web 应用中常常使用 HTTP 短连接来实现。
但也存在例如服务端推送、流式传输等,基于 发布/订阅 模型的用法。例如帧同步广播、聊天消息广播等,常常使用 WebSocket 长连接来实现。
HTTP 常用 ExpressJS
等框架,而 WebSocket 常用 SocketIO
等框架。二者框架、API、技术方案均不一致,常常不得不拆分为多个不同项目。但实际上它们的业务逻辑又高度雷同,这导致统一维护难,学习成本高。
安全 安全 安全
重要的事情说三遍:安全!安全!安全!游戏行业最怕什么?外挂。
抓包破解
目前,Web 应用大多通过 JSON 字符串来传输。明文的 JSON 太容易被抓包和破解,这对于游戏来说是灾难性的。而字符串加密的算法本身十分有限,很多项目组选择转为 base64 字符串,但这将显著增大包体。
低级错误
100 + '100' === '100100'
应该传数字却不小心传了字符串?小小的类型错误可能导致很严重的后果。墨菲定律告诉我们,可能犯错的一定会犯错。仅仅依靠人工来保证类型安全,是不可靠的。
安全隐患
用户的输入总是不可靠的!请求参数的类型非法以及字段过滤不严格,都可能导致严重的安全隐患!例如有一个更新用户信息的接口 user/Update
,其请求格式定义为:
export interface ReqUpdate {id: number,update: {nickname?: string,avatar?: string}
}
如果客户端构造了一个恶意请求,在 update
中包含了一个不应该出现的敏感字段 role
:
{"id": 123,"update": {"nickname": "test","role": "超级管理员" // 敏感字段,不在协议中,不允许更新!}
}
后端极有可能因为检查不严格,而导致安全隐患!
所以
我们无法找到一个能完美解决这些问题的现成框架, 于是我们全新设计和创造了 TSRPC。至今已经历时 5 年,经多个千万级用户项目验证。
TSRPC
接下来就来介绍,专为 TypeScript 设计、更适合 Cocos 的 RPC 框架 —— TSRPC 。
官网:https://tsrpc.cn
文档:https://tsrpc.cn/docs/introduction.html
例子:https://github.com/k8w/tsrpc-examples
Github:https://github.com/k8w/tsrpc (求小星星~)
专为 TypeScript 设计
TSRPC 是专为 TypeScript 设计的,所以天然更适合 Cocos。
TS 也能开发多人实时对战?相关推荐
- NodeJS 开发多人实时对战游戏服务器 (一)
从一个游戏情怀说起 接触的第一款多人对战游戏是帝国时代,依稀记得那时候上学每周最期待的就是冲到电脑课撸一把罗马复兴,高中开始接触<魔兽争霸3>,一款真正让我迷恋十多年的游戏,怀念那时候的& ...
- 钉钉 6.0 开放底层“协同框架” 开发多人实时协作程序像编本地程序一样简单...
2021 年新年伊始,钉钉在 1 月 14 日发布了最新 6.0 版本,同时宣布战略定位全面升级,钉钉将从过去基于IM的协同办公平台,升级为企业协同办公和应用开发平台. 从产品和市场表现,钉钉已经杀出 ...
- TSRPC + Cocos,多人实时对战 So Easy
原文链接:TSRPC + Cocos,多人实时对战 So Easy! | TSRPC - 专为 TypeScript 设计的 RPC 框架 内容介绍 ❓ 帧同步和状态同步可以并用? ❓ 200ms 毫 ...
- 浅析即时通讯音视频开发多人实时音视频聊天架构
移动互联网发展迅猛,目前实时音视频技术已被广泛地应用在了实时在线教育.智能家居.在线直播.安防监控等领域.这之中,诸如多人视频会议.在线实时视频教育等场景,跟传统的一对一实时音视频聊天,在技术架构的实 ...
- 下载中心2周年大型活动:重金悬赏开发牛人、分享达人!【已结束】
2011年9月25日,是51CTO下载中心2岁的生日.为了回馈广大Down友2年来的支持,下载中心特举办系列精彩活动.丰厚的奖品.热闹的氛围.便捷的参与方式,您还等什么呢?! 20 ...
- 游戏联网必备: 国内外实时对战服务详细对比
随着休闲竞技类游戏和io类游戏的成功,不管是游戏大厂还是中小开发者都纷纷入局开发带有竞技元素的各类游戏,但后者碍于技术实力的限制在实时对战方面无法突破瓶颈,这种情况下他们一般都会借助一些可以帮助实现游 ...
- 从零学习游戏服务器开发(一) 从一款多人联机实时对战游戏开始
写在前面的话 经常有学生或者初学者问我如何去阅读和学习一个开源软件的代码,也有不少朋友在工作岗位时面对前同事留下的项目,由于文档不完善.代码注释少.工程数量大,而无从下手.本文将来通过一个多人联机实时 ...
- ios开发 多人语音聊天_iOS语音通话功能实现流程(实时语音通话二)
上一篇我们讲述了iOS语音通话SDK集成指引,今天就来看下iOS下实时语音通话功能实现的流程.实时语音场景的典型之一是同一会话中的成员进行实时语音对话. 以 2 人间的实时语音为例,主要流程如下: 请 ...
- ios开发 多人语音聊天_手游语音市场的现状、机遇与挑战
文/手游那点事小鱼原创 2014年持续火热的手游市场成就了一大批企业的上市梦,同时也在大环境下产生了对系列手游增值服务的需求.基于玩家对手游社交性的需求,以及借鉴传统端游,页游中成功的社交体系,手游市 ...
最新文章
- Electron Built-in AutoUpdater 踩坑记录
- java warning 编译_关于性能:Java编译器警告会影响编译时间吗?
- DefaultServlet
- 【机器学习】SVM理论与python实践系列
- python中log1p用法_python中logging模块的基本用法
- 【期望】期望收益(金牌导航 期望-3)
- fusionsphere的核心组件_FusionSphere架构详解
- 螺旋桨设计软件_第四届智能工业软件及设计技术研讨会暨2019天洑软件用户大会成功举办...
- Java 技术体系(JDK 与 JRE 的关系)、POJO 与 JavaBeans
- tflite C++ API 部署分类模型
- java某校在积极推行无人监考,结构化面试题:高校无人监考你怎么看?
- Alibaba Code代码索引技术实践:为Code Review提供本地IDE的阅读体验
- 在更改计算机的设置路由器,怎么改路由器wifi密码 怎么修改路由器wifi密码
- 作为一名平面设计师,你必须知道的一些素材网站
- [Matlab]糖葫芦代码实现
- 二叉树的遍历-先序遍历、中序遍历、后序遍历
- 中国芯片迎难而上,4纳米芯片量产,美媒:美国或肠子都悔青了
- python两层嵌套 [i for x in L for i in x]
- .obj是什么文件?
- python画spc控制图_SPC系列8:如何选择计数型数据的SPC控制图?
热门文章
- OSChina 周二乱弹 —— 我妈都觉得我是个病毒
- SparkFun-RGB-LED-可视化音乐项目
- PC和移动端该如何快速清理缓存呢?百数教你一招
- 半监督学习(Semi-Supervised Learning, SSL)-简述及论文整理
- 特征工程入门与实践_笔记_sklearn_python
- 拯救者Y7000p Windows 10 + deepin(Linux)双系统的安装(单盘)
- cordova打包项目启动页面和图标的设置
- ueeditor无法上传图片_ueditor上传图片中文文件名失败的解决办法
- 快扫描循环伏安法及其在电化学中的应用
- testflight 公开版本中应用90天到期失效了该怎么处理-testflight 到期如何续期
- NodeJS 开发多人实时对战游戏服务器 (一)