前言

这是笔者之前撰写的一篇专利的技术交底书。现在这份专利已经处于公开阶段,可以免费查阅。不过经常写专利的同学都知道,正式的专利文本往往是很难直接看懂的。于是我在咨询专利接口人确定许可后,决定把我最初的专利交底书公开发表,这就是本文的内容。


本发明的技术关键点(欲保护点)

实现一个游戏助手,可适用于绝大部分对局类游戏,对玩家当前以及下一步的游戏行为和操作进行持续的推荐。

该推荐方案的主要关键技术包含以下几部分:

  • 历史对局记录
  • 对局特征序列生成
  • 实时玩家特征
  • 特征-离线数据匹配
  • 推荐关键帧提取
  • 推荐序列生成
  • 序列的截断和更新
  • 话术转换和播报
  • 推荐关键帧的挖掘

与本发明相近的现有技术

现有技术的技术方案

目前一些手游推出了语音陪伴助手功能,在玩家进行对局游戏的时候,在一旁进行一些语音提示,包括但不限于以下功能:

  • 对玩家进行语言上的激励
  • 讲解一些装备的用法或战术选择
  • 闲聊、活跃气氛

现有技术方案一般是基于一些基础的条件逻辑触发对应的预定义话术,比如以下几种场景:

  • 吃鸡类游戏,当玩家离开飞机机舱跳伞时,播报闲聊话术

    • 如: “哇,救救孩子吧,小几恐高呀~”
  • 吃鸡和 MOBA 类游戏,当玩家拿下首杀时,播报赞赏话术

    • 如: “哇哦~好枪法哦”
  • 吃鸡和 MOBA 类游戏,当玩家装备某武器时,播报武器对应的使用技巧

    • 如: “M16A1 的特点是三发点射,适合搭配三倍镜和消声器使用,中远距离威力很大哦”
  • 较为复杂的:MOBA 类游戏,当敌人均(或者大部分)进入团队视野时,如果本队伍成员不在敌人所在区域时,则播报需要支援的话术

    • 如: “敌人在下路出现了,推荐前去支援”

方案不足

使用预设条件进行触发的方案局限性在于以下几点:

逻辑复杂,投入产出比低

对于一些较为复杂的情况,预设条件方案需要人工预先设定复杂的条件逻辑。一般而言,判断条件越复杂,则越精确、指导意义更高。但是条件越复杂,则人工投入的成本越高,而同时播报概率却更低——之所以播报概率低,是因为需要满足的条件更多、更严苛。因此,对于较为精准的话术场景而言,人力投入产出比较低。

通用性不足

预设条件进行触发的逻辑中,每一个具体场景的工作,换到另一个场景下几乎没有可重用的地方。比如说对于 MOBA 类游戏,针对同一个英雄所设计的预设条件和话术,换到另一个英雄就完全不可复用,需要重新设计;而对于吃鸡类游戏,针对一个地图所设计的预设条件,换到另一个地图,也需要重新投入人力进行分析、设计,重复劳动大。


本技术方案的详细阐述

以下是本技术方案的总体过程图,图中以虚线为界分为上下两部分,上半部分为在线(实时)过程,下半部分为离线过程

220426_图-总体过程图.png

下文首先基于上面的总体过程图,阐述一般性的技术方案,然后再以和平精英为例子,说明一个具体的应用范例。

实时流水数据

为实现本技术方案,我们需要将玩家在游戏对局过程中的各项相关操作、队友/敌人状态变化、对局全局参数变化等事件信息,尽可能快地实时推送出来,供本技术方案所涉及的离线和在线过程进行进一步处理。

离线过程

由于实时过程依赖于离线过程所产生的数据,因此我们首先阐述离线过程方案。

离线数据缓存和清洗

220426_图-总体过程图_01_离线数据缓存和清洗.png

实时流水数据在一整天内有峰值峰谷,不同的时间段内所需的计算资源不同。因此将实时数据尽快缓存下来,将离线过程的算力平摊到全天24小时中,可以大大降低云服务成本。

需要注意的是,实时流水数据在缓存时,需要记录该数据的发生时间,便于离线过程区分事件在整个对局过程中所处的时序位置。

一般而言,实时流水数据是较为全面的数据,在本方法中,针对实时流水数据需要进行初步清洗,这个清洗的过程需要完成以下工作:

  • 将多个不同的实时流水数据,按照对局+用户ID的维度进行区分,同一个 “对局-用户” 的数据聚合为一次完整的对局数据。
  • 聚合了对局数据之后,我们需要决定哪些对局是需要推送到后足进行记录的。记录的原则如下:

    • 该对局是一个较为成功的对局范例,能够对玩家产生有益的指导作用
    • 对局能够尽可能给玩家提供一个完整、序列化的行为模式参考
  • 流水数据一般比较详细和全面,在数据清洗中,需要清除方案中不需要的数据,节省后续的数据存储成本

上面提到,我们将 “对局-用户” 数据聚合成了一次完整的对局数据,针对这个对局数据,我们可以生成一个唯一的对局 ID,在后续的逻辑中用于标识和映射用。

生成用于关键帧提取的特征序列并计算关键帧

1. 序列矩阵生成

220426_图-总体过程图_02_计算关键帧.png

经过上一阶段清洗之后的离线对局数据包含了该玩家在这一个对局中各不同时间段内的具体行为,以及在各时间段和时间点中的对局状态信息。我们需要从这些信息中将所有需要用于判断玩家后续行为模式的数据抽取出来,并将这些数据进行量化。

比如玩家在不同时间点的血量、装备类别和数量、玩家坐标等等信息。这些信息均包含时间属性,因此我们可以以整个完整对局的开始时间点作为原点,组合量化后的所有数据,计算出在每一个时间片中玩家的完整状态信息。每一个已量化的状态信息就是一个列向量。

从对局开始到对局结束的所有时间片所对应的列向量,则从左到右拼接成为一个 m 行、n 列的矩阵,其中 m 等于列向量的维度,n 等于时间片的数量。假设第 n 个时间片列向量表示为 $V_{Kn}$ (V 代表 vector,下标 K 代表 keyframe,M 代表 matrix,下同)即:

$$
M_{K} = M_{m\times n} = \left( V_{K1}, V_{K2}, ..., V_{Kn} \right) =
\begin{bmatrix}
f_{K11} & f_{K12} & ... & f_{K1n} \\
f_{K21} & f_{K22} & ... & f_{K2n} \\
... & ... & ... & ... \\
f_{Km1} & f_{Km2} & ... & f_{Kmn}
\end{bmatrix}
$$

其中 $f_{Kmn}$ (f 代表 feature,下同)表示第 n 个时间片中的第 m 个特征值

2. 关键帧提取

获得了矩阵 $M_K$ 之后,我们可以进行关键帧提取计算。在计算之前,我们首先需要对矩阵进行一次加权计算,以调整不同特征的权重,因为不同特征的重要程度不同(W 表示 weighted,下同):

$$
M_{WK} = W_{K} \times M_{K} = \left( V_{WK1}, V_{WK2}, ..., V_{WKn} \right) =
\begin{bmatrix}
f_{WK11} & f_{WK12} & ... & f_{WK1n} \\
f_{WK21} & f_{WK22} & ... & f_{WK2n} \\
... & ... & ... & ... \\
f_{WKm1} & f_{WKm2} & ... & f_{WKmn}
\end{bmatrix}
$$

其中 $W_{K}$ 是一个 $m\times m$ 的对角矩阵,对角线上的值 $w_{ii}$ 表示每一个列向量 $V_{Kj}$ 中第 $i$ 个特征 $f_{Kij}$ 的权重值。经过上述矩阵乘法,则获得加权后的矩阵 $M_{WK}$

也可以在左乘权重之前,对矩阵以行为单位进行归一化处理。

获得了加权序列矩阵之后,我们从 $i = 2$ 开始,计算 $V_{WKi}$ 与 $V_{WKi-1}$ 两个列向量之间的余弦距离 $d_{WKi}$。将这些距离值以行向量表示,则为:

$$
D_{WK} =
\begin{bmatrix}
0, d_{WK2}, d_{WK3}, ..., d_{WKn}
\end{bmatrix}
$$

由于 $d_{WKi}$ 是标量,因此 $D_{WK}$ 可以表示为一个离散曲线,曲线上的每一个极大值点,就代表一个关键帧,这就是本技术方案中 “关键帧” 的数学含义。在后文的具体例子中我们会看到一个具体的关键帧计算结果。

获得关键帧之后,我们则可以以对局 ID 为唯一键,将关键帧提取结果存储到支持 KV 的缓存系统中,便于后续使用。

3. 关键帧的物理含义

关键帧的数学含义是标量序列中的每一个极大值点,对应到游戏中,则表示基于已选择的特征空间内,玩家行为、游戏进程、对局状态中发生较大变化的点。比如说玩家的位置发生了较大改变、玩家的行为轨迹发生转变、对局进入一个全新的阶段等等。关键帧往往揭示了玩家进行下一步决策的选择,或者是提示了玩家行动的方向。下文我们在举例的时候,可以更加明显地看出来。

生成用于匹配的特征向量序列

220426_图-总体过程图_03_用于匹配的序列.png

经过清洗之后的离线对局数据,除了用于生成用于关键帧提取的特征序列之外,另外一个作用就是用于生成用于匹配的特征向量序列。如何匹配这些特征向量,请参见后文,本小节仅说明生成该特征向量的方法。

类似地,我们需要从对局信息中提取出一系列的特征向量,生成该特征向量的方法与生成用于关键帧提取的特征序列的方法类似,也是生成一个序列矩阵,选取和量化特征向量的方法也类似。

选取这些特征之后,组成的特征向量,主要是为了表征某个对局下的玩家在每一个时间片中所处的状态,用于后续在实时过程中,将实时玩家的状态与某一个历史玩家进行匹配,从而匹配出一条关键帧序列。

生成的特征向量序列,是一个 $p\times q$ 大小的矩阵 $M_{Mp\times q}$(下标 M 代表 match,即 “匹配” 含义):

$$
M_{M} = M_{p\times q} = \left( V_{M1}, V_{M2}, ..., V_{Mq} \right) =
\begin{bmatrix}
f_{M11} & f_{M12} & ... & f_{M1q} \\
f_{M21} & f_{M22} & ... & f_{M2q} \\
... & ... & ... & ... \\
f_{Mp1} & f_{Mp2} & ... & f_{Mpq}
\end{bmatrix}
$$

其中 $V_{Mq}$ 表示时间片 q 所对应的特征向量,$f_{Mpq}$ 表示第 q 个时间片中的第 p 个特征值。 $M_{M}$ 针对时间片粒度的选择与 $M_{K}$ 可以相同也可以不同,完全取决于游戏时长、性能、游戏典型决策时间等要素进行折中选择。

得到 $M_{M}$ 之后,也需要按照实际需要,进行加权操作。但这一步就没有必要进行归一化了。加权之后的特征向量序列为:

$$
M_{WM} = W_{M}\times M_{M} =
\begin{bmatrix}
f_{WM11} & f_{WM12} & ... & f_{WM1q} \\
f_{WM21} & f_{WM22} & ... & f_{WM2q} \\
... & ... & ... & ... \\
f_{WMp1} & f_{WMp2} & ... & f_{WMpq}
\end{bmatrix}
$$

我们可以将每一个 $V_{M}$ 存入向量匹配引擎,同时在存入引擎的时候,也应将对局 ID 作为向量的属性一并存入,这样在匹配到向量之后,就可以与对局 ID 所对应的关键帧关联起来。

虽然量化特征的方法类似,但与生成用于关键帧提取的特征的选取原则上略有差异,这主要是两者的取向不同:

  1. 用于关键帧提取的序列中,偏向于记录玩家的具体行为,状态信息辅之,主要是用于区分关键帧所处的阶段用
  2. 用于匹配的特征向量中,更多地考量的是玩家所处的状态,因此对于一些的具体行为,则不一定要纳入特征向量中

举个例子,在用于关键帧提取的序列中,“历史玩家击杀了敌人” 这是一个具体行为,可能会影响关键帧的计算,并且进一步影响对在线玩家的提示逻辑,因此这个事件需要纳入并且用于关键帧计算中。但是在用于匹配的特征向量中,“当前玩家击杀了敌人” 作为行为而言,就可以无需纳入特征向量的取值考量,因为用于匹配的特征向量更加重视状态,而玩家当前具体做了什么操作,只是一个时间点的事件,而不是一个有较强持续特性的状态。

当然了,如果更换一个角度,也可能有一些属性是适合纳入用于匹配的特征向量,而无需纳入关键帧提取的,比如 “玩家截至指定时间为止击杀了5个敌人”,这就很明显是一个状态信息,可以考虑纳入用于匹配的特征向量序列中;但是这又很明显不是一种行为,充其量只能从侧面体现出玩家的风格、偏好、技术等等辅助信息,因此不会计入关键帧计算的范畴中

此外,用于特征向量的序列中,相比起用于关键帧计算的序列中,可能还包含一些纯辅助用的特殊特征值,这些特征值主要是根据向量匹配引擎所能提供的功能有关。关于这一点,请参见后文示例中的 “对局时间” 特征的选取原因。

实时(在线)过程

总体架构图的上半部分表示在线(实时)过程。本源上,离线和在线过程使用的数据源是一样的,都是玩家真实进行过的操作以及真实对局的其他一些状态变化,因此架构图中绘制的数据源均为 “实时流水数据”。

与离线过程不同,在线过程需要尽可能快地根据玩家信息或对局状态作出响应,因此在线过程并不会将实时流水数据进行缓存后再操作,而是获取了实时流水数据之后立刻进行数据清洗。

在线数据清洗

220426_图-总体过程图_04_在线数据清洗.png

数据清洗的目的是将对局状态的特征向量化,这个向量也即等于当前玩家、当前对局、当前时间片的特征向量,并且需要保证该特征向量生成的算法和结构与前文 ”生成用于匹配的特征向量序列“ 完全相同,并且对于时间片粒度的选择,也应当与 $M_{M}$ 完全相同。对于实时对局中,指定时间片 $t$ 所对应的玩家特征向量,我们记为 $V_{Rt}$,其中 R 表示 real-time,特征向量的维度与 $V_M$ 相同:

$$
V_{Rt} =
\begin{bmatrix}
f_{R1t} & f_{R2t} & ... & f_{Rpt}
\end{bmatrix}^{T}
$$

由于在线过程是事件驱动的逻辑,单一的事件无法完全表征玩家的对局状态,也不足以构建对局状态的特征向量,因此在在线过程中,也需要将之前计算出来的对局状态数据进行必要的缓存。在新的对局事件到达时,在线逻辑应当结合对局状态缓存和事件详情,综合计算出当前时间片的对局状态特征向量。

匹配历史对局

“当前关键帧是否有效” 和 “需要更新关键帧” 阶段的判定逻辑,与后续逻辑直接相关,因此我们暂时先跳过这个阶段,首先讲解后续逻辑之后再返回来说明。

220426_图-总体过程图_05_匹配历史对局.png

在获取了当前时间片的对局状态特征向量 $V_{Ri}$ 之后,在这一阶段,这个特征向量也应与离线的特征向量矩阵 $M_M$ 左乘上完全相同的权重:

$$
V_{WRt} = W_M\times V_{Rt} =
\begin{bmatrix}
f_{WR1t} & f_{WR2t} & ... & f_{WRpt}
\end{bmatrix}^{T}
$$

我们可以使用 $V_{WRt}$ 在向量匹配引擎中进行匹配。匹配的原则是向量相似度,根据特征向量的选取原则,主要考虑使用余弦相似度和欧几里得距离两种算法:

  • 当选取的特征向量具体值,包含玩家所处位置坐标信息,并且这些信息难以进行方向化的编码时(比如坐标 <10, 20> 与 <20, 40> 视为完全不同的值),则需要采取欧几里得距离
  • 当选取的特征向量包含的都是玩家的转向、战术取向等在数学意义上偏 “方向” 性的操作,那么可以采用余弦距离

匹配的时候还需要包含以下限制:

  • 仅匹配与当前时间片相同的历史特征向量,也即对于每一个 $t = i$ 的 $V_{WRi}$,限定匹配 $V_{WMi}$。
  • 其他与游戏相关的限制,比如说对于吃鸡类游戏,不同的对局可能采用了不用的地图,因此必须限定匹配范围,仅在该地图所对应的对局范围中进行匹配。

匹配虽然首先采纳向量相似度优先原则,但也可以多匹配数个或数十个特征向量,也即多匹配一些历史对局,然后从符合条件的结果中进行一定的随机选取,以提高随机性和多样性。

获取和选择关键帧

220426_图-总体过程图_06_获取和选择关键帧.png

1. 获取关键帧序列详情

匹配到了一个历史特征向量之后,由于在特征向量的附加属性中包含了对局 ID,因此可以循此对局 ID 在 KV 缓存中获取到指定对局 ID 中已经在离线阶段中计算出来的所有关键帧数据。

获取了关键帧数据后,下一步的逻辑就是从关键帧中选取出后续需要推送给实时玩家的一个或多个关键帧。

2. 关键帧序列的截断

由于关键帧是包含了时间片属性的,时间片的定义,指的是相对于对局开始的时间偏移量。我们获取历史对局的关键帧列表之后,我们可以把所有的关键帧按时间片排序之后,分为两类:

  • “过去” 了的关键帧:指的是时间片值小于或等于当前实时对局所处的时间片。这些关键帧类比到当前对局中,可以视为已经 “发生过了”,或者是 “以前” 的关键帧,这些关键帧对当前玩家来说没有推荐意义
  • “将来” 的关键帧:指的是时间片值大于当前实时对局所处的时间片。这种关键帧表征了未来可能发生的事件,或者是未来推荐玩家进行的操作,因此对玩家有推荐意义

选取关键帧的步骤,第一步是删除所有 “过去” 了的关键帧,因为这些关键帧在当前对局来说没有保存的意义。

第二步是要对 “将来” 的关键帧序列,做一个 “截断逻辑”。截断逻辑的效果,是去除 “将来” 某个时间片之后的关键帧。抛弃的原因如下:

关键帧序列所代表的一个历史玩家的行为,是一条已经发生了的、确定的时间线。但是当前对局中的用户,ta 将来的行为,是一个尚未发生的、不确定的时间线。我们基于一条历史的时间线给现实玩家进行推荐,那么玩家在未来可能会遇到以下几种需要重新推荐关键帧的情况:

  1. 不跟随推荐的关键帧进行实际行动——在这种情况下,我们需要考虑给玩家推荐另外一条关键帧序列
  2. 跟随推荐的关键帧进行下一步行动。那么当玩家进行或完成了下一个关键帧中所列举的行为时,可能会有以下两种情况

    • 2.1 - 玩家决定不跟随推荐的关键帧,而是进行别的选择,这个时候我们回到 1,也即需要考虑推荐另外一条关键帧序列
    • 2.2 - 玩家虽然继续按照关键帧行动,但是对局环境发生了较大变化,以至于当前关键帧已经不再适应对局的情况,需要考虑重新推荐

针对情况 1 和 2.1,这属于我们前文跳过的 “当前关键帧是否有效” 和 “需要更新关键帧” 阶段逻辑,请参见下文。而针对情况 2.2,我们需要考虑的是:在玩家这条已选择的历史路线(关键帧序列)行动的前提下,这条路线到什么时间片为止,依然是适合玩家当前状态的;而到了什么时间片之后,即便玩家按照推荐采取行动,我们也应该重新给玩家匹配一条的关键帧序列。这一标准的选择因不同的游戏而异,也因应不同的游戏特性会有不同的算法。当然,也有可能完全不做 “将来” 的关键帧的截断,而完完全全地将关键帧更新的逻辑放在 “当前关键帧是否有效” 和 “需要更新关键帧” 阶段中。

3. 关键帧缓存

获得了截断后的关键帧,我们也需要缓存下来,便于下一个事件到来时的在线过程的迭代。

关键帧序列的刷新

220426_图-总体过程图_07_关键帧刷新.png

说明了后续的关键帧匹配逻辑之后,我们回过头来说明这一步在之前被跳过的逻辑。这两步逻辑决定了当前事件到来时,主逻辑是否需要继续往下持续到 “向玩家展示” 的步骤。

从示意图上我们可以看到,当事件到来时,更新了对局状态缓存之后,我们首先判断一下当前关键帧序列是否有效,如果关键帧序列已经无效了,那么直接开始匹配历史对局。显然,如果当前对局从来没有匹配过关键帧(推荐关键帧缓存为空),这显然是需要刷新关键帧序列的。

而第二个判断是判断当前玩家是否已经完成当前已推荐的关键帧,如果未完成,那么当前事件逻辑结束,等待下一次事件驱动;如果已经完成了,那么进入到下一个阶段的检查。

下一阶段是检查是否前进到下一个关键帧。当玩家已经完成当前关键帧的时候进行该阶段检查。如果需要前进到下一个关键帧,那么就将当前推荐的关键帧视为 “过去” 了的关键帧进行截断之后,推荐紧接着的下一个关键帧,如果不需要,那么也一样,结束当前事件逻辑,等待下一次事件。

1. 判断当前关键帧序列有效

判断当前关键帧序列,主要是进行以下条件的判断:

  • 如果之前没有成功推荐过关键帧(关键帧缓存为空),那么显然需要刷新关键帧序列
  • 关键帧是带时间片属性的,如果玩家行动的进度相比关键帧而言明显落后时,需要考虑刷新关键帧序列

    • 比如从关键帧的角度,玩家应该在1分钟前就已经到达了某地,但是从目前玩家的状态来看,玩家最快也要3分钟才能到达,那么明显我们需要重新匹配
  • 如果玩家的行为明显偏离关键帧所推荐的行为模式,那么有必要重新匹配关键帧序列。

    • 比如推荐玩家往西北方向1000米移动,但是从推荐开始后,玩家往东南方向移动了1500米,这明显就偏离了关键帧的路线。
  • 如果发生了重大事件,导致之前匹配到的关键帧明显不合适,或者是大概率不再合适时,那么这个时候需要考虑刷新关键帧序列

    • 比如对于 MOBA 类游戏,之前推荐的关键帧是基于上、中、下路的塔防均完整的情况下进行的推荐,但现在其中一个塔被摧毁了,那么需要考虑重新匹配关键帧,采用新的战术。

2. 判断当前关键帧是否已实现

这一阶段主要是判断实时玩家的操作是否已经完成了关键帧中所指定的行为或目标,判断标准也因游戏而异。比如说玩家是否已经到达了目的地,或者是玩家已经将血量补满,或者是玩家已经击杀了指定目标等等。

3. 判断是否前进到下一个关键帧

当推荐关键帧序列中有下一个关键帧时,那么我们可以将当前关键帧截掉,并且将下一个关键帧推荐给用户。

如果没有下一个关键帧时,我们可以分两种情况来处理

  • 如果可遇见下一个重大事件节点非常近时,那么即便没有下一关键帧,我们依然无需重新推荐,而是等待下一个重大事件到来的时候,自然会触发新的匹配过程
  • 也可以简单删除关键帧缓存,触发下一个事件迭代过程时的关键帧匹配过程。

向玩家提示

当获得关键帧之后,我们即可以按照关键帧所代表的物理含义,或者说是所代表的在游戏中的操作,将关键帧的信息向玩家提示。提示方式不限,可以通过地图标注、语音提示、文字提示等等方式,给玩家进行提示。提示的具体手段不属于本专利的关注范围。


应用举例

实时流水数据

以和平精英为例,在进行整个系统方案的设计和开发之前,我们首先需要决定一个玩家在一个对局中的哪些信息是需要记录的。

原则上,尽可能多地包含本技术方案所需要的对局信息,包括但不限于玩家在对局的不同时刻下的以下信息:

  • 玩家在所处的位置
  • 玩家血量
  • 玩家行为
  • 玩家能观测到的对手的状态
  • 玩家装备信息等附属属性
  • 其他会影响玩家决策的游戏全局信息

按照上述的分类,我们可以采集玩家在不同时刻的以下信息:

信息说明举例
玩家位置玩家在游戏开始之后各时刻所在的地点的地图坐标
玩家血量玩家在各时刻的血量变化
玩家行为在和平精英中,需要统计的玩家行为包括: <br/>- 搜刮地面散落的物资,如果必要的话还需要统计玩家搜刮的一些关键物品信息,如枪支、子弹、血包等<br/>- 向敌人开火,如果命中敌人的话,还包括命中敌人与玩家本身的距离信息<br/>- 驾驶车辆(可通过上车/下车区分玩家是步行还是乘车移动)
对手状态玩家能观测到的对手的状态- 玩家视野中发现敌人,以及敌人的距离<br/>- 小地图上显示敌人脚步、枪声等信息。在和平精英中,这些信息还包含估算距离信息
玩家其他信息玩家装备信息等附属属性- 玩家装备的头盔级别(无则表示为0)、护甲级别(无则表示为0)<br/>- 玩家装备的突击步枪、冲锋枪、霰弹枪、机枪、手枪的数量<br/>- 玩家的子弹数量<br/>- 玩家的血包数量
其他信息其他会影响玩家决策的游戏全局信息- 对局的地图 ID<br/>- 不同时刻的信号区中心点和半径

当然,在实际操作中,统计信息也可以由简到繁,这也对应着一个方案从简到繁的迭代过程,因为该技术方案的另一个优点,就是不需要一开始就完成最终版,而可以随着游戏进展和社区/玩家反馈进行逐步迭代,在服务器性能与玩家体验之间逐步寻找平衡点。

还是以和平精英为例子,上述对局信息可以在早期进行简化,只记录以下信息(信息的减少并不会影响整体技术方案的完整性):

  • 对局的地图 ID
  • 玩家在游戏开始之后各时刻所在的地点的地图坐标
  • 不同时刻的信号区中心点和半径
  • 玩家装备的头盔级别(无则表示为0)、护甲级别(无则表示为0)
  • 玩家装备的突击步枪、冲锋枪、霰弹枪、机枪、手枪的数量
  • 小地图上显示敌人脚步、枪声等信息。这记录为遭遇敌人
  • 玩家搜刮地面散落的物资行为的时刻
  • 向敌人开火的时刻
  • 驾驶车辆的时刻

上述数据的格式可以以简单格式进行初步记录数据表如下:

时刻 (秒)事件类型事件值
0地图 ID01
60玩家位置(586029, 285103)
68搜刮物资
68拾取手枪1
65玩家位置(586140, 285072)
68搜刮物资
68拾取冲锋枪1
70搜刮物资
70拾取霰弹枪1
71搜刮物资
72拾取突击步枪1
72丢弃霰弹枪1
75遭遇敌人
75玩家位置(582271, 287956)
80遭遇敌人
81开火
81玩家位置(582395, 287003)
83开火
87玩家位置(582627, 287037)
95开火
100上车
112玩家位置(581480, 287175)
120信号区位置(285610, 673190)
122玩家位置(590850, 287164)
.........

从上述的记录中,我们可以看到玩家从游戏开始后一小段时间的对局信息为:

  • 玩家从坐标 (586029, 285103) 开始游戏
  • 玩家在游戏开始之后当即在当地进行物资的搜刮,并且装备手枪与突击步枪各一
  • 玩家在搜刮物资的时候,发现了敌人,并且开火攻击
  • 玩家在开局后100秒的时候乘坐载具,并且开始转移
  • 开局2分钟的时候,信号区刷新
  • ……

离线过程

样本数据选取

吃鸡类游戏的特点是每一局至少有一名玩家生存到最后;虽然和平精英也推出过复活/召回机制,让被击杀的玩家也有机会返回比赛,但是该对局的最终获胜者也依然大概率是所谓 “一命通关” 的,因此我们可以简单地按照以下原则进行选取:

  • 存活到最后的玩家
  • 玩家段位比较高

这种情况下,一方面这是一个成功 “吃鸡” 对局,是一个好的范例;另一方面,由于玩家在整个对局中没有死亡过,因此整个序列非常完整。

而对于在整个对局中频繁出现死亡/复活事件的游戏模式(如王者荣耀),则选取标准可以参考以下原则:

  • 玩家是当局 MVP
  • 玩家段位比较高
  • 玩家死亡次数较少

前两个原则很好理解,最后一个原则主要是考虑能够尽可能产生出时间较长的行为序列。可以参考后文 ”序列的截断和更新“。

生成对局特征序列

获得了历史对局记录之后,针对每一个对局,我们需要将列表化的数据,进行建模,转换为数字化的、便于计算机处理的数据。

为节省数据存储空间以及提高计算效率,我们可以对对局数据进行进一步的简化。如果认为不必要简化的话,这一步也可以跳过。

以上文完成了 “初步记录” 的数据表为例:首先我们可以发现,表中的玩家位置数据信息比较多,但是玩家的移动范围可能比较小。在这种情况下,我们可以降低玩家位置信息的精度,只取十万和万位。

其次,我们仅记录玩家的状态变化数据。这样,上文的数据表,我们可以进一步简化为:

时刻 (秒)事件类型事件值
0地图 ID01
60玩家位置(58, 38)
68搜刮物资
68装备变化手枪+1
68搜刮物资
68装备变化冲锋枪+1
70搜刮物资
70装备变化霰弹枪+1
71搜刮物资
72装备变化突击步枪+1, 霰弹枪-1
75遭遇敌人
80遭遇敌人
81开火
83开火
95开火
100上车
120信号区位置(28, 67)
122玩家位置(59, 28)

生成用于关键帧提取的特征序列

上文历史对局数据表的第二个作用是生成用于关键帧提取的特征序列。该序列具体作用,依然是在后面再做说明,本小节说明如何使用这些数据生成用于关键帧提取的特征序列

1. 选定需要量化的状态特征

用于关键帧提取的特征序列,以及用于匹配的特征序列两者,可以是相同的,也可以是根据实际需要有所不同。比如在匹配中,包含了玩家的装备信息;而在用于关键帧提取的特征序列中,可能并不需要这些信息,那么我们在生成序列的时候,可以不包含这些信息。

在大部分情况下,用于关键帧提取的特征序列,比用于匹配的特征序列维度要小,因为后者的目的是为了尽量找到一个与实时玩家当前对局状态接近的历史对局,为了提升匹配效果和准确度,用于匹配的特征应尽可能完备;而前者是直接服务于话术生成的,如果某些特征对生成AI话术没有帮助,那么可以忽略相应的特征值。比如在用于匹配的特征中,包含了玩家装备的枪支信息;但是在生成话术中,如果没有计划针对枪支装备进行说明,因此我们可以无需将枪支信息纳入特征序列中。

反之亦然,允许部分特征信息在关键帧特征序列中是存在的,而在状态特征序列中不存在,完全视乎特征而定。

2. 状态特征量化

用于关键帧提取的状态特征量化方法,与用于匹配的状态特征量化方法相同,这里就不再赘述。

举例说明我们可能只选取玩家位置、各信号区信息、遭遇敌人的时刻、交火的时刻等数据,那么我们可以将前文的一个对局,以 JSON 的形式进一步进行简化,如以下格式为例,这是某位玩家一次完整的对局信息:

{"drive":[476,479,482,485,488,490,493,497,500,502,505,508,511,513,516,518,522,525,528,532,535,537,541,545,548,551,553,557,559,561,564,566,570,572,576,579,791,794,798,800,804,806,810,813,816,819,823,826,830,833,836,838,842,845,849,851,853,856,859,862,865,868,871,874,878,882,884,887,890,893,896,898,901,904,907,910,913,915,919,921,923,926,930,934,937,941,944,947,951,955,959,962,965,968,971,975,978,980,983,986,990,992,995,998,1000,1003,1006,1009,1013,1015,1018,1021,1026,1067,1069,1073,1075,1078,1081,1085,1089,1092,1096,1099,1103,1106,1108,1111,1116,1119,1122,1125,1128,1131,1134,1136,1139,1141,1144,1147,1150,1153,1156,1158,1161,1164,1166,1169,1173,1176,1348,1352,1356,1359,1362,1364,1368,1371,1373,1376,1380,1382,1386,1389,1392,1396,1398,1400,1402,1406,1409,1411,1413,1426,1430,1434,1438,1440,1443,1447,1450,1452,1456,1458,1461,1463,1466,1469,1472,1474,1479,1482,1484,1486,1489],"shoot":[154,156,157,1034,1054,1189,1191,1292,1736,1739,1760,1792,1820,1830,1926,1950,1952,1963,1964,1974,1975,1979],"robbery":[0,0,0,0,99,100,121,122,123,124,128,132,134,135,145,145,146,148,168,172,176,177,178,179,180,181,181,182,186,187,207,216,239,244,245,257,258,261,271,271,272,280,282,292,295,307,321,322,326,1506,1750,1753,1756,1766,1773,1786,1803,1863,1870,1894],"skirmish":[153,153,154,155,156,181,186,199,200,202,302,304,304,305,305,306,666,666,668,669,672,675,676,676,713,713,713,714,715,716,717,720,722,837,839,1024,1024,1027,1028,1056,1062,1074,1155,1156,1157,1159,1171,1171,1172,1173,1173,1174,1185,1187,1197,1206,1211,1212,1213,1214,1215,1239,1246,1247,1248,1248,1249,1250,1252,1252,1252,1253,1254,1255,1256,1256,1257,1258,1259,1259,1260,1260,1261,1262,1263,1264,1265,1265,1266,1267,1269,1269,1270,1279,1279,1281,1281,1283,1283,1285,1286,1287,1288,1289,1290,1290,1291,1291,1292,1293,1302,1303,1304,1305,1306,1315,1316,1317,1318,1320,1321,1323,1324,1324,1326,1347,1348,1349,1350,1350,1350,1351,1351,1351,1352,1353,1353,1353,1354,1354,1403,1404,1406,1523,1526,1527,1682,1685,1702,1711,1717,1733,1747,1756,1758,1762,1773,1781,1809,1825,1828,1829,1829,1829,1829,1835,1836,1837,1838,1838,1838,1838,1838,1841,1841,1841,1842,1842,1876,1876,1877,1878,1878,1879,1880,1912,1926,1926,1960,1962,1962,1969,1970,1971,1972,1972,1973,1973,1973,1973,1974,1974,1980],"zones":[{"xy":[69,34],"t":94},{"xy":[70,34],"t":99},{"xy":[70,33],"t":114},{"xy":[71,32],"t":153},{"xy":[70,32],"t":160},{"xy":[71,32],"t":164},{"xy":[70,32],"t":168},{"xy":[71,32],"t":173},{"xy":[70,32],"t":236},{"xy":[71,32],"t":237},{"xy":[70,32],"t":243},{"xy":[70,31],"t":244},{"xy":[70,32],"t":245},{"xy":[70,31],"t":246},{"xy":[70,32],"t":254},{"xy":[70,31],"t":279},{"xy":[70,32],"t":283},{"xy":[70,31],"t":287},{"xy":[69,31],"t":306},{"xy":[69,32],"t":317},{"xy":[70,32],"t":328},{"xy":[70,33],"t":344},{"xy":[69,33],"t":351},{"xy":[69,34],"t":376},{"xy":[68,34],"t":390},{"xy":[68,35],"t":398},{"xy":[67,35],"t":416},{"xy":[66,34],"t":446},{"xy":[65,34],"t":475},{"xy":[66,35],"t":482},{"xy":[67,35],"t":488},{"xy":[68,35],"t":490},{"xy":[67,35],"t":505},{"xy":[67,34],"t":508},{"xy":[66,34],"t":511},{"xy":[65,34],"t":516},{"xy":[65,33],"t":518},{"xy":[64,34],"t":522},{"xy":[63,34],"t":525},{"xy":[62,34],"t":535},{"xy":[61,34],"t":545},{"xy":[60,34],"t":548},{"xy":[59,33],"t":551},{"xy":[58,33],"t":553},{"xy":[57,33],"t":557},{"xy":[57,32],"t":559},{"xy":[56,32],"t":561},{"xy":[55,32],"t":566},{"xy":[54,31],"t":570},{"xy":[53,31],"t":572},{"xy":[53,32],"t":804},{"xy":[52,32],"t":806},{"xy":[51,32],"t":810},{"xy":[50,32],"t":813},{"xy":[49,31],"t":816},{"xy":[48,31],"t":819},{"xy":[48,30],"t":823},{"xy":[47,29],"t":826},{"xy":[47,28],"t":830},{"xy":[48,28],"t":833},{"xy":[48,27],"t":836},{"xy":[49,27],"t":845},{"xy":[48,26],"t":882},{"xy":[47,26],"t":887},{"xy":[47,25],"t":890},{"xy":[47,24],"t":893},{"xy":[46,23],"t":898},{"xy":[45,23],"t":901},{"xy":[44,23],"t":904},{"xy":[43,22],"t":910},{"xy":[42,22],"t":913},{"xy":[41,21],"t":915},{"xy":[40,21],"t":919},{"xy":[39,21],"t":921},{"xy":[39,22],"t":923},{"xy":[38,21],"t":926},{"xy":[37,21],"t":930},{"xy":[35,21],"t":934},{"xy":[35,20],"t":937},{"xy":[34,20],"t":941},{"xy":[34,19],"t":944},{"xy":[34,18],"t":951},{"xy":[33,18],"t":962},{"xy":[32,18],"t":968},{"xy":[31,19],"t":971},{"xy":[30,19],"t":975},{"xy":[29,19],"t":978},{"xy":[29,20],"t":990},{"xy":[28,20],"t":992},{"xy":[27,21],"t":995},{"xy":[26,21],"t":998},{"xy":[25,21],"t":1000},{"xy":[24,22],"t":1013},{"xy":[23,22],"t":1015},{"xy":[23,23],"t":1018},{"xy":[24,23],"t":1026},{"xy":[24,24],"t":1032},{"xy":[25,24],"t":1089},{"xy":[25,25],"t":1092},{"xy":[26,25],"t":1096},{"xy":[27,25],"t":1099},{"xy":[27,26],"t":1111},{"xy":[27,27],"t":1116},{"xy":[26,27],"t":1119},{"xy":[26,28],"t":1122},{"xy":[26,29],"t":1125},{"xy":[27,30],"t":1128},{"xy":[27,31],"t":1131},{"xy":[26,32],"t":1134},{"xy":[26,33],"t":1136},{"xy":[25,33],"t":1139},{"xy":[25,34],"t":1141},{"xy":[24,34],"t":1144},{"xy":[23,35],"t":1147},{"xy":[22,35],"t":1150},{"xy":[21,36],"t":1153},{"xy":[21,37],"t":1156},{"xy":[20,37],"t":1158},{"xy":[20,36],"t":1161},{"xy":[19,36],"t":1164},{"xy":[18,35],"t":1169},{"xy":[17,34],"t":1173},{"xy":[16,34],"t":1176},{"xy":[16,33],"t":1181},{"xy":[16,34],"t":1386},{"xy":[17,34],"t":1389},{"xy":[17,35],"t":1392},{"xy":[17,36],"t":1396},{"xy":[17,37],"t":1402},{"xy":[18,37],"t":1406},{"xy":[18,38],"t":1409},{"xy":[19,38],"t":1413},{"xy":[19,39],"t":1419},{"xy":[19,40],"t":1450},{"xy":[20,40],"t":1452},{"xy":[21,40],"t":1456},{"xy":[21,39],"t":1458},{"xy":[22,39],"t":1461},{"xy":[23,40],"t":1469},{"xy":[22,40],"t":1474},{"xy":[22,39],"t":1484}],"safe_zones":[{"center":{"x":297799.5,"y":253278.5},"radius":254520,"sec":120},{"center":{"x":261669.39,"y":294625.9},"radius":165438,"sec":720},{"center":{"x":235103.33,"y":365767.5},"radius":82719,"sec":1060},{"center":{"x":237436.88,"y":364939.44},"radius":41359,"sec":1300},{"center":{"x":231202.84,"y":381825.4},"radius":20679,"sec":1480},{"center":{"x":226598.3,"y":390148.88},"radius":10339,"sec":1640},{"center":{"x":223320.33,"y":393406.94},"radius":5169,"sec":1760},{"center":{"x":223549.69,"y":394897.2},"radius":2584,"sec":1880}]}

这段数据包含了玩家从游戏开始之后的以下信息:

  • 有开车(或者坐车转移)事件的时间点列表
  • 有搜刮事件的时间点
  • 有开枪事件的时间点
  • 有小地图事件的时间点(代表遇到敌人)
  • 不同时间点玩家所处位置(精确到小格子)
  • 信号区中心、半径、时间信息

各时间点单位均为秒。

数据看起来非常抽象,我们可以将数据可视化。同样地,我们把开车、战斗、遭遇、搜刮事件次数,以及玩家所在位置的变化值,以10秒为单位,计算该10秒内所对应的列向量。列向量包含五个维度的数据,分别为:

  • 开车: 10秒内开车事件的个数
  • 战斗: 10秒内开火事件的个数
  • 遭遇: 10秒内遭遇敌人事件的个数
  • 搜刮: 10秒内搜刮行为的个数
  • 转移: 玩家位置发生变化的距离(除以10000)

由于该对局最后一个事件的时间为 1980 秒,也就是 33 分钟,除以 10,可以得到 199 个分片(包括 t=0 的情况),每个分片对应一个列向量。这 199 个列向量即组成一个 5行、199列的矩阵。

矩阵可视化如下,可以大致看出事件分布的情况:

220426_序列图.png
220426_序列图_图例.png

我们可以基于上述数据,构建玩家在每一个10秒分片中的状态数据。参见前文 “用于匹配的特征序列” 的 “状态特征量化” 小节,我们以类似的方法将上述六个信息,按顺序进行以下量化:

序号说明
1开车事件数
2搜刮事件数
3开枪事件数
4遭遇事件数
5玩家位置X坐标值,精确到100米
6玩家位置Y坐标值,精确到100米

其中信号区信息不列入特征中,仅做辅助参考。

由于该对局最后一个事件的时间为 1980 秒,也就是 33 分钟,除以 10,可以得到 199 个分片(包括 t=0 的情况),每个分片对应一个列向量。这 199 个列向量即组成一个 6行、199列的矩阵。

$$
M_{keyframe} =
\begin{bmatrix}
0 & 0 & ... & 1 & 0 & ... & 0 \\
0 & 0 & ... & 0 & 1 & ... & 10 \\
0 & 0 & ... & 0 & 1 & ... & 5 \\
0 & 0 & ... & 5 & 3 & ... & 0 \\
0 & 0 & ... & 69 & 34 & ... & 22 \\
0 & 0 & ... & 69 & 34 & ... & 39
\end{bmatrix}
$$

关键帧提取

1. 计算差分序列

在前文生成了 “关键帧提取的特征序列” 之后,我们可以知道,这个特征序列实际上是一个 6xn 的矩阵,其中 n = 对局总秒数/10+1。在本例中,我们使用余弦距离计算差分序列 $D_{WK}$,并进行 0-1 归一化后,作折线图大致如下:

220223_夹角.png

肉眼可见 $\vec d$ 中有数个明显的极大值,但仍存在不少抖动,不方便程序进行提取。过滤掉较低的夹角值(< 0.3),再作三阶高斯模糊后,得到较为平缓的曲线如下:

220223_高斯模糊后.png

得到明显的极大值点,这些极大值点代表了历史玩家行为发生较大变化的点,我们将这些时间点对应的 $\vec m_j$ 定义为关键帧。

将关键帧所处的位置,结合当前玩家的移动路径和信号圈变化,可以直观地在图上标记:

220223_brett_移动路径.png

  • 红色是玩家实际移动路径
  • 绿色是信号圈和信号圈中心移动
  • 白色是关键帧所在的点和推荐转移路径

2. 关键帧的物理含义

具体查询前文的矩阵 $M_{keyframe}$,计算 $d_{WKj}$ 与 $d_{WKj-1}$ 的特征差异,由于 $d_{WK}$ 的维度很低且每一维度的含义非常明确,因此我们可以很简单地将该关键帧所表征的事件转换为词组的组合,可以看到关键帧所表征的物理含义:

  0 -  1m30s (  90): 搜刮 | 移动,           location: 70_34
  1 -  2m40s ( 160): 搜刮 | 移动,           location: 70_32
  2 -  5m20s ( 320): 搜刮 | 移动,           location: 70_32
  3 -   8m0s ( 480): 上车 | 移动,           location: 67_35
  4 -  8m30s ( 510): 上车 | 移动,           location: 65_33
  5 -  9m30s ( 570): 上车 | 移动,           location: 53_31
  6 - 11m50s ( 710): 遭遇敌人,              location: 53_31
  7 - 13m40s ( 820): 下车 | 移动,           location: 47_29
  8 - 15m10s ( 910): 上车 | 移动,           location: 40_21
  9 -  17m0s (1020): 下车 | 遭遇敌人 | 移动, location: 24_23
 10 - 17m40s (1060): 上车 | 遭遇敌人,        location: 24_24
 11 - 19m30s (1170): 下车 | 遭遇敌人 | 移动, location: 16_34
 12 - 22m40s (1360): 上车,                 location: 16_33
 13 - 23m20s (1400): 上车 | 遭遇敌人 | 移动, location: 18_38
 14 - 24m50s (1490): 下车 | 移动,           location: 22_39
 15 -  29m0s (1740): 遭遇敌人,              location: 22_39
 16 - 30m20s (1820): 战斗 | 搜刮结束 | 遭遇敌人, location: 22_39

3. 生成用于匹配的特征向量序列

参照前文清洗离线数据的方案,我们也可以计算出用于匹配的特征,与关键帧所需的维度不同,为了能够更加准确地匹配到一个历史玩家,我们可以提取历史事件中的信息,同样划分为以10秒为单位的时间片,使用以下16个数据组成一个特征向量:

序号说明
1地图 ID
2对局开始时间 (秒数) / 10
3信号区中心X坐标值,精确到100米,即 0~79,0 代表没有信号区
4信号区中心Y坐标值,精确到100米,即 0~79,0 代表没有信号区
5玩家位置X坐标值,精确到100米,即 0~79
6玩家位置Y坐标值,精确到100米,即 0~79
7玩家在这10秒内是否驾驶车辆,0代表无,1代表有
8玩家装备的狙击枪数
9玩家装备的精确射手步枪数
10玩家装备的突击步枪数
11玩家装备的冲锋枪数
12玩家装备的霰弹枪数
13玩家装备的机枪数
14玩家装备的手枪数
15玩家装备的头盔级别,0~3,0代表无头盔,1~3分别代表一至三级头
16玩家装备的防弹衣级别,0~3,0代表无,1~3分别代表一至三级甲

从时刻 0 到结束时刻 1980 秒(上例),按照 10 秒一个时间片划分,总共 198 个特征向量。可将特征向量进行加权后写入特征匹配引擎中。权重的选取请参见下一节。

实时(在线)过程

在线数据清洗

在实时对局中,客户端在实现玩家对局逻辑功能的同时,也将对局情况打包成一个个的事件(参见 “实时流水数据”)发送到路径推荐后台服务器上。这些事件是增量事件(也即表示这些事件表征玩家状态变化,而不是玩家状态本身),后台服务器组合这些增量事件,可以计算并更新玩家和对局状态,从而计算获得玩家的实时特征,格式与离线算出的 “用于匹配的特征向量序列” 相同。同样地,计算出特征向量之后,我们应该将特征向量乘以权重。

在权重的选择中,我们需要考量特征中每一项的含义。其中注意到,“对局开始时间” 特征中,是一个非常关键的特征,我们应当尽量使得匹配命中的历史特征所表征的事件,尽可能与玩家当前对局时间接近(甚至相同)。为达到此目的,该特征向量应乘以一个相对于其他特征来说极大的常量,使得在进行特征向量匹配时,时间特征成为一个接近于过滤器的特征项。

特征向量匹配

生成实时对局的特征向量之后,我们通过向量匹配引擎,基于欧几里得距离找到与该实时对局的特征向量最为相似的向量列表。从匹配到的向量列表中,我们可以查找距离最短的一个向量,或者是基于一些简单的规则选择一个向量(比如在最相似的十个向量中随机提取一个,以避免趋同性)。

获得向量之后,这个向量则表示某一个最接近玩家当前状态的历史对局的某个时间点的状态。此时,我们可以查找这个向量的属性,从而找到向量的完整对局序列矩阵 $M_{K}$。根据不同的向量匹配引擎,对局 ID 可以是作为属性保存在向量匹配引擎里的,也可能是需要从其他数据库中重新提取的信息。

获取和选择关键帧

前文离线过程中,我们已经计算出了关键帧,因此在实时逻辑中,我们只需要从数据库中提取并直接推送即可。

我们按照玩家所处的时间点往后找到第一个关键帧,然后给玩家进行推荐。

关键帧序列的截断

针对和平精英来说,信号区的变化是影响玩家下一步决策的关键因素。因此我们获取了历史对局之后,我们仅取下一个刷圈时间到来之前的关键帧列表。当玩家当前已无关键帧可推荐时,可以推荐玩家随机应变。

判断玩家是否完成了当前关键帧,只需要比对关键帧的位置,和时间即可。

关键帧序列的刷新

刷新关键帧序列,等价于重新匹配历史对局。刷新的时机有三个:

  • 新信号区刷新
  • 玩家来不及赶到关键帧位置
  • 玩家偏离推荐路线

其中信号区刷新逻辑很好判断。

而玩家来不及赶到关键帧位置,则可以简单判断当前对局的时间,是否已经到了后一个关键帧的时间。

至于最后一种:玩家偏离推荐路线的判断,我们使用了一个简单的判断逻辑:

我们在二维地图上取两个点:第一个是关键帧所在的目标点 $P_{target}$ ,第二个是给玩家发出推荐话术时,玩家所处的位置点 $P_{from}$ 。我们期望的是玩家从 $P_{from}$ 前往 $P_{target}$。我们要判断出,玩家是否正在做这个动作。在实际上,玩家几乎不可能直线往目标点跑动,那么我们要如何判断玩家的行为是否符合我们的预期,又希望能够尽量减少计算量呢?我们采用的是一个简单的椭圆法。椭圆的定义是,在一个二维平面上,所有距离两个指定的、不重合的点的距离之和为一个常量的点的集合。判断玩家所处的位置点,与 $P_{from}$ 和 $P_{target}$ 的距离之和是否等于某个常数即可。如果这个距离和小于指定的常数,那就说明玩家仍处于这两个点和该常数所表征的椭圆范围内,那么我们就视玩家仍然在前往目标点。该常数可以简单取 $P_{from}$ 和 $P_{target}$ 之间距离的两倍,也可以调整。

向玩家提示

在我们的应用中,使用的是语音+地图标点的方法,如下图截图所示:

220928_落地标点.jpg

可以看到,在小地图中标记了一个点,这个点并不是玩家手动标记的,而是系统自动标上的。图示中标上的特征向量为:

$$
V_{Rt} =
\begin{bmatrix}
49 & 18 & 42 & 39 & 36 & 42 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0
\end{bmatrix}^{T}
$$

可以看到,实时对局的玩家当前还处于很早期(刚跳伞落地)、没有武器的状态.

配合语音播报即可以实现给玩家的推荐:推荐往西北方向去搜刮物资。小几已经在小地图上给你标记好啦,要小心附近的敌人哦。其中 “小几” 是 “腾讯游戏知几” 虚拟人的昵称。

当然这些语言可以搭配一些人工标注功能实现更加丰富和精准的推荐,比如说图上的地点是和平精英 “科学之轮” 活动中的 “光子鸡通讯站” 边上的房子,这可以作为一个参数填充语音模板。比如语音模板为:推荐往${direction}的${location_name}去搜刮物资,小几已经在小地图上给你标记好啦,要小心附近的敌人哦,然后传入以下两个参数:

  • direction: 西北方向
  • location_name: 通讯站边上的房子

完整的推荐语音为:推荐往西北方向的通讯站边上的房子去搜刮物资,小几已经在小地图上给你标记好啦,要小心附近的敌人哦


技术方案产生的有益效果

该技术方案提供了一种应用场景:在玩家(特别是新手玩家)进行游戏时,对玩家的下一步行为进行推荐和指导,并且指出执行该行为时需要注意的其他属性。一方面,能够对玩家进行机器化的指导,另一方面又避免了用 “上帝视角” 基于对局的真实信息,对游戏作出破坏平衡性的播报以实现 “外挂” 的效果。


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可,原文发布于腾讯云开发者社区,也是本人的博客。欢迎转载,但请注明出处。

原文标题: 《分享一个专利: 一种在吃鸡游戏中模仿历史胜利玩家打法并对当前玩家进行打法推荐的方案》

发布日期: 2026-02-26

原文链接: https://cloud.tencent.com/developer/article/2631559

CC BY-NC-SA 4.0 DEED.png

标签: none

添加新评论