• 产品手册
  • 编辑器功能手册
  • 触发器相关
  • 案例

介绍

本章将对触发器所介绍的 ECA 脚本功能进行进一步说明。可配合几个 ECA 使用情境例如:NPC 对话装备限制行为树等案例进一步提升您的脚本能力。

Y3 编辑器也支持使用 Lua 编程,框架代码及 API 文档请访问:https://github.com/y3-editor/y3-lualib

NPC 对话

概览

NPC 即 Non-Player Character 非玩家控制角色。你将在本章学习如何开启或结束与 NPC 的对话。

界面内容读取与展示

第一部分是 UI 内容的读取和显示。你需要前往物体编辑器,给 NPC 添加一个自定义属性“Talk”存储谈话内容。

C1

我们还需要在界面编辑器设计聊天 UI. 它应该包括背景,NPC 头像,文本,和退出按钮

C2

聊天 UI 需要能读取并显示正确信息。你可以用创建一个带参数函数,参数是“NPC”单位。 在函数里创建两个本地变量存储 NPC 头像的 ID 和我们先前预存的自定义属性“Talk”。然后再创建界面组件“Icon”和“Talk text”并读取这两个变量。

这样当你具体调用与某个 NPC 的对话函数时,玩家 UI 就会出现其头像和文字内容。

C3

统一 NPC 对话入口

第二部分是 NPC 聊天的统一入口。

当玩家试图靠近并点击任何 NPC 时,程序将会得到通知,并把各种触发方式统一为同一个入口,通过自定义的函数,把触发事件的玩家角色想要对话的 NPC这两个单位类型变量作为参数传递给函数中去。

通过调用函数,获取到对应 NPC 的一系列信息参数,并把它们写入到聊天界面,显示给玩家。

同时我需要一个退出按钮,当玩家按下按钮时,对应的标记真值被修正为‘true’,即会执行界面的关闭。

C4

然后,在函数Z02.一次 NPC 对话事件中,还需要使用循环计时器每秒检测玩家单位与 NPC 之间的距离,如果检测到距离大于 500,也会触发对应的结构,使得界面结构停止运行并销毁,在统一出入口这件事上,和前面的统一入口的实现逻辑是类似的,我们需要把不同的退出方式进行单独处理,并最后通知在同一个出口上。

C5

为了让所有的处理结构都处在同一个函数下,这里我们选择一种相对比较消耗性能的方式,以让玩家点击事件导致的界面关闭不会和显示界面的功能分离成两个模块。

我们使用一个真值类型的Swith对开关的状态进行储存,默认状态下,Swich 为 false,当玩家点击退出按钮时,我们在事件的动作中设置 Swich 为 True。

视角回到函数内部,界面在启动时会循环检测 Swich 的状态,如果检测到玩家点击了离开,则会运行对应的退出功能。

C6

功能差不多完成了,在核心的结构实现以后,需要对其他触达玩家操作的‘传感器’进行落地实现。

玩家角色进入一个 NPC 的对话区域时,他们会调用函数Z02.一次 NPC 对话事件

当玩家在地图上选择一个单位是,首先比较该单位,如果检测到其是一个 NPC,则调用函数Z02.一次 NPC 对话事件,同时需要让玩家重新选中玩家角色

C7

为了防止玩家能够远程和 NPC 对话,以让游戏效果更拟真,只有当玩家选择的 NPC 距离玩家角色的距离小于 400 时,才会启动函数,否则向其提示距离过远。

C8

装备限制

概览

对装备的品类或者数量做限制是 RPG 类游戏中一个常见的机制,例如一个玩家觉得只能携带一把武器和一件装甲,比如在 FPS 游戏中,玩家同时只能持有一把枪,想要切换到另一把枪就会把当前持有的武器移除掉,这些实际上的游戏效果都是装备限制

装备属性和类型的储存

当玩家拾取装备时,如果玩家单位上没有该类型的装备,则会直接进行装备。

如果已经装备有同类型的装备,那么此时拾取的装备将被直接丢弃。

同时这个丢弃会被定义为系统丢弃,也即是不会触发对应的玩家丢弃流程。

第一部分是对数据进行存储,需要现在物编编辑器中定义每个装备的名称,图标等等,以及使用自定义值功能为其添加一个type项以表示其装备类型。

EE1

拾取物品

当玩家拾取一个物品时,创建两个局部变量来接收这个单位和对应的物品。

EE2

获取物品的‘type’属性,来判断当前单位是否具有此类型的物品。

EE3

如果判定为具有该类型物品,则需要将物品系统丢弃。

通过在物品的自定义值真值‘SystemDiscard’标记以让丢弃系统过滤掉本次操作。

如果判定为不具有该类型物品,则玩家成功获得物品,并将对应的‘type’类型标记为有。

并且储存当前持有的本物品,以方便后续根据物品变量显示对应的特效效果。

EE4

特效生成

想要为一个已有的事件附加另一个效果,可以使用自定义事件功能。

我们可以通过发送自定义事件和接收自定义事件来传输信息,在获得新装备后,可以发送一个自定义事件,通过事件来激活对应的特效功能。

对于武器和铠甲,我们使用了两种格式来对特效进行创建。

武器的特效是直接绑定在角色手部的拖尾,所以我们只需要直接将对应的特效路径以自定义值的形式保存在物品上,就可以用同一个函数启用所有的武器特效。

护甲的特效则复杂一些,它们由多个特效组合盘旋而成,所以我们需要一个函数内部对这些特效进行不间断的生成和销毁。

EE5

然后,我们根据自定义事件传递的物品和 type 参数进行分敛,以通知到不同类型的特效创建函数,执行对应的效果即可。

EE6

特效销毁

在函数库中,我们需要新建三个函数,分别是武器,盔甲 1 和盔甲 2.

对于武器特效功能,在单位的节点添加一个拖尾特效,启动一个循环计时器,检测当前玩家持有的武器是否是该装备,如果已经丢弃,则将其移除,并对所有结构进行销毁。

EE7

盔甲 1 的特效显示逻辑和武器特效类似,不同的是,盔甲的特效将在每秒计时器到时后生成,并在两秒后将其销毁。

E8-E9

为了显示盔甲 2 的特殊效果,我们为单位创建了两个投射物,并使用局部变量进行储存。

为了实现球体的环绕效果,我们需要定义一个角度,这个角度将会每帧进行递增,以实现帧叠加以后的球体圆周运动。

EE10

我们使用一个帧计时器(0.03 秒)来检测装备是否还在被装备,如果它被丢弃,对所有结构进行销毁。

E11-E12

当程序一直持续循环运行,球体的角度会在每一帧减去 6,并根据一个固定的距离,以角色位置为圆心,通过极坐标系确定本帧投射物该在的位置。

EE13

丢弃物品

与拾取物品类似,当一个单位丢弃物品时,先使用局部变量对参数进行缓存。

如果该物品被检测为系统丢弃,则跳过整个流程,当做无事发生,因为一个未被允许装备的物品是不会运行对应的装备流程的。

如果判断物品是被玩家手动抛弃,可以先获取其对应的 type,根据 type 对单位的对应自定义值项进行清空,这项数据的清空同时也会被一直检测本数据的特效结构得知,从而进行特效的销毁。

E14-E15

行为树

概览

行为树是实现游戏 AI 的一种方式。游戏中的 AI 让单位通过模拟真实玩家的行动而拥有“智能”行为。例如 PVE 的敌人,BOSS 的战斗模式,玩家召唤物等。作为玩法的重要组成部分,增强玩家的参与感。行为树即是用树状结构分支使 AI 能根据条件执行不同行为(如走、跑、跳、攻击)。

初始化

使用行为树之前,先初始化。们可以建立全局变量储存经常使用的玩家角色和需要被控制的敌方单位。

建立全局变量“unitPlayer”储存玩家单位。建立另一个全局变量单位组以储存所有敌方单位。

为了更沉浸的游玩体验,你可以设置“镜头 - 跟随单位”以让玩家的视角能始终跟随在自己的英雄上。

可以通过动作:“玩家-选择单位”,让玩家选中自己的主控单位。

S3-1

循环检查所有地方单位并对他们应用行为树

每个单位都需要具有自己的智能行为,并且不同类型的敌人面对相同的情况会采取不同的措施,所以我们需要先遍历全局中的敌方单位组,并根据其单位类型进行分敛。

当游戏开始经过‘1 秒’时,也即是游戏初始化结束,游戏中的 AI 就应当开始行动了,我们所有的敌人已经事先储存在全局的单位组中,所以只需要对这个单位组进行循环的遍历即可。

也即是说,存在两个循环,每‘N’固定事件间隔遍历一次单位组,每次遍历单位组时对每个单位都应用其对应的行为树判断一次。

在本次的示例中,我们存在两种不同的 AI,它们分别是‘近卫’和‘治疗者’,根据单位的单位类型即可分辨出应该对其运行哪个行为树。

S3-2

在过滤了单位类型以后,通过条件判断语句将单位输送给不同的自定义事件,并设置好参数,也即是将需要执行行为树的那个单位作为参数传递。

在 Y3 编辑器中,你可以在自定义事件中先新建一个事件,之后就可以使用动作发出,并在其他触发器的事件出选择接收。

在新建事件时,根据需要添加需要传递的参数,参数必须是变量,但不支持数组。

比如行为树是针对某单位执行,那么就需要将单位作为参数传递出去。

之后在执行行为树的触发器内,将事件设置为自定义事件,并接收参数,即完成了对单位的分敛和分发。

s3-3

s3-4

而独立的‘守卫’行为树,或者‘治疗者’行为树则只对传输过来的这个单位执行效果,判断这个单位的状态和周围的状态,并根据状态决定出其接下来将要执行的动作,截止到此处,您已经完成了该功能的结构框架。

‘守卫 AI’的实现

守卫通过自定义事件:"AI_Guard"来接收需要执行效果的单位。

新建一个单位类型局部变量,命名为unit,用它来储存这个从上一个触发器传递过来的单位。

守卫会先判断是否有敌人能够威胁到治疗者,如果有,守卫会优先保护治疗者。

新建一个实数类型局部变量,命名为distance,用于储存敌人和治疗者之间的距离。

通过判断这个数据是否小于 300 来判断治疗者是否安全。

s3-5

如果距离小于 300,则给单位unit向储存在全局变量中的玩家角色unitPlayer一个攻击指令。

如果治疗者是安全的,那么继续判定守卫单位自身的 HP 是否是安全的,检测unit的 HP 是否低于其 MaxHP 的 50%。

如果守卫单位本身的 HP 不安全,则给单位unit向全局变量中的治疗者unitHealer一个移动指令。

如果守卫单位本身是安全的,则判断 AI 警戒区域(在本示例中为一个圆形区域)内是否有敌人。

如果有敌人,则攻击这个敌人。

如果没有,守卫则会在警戒区域内随机移动巡逻。

s3-6

‘治疗者 AI’的实现

治疗者通过自定义事件:“AI_Healer”来接收需要执行效果的单位。

与守卫 AI 类似的,先对需要执行的单位以局部变量进行储存。

先判断治疗者自己的 HP 是否等于其 MaxHP,它会优先保障自身的安全。

在治疗者本身是满 HP 状态下,它会治疗自己的其他友军。

在这里,我们需要新建一个‘获取当前 HP 最低的友军’函数来查找当前最需要治疗的友军。

S3-7

在函数一开始,我们需要先新建一些局部变量,一个实数类型,另一个是单位类型。

这两个变量分别用于表示 HP 的百分比以及我们想要的那个单位,单位初始为‘空’,表示没有单位需要治疗。

通过遍历所有单位的单位组来获取 HP 最低的单位。

我们比较每一个单位的当前 HP 百分比,并和我们的实数变量作比较。

如果如果它比储存的实数更小,说明它比之前储存的单位更需要治疗,我们把新的百分比和单位都写入到变量中。

当遍历完全以后,我们就获取到了单位组中 HP 百分比最低的那一个单位。

把储存有这个单位的变量作为返回值,我们就完成了一个可以筛选出最需要治疗单位的函数。

S3-8

在创建好函数以后,我们就可以使用这个函数获取我们想要的单位,当然别忘了为它新建一个单位类型的局部变量用来储存它。

s3-9

接下来,我们确定获取到的这个单位是否是一个空单位。

如果是,说明当前没有单位需要治疗,那么它将充当一个‘法师’的身份。

判断自己身边是否有敌人,如果有,就使用飞弹攻击它。

如果周围不存在敌人,那么治疗师会保持在警戒区域的中心不动,以方便巡逻的守卫保护自己。

s3-10

如果我们获取到的返回值单位并不是一个空单位,说明当前有需要治疗的单位,治疗师就会停止当前的动作,并为这个单位治疗。

为了让这种动作更具真实感,我们可以播放对应的动画动作,并为治疗效果添加一个特效。

因为在这里,我们并非使用‘技能’来实现治疗,所以为了让治疗者不会边攻击边治疗,我们需要为其发布一个停止命令。

S3-11

排行榜使用教程

1.点击主界面【细节】-【存档设置】打开存档槽设置

Rank1

2.点击加号创建一个新的存档槽并修改存档槽数据类型为整数

Rank2

3.此时右侧面板就会出现排行榜的配置,选择确认计入排行榜,这个存档槽位就变成了排行榜存档

  • 可以选择排行榜排序规则,有降序与升序两种规则

  • 也可以设置排行榜最大人数,

Rank3

4.使用存档相关 eca 对玩家进行存储之后即可查看排名

Rank4

  • 使用该 eca 可以让整数类型存档保持只增效果

Rank5

  • 使用该 eca 可以获取排行榜上所有玩家的存档值,配合界面 eca 让排行榜在界面显示

Tips

  • 排行榜数据在游戏启动后不会再刷新

实战范例:使用双槽位实现周排行榜

  • 创建两个整数类型存档槽位:A 与 B

  • 首先用 A 存储所有玩家的排行榜数据

  • 当一周结束时,通过时间戳判断,使玩家在第二周的排行数据存储在 B 榜

  • 第二周开始时通过 ECA 清除玩家 A 槽位数据

  • 通过在作者之家清除 A 榜数据(权限需找运营申请),以备第三周存储玩家排行数据

  • Rank6

  • 当第二周结束时,通过时间戳判断,使玩家在第三周数据存储在 A 榜并清除 B 槽位数据,循环往复

特效可见性

1.特效默认为全玩家可见

2.可通过 ECA 控制特效可见性

EV1 EV2

a.可以通过“玩家”列表操作,实现对某个玩家的特效显示/屏蔽

EV3

b.也可以通过“玩家组”列表操作,实现对某个玩家组的特效显示/屏蔽,如某玩家的所有同盟玩家,某玩家的所有敌对玩家等等

本地多开定位不同步说明

说明

用户由于使用 ECA 不当,频繁出现游戏逻辑不同步的问题,影响游戏正常运行。用户可在本地多开测试游戏时借助调试方法进行调试。 关于容易引发游戏逻辑不同步的常见问题,可以参考这篇文档多人联机同步机制,详细列举了常见导致逻辑不同步的问题,可以结合案例说明进行理解,对自己项目中的问题进行排查修正。

本地配置不同步日志环境

1.打开【通用设置-调试】,进行本地多开相关配置

BD01

打开本地多开同步检测后,会在本地测试出现不同步后弹窗提示,并提供不同步日志文件以供用户定位不同步问题

2.还可以借助 Lua 文件配置更详细的不同步日志。

BD02

3..打开地图路径下 Script 文件夹下的 main.lua 文件,在 lua 文件中配置不同步日志相关 API,例如:

BD03

详细接口介绍见下文【不同步日志 Lua 配置 API】,可根据自己项目状况考虑具体性能影响选择合适的配置。

4.配置完毕后,本地多开运行游戏。如遇游戏逻辑不同步,弹窗提示并在本地生成不同步日志

BD04 BD05

定位不同步问题

1.打开下载的不同步日志文件夹,使用外部文本对比工具进行对比(推荐使用 BeyondCompare)

不同步日志文件中包含的是出现不同步情况的帧信息,通过对比玩家日志差异,可以大致定位到问题所在。

举例说明

1.地图中在 main.lua 文件中进行配置

GameAPI.api_set_enable_detail_snapshot(true);
GameAPI.api_set_enable_timer_snapshot(true);

2.本地多开测试游戏,遇到不同步状况,编辑器弹窗提示不同步并提供不同步日志以供定位问题。

3.打开不同步日志,查看逐帧日志,了解不同步帧运行情况,辅助定位问题

BD06

4.将多名玩家日志成对拖放到 beyondcompare 中进行对比,对差异部分附近信息重点分析

5.下面对比截图是玩家 2 比玩家 1 多了一个 timer,各字段含义见图

BD07

通过对不同步信息附近的帧信息进行理解,可大致定位问题为某个客户端上多了一个循环计时器,在项目中进行查找可能的问题所在。

BD08

不同步日志 Lua 配置 API

API描述参数返回值
api_set_enable_detail_snapshot开启/关闭不同步详细日志的总开关,默认关闭。(这个是总开关,关了这个之后别的设置接口都不生效了了,但性能最好)enable:是否开启,类型为 bool,默认为 false
api_set_snapshot_traceback_level设置某些日志的堆栈记录详细等级,默认为 0;0 代表不记录堆栈; 1 代表仅记录最近一层堆栈;2 代表完整堆栈(带压缩,数据量小但有一点性能开销); 3 代表完整日志(不压缩,数据量稍大);越完整的堆栈记录越便于定位不同步产生点,但是性能消耗会增高level:堆栈记录等级,类型为 Int32,默认值为 0
api_set_enable_timer_snapshot开启/关闭 timer 不同步检测日志。默认关闭。开启后可以检测出哪里多创建了 ECA 计时器,但计时器不一致并不一定代表着实际游戏内容不同步(比如计时器回调里只做表现层修改就是安全的)enable:是否开启,类型为 bool,默认值为 false
api_set_enable_eca_snapshot开启/关闭 ECA 不同步检测日志。默认关闭,开销较高。可通过参数过滤掉一些安全的 API 以防止误报,例如创建特效、UI 操作等enable:是否开启,类型为 bool,默认值为 false;string:开启结果
filter_mode:过滤模式。类型为 Int32,默认为 1;1: 剔除模式,不记录 filter_set 中指定的 api;0 :包含模式,仅记录 filter_set 中指定的 api;
filter_set:过滤集合,类型为 table,默认为"client_only","client_possible";可传入想要剔除/包含的 API(取决于上个参数),如"client_only", "client_possible","GameAPI:print_to_dialog","GameAPI:get_function_return_value。"
client_only 和 client_possible 为官方确认安全/较安全的 API 集合,即在使用得当的情况下即使调用次数不一致也不会影响游戏核心逻辑,通常可以将其加入剔除集合中以避免误报
api_set_detail_snapshot_enable_tag设置不同步详细日志级别。越详细越利于定位不同步产生点,但性能消耗会增高tag:用于控制开启哪些日志的 mask。类型为 UInt64。;0xFFFFFFFF 全部开启,默认开启 16+32。;1 运动器 tick;2 运动器碰撞检测;4 寻路回调;8 寻路坐标更新;16 血量变化;32 坐标瞬变
add_detail_log记录自定义日志,用于定位不同步log:日志内容,类型为 stringbool,值恒定为 true