介绍
本章将对触发器所介绍的ECA脚本功能进行进一步说明。可配合几个ECA使用情境例如:NPC对话,装备限制和行为树等案例进一步提升您的脚本能力。
Y3编辑器也支持使用Lua编程,框架代码及API文档请访问:https://github.com/y3-editor/y3-lualib
NPC对话
概览
NPC即Non-Player Character非玩家控制角色。你将在本章学习如何开启或结束与NPC的对话。
界面内容读取与展示
第一部分是UI内容的读取和显示。你需要前往物体编辑器,给NPC添加一个自定义属性“Talk”存储谈话内容。
我们还需要在界面编辑器设计聊天UI. 它应该包括背景,NPC头像,文本,和退出按钮。
聊天UI需要能读取并显示正确信息。你可以用创建一个带参数的函数,参数是“NPC”单位。 在函数里创建两个本地变量存储NPC头像的ID和我们先前预存的自定义属性“Talk”。然后再创建界面组件“Icon”和“Talk text”并读取这两个变量。
这样当你具体调用与某个NPC的对话函数时,玩家UI就会出现其头像和文字内容。
统一NPC对话入口
第二部分是NPC聊天的统一入口。
当玩家试图靠近并点击任何NPC时,程序将会得到通知,并把各种触发方式统一为同一个入口,通过自定义的函数,把触发事件的玩家角色与想要对话的NPC这两个单位类型变量作为参数传递给函数中去。
通过调用函数,获取到对应NPC的一系列信息参数,并把它们写入到聊天界面,显示给玩家。
同时我需要一个退出按钮,当玩家按下按钮时,对应的标记真值被修正为‘true’,即会执行界面的关闭。
然后,在函数Z02.一次NPC对话事件中,还需要使用循环计时器每秒检测玩家单位与NPC之间的距离,如果检测到距离大于500,也会触发对应的结构,使得界面结构停止运行并销毁,在统一出入口这件事上,和前面的统一入口的实现逻辑是类似的,我们需要把不同的退出方式进行单独处理,并最后通知在同一个出口上。
为了让所有的处理结构都处在同一个函数下,这里我们选择一种相对比较消耗性能的方式,以让玩家点击事件导致的界面关闭不会和显示界面的功能分离成两个模块。
我们使用一个真值类型的Swith对开关的状态进行储存,默认状态下,Swich为false,当玩家点击退出按钮时,我们在事件的动作中设置Swich为True。
视角回到函数内部,界面在启动时会循环检测Swich的状态,如果检测到玩家点击了离开,则会运行对应的退出功能。
功能差不多完成了,在核心的结构实现以后,需要对其他触达玩家操作的‘传感器’进行落地实现。
当玩家角色进入一个NPC的对话区域时,他们会调用函数Z02.一次NPC对话事件。
当玩家在地图上选择一个单位是,首先比较该单位,如果检测到其是一个NPC,则调用函数Z02.一次NPC对话事件,同时需要让玩家重新选中玩家角色
为了防止玩家能够远程和NPC对话,以让游戏效果更拟真,只有当玩家选择的NPC距离玩家角色的距离小于400时,才会启动函数,否则向其提示距离过远。
装备限制
概览
对装备的品类或者数量做限制是RPG类游戏中一个常见的机制,例如一个玩家觉得只能携带一把武器和一件装甲,比如在FPS游戏中,玩家同时只能持有一把枪,想要切换到另一把枪就会把当前持有的武器移除掉,这些实际上的游戏效果都是装备限制。
装备属性和类型的储存
当玩家拾取装备时,如果玩家单位上没有该类型的装备,则会直接进行装备。
如果已经装备有同类型的装备,那么此时拾取的装备将被直接丢弃。
同时这个丢弃会被定义为系统丢弃,也即是不会触发对应的玩家丢弃流程。
第一部分是对数据进行存储,需要现在物编编辑器中定义每个装备的名称,图标等等,以及使用自定义值功能为其添加一个type项以表示其装备类型。
拾取物品
当玩家拾取一个物品时,创建两个局部变量来接收这个单位和对应的物品。
获取物品的‘type’属性,来判断当前单位是否具有此类型的物品。
如果判定为具有该类型物品,则需要将物品系统丢弃。
通过在物品的自定义值真值‘SystemDiscard’标记以让丢弃系统过滤掉本次操作。
如果判定为不具有该类型物品,则玩家成功获得物品,并将对应的‘type’类型标记为有。
并且储存当前持有的本物品,以方便后续根据物品变量显示对应的特效效果。
特效生成
想要为一个已有的事件附加另一个效果,可以使用自定义事件功能。
我们可以通过发送自定义事件和接收自定义事件来传输信息,在获得新装备后,可以发送一个自定义事件,通过事件来激活对应的特效功能。
对于武器和铠甲,我们使用了两种格式来对特效进行创建。
武器的特效是直接绑定在角色手部的拖尾,所以我们只需要直接将对应的特效路径以自定义值的形式保存在物品上,就可以用同一个函数启用所有的武器特效。
护甲的特效则复杂一些,它们由多个特效组合盘旋而成,所以我们需要一个函数内部对这些特效进行不间断的生成和销毁。
然后,我们根据自定义事件传递的物品和type参数进行分敛,以通知到不同类型的特效创建函数,执行对应的效果即可。
特效销毁
在函数库中,我们需要新建三个函数,分别是武器,盔甲1和盔甲2.
对于武器特效功能,在单位的节点添加一个拖尾特效,启动一个循环计时器,检测当前玩家持有的武器是否是该装备,如果已经丢弃,则将其移除,并对所有结构进行销毁。
盔甲1的特效显示逻辑和武器特效类似,不同的是,盔甲的特效将在每秒计时器到时后生成,并在两秒后将其销毁。
为了显示盔甲2的特殊效果,我们为单位创建了两个投射物,并使用局部变量进行储存。
为了实现球体的环绕效果,我们需要定义一个角度,这个角度将会每帧进行递增,以实现帧叠加以后的球体圆周运动。
我们使用一个帧计时器(0.03秒)来检测装备是否还在被装备,如果它被丢弃,对所有结构进行销毁。
当程序一直持续循环运行,球体的角度会在每一帧减去6,并根据一个固定的距离,以角色位置为圆心,通过极坐标系确定本帧投射物该在的位置。
丢弃物品
与拾取物品类似,当一个单位丢弃物品时,先使用局部变量对参数进行缓存。
如果该物品被检测为系统丢弃,则跳过整个流程,当做无事发生,因为一个未被允许装备的物品是不会运行对应的装备流程的。
如果判断物品是被玩家手动抛弃,可以先获取其对应的type,根据type对单位的对应自定义值项进行清空,这项数据的清空同时也会被一直检测本数据的特效结构得知,从而进行特效的销毁。
行为树
概览
行为树是实现游戏AI的一种方式。游戏中的AI让单位通过模拟真实玩家的行动而拥有“智能”行为。例如PVE的敌人,BOSS的战斗模式,玩家召唤物等。作为玩法的重要组成部分,增强玩家的参与感。行为树即是用树状结构分支使AI能根据条件执行不同行为(如走、跑、跳、攻击)。
初始化
使用行为树之前,先初始化。们可以建立全局变量储存经常使用的玩家角色和需要被控制的敌方单位。
建立全局变量“unitPlayer”储存玩家单位。建立另一个全局变量单位组以储存所有敌方单位。
为了更沉浸的游玩体验,你可以设置“镜头 - 跟随单位”以让玩家的视角能始终跟随在自己的英雄上。
可以通过动作:“玩家-选择单位”,让玩家选中自己的主控单位。
循环检查所有地方单位并对他们应用行为树
每个单位都需要具有自己的智能行为,并且不同类型的敌人面对相同的情况会采取不同的措施,所以我们需要先遍历全局中的敌方单位组,并根据其单位类型进行分敛。
当游戏开始经过‘1秒’时,也即是游戏初始化结束,游戏中的AI就应当开始行动了,我们所有的敌人已经事先储存在全局的单位组中,所以只需要对这个单位组进行循环的遍历即可。
也即是说,存在两个循环,每‘N’固定事件间隔遍历一次单位组,每次遍历单位组时对每个单位都应用其对应的行为树判断一次。
在本次的示例中,我们存在两种不同的AI,它们分别是‘近卫’和‘治疗者’,根据单位的单位类型即可分辨出应该对其运行哪个行为树。
在过滤了单位类型以后,通过条件判断语句将单位输送给不同的自定义事件,并设置好参数,也即是将需要执行行为树的那个单位作为参数传递。
在Y3编辑器中,你可以在自定义事件中先新建一个事件,之后就可以使用动作发出,并在其他触发器的事件出选择接收。
在新建事件时,根据需要添加需要传递的参数,参数必须是变量,但不支持数组。
比如行为树是针对某单位执行,那么就需要将单位作为参数传递出去。
之后在执行行为树的触发器内,将事件设置为自定义事件,并接收参数,即完成了对单位的分敛和分发。
而独立的‘守卫’行为树,或者‘治疗者’行为树则只对传输过来的这个单位执行效果,判断这个单位的状态和周围的状态,并根据状态决定出其接下来将要执行的动作,截止到此处,您已经完成了该功能的结构框架。
‘守卫AI’的实现
守卫通过自定义事件:"AI_Guard"来接收需要执行效果的单位。
新建一个单位类型局部变量,命名为unit,用它来储存这个从上一个触发器传递过来的单位。
守卫会先判断是否有敌人能够威胁到治疗者,如果有,守卫会优先保护治疗者。
新建一个实数类型局部变量,命名为distance,用于储存敌人和治疗者之间的距离。
通过判断这个数据是否小于300来判断治疗者是否安全。
如果距离小于300,则给单位unit向储存在全局变量中的玩家角色unitPlayer一个攻击指令。
如果治疗者是安全的,那么继续判定守卫单位自身的HP是否是安全的,检测unit的HP是否低于其MaxHP的50%。
如果守卫单位本身的HP不安全,则给单位unit向全局变量中的治疗者unitHealer一个移动指令。
如果守卫单位本身是安全的,则判断AI警戒区域(在本示例中为一个圆形区域)内是否有敌人。
如果有敌人,则攻击这个敌人。
如果没有,守卫则会在警戒区域内随机移动巡逻。
‘治疗者AI’的实现
治疗者通过自定义事件:“AI_Healer”来接收需要执行效果的单位。
与守卫AI类似的,先对需要执行的单位以局部变量进行储存。
先判断治疗者自己的HP是否等于其MaxHP,它会优先保障自身的安全。
在治疗者本身是满HP状态下,它会治疗自己的其他友军。
在这里,我们需要新建一个‘获取当前HP最低的友军’函数来查找当前最需要治疗的友军。
在函数一开始,我们需要先新建一些局部变量,一个实数类型,另一个是单位类型。
这两个变量分别用于表示HP的百分比以及我们想要的那个单位,单位初始为‘空’,表示没有单位需要治疗。
通过遍历所有单位的单位组来获取HP最低的单位。
我们比较每一个单位的当前HP百分比,并和我们的实数变量作比较。
如果如果它比储存的实数更小,说明它比之前储存的单位更需要治疗,我们把新的百分比和单位都写入到变量中。
当遍历完全以后,我们就获取到了单位组中HP百分比最低的那一个单位。
把储存有这个单位的变量作为返回值,我们就完成了一个可以筛选出最需要治疗单位的函数。
在创建好函数以后,我们就可以使用这个函数获取我们想要的单位,当然别忘了为它新建一个单位类型的局部变量用来储存它。
接下来,我们确定获取到的这个单位是否是一个空单位。
如果是,说明当前没有单位需要治疗,那么它将充当一个‘法师’的身份。
判断自己身边是否有敌人,如果有,就使用飞弹攻击它。
如果周围不存在敌人,那么治疗师会保持在警戒区域的中心不动,以方便巡逻的守卫保护自己。
如果我们获取到的返回值单位并不是一个空单位,说明当前有需要治疗的单位,治疗师就会停止当前的动作,并为这个单位治疗。
为了让这种动作更具真实感,我们可以播放对应的动画动作,并为治疗效果添加一个特效。
因为在这里,我们并非使用‘技能’来实现治疗,所以为了让治疗者不会边攻击边治疗,我们需要为其发布一个停止命令。
排行榜使用教程
1.点击主界面【细节】-【存档设置】打开存档槽设置
2.点击加号创建一个新的存档槽并修改存档槽数据类型为整数
3.此时右侧面板就会出现排行榜的配置,选择确认计入排行榜,这个存档槽位就变成了排行榜存档
-
可以选择排行榜排序规则,有降序与升序两种规则
-
也可以设置排行榜最大人数,
4.使用存档相关eca对玩家进行存储之后即可查看排名
- 使用该eca可以让整数类型存档保持只增效果
- 使用该eca可以获取排行榜上所有玩家的存档值,配合界面eca让排行榜在界面显示
Tips
- 排行榜数据在游戏启动后不会再刷新
实战范例:使用双槽位实现周排行榜
-
创建两个整数类型存档槽位:A与B
-
首先用A存储所有玩家的排行榜数据
-
当一周结束时,通过时间戳判断,使玩家在第二周的排行数据存储在B榜
-
第二周开始时通过ECA清除玩家A槽位数据
-
通过在作者之家清除A榜数据(权限需找运营申请),以备第三周存储玩家排行数据
-
-
当第二周结束时,通过时间戳判断,使玩家在第三周数据存储在A榜并清除B槽位数据,循环往复
特效可见性
1.特效默认为全玩家可见
2.可通过ECA控制特效可见性
a.可以通过“玩家”列表操作,实现对某个玩家的特效显示/屏蔽
b.也可以通过“玩家组”列表操作,实现对某个玩家组的特效显示/屏蔽,如某玩家的所有同盟玩家,某玩家的所有敌对玩家等等
本地多开定位不同步说明
说明
用户由于使用ECA不当,频繁出现游戏逻辑不同步的问题,影响游戏正常运行。用户可在本地多开测试游戏时借助调试方法进行调试。 关于容易引发游戏逻辑不同步的常见问题,可以参考这篇文档多人联机同步机制,详细列举了常见导致逻辑不同步的问题,可以结合案例说明进行理解,对自己项目中的问题进行排查修正。
本地配置不同步日志环境
1.打开【通用设置-调试】,进行本地多开相关配置
打开本地多开同步检测后,会在本地测试出现不同步后弹窗提示,并提供不同步日志文件以供用户定位不同步问题
2.还可以借助Lua文件配置更详细的不同步日志。
3..打开地图路径下Script文件夹下的main.lua文件,在lua文件中配置不同步日志相关API,例如:
详细接口介绍见下文【不同步日志Lua配置API】,可根据自己项目状况考虑具体性能影响选择合适的配置。
4.配置完毕后,本地多开运行游戏。如遇游戏逻辑不同步,弹窗提示并在本地生成不同步日志
定位不同步问题
1.打开下载的不同步日志文件夹,使用外部文本对比工具进行对比(推荐使用BeyondCompare)
不同步日志文件中包含的是出现不同步情况的帧信息,通过对比玩家日志差异,可以大致定位到问题所在。
举例说明
1.地图中在main.lua文件中进行配置
GameAPI.api_set_enable_detail_snapshot(true);
GameAPI.api_set_enable_timer_snapshot(true);
2.本地多开测试游戏,遇到不同步状况,编辑器弹窗提示不同步并提供不同步日志以供定位问题。
3.打开不同步日志,查看逐帧日志,了解不同步帧运行情况,辅助定位问题
4.将多名玩家日志成对拖放到beyondcompare中进行对比,对差异部分附近信息重点分析
5.下面对比截图是玩家2比玩家1多了一个timer,各字段含义见图
通过对不同步信息附近的帧信息进行理解,可大致定位问题为某个客户端上多了一个循环计时器,在项目中进行查找可能的问题所在。
不同步日志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:日志内容,类型为string | bool,值恒定为true |