文章目录
- 一、触发器简介
- 1.1 触发器界面
- 1.2 ECA语句编辑及快捷键
- 1.3 参数设置
- 1.4 变量设置
- 1.5 实体触发器
- 1.6 函数库与触发器复用
- 二、触发器的多层结构
- 2.1 子触发器(在游戏内对新的事件进行注册)
- 2.2 触发器变量作用域
- 2.3 复合条件
- 2.4 循环
- 2.5 计时器
- 2.6 单位组
- 2.7 玩家组
- 2.8 运动器
- 三、案例
- 3.1 NPC对话
- 3.2 装备限制
- 3.2.1 装备拾取
- 3.2.2 物品丢弃
- 3.3 特效
- 3.3.1 特效生成
- 3.3.2 特效销毁
- 3.4 行为树
- 3.5 使用排行榜
- 3.6 特效可见性
- 3.7 不同步问题
- 3.7.1 本地配置不同步日志环境
- 3.7.2 线上游戏不同步定位
- 3.7.2 定位不同步问题
- 3.7.3 游戏不同步的原因
- Y3编辑器文档1:编辑器简介及菜单栏详解(文件、编辑、窗口、细节、调试)
- Y3编辑器文档2:场景编辑(地形编辑、物件放置)
- Y3编辑器文档3:物体编辑器
- Y3编辑器文档4:触发器1(对话、装备、特效、行为树、排行榜、不同步问题)
一、触发器简介
参考官方文档《触发器》
触发器是Y3编辑器中实现游戏逻辑的核心组件,它通过“事件-条件-动作”(Event-Condition-Action,简称ECA)的模式来实现各种效果。
- 事件(Event):触发器的导火索,当设定的事件发生时触发器才会执行。
- 条件(Condition):触发执行必须满足的条件。
- 动作(Action):触发执行的结果。
1.1 触发器界面
通过主界面顶部功能栏或使用快捷键F4
可进入触发器。进入触发器窗口后,你可以添加或删除文件夹和触发器,并使用搜索功能快速找到你想找的触发器;还可以对选定的触发器进行以下操作: 复制, 剪切, 粘贴, 删除, 重命名, 禁用, 和转化成Lua代码。
右键单击触发器并选择禁用使其无效。单击启用以重新激活无效的触发器
你可以在变量管理与事件管理中提前写入常用的变量和事件,以便在游戏中随时调用,从而减少开发项目的记忆成本和后期维护成本。比如可以提前设置事件并添加事件中需要的参数,然后在触发器-自定义事件中进行调用。
在主界面-编辑-通用设置-编辑设置中勾选“缩略图滚动条”,可以显示ECA缩略图滚动条。左键点击可以定位到想要的行数,长按左键可以拖动滚动条。错误、点选与搜索结果也会以色块形式显示到普通滚动条上。缩略图可以收起,收起后需要重新在“主界面-编辑-通用设置-编辑设置”打开。
1.2 ECA语句编辑及快捷键
点击事件/条件/动作后面的"+"为当前部分新建语句,也可以在空白处点击鼠标右键,新建事件,条件,动作,子触发器与注释。如果想要更改已有的事件,条件或者动作,双击对应语句即可弹出选项窗口,选择新的内容后即可进行覆盖。
功能 | 快捷键 | 说明 |
---|---|---|
新建触发器 | Ctrl + T | |
新生子触发器 | Ctrl + Q | |
新建事件 | Ctrl + E | |
新建条件 | Ctrl + D | |
新建动作 | Ctrl + R | |
新建注释 | Ctrl + N | |
ECA注释开关 | Ctrl + M | 将当前语句转为注释 |
设为无效 | Ctrl + W | 使触发器文件或eca条目无效(被跳过) |
条目下移 | Ctrl + ↓ | 快速将当前选中的eca下移一行 |
条目上移 | Ctrl + ↑ | 快速将当前选中的eca上移一行 |
新建收纳盒 | Ctrl + P | |
添加至收纳盒 | Ctrl + O | 添加当前选中eca到收纳盒 |
变量引用查看 | A | |
全局元素引用查看 | S | |
跳转到函数 | D |
功能 | 快捷键 | 说明 |
---|---|---|
查看上一个触发器 | Ctrl + ← | |
查看下一个触发器 | Ctrl + → | |
搜索框 | Ctrl + F | |
跳转到物编器 | Ctrl + J | |
切换到触发器 | Ctrl + 1 | |
切换到函数库 | Ctrl + 2 | |
切换到触发总览 | Ctrl + 3 | |
跳转 | F3 | |
复制参数 | Shift + C | 右键单击参数使用。复制时,整个参数的值和类型都将被复制到剪贴板上 |
粘贴参数 | Shift + V | 右键单击参数使用,复制的参数类型与目标参数类型必须相同 |
1.3 参数设置
未赋值的参数会以红色显示,已赋值的参数会以填入的数据类型分配颜色,可选的参数则在次级菜单中选用,额外的选项会以蓝色显示,文本信息说明了当前触发器语句的含义。需要指定所有的必选参数,本条动作以及所在的触发器才能正常运行。
参数有以下几种类型:
- 预设对象:当参数类型为可以放置在场景中的对象时( 单位,装饰,物体,特效等),可以直接选择该预设对象作为参数。
- 数值:你可以直接将一个固定的数字、文本或布尔值赋给一个参数
- 变量:包括全局变量和局部变量
- 函数:一组官方打包的获取数据的动作,可以直接提供对应的返回值。
- 通用:用于设置 自定义值 和调用表格编辑器。
当您单击转换为变量时,会在当前语句之前创建一个变量赋值语句,将当前语句结果赋值给一个变量。转换为变量后,您可以单击取消变量转换以恢复到之前的状态。
1.4 变量设置
你可以通过触发器界面设置变量,或通过ECA选择界面设置变量。
变量在创建时,需要声明其类型,部分变量类型必须设置默认值。
- 非对象型变量:字符串、浮点数、整数、布尔值
- 对象型变量:单位、单位组、玩家组、单位命令
变量根据作用域类型分为全局变量和局部变量。
- 全局变量:可以在本项目内的任何触发器中读取和写入。
- 局部变量:只能在当前触发器中生效。
数组是相同数据类型的集合,数组中的每个元素都在其基本值上添加了一个索引,使我们在数组中更容易找到和访问它们。整数、单位、实体、单位名字、物品实体等变量都可以设置为数组。比如下面代码设为按下H键之后,在Boss周围召唤6个黑暗守卫(使用数组guards
实现)。
变量只能用于储存数据的媒介,并不会实时更新,任何对变量的操作都需要使用赋值动作进行实现。请不要忘记在触发器中编写更新变量的语句。例如,在游戏运行的时候设置变量“玩家人数”为6,5分钟后有一位玩家退出游戏,此时游戏中玩家人数为5,如果没有编写变量更新语句,那么变量“玩家人数”依然为6。你需要编写能表达“在玩家退出游戏的时候玩家人数减1”的逻辑。
1.5 实体触发器
在物体编辑器->触发器中可进行实体的逻辑编写,即为实体触发器,其优势在于它可以更简单地将逻辑和目标物体绑定,而与其他摆件无关,所以可以减少性能的消耗。实体触发器的编辑与上述触发器操作一致,一个摆件也可以设置多条触发。
1.6 函数库与触发器复用
你可以将自己常用的触发器语句/功能模块添加到函数库,接着就可以在触发器中找到该函数语句进行重用。与触发器的操作逻辑相同,在左侧创建函数后,在右侧编辑功能库函数。
函数描述定义了函数在操作列表中的显示方式。 函数描述包括以下内容:
- 名称:要生成的触发器语句的名称。
- 说明:要生成的触发器语句的内容和格式。
- 注意事项:当前函数的说明。
函数体包括以下内容:
- 参数:定义了该函数的输入数据,可设置参数类型,如定时器、单位和整数。
- 返回值:定义了函数执行后返回的结果(输出数据)。你可以点击 "+"来设置返回值的名称和类型,以方便触发器的后续调用。
- 动作:显示了函数的具体行动逻辑
在触发器中选择想要复制的触发进行复制,这条触发器的源代码内容会写入到粘贴板中,此时可以在记事本等文本文件中进行本地粘贴保存,或者在新项目的触发器界面选择右键粘贴(Ctrl+V)。
二、触发器的多层结构
2.1 子触发器(在游戏内对新的事件进行注册)
触发器具有一个事件,这个事件是通过一个注册来实现的,即对一个事件进行注册,从而让系统在对应的事件触发时能继续按照条件与动作顺序执行。全局触发器是在游戏初始化时就注册完成的,如果要在游戏内对新的事件进行注册,实现更复杂的游戏逻辑,就需要通过子触发器来实现。
子触发器是在触发器的“动作”中创建的,用于在游戏进程中对新的事件进行注册,注册后会独立运行(按ECA的逻辑执行)。子触发器是一种变量类型,可以返回这个触发器实例,以方便在使用完毕后销毁它(如果需要的话)。因为是游戏进程中注册,所以我们可以把变量中的数据传递给子触发器进行注册。
下面是一个简单的示例,使用子触发器将单位(关羽)移动到相应的点。
2.2 触发器变量作用域
作用域是指变量在程序中被定义后可以被访问的范围,它决定了变量的可见性和生命周期。全局变量可以在任何触发器及其子触发器的范围内有效,局部变量可以在当前触发器及其子触发器的范围内有效,子触发器局部变量只能在当前子触发器的范围内有效。作用域的存在可以防止变量命名冲突,提供封装和隔离性,提高程序的可靠性和可维护性。
局部作用域中的变量优先级更高。比如在子触发器中定义了一个局部变量,这个变量与全局变量或者外部作用域中的变量(另外一个触发器中定义的变量)同名时,子触发器内部只能访问和修改这个局部变量,其余两个同名变量被隐藏,无法直接访问或修改。所以说,作用域机制提供了一定程度的隔离,可以防止子触发器意外修改外部变量,从而避免潜在的错误和不可预测的行为,保持代码的清晰和可维护性。
根据作用域划分,触发器中的变量可以分为全局变量、函数局部变量、触发局部变量、子触发器局部变量和组变量。根据类型划分,可以分为对象型变量(如单位、单位组、玩家组、单位命令)和非对象型变量(如字符串、实数、整数、布尔值)。
子触发器内部对于赋值运算(非单位类型局部变量的修改)只会在子触发器内部生效,不会影响外部;但是对对象型变量(如单位组、玩家组等)的操作以及对单位属性的修改会同步影响到外部。这种设计有助于在保持局部变量隔离性的同时,允许对共享资源进行必要的修改(单位对象是一个全局可访问的对象)。
下面使用全局字符串、全局单位类型、局部字符串、局部单位类型;全局单位组、局部单位组这6个变量进行测试。
-
先打印每个类型的初始值
[Info]: 全局字符串AAA [Info]: 全局单位类型UnitName:134219749张飞) [Info]: 局部字符串BBB [Info]: 局部单位类型UnitName:134274912(关羽)[Info]: 全局单位组中单位数量1 [Info]: 局部单位组中单位数量1
-
通过子触发器修改这4个变量的值并打印
[Info]: 全局字符串CCC [Info]: 全局单位类型UnitName:134220068(大乔) [Info]: 局部字符串DDD [Info]: 局部单位类型UnitName:134257382(小乔)[Info]: 全局单位组中单位数量2 [Info]: 局部单位组中单位数量2
-
通过其他子触发器再次打印变量的值:
[Info): 全局字符串CCC [Info): 全局单位类型UnitName:134220068(大乔) [Info]: 局部字符串BBB [Info): 局部单位类型UnitName:134274912(关羽)[Info]: 全局单位组中单位数量2 [Info]: 局部单位组中单位数量2
2.3 复合条件
if
流程语句的结构如下,条件可以是取反(not语句)、所有条件成立(and语句)、任意条件语句(or语句)或者所有条件不成立。
所有条件成立:A和B都等于1 ,则单位会移动到指定的点
任意条件成立:A和B有一个等于1,则单位会移动到指定的点。
所有条件不成立:整数A 和整数B 都不等于 1,则单位会移动到指定的点。
如果A等于1,则单位会移动到指定的点A;否则,会移动到点B。
2.4 循环
循环是指重复执行某项动作。在Y3编辑器中,循环有三种类型:
-
指定次数或指定整数变量重复执行:这是最常用的循环触发方式,例如,设定NPC敲门三次,动作会重复三次后自动停止。
-
条件成立,重复执行:在条件满足的情况下,进行无限次的循环,直到条件不再满足。例如,如果条件是门关着就敲门,那么敲门动作会一直重复,直到门开了才会停止。
-
遍历数组变量循环:根据数组的索引数字,对数组中的每个单位重复执行动作。例如,有一个不同门的列表,NPC会按照顺序敲门,直到所有门都被敲过。
下面是《触发器2:循环》中的例子,通过第一种循环实现在BOSS周围出现6个次元种子的效果。
2.5 计时器
计时器就好比一个沙漏,在计时结束后开始执行某项动作。我们可以用计时器来处理和时间有关的游戏逻辑。在编辑器中,计时器一共有三种:
- 运行单次计时器:计时器运行一次后开始执行动作。例如,倒数三秒后约翰的门就会打开。
- 运行循环计时器:计时器循环运行,每次运行后都会执行动作。例如,每隔一秒约翰的门就会打开一次。
- 运行固定次数的计时器:计时器中间停留固定时间,固定次数的计时器。例如,每隔一秒约翰的门就会打开一次,一共打开三次。
下面是《触发器:计时器》中,使用计时器实现一只只次元入侵者每隔2s在“裂缝”处爬出的效果。
注意:你可以选择
True
并立即执行动作,或者选择False
在2秒后执行第一个动作。
2.6 单位组
单位组是由一个或多个单位组成的集合,在单位组中,我们能同时对所有的单位或选择某些单位发布命令。合理利用单位组,我们可以在游戏制作中节省大量的时间。下面是在《触发器:单位组》中,使用单位组实现主人公发送特技“次元入侵”,入侵者瞬间死伤殆尽的效果。
2.7 玩家组
玩家组是一个包含一个或多个玩家的集合。您可以直接对玩家组中的所有玩家执行操作。例如,下面的语句表示在点A创建一个关羽单位,并将该单位分配给玩家组BB中的每个玩家,单位面向180°角度。
2.8 运动器
运动器可以为单位或者特效等添加运动效果,比如沿着直线运动或追踪某一个单位等,是制作技能、特效等场景中十分常用的功能。
在Y3编辑器中,运动器分为追踪运动器、曲线运动器、直线运动器、环绕运动器(对单位)、环绕运动器(对点)。您可以设置运动器的 方向、距离、初始速度和加速度,以及一些可选参数。
下面是《触发器:运动器》中,使用环绕运动器(对单位),实现电球的环绕和爆炸效果。
- 在物编器中创建一个自定义投射物
"Electric ball"
(球形蓝色电流攻击)作为环绕的球体,设置缩放0.4倍大小,勾选循环播放,让特效一直播放。 - 设置自定义投射物
"Explosion"
(蓝色电流冲击波,0.3倍缩放)作为结束时的爆炸效果。 - 新建电球投射物:创建一个投射物变量
"Electric ball"
,投射物类型选择预设的投射物"Electric ball"
,选择在点创建,创建的位置点,选择探险者所在的点。 - 新建运动器:选择运动器->环绕运动器对单位,两个参数分别设为变量
"Electric ball"
和探险者,环绕半径为200
,角速度为100
。在可选参数中,添加环绕时间5s
和环绕高度100
。 - 新建投射物爆破动作:在运动完成动作列表下添加动作,在探险者所在的点创建投射物
"Explosion"
,持续时间1s
。 - 优化游戏内存:销毁投射物
"Electric ball"
并移除运动器(为单位移除运动器,单位选择探险者)。
三、案例
3.1 NPC对话
-
添加“Talk”属性:通过物体编辑器给NPC添加自定义属性“Talk”来存储对话内容。
-
UI设计:通过界面编辑器设计聊天UI,这包括背景、NPC头像、文本和退出按钮。
3. 显示聊天内容:创建一个显示函数,参数为“NPC”单位。在函数里创建两个本地变量存储NPC头像的ID和自定义属性“Talk”。然后再创建界面组件“Icon”和“Talk text”并读取这两个变量。这样当你具体调用与某个NPC的对话函数时,玩家UI就会出现其头像和文字内容。
- 统一NPC对话入口:
- 当玩家靠近并点击NPC时,程序通过自定义函数统一处理触发事件,传递玩家角色和NPC作为参数。
- 函数获取对应NPC的一系列信息参数,并显示在聊天界面。
- 设计退出按钮,当玩家点击时,设置一个布尔值标记为‘true’以关闭界面。另外使用循环计时器检测玩家与NPC的距离,如果超过500也会关闭界面。和前面的统一入口的实现逻辑是类似的,我们需要把不同的退出方式进行单独处理,并最后通知在同一个出口上。
-
功能优化:为了保持界面关闭和显示功能在同一模块下,选择一种相对消耗性能的方式,即使用一个布尔值类型的变量
Switch
来控制界面的开启和关闭状态。界面在启动时会循环检测Swich的状态,默认状态下,Swich为false
,当玩家点击退出按钮时,我们在事件的动作中设置Swich为True
。
-
整体实现:当玩家在地图上选择一个单位时,如果检测到是NPC,则调用函数Z02.一次NPC对话事件,并让玩家重新选中玩家角色。只有当玩家与NPC的距离小于400时,才启动对话功能,否则提示距离过远。
3.2 装备限制
对装备的品类或者数量做限制是RPG类游戏中一个常见的机制,例如一个玩家觉得只能携带一把武器和一件装甲,比如在FPS游戏中,玩家同时只能持有一把枪,想要切换到另一把枪就会把当前持有的武器移除掉,这些实际上的游戏效果都是装备限制。
3.2.1 装备拾取
当玩家拾取装备时,如果玩家单位上没有该类型的装备,则会直接进行装备。否则拾取的装备将被直接丢弃(系统丢弃,不会通知玩家,也不会触发任何与玩家丢弃装备相关的游戏事件或流程)。
-
装备数据存储:使用自定义值功能为每个装备添加一个
type
属性,用于标识装备的类型。设置一个SystemDiscard
标记,用于指示某个装备的丢弃是由系统自动处理的,而不是玩家的操作。 -
拾取物品:当玩家拾取一个物品时,系统会创建两个局部变量,分别存储玩家单位和拾取的物品。
-
逻辑判断:
- 如果玩家单位已经装备了相同
type
的装备,那么新拾取的装备将被系统丢弃。 - 如果玩家单位没有装备相同
type
的装备,玩家将成功获得新装备,同时,系统会更新玩家持有物品的状态,以便后续可以显示相应的特效或其他游戏效果。
- 如果玩家单位已经装备了相同
以上代码逻辑为:
-
初始化变量:设置变量
unit
为获得物品的单位,item
为单位获得的物品。 -
检查物品类型:如果
item
存在自定义键值"type"
,则继续执行。 -
检查玩家是否已装备相同类型的物品:
- 如果玩家单位
unit
已经存在自定义键值type
,并且与item
的type
相同,则执行系统丢弃逻辑。 - 如果
unit
不存在自定义键值type
,或者type
与item
的type
不同,则执行装备逻辑。
- 如果玩家单位
-
系统丢弃逻辑:
- 设置
item
的自定义键值"SystemDiscard"
为True
,表示这是一个系统自动处理的丢弃。 - 将
item
从unit
所在位置移除。
- 设置
-
装备逻辑:
- 向玩家发送消息,告知玩家已经获得了新的物品。
- 更新玩家单位的自定义键值
type
为item
的type
。 - 发送自定义事件
ProjectCreation
,参数为unit
和item
,用于可能的特效或其他逻辑的触发。
3.2.2 物品丢弃
与拾取物品类似,当一个单位丢弃物品时,先使用局部变量对参数进行缓存,然后执行判断:
- 玩家丢弃:如果物品被判断是玩家手动抛弃,获取物品type,清空单位对应自定义值项,这个清空的动作会导致特效结构检测到数据清空,从而触发特效的销毁。
- 系统丢弃:如果该物品被检测为系统丢弃,则跳过整个流程,当做无事发生(系统丢弃是静默的,不会通知玩家,不更新玩家状态,因此不会有新的特效或其他游戏效果被激活)。
以上代码逻辑为:
-
初始化变量:设置变量
unit
为失去物品的单位,item
为单位失去的物品。 -
检查物品类型:如果
item
存在自定义键值"type"
,则继续执行。否则发送调试信息“丢弃的物品无类型” -
检查是否为系统丢弃:
- 如果
item
的自定义键值"SystemDiscard"
为False
,则执行玩家丢弃逻辑。 - 如果为
True
,则表示这是一个系统自动处理的丢弃,不执行任何操作。
- 如果
-
玩家丢弃逻辑:
- 设置
type
为item
的自定义键值type
(在玩家丢弃装备的过程中,临时存储装备的类型信息,以便在后续的逻辑中使用这个信息来更新玩家状态、触发相关逻辑或发送消息等) - 删除
unit
的自定义键值type
,表示玩家已经失去了这个类型的装备。 - 向玩家发送消息,告知玩家已经丢弃了物品。
- 设置
-
系统丢弃逻辑:
- 删除
item
的自定义键值"SystemDiscard"
,重置物品的状态,这样,如果物品再次被拾取或处理,系统可以重新判断其状态,而不是基于之前的标记。 - 向玩家发送调试消息“丢弃的物品为系统丢弃”
- 删除
3.3 特效
想要为一个已有的事件附加另一个效果,可以使用自定义事件功能,比如在获得新装备后,可以发送一个自定义事件,来激活对应的特效功能。
3.3.1 特效生成
通过发送和接收自定义事件来传输信息,实现特效的激活:
-
武器特效:武器特效是直接绑定在角色手部的拖尾,所以我们只需要直接将对应的特效路径以自定义值的形式保存在物品上,就可以用同一个函数启用所有的武器特效。
-
护甲特效:由多个特效组合而成,需要一个函数来不断生成和销毁这些特效。
-
特效分敛:根据自定义事件传递的物品和type参数,分敛到不同类型的特效创建函数,执行对应的效果。
如果自定义事件ProjectCreation
被触发,则根据物品类型通知不同的特效生成机制:
- 如果自定义事件参数
item
的物品类型等于“寒冰剑”或“闪电发生器”,则设置变量projectType
为物品的projectType
,并为事件单位unit
的物品item
生成特效projectType
。 - 如果
item
的物品类型等于“冰霜斗篷”,则为事件单位unit
的物品item
生成盔甲01特效 - 如果
item
的物品类型等于“地精的护服”,则为事件单位unit
的物品item
生成盔甲02特效
3.3.2 特效销毁
在函数库中,我们新建三个函数,分别是武器,盔甲1和盔甲2,来分别执行武器和盔甲的特效创建和销毁过程。
对于武器特效功能,是在单位的节点添加一个拖尾特效projectType
,同时启动一个循环计时器,定期检查玩家是否仍然持有该武器。如果玩家更换或丢弃了武器,特效将被移除,并对所有结构进行销毁。
- 定义了一个名为“X01: 生成武器特效”的函数,该函数接收三个参数:
unit
(单位),item
(物品),projectType
(特效类型)。 - 为单位添加魔法效果
projectType
,并将此创建魔法效果赋值给变量project
,方便后续删除 - 设置变量
type
为item
的type
- 启动循环计时器,每隔一秒检测一次,检测
unit
的type
是否等于item
。是则继续,否则删除计时器和project
。
盔甲1的特效显示逻辑和武器特效类似,也是每秒检测一次。不同的是,盔甲的特效将在每秒计时器到时后立即生成,并在两秒后将其销毁。
为了显示盔甲2的特殊效果,我们为单位创建了两个投射物,并使用局部变量进行储存。
- 角度:为了实现球体的环绕效果,我们需要定义一个角度,这个角度将会每帧进行递增,以实现帧叠加以后的球体圆周运动。
- 检测:使用一个帧计时器(0.03秒)来检测装备是否还在被装备,如果它被丢弃,对所有结构进行销毁;否则继续执行
- 循环:当程序一直持续循环运行,球体的角度会在每一帧减去6,并根据一个固定的距离,以角色位置为圆心,通过极坐标系确定本帧投射物该在的位置。
-
函数定义:定义了一个名为
X03: 生成护甲2特效
的函数,接收两个参数:unit
(单位)和item
(物品)。 -
变量初始化:
type
变量被设置为item
的type
属性,用于区分不同类型的装备。int
变量被初始化为0,可能用于控制特效的生成或更新。angle01
和angle02
变量被初始化为90.0和270.0,用于确定特效的初始位置。- 在单位的位置创建两个护甲特效,分别命名为
project[1]
和project[2]
。
-
循环计时器:创建一个循环计时器,每0.03秒执行一次,用于更新特效的位置。 如果单位装备了“地精的护服”,则继续执行循环,否则销毁所有特效。
-
更新特效位置:
angle01
和angle02
变量用于控制特效的旋转角度,每帧都会变化。根据旋转角度,更新两个特效project[1]
和project[2]
的位置point1
和point2
.。- 设置变量int每帧加1,然后进行条件判断:
- 如果
int = 1
,在point2
创建特效project[3]
- 如果
2 ≤ int ≤ 13
,持续更新特效project[3]
的位置(点point2
)。 - 如果
int = 60
,重置int = 0
- 如果
总的逻辑是每帧检测一次,如果依旧装备“地精的护服”,创建两个特效project[1]
和project[2]
环绕其持有角色,然后每一秒的前13帧,在project[2]
的位置同时创建和更新特效project[3]
。
3.4 行为树
行为树即是用树状结构分支使AI能根据条件执行不同行为(如走、跑、跳、攻击),让单位通过模拟真实玩家的行动而拥有“智能”行为,增强玩家的参与感,例如PVE的敌人,BOSS的战斗模式,玩家召唤物等。
-
初始化行为树:初始化前,需要创建一个全局变量(比如unitPlayer)来存储玩家角色,创建一个全局变量单位组来存储敌方单位。通过动作:“玩家-选择单位”,让玩家选中自己的主控单位。为了更沉浸的游玩体验,使用“镜头 - 跟随单位”功能让玩家的视角能始终跟随在自己的英雄上。
-
行为树分敛:
- 游戏初始化结束(游戏开始0.1秒后),每隔1秒遍历一次地方单位组,每次遍历所有单位,根据地方单位类型(近卫和治疗者),对其应用对应的行为树。
- 判断完单位类型后,通过自定义事件传递单位参数来实现行为树的分发。参数必须是变量,不支持数组。比如行为树是针对某单位执行,那么就需要将单位作为参数传递出去。
-
实现守卫AI:
- 新建一个单位类型局部变量
unit
,存储通过“自定义事件:AI_Guard”发送过来的单位。 - 新建一个实数类型局部变量
distance
,用于储存敌人和治疗者之间的距离,通过判断distance
是否小于300来判断治疗者是否安全。- 如果治疗者不安全(距离小于300),则给单位unit向玩家角色unitPlayer发出一个攻击指令。
- 如果治疗者安全,判断守卫自身HP,低于50%则移动至治疗者(给单位unit向unitHealer发出一个移动指令)。
- 如果守卫安全,检查警戒区域内是否有敌人,有则攻击,无则巡逻。
- 新建一个单位类型局部变量
- 实现治疗者AI:新建一个单位类型局部变量
unit
,存储通过“自定义事件:AI_Healer”发送过来的单位。优先保障自身安全,满HP(自己的HP是否等于其MaxHP)则治疗友军中HP最低的单位(如果有的话),没有需要治疗的单位则进行攻击。- 使用一个函数获取当前最需要治疗的友军。
- 新建局部变量
Temp
(百分比格式,初始化为1)和unit
(单位类型)分别存储单位组中最低的HP百分比及遍历到的单位。 - 逐一将
unit
血量和Temp
对比,如果unit
血量更低则将Temp
更新为这个值。当遍历完全以后,我们就获取到了单位组中HP百分比最低的那一个单位。 - 返回HP百分比最低的那一个单位
- 新建局部变量
- 使用一个函数获取当前最需要治疗的友军。
- 如果HP百分比最低单位为空(友军都是满血),判断自己身边是否有敌人,如果有,就使用飞弹攻击它;没有则在警戒区域的中心不动,以方便巡逻的守卫保护自己。
- 如果有HP百分比最低的单位,则停止当前的动作(这里并非使用‘技能’来实现治疗,所以为了防止治疗者边攻击边治疗,需要先停止攻击动作)并对其进行治疗。为了让这种动作更具真实感,我们可以播放对应的动画动作,并为治疗效果添加一个特效。
3.5 使用排行榜
-
点击主界面【细节】-【存档设置】打开存档槽设置
-
点击加号创建一个新的存档槽并修改存档槽数据类型为整数
-
选择确认计入排行榜,这个存档槽位就变成了排行榜存档。可以选择排行榜排序规则(升序降序)与排行榜最大人数。
-
使用以下eca可以获取排行榜上所有玩家的存档值,配合界面eca让排行榜在界面显示。玩家->整数型增量存档eca可以让整数类型存档保持只增效果。
排行榜数据在游戏启动后不会再刷新,使用双槽位可以实现周排行榜。
-
创建两个整数类型存档槽位:A与B,A存储第一周所有玩家的排行榜数据
-
当一周结束时(通过时间戳判断),使用B榜存储玩家在第二周的排行数据
-
在第二周开始时通过ECA清除玩家A槽位数据,以备第三周存储玩家排行数据。需通过在作者之家清除A榜数据(权限需找运营申请)。
-
当第二周结束时,将玩家排行榜数据存储在A榜并清除B槽位数据,循环往复
3.6 特效可见性
特效默认为全玩家可见,可通过ECA控制特效可见性。
- 通过“玩家”列表操作,实现对某个玩家的特效显示/屏蔽
- 通过“玩家组”列表操作,实现对某个玩家组的特效显示/屏蔽,比如某玩家的所有同盟玩家,某玩家的所有敌对玩家等等
3.7 不同步问题
参考《不同步相关》
3.7.1 本地配置不同步日志环境
用户由于使用ECA不当,频繁出现游戏逻辑不同步的问题,会影响游戏正常运行。此时可通过在本地多开测试来进行调试。在【通用设置-调试】打开本地多开同步检测。
使用Lua文件配置更详细的不同步日志:打开地图路径下Script文件夹下的main.lua
文件,在lua文件中配置不同步日志相关API
GameAPI.api_set_enable_detail_snapshot(true)
GameAPI.api_set_detail_snapshot_enable_tag(0xfffffff)
GameAPI.api_set_enable_eca_snapshot(true)
GameAPI.api_set_snapshot_traceback_level(2)
配置完毕后,本地多开运行游戏。如遇游戏逻辑不同步,会有弹窗提示,并在本地生成不同步日志以供用户定位不同步问题。
不同步日志Lua配置API说明:
API | 描述 | 参数 | 返回值 |
---|---|---|---|
api_set_enable_detail_snapshot | 控制不同步日志记录的总开关。关闭后,其他设置接口将不生效,但可以提升性能。 | enable(bool) :是否开启,默认为false | 无 |
api_set_snapshot_traceback_level | 设置日志堆栈记录详细等级,默认0 不记录1 :仅记录最近一层堆栈2 :记录完整堆栈(数据压缩)3 :记录完整日志(不压缩,数据量稍大)记录越完整越有利于定位问题,但开销增大 | level(int32) :堆栈记录等级,默认值为0 | 无 |
api_set_enable_timer_snapshot | 开启或关闭计时器不同步检测日志(检测额外创建的ECA计时器),但计时器不一致并不一定意味着游戏内容不同步。 | enable(bool) :默认值为false | 无 |
api_set_enable_eca_snapshot | 开启或关闭ECA不同步检测日志,开销较高。可通过参数过滤掉一些安全的API以防止误报,例如创建特效、UI操作等 | enable(bool) :默认值为false | 无 |
filter_mode(int32) :过滤模式,默认为1 1 :剔除模式,不记录filter_set中指定的api;0 :包含模式,仅记录filter_set中指定的api; | |||
filter_set(table) :过滤集合,默认为"client_only","client_possible" 可传入想要剔除/包含的API(取决于上个参数) 如"GameAPI:print_to_dialog",“GameAPI:get_function_return_value。” | |||
api_set_detail_snapshot_enable_tag | 设置不同步详细日志级别。越详细越利于定位不同步产生点,但性能消耗会增高 | tag(UInt64) :控制开启哪些日志的mask 1 运动器tick;2 运动器碰撞检测4 寻路回调;8 寻路坐标更新16 血量变化;32 坐标瞬变0xFFFFFFFF 全部开启;默认开启16+32 | 无 |
add_detail_log | 记录自定义日志 | log(string) :日志内容 | bool,值恒定为true |
3.7.2 线上游戏不同步定位
- 在本地配置不同步日志环境
打开地图路径下Script文件夹下的main.lua文件,在lua文件中配置不同步日志相关API,这样线上游戏发生不同步时,根据配置API玩家客户端会自动上传日志。例如:
GameAPI.api_set_enable_detail_snapshot(true)
GameAPI.api_set_detail_snapshot_enable_tag(0xfffffff)
GameAPI.api_set_enable_eca_snapshot(true)
GameAPI.api_set_snapshot_traceback_level(2)
- 下载不同步日志
使用KK账号登录编辑器,打开任意地图,点击【菜单栏】【调试】【查看不同步日志】查看线上游戏的不同步日志。
查找需要定位问题的对局,下载不同步日志,不同步日志包含对局中所有玩家的对战信息。
3.7.2 定位不同步问题
不同步日志文件中包含的是出现不同步情况的帧信息,通过对比玩家日志差异,可以大致定位到问题所在。打开不同步日志文件夹,使用第三方文本对比工具进行对比(推荐使用BeyondCompare
)。比如在main.lua
文件中进行了如下配置:
GameAPI.api_set_enable_detail_snapshot(true);
GameAPI.api_set_enable_timer_snapshot(true);
打开不同步日志,查看逐帧信息:
将多名玩家日志成对拖放到beyondcompare中进行对比,发现玩家2比玩家1多了一个timer:
通过对不同步信息附近的帧信息进行理解,可大致定位问题为某个客户端上多了一个循环计时器,在项目中进行查找可能的问题所在。
3.7.3 游戏不同步的原因
- 本地值:
- 在单个客户端(即单个玩家的设备)上有效的数据,比如界面显示、特效、镜头位置、声音等,这些数据在不同的客户端之间不需要同步,因为它们只影响单个玩家的体验。
- 任意值与【本地值】进行逻辑运算后得到的值等价于【本地值】
- 动作类ECA中,修改【本地值】,不会影响游戏的全局逻辑,因此不会影响游戏的同步性。
- 本地操作:针对本地值的操作,如【获取本地玩家】、【获取本地控件坐标】、【获取镜头焦点】、【获取滑动条当前值】等,这些获取本地值的ECA函数,在不同客户端得到的结果也是不同的。
- 全局值:在所有客户端之间需要保持一致的值,比如单位、技能、物品等。这些值的一致性对于游戏的公平性和逻辑一致性至关重要。
- 全局操作:针对全局值的操作,比如修改单位的状态、技能的效果、物品的数量等。这些操作会影响游戏的全局逻辑,因此需要确保在所有客户端上都能正确同步。
导致游戏逻辑不同步的一个重要原因就是将【本地值】作为参数,传递到【全局操作】中,或者是使用【本地值】进行逻辑判断后,进行【全局操作】。这会导致不同客户端执行不同的逻辑,最终导致不同客户端上【全局值】不同,即游戏逻辑不同步。更多内容详见文档多人联机同步机制。