主题
效果系统(modifier_system)
认识效果系统
效果系统就是管理生物身上的状态:中毒、加速、无敌、眩晕……每个状态就是一个效果。
它帮你处理好了这些麻烦事:
- 效果什么时候开始、什么时候到期;
- 同一个效果被重复获得时怎么叠加(加层数还是刷新时间);
- 是不是要暂停 / 恢复倒计时;
- 获得和失去时播什么特效、音效;
- 怎么在效果生效前拦下来「不让它获得」。
能力清单
| 能力 | 说明 |
|---|---|
| 添加 / 移除 | 按预设动态添加效果;按 Key、按实体、整片清除都能做 |
| 编辑器直配 | 在生物预设下挂「效果预设」子组件,用面板配好,游戏内的生物就自动带上这个效果 |
| 叠加 | 同一种效果可叠加层数、叠加时间,有 4 种时间策略 + 3 种层数策略 |
| 时长 | 有限时长自动到期消失,时长填 0 就是永久 |
| 暂停 / 恢复 | 暂停时倒计时冻结,恢复时把暂停的时长补回来 |
| 获得拦截 | 可以在「生物即将获得效果」时阻止这次获得 |
| 事件 | 效果级 8 个事件 + 拥有者级 4 个事件,用脚本监听 |
| 表现 | 获得 / 失去时播放特效、音效,支持皮肤材质替换 |
| 属性修改 | 效果生效时累加属性、移除时回滚(依赖属性系统,见 FAQ) |
包结构
三个目录是什么意思
效果系统包按 common / server / client 三层组织:
| 目录 | 含义 | 作者视角 |
|---|---|---|
common/ | 双端共用:预设类型声明、配置表、事件定义等共享内容 | 查枚举、看事件名 |
server/ | 服务端:权威逻辑,所有状态变更都在这里 | 主要写代码的地方 |
client/ | 客户端:镜像状态与表现播放 | 查询、自绘 UI |
一句话记住权威性:效果的添加、移除、层数、时长、暂停全在服务端裁决;客户端调修改类接口只是「发出请求」,真正的结果由服务端处理完再广播回来。
目录与文件职责
LuaSource_作者版效果系统/ 效果系统包(modifier_system)
│
├─ common/
│ └─ packages/modifier_system/
│ ├─ editor.lua 预设类型注册(效果)
│ ├─ config.lua 包内配置表:创建结果、事件名、叠加策略、默认值(勿改)
│ ├─ data.lua 结构体字段定义
│ ├─ enums.lua 枚举
│ ├─ event_defs.lua 事件定义 + 事件单位创建与触发
│ └─ util.lua 内部工具
│
├─ server/
│ ├─ main.lua 空壳入口,无业务逻辑
│ └─ packages/modifier_system/
│ ├─ api.lua ★ 服务端公开接口(权威)
│ ├─ ModifierItem_Script.lua 效果预设的「壳脚本」:声明全部可编辑字段
│ ├─ modifier_system.lua 服务端全局管理器(单例,按生物分容器)
│ ├─ ModifierHandler.lua 单个效果实例的操作句柄(进阶用法)
│ ├─ ModifierStack.lua 叠加处理(层数策略 / 时间策略)
│ └─ event_bridge.lua 事件桥接
│
└─ client/
├─ main.lua 空壳入口,无业务逻辑
└─ packages/modifier_system/
├─ api.lua ★ 客户端公开接口(请求 + 查询)
├─ ModifierItem_LocalScript.lua 效果预设的客户端「壳脚本」(不需要配任何字段)
├─ modifier_system.lua 客户端镜像管理器:槽位镜像 + 材质栈
├─ ClientModifierHandler.lua 客户端单实例句柄
└─ performance.lua 表现播放(特效 / 音效)几个要点
- ★ 标记的是作者要用的入口:两份
api.lua。 - 「壳脚本」是什么:预设上挂的脚本只做两件事——声明有哪些可编辑字段、把自己注册到效果系统。业务逻辑全在包里。所以升级包版本时,你已经建好的效果预设不需要改动。壳脚本就是上面列出的
ModifierItem_Script.lua(服务端)和ModifierItem_LocalScript.lua(客户端)。 - 客户端镜像管理器就是
client/packages/modifier_system/modifier_system.lua,它维护每个生物的槽位镜像和材质栈。作者一般不需要直接接触,用客户端接口查询即可。 - 包里还有引擎脚手架文件(入口空壳、触发器占位、资源描述等),与玩法无关,未列出。
核心概念
| 概念 | 一句话解释 |
|---|---|
| 效果(modifier) | 一个 buff / debuff 实例。实例本身就是一个场景单位,直接挂在生物下面 |
| 效果预设 | 编辑器里的一个资产,就是这份效果的配置模板。新建时选类型「效果」 |
| Key(效果标识) | 区分「是不是同一种效果」的标识。动态添加时,Key 就是预设的资产 ID,无需作者自己起名 |
| 拥有者(owner) | 效果挂在哪个生物上。效果只能挂生物,挂非生物会被拒绝 |
| 效果实体 | 查询接口返回的那个实例对象,它的各种状态直接从实体上读 |
「预设即 Key」是什么意思
动态添加效果时要传一个预设资产 ID(形如 map://preset/...)。系统直接把这个字符串本身当作 Key 使用。带来的结果:
- 同一个预设添加第二次 = 同一个 Key = 走叠加流程;
- 不同预设 = 不同 Key = 各自独立共存;
- 作者无需自行维护「效果名字 → Key」的映射,零配置。
效果实例挂在哪
生物(角色)
└─ 效果实例(运行时就是一个 Script 单位)
├─ 属性:IsActive / CurrCount / EndTime / ModifierKey …
└─ 事件单位:ModifierObtain / ModifierLoss / StackChange …(挂在上面的可监听事件)快速上手
步骤 1 · 建一个「效果」预设
在资源库按预设类型新建,类型选「效果」。编辑器会生成一个已经接好的模板:一个根脚本(装全部配置)+ 一个客户端子脚本。改个名字,然后在属性面板里调参数:持续时间、能否叠加、获得表现、失去表现……(字段见第 5 节)。
保存后拿到这个预设的资产 ID(形如 map://preset/...),后面添加效果就用这个字符串。
两种用法怎么选
效果有两种用法:接口动态添加(就是本节这种,推荐)和在生物预设下挂「效果预设」子组件直配(见第 5 节)。前者场景树更干净、不需要任何管理器节点,但要写脚本;后者不用写代码。
步骤 2 · 添加效果(服务端一行代码)
lua
-- 服务端脚本
local ModifierAPI = require("server.packages.modifier_system.api")
local result = ModifierAPI.AddModifier(character, "map://preset/你的效果预设", {
duration = 3, -- 可选:覆盖预设的持续时间(秒)
source = attacker, -- 可选:来源生物(仅同源叠加时会用到)
})
-- result 取值:"added" / "reobtain" / "rejected" / "failed",含义见第 6 节步骤 3 · 查询与读取
lua
-- 取出效果实例,状态直接从实例上读
local entity = ModifierAPI.GetUnitModifiers(character, "map://preset/你的效果预设")[1]
if entity then
print("[demo] 激活:", entity:GetAttribute("IsActive"),
"层数:", entity:GetAttribute("CurrCount"),
"剩余:", ModifierAPI.GetRemainingTimeByKey(character, "map://preset/你的效果预设"))
end步骤 4 · 监听事件
lua
-- 事件单位挂在效果实例下面
entity:FindFirstChild("ModifierObtain"):Connect(function(modifier, owner)
print("[demo] 效果获得", modifier.UnitId)
end)
entity:FindFirstChild("StackChange"):Connect(function(modifier, oldCount, newCount)
print("[demo] 层数", oldCount, "→", newCount)
end)更多事件名与监听方式见第 8 节。
步骤 5 · 移除 / 暂停 / 拦截
lua
ModifierAPI.ClearUnitModifiers(character, key) -- 按 Key 移除;Key 省略 = 清除该生物全部效果
ModifierAPI.RemoveModifier(entity) -- 按实例移除(事件回调参数可以直接传进来)
ModifierAPI.Pause(character, key) -- 暂停(倒计时冻结)
ModifierAPI.Resume(character, key) -- 恢复(把暂停时长补回来)
-- 获得拦截:只能在「生物即将获得效果」事件的回调里同步调用
ModifierAPI.SetInterruptModifierObtain()编辑器使用
先搞清楚:引用还是导入
效果包有两种拿到手的方式,选哪种决定了你后面能不能改:
| 引用 | 导入 | |
|---|---|---|
| 怎么操作 | 在资源包面板里引用效果包;不需要时取消引用 | 先取消引用,再执行导入 |
| 代码和预设会随包体更新吗 | 会 | 不会 |
| 能在包内预设上直接改数值吗 | 不能,要先复制或新建预设再改 | 能,直接在地图里编辑并保存 |
| 能看到 Lua 逻辑吗 | 看不到 | 能,逻辑就在地图的 lua 里 |
| 能反悔吗 | 能,随时取消引用 | 不能,导入是一次性操作 |
只想用、想跟着官方更新,就引用;想改逻辑、想二创,就导入。导入后不想要了,需要在地图 lua 里手动删。
在生物预设下挂「效果预设」子组件
不写代码的做法:
- 打开生物预设(要加效果的那个生物);
- 在它下面挂载子组件:分类选「服务端脚本」,类型选「效果预设」;
- 选中这个子组件,在右侧参数面板里设置参数(持续时间、能否叠加、获得 / 失去表现……逐项见第 5.4 节)。
配好保存后,游戏里该生物就带上了这个效果,不需要任何运行时脚本。
两种用法怎么选
| 接口动态添加(第 4、6 节) | 预设直配(本节) | |
|---|---|---|
| 要写脚本吗 | 要 | 不用 |
| 生效范围 | 你指定的单位、你指定的时机 | 所有使用该生物预设的生物,生成即生效 |
| 时长 / 层数能运行时改吗 | 能 | 只能按面板配好的值走 |
| 适合场景 | 条件触发、动态增减、关卡逻辑 | 固定配置、批量行为、快速验证 |
两种方式可共用
两种方式可以同时用,互不冲突。
效果预设的参数逐项说明
字段名就是运行时读属性的名字,用 GetAttribute("字段名") 读。
| 分组 | 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
| 基础 | Duration | 数字 | 5 | 持续时间(秒)。填 0 表示永久 |
UgcModifierType | 枚举 | 2 | 0 有益 / 1 有害 / 2 中立 | |
RemoveMode | 枚举 | 1 | 0 无 / 1 击败后清除。当前版本暂未生效 | |
| 叠加 | Stackable | 布尔 | false | 是否允许叠加。关掉时下面的叠加字段会全部隐藏 |
SameSourceStack | 布尔 | false | 仅同源叠加(来源不同时各自独立共存) | |
StackCountStep | 整数 | 1 | 每次获得增加多少层 | |
MaxStackCount | 整数 | 999 | 最大层数 | |
StackDurationMode | 枚举 | 0 | 时间策略:0 不变 / 1 覆盖 / 2 增加 / 3 独立计时 | |
StackCountMode | 枚举 | 0 | 层数策略:0 不变 / 1 覆盖 / 2 增加 | |
| 属性修改 | AttrConfigs | 列表 | 空 | 效果生效时累加、移除时回滚。依赖属性系统,见 FAQ |
| 获得表现 | ObtainPerformanceList | 列表 | 空 | 获得时播放:特效 / 音效 / 材质替换 |
| 失去表现 | LostPerformanceList | 列表 | 空 | 失去时播放:特效 / 音效(材质还原由系统自动处理) |
没有面板字段的项
名称、图标、描述、状态栏显示没有面板字段:要么动态添加时通过参数传进去,要么运行时写属性(Name / Icon / ModifierDesc / StatusDisplay)。表里标了「当前版本暂未生效」的字段(RemoveMode、AffectCamp、MtgAffectUgcModel)当前只是配置占位、不产生行为,需要这些效果请由作者自行实现。
效果能提供的四种预制能力
一个效果预设能干的事情,就这 4 类:
| 能力 | 说明 | 需要什么 |
|---|---|---|
| 修改属性参数 | 生效期间改动生物的数值(移速、攻击、生命……),失去时自动还原 | 要额外引用或导入「复杂属性包」,否则这一项不生效(不报错) |
| 特效 | 获得 / 失去时播放特效,可设挂点、继承形式、缩放、速率、是否循环 | 直接能用 |
| 音效 | 获得 / 失去时播放音效,支持 2D / 3D、衰减范围、循环时长 | 直接能用 |
| 皮肤材质 | 替换生物的皮肤材质(冰冻、隐身、无敌、剪影……),失去时自动还原 | 直接能用 |
依赖复杂属性包
「修改属性参数」对应面板上的 AttrConfigs。它依赖属性包,所以要先在资源包里引用或导入「复杂属性包」(属性系统所在的那个包)。引用之后,效果就能像加减数值一样改属性,移除时自动回滚;没引用的话这一项会不生效。
运行时属性(系统写入,双端都能看到)
这些属性只读使用,不要手动改:
| 属性 | 含义 |
|---|---|
IsActive | 是否激活 |
CurrCount | 当前层数 |
CharMtg | 当前材质 ID(0 表示无) |
IsPaused | 是否暂停 |
EndTime | 绝对结束时间戳:-1 = 永久,0 = 未激活,大于 0 = 到期时刻 |
ModifierKey | 效果 Key(动态添加时等于资产 ID;预摆放时为空,需由作者自行写入) |
OwnerUnitId | 拥有者单位 ID(注册时写入,客户端定位用) |
读的时候按这三类找属性:
- 效果配置:
Name/ModifierDesc/Icon/UgcModifierType/MaxStackCount - 运行时状态:
IsActive/CurrCount/CharMtg/IsPaused/EndTime/ModifierKey - 实体自身:
UnitId
服务端脚本接口
lua
local ModifierAPI = require("server.packages.modifier_system.api")服务端是权威端:查询和修改都直接生效。
| 函数 | 参数 | 返回 | 说明 |
|---|---|---|---|
AddModifier | ownerUnit, assetId, addConfig? | 结果字符串 | 添加效果,返回值含义见下表 |
RemoveModifier | modifierUnit | 布尔 | 按效果实例移除(带失活处理) |
ClearUnitModifiers | ownerUnit, modifierKey? | 整数 | 按 Key 移除全部匹配;Key 为空则清除全部 |
GetUnitModifiers | ownerUnit, modifierKey? | 效果实例列表 | 查效果实例;Key 为空返回全部 |
IsInModifier | ownerUnit, modifierKey | 布尔 | 是否拥有指定 Key;Key 为空时表示「是否有任意效果」 |
SetModifierStackCount | modifierUnit, count | 布尔 | 设置层数;设为 0 会触发带失活的移除 |
AddModifierStackCount | modifierUnit, delta | 整数 | 增减层数(可为负),返回新层数 |
AddModifierDurationByInstance | modifierUnit, extra | 布尔 | 延长持续时间(可为负) |
SetModifierRemainTime | modifierUnit, remaining | 布尔 | 设置剩余时间(秒) |
GetModifierOwner | modifierUnit | 拥有者或 nil | 取效果实例的拥有者 |
Pause | ownerUnit, modifierKey, unitId? | 布尔 | 暂停:冻结倒计时与叠加计时器 |
Resume | ownerUnit, modifierKey, unitId? | 布尔 | 恢复:补偿暂停时长并重建倒计时 |
SetInterruptModifierObtain | — | true | 阻止当前这次效果获得;仅「生物即将获得效果」事件内有效 |
GetRemainingTimeByKey | ownerUnit, modifierKey | 数值 | 剩余时间;永久效果返回 -1 |
GetSourceByKey | ownerUnit, modifierKey | 来源单位或 nil | 未指定来源时返回 nil |
AddModifier 返回值的含义
用 ModifierAPI.Enums.CreateResult 可以取到这几个常量:
| 返回值 | 含义 |
|---|---|
"added" | 新实例创建并激活成功 |
"reobtain" | 与同 Key 的已有实例叠加(没有产生新实例,已有实例的层数 / 时间被更新) |
"rejected" | 被规则拒绝:拥有者不是生物 / 不可叠加 / 被拦截 / 实例数达到上限(新实例已被销毁) |
"failed" | 系统失败:参数缺失、资产创建失败等 |
添加时可以临时覆盖哪些配置
第二个参数里的配置会覆盖预设上的同名配置(大写属性名和小写别名都认,大写优先):
lua
ModifierAPI.AddModifier(ownerUnit, assetId, {
duration = 10, -- 持续时间(覆盖 Duration)
stackable = true, -- 允许叠加(覆盖 Stackable)
stackCountStep = 1, -- 每次获得增加的层数
maxStackCount = 5, -- 最大层数
stackDurationMode = 1, -- 时间策略(0 不变 / 1 覆盖 / 2 增加 / 3 独立计时)
stackCountMode = 2, -- 层数策略(0 不变 / 1 覆盖 / 2 增加)
modifierType = 1, -- 效果类型(0 有益 / 1 有害 / 2 中立)
name = "中毒", -- 名称(没有面板字段,只能这样传)
icon = "official://image/...", -- 图标(同上)
desc = "每层每秒掉血", -- 描述(同上)
statusDisplay = false, -- 状态栏显示(当前暂无 UI 消费)
removeMode = 1, -- 清除规则(当前暂无消费)
sameSourceStack = false, -- 仅同源叠加
attrConfigs = {}, -- 属性修改列表(依赖属性系统,见 FAQ)
source = attacker,-- 来源生物(也可以写成 sourceUnit)
})必填参数
ownerUnit 和 assetId 是必填的,缺失会直接报错(快速失败)。
客户端脚本接口
lua
local ModifierAPI = require("client.packages.modifier_system.api")客户端没有添加接口(添加是服务端的权威操作)。修改类接口全部是发请求给服务端执行,返回值只代表「请求是否已发出」;查询类读的是本地镜像。
| 函数 | 参数 | 返回 | 说明 |
|---|---|---|---|
RemoveModifier | modifierUnit | 布尔 | 请求移除效果实例 |
ClearUnitModifiers | ownerUnit, modifierKey? | 整数(恒为 0) | 请求按 Key 移除 / 清除全部 |
SetModifierStackCount | modifierUnit, count | 布尔 | 请求设置层数 |
AddModifierStackCount | modifierUnit, delta | 整数(恒为 0) | 请求增减层数 |
AddModifierDurationByInstance | modifierUnit, extra | 布尔 | 请求延长持续时间 |
SetModifierRemainTime | modifierUnit, remaining | 布尔 | 请求设置剩余时间 |
Pause | ownerUnit, modifierKey, unitId? | 布尔 | 请求暂停 |
Resume | ownerUnit, modifierKey, unitId? | 布尔 | 请求恢复 |
GetUnitModifiers | ownerUnit, modifierKey? | 效果实例列表 | 查询(本地镜像) |
IsInModifier | ownerUnit, modifierKey | 布尔 | 查询(本地镜像) |
GetRemainingTimeByKey | ownerUnit, modifierKey | 数值 | 查询;永久效果返回 -1 |
参数怎么传:客户端的修改类接口接收效果实例,内部会自动解析出「拥有者 + Key + 实例 ID」再发请求;解析失败返回 false。
客户端没有 GetSourceByKey 和 GetModifierOwner——来源信息只有服务端知道。要在客户端用来源信息,需由作者自行让服务端转存到属性或事件里。
lua
-- 客户端:查本地镜像 + 发修改请求
local entity = ModifierAPI.GetUnitModifiers(character, key)[1]
if entity and entity:GetAttribute("IsPaused") == false then
ModifierAPI.Pause(character, key) -- 请求暂停,最终结果以服务端回推为准
end事件监听
事件怎么用
- 事件就是挂在单位下的一个可监听事件单位,名字就是事件名;
- 监听写法:
单位:FindFirstChild("事件名"):Connect(function(...) end); - 事件是本地广播:服务端触发只对服务端的监听者有效。客户端表现靠属性同步和服务端广播驱动;
- 效果级的事件单位在效果注册时才创建。想第一时间拿到新实例,先监听拥有者级的
ModifierAdded,再给新实例挂效果级事件。
效果级事件(挂在效果实例下,共 8 个)
| 事件名 | 参数 | 触发时机 |
|---|---|---|
BeforeObtain | (modifier) | 即将激活(在「即将获得」判定之前,可以被拦截) |
ModifierObtain | (modifier, owner) | 获得并激活 |
ModifierLoss | (modifier, owner) | 失去(到期 / 被移除 / 层数归零) |
ModifierReobtain | (modifier, owner) | 被叠加刷新(重复获得同一效果) |
StackChange | (modifier, oldCount, newCount) | 层数变化 |
Pause | (modifier) | 暂停 |
Resume | (modifier) | 恢复 |
DurationFinish | (modifier) | 倒计时到期(在失去之前触发) |
拥有者级事件(挂在生物单位下,共 4 个)
| 事件名 | 参数 | 触发时机 |
|---|---|---|
ModifierAdded | (modifier) | 有新效果实例加入该生物 |
ModifierRemoved | (modifier) | 有实例被移除 |
ModifierRefresh | (modifier) | 叠加刷新(同 Key 叠加到已有实例) |
ModifierObtainBefore | (modifier, owner) | 即将获得(拦截入口,见第 10.6 节) |
两个事件名别写混
注意别把两个名字写混:「即将获得」在效果级叫 BeforeObtain,在拥有者级叫 ModifierObtainBefore,两者是不同的事件。
lua
-- 监听「有新效果加入」,再给每个新实例挂效果级事件
character:FindFirstChild("ModifierAdded"):Connect(function(modifier)
local key = modifier:GetAttribute("ModifierKey")
print("[demo] 获得", key)
modifier:FindFirstChild("ModifierLoss"):Connect(function()
print("[demo] 失去", key)
end)
end)表现系统
获得表现 / 失去表现
获得表现支持 15 个字段(按「表现形式」显隐):
| 字段 | 类型 | 默认值 | 显隐 | 说明 |
|---|---|---|---|---|
AffectCamp | 枚举 | 7 | — | 生效阵营。当前版本暂未生效,表现对所有人播放 |
PerformanceType | 枚举 | 0 | — | 0 特效 / 1 音效 / 2 皮肤材质替换 |
EffectID | 特效资源 | "-1" | 特效 | 特效资源 |
EffectAttachPoint | 枚举 | origin | 特效 | 挂在哪个部位(见枚举表) |
EffectInherit | 枚举 | 7 | 特效 | 继承形式(1 位置 / 2 旋转 / 4 缩放,可组合) |
EffectScale | 数值 | 1.0 | 特效 | 缩放系数 |
EffectFrameRate | 数值 | 1.0 | 特效 | 播放速率 |
IsLoop | 布尔 | false | 特效 | 循环播放(存活期间持续循环,跟随销毁) |
SoundID | 音效资源 | -1 | 音效 | 音效资源 |
SoundType | 枚举 | 1 | 音效 | 0 2D / 1 3D |
SoundDuration | 数值 | -1.0 | 音效 | 持续时间(大于 0 时循环播放到总时长) |
SoundDistance | 数值 | 10 | 音效 | 3D 衰减范围 |
Mtg | 枚举 | 0 | 材质(仅获得) | 替换皮肤材质(见枚举表) |
DestroyWithModifier | 布尔 | true | 仅获得 | 表现单位是否跟随效果销毁 |
失去表现支持 11 个字段,比获得表现少了 Mtg / MtgAffectUgcModel / DestroyWithModifier,表现形式只有 0 特效 / 1 音效(材质还原由系统自动处理)。
表里标了「当前版本暂未生效」的字段当前只是配置占位、不产生行为,需要时请由作者自行实现(见第 5.4 节的说明)。
枚举速查
| 枚举 | 取值 |
|---|---|
| 生效阵营 | 1 自己 / 2 友军 / 4 敌人(组合:3 自己+友军 / 5 自己+敌人 / 6 友军+敌人 / 7 全部) |
| 表现形式 | 0 特效 / 1 音效 / 2 皮肤材质替换 |
| 失去表现 | 0 特效 / 1 音效 |
| 特效挂点 | socket_head 头 / socket_body 身体 / socket_origin 底面中心 / socket_weapon_l、socket_weapon_r 武器 / socket_foot_l、socket_foot_r 脚 / socket_hand_l、socket_hand_r 手 / socket_forearm_l、socket_forearm_r 臂 |
| 特效继承 | 1 位置 / 2 旋转 / 4 缩放(组合:3 位+旋 / 5 位+缩 / 6 旋+缩 / 7 全部) |
| 音效类别 | 0 2D / 1 3D |
| 皮肤材质 | 0 无 / 1 奶油蛋糕 / 2 隐身 / 3 冰冻 / 4 无敌 / 5 剪影 / 6 灵魂 / 7 穿梭之门 / 8 测试 |
| 属性分量类型 | 0 基础值 / 1 基础额外值 / 2 加成比例 / 3 额外加成 |
播放机制
- 特效:创建特效单位 → 按挂点和继承绑定拥有者 → 应用缩放与速率 → 循环类特效持续循环;循环特效或「跟随销毁」的特效会跟随效果一起销毁(循环特效必须跟随,否则会永久播放)。
- 音效:创建音效单位;2D / 3D 参数透传;持续时间为正数时循环播放到总时长;3D 音效带位置和衰减范围。
- 材质:不创建单位,走客户端的材质栈(见第 10.8 节)。
- 播放时机:获得表现在效果激活时播放,失去表现在失去时播放。
关键规则
Key 与定位
- 动态添加:Key 恒等于效果预设的资产 ID,同预设自动视为同一种效果(走叠加),异预设各自共存;
- 场景预摆放:没有 Key。需要按 Key 查找或叠加时,由作者自行在服务端运行时写
SetAttribute("ModifierKey", "...")补上; - Key 为空的效果:不参与叠加,也不会被「按 Key 查询 / 修改」命中(但
GetUnitModifiers(owner)不传 Key 时仍能取到它)。
叠加规则(重复获得同一个 Key)
新实例注册时如果命中同 Key 的已有实例,按这个顺序判断:
- 已有实例不可叠加(
Stackable = false)→ 新实例销毁,返回rejected; - 勾了「仅同源叠加」且来源不同(两边都有来源时)→ 不叠加,作为独立实例共存;
- 其他情况 → 叠加:把层数和时间策略应用到已有实例上,新实例销毁,返回
reobtain。 此时会触发效果级的ModifierReobtain和StackChange,以及拥有者级的ModifierRefresh。
层数策略
| 取值 | 含义 |
|---|---|
| 0 | 层数不变 |
| 1 | 覆盖(层数变成 StackCountStep) |
| 2 | 增加(在当前层数上 + StackCountStep) |
层数一律夹在 [0, MaxStackCount] 之间;层数归零会自动触发带失活的移除。
时间策略
| 取值 | 含义 |
|---|---|
| 0 | 时间不变 |
| 1 | 覆盖(重置倒计时) |
| 2 | 增加(在当前剩余时间上 + Duration) |
| 3 | 独立计时:每层各自倒计时、逐层回收。这种模式下层数固定按「增加」处理,外部调设置层数 / 增减层数都无效 |
时长
Duration大于 0:到点 → 触发DurationFinish事件 → 失去(ModifierLoss)→ 移除;Duration等于 0:永久(EndTime为 -1)。永久效果调延长时长 / 设置剩余时间都无效,会直接跳过。
暂停与恢复
- 暂停:冻结倒计时和叠加计时器(
IsPaused变 true),暂停期间时间不流逝; - 恢复:把暂停的时长补偿回去(结束时间往后推),并重建倒计时;独立计时模式下逐层恢复,已经到期的层立刻回收。
实例上限
单个生物最多同时存在 99 个效果实例。达到上限时,新实例会被销毁并返回 rejected。
获得拦截
- 触发点:只在「新实例激活」这条路径上——激活前依次触发效果级的
BeforeObtain和拥有者级的ModifierObtainBefore,在这两个回调里同步调用SetInterruptModifierObtain()即可拦截; - 被拦截后:不激活、不播表现、注册回滚(新实例销毁),
AddModifier返回rejected; - 只对「新实例获得」有效:叠加刷新(reobtain)不会触发拦截;拦截标记用一次就失效,多个效果同时并发也不会串位。
移除与销毁
| 触发路径 | 行为 |
|---|---|
| 调移除接口 / 清除接口 | 带失活处理:先走失去流程(事件 + 表现)→ 从容器移除 → 销毁实例 |
| 层数归零 / 时长到期 | 同上(带失活处理) |
| 外部脚本直接销毁效果单位 | 系统会补上失活与注销处理,最终状态一致 |
材质共存规则
- 材质只来自获得表现里的材质项;同一个效果配了多条时,取最后一条;
- 客户端按材质栈:多个效果共存时后到的覆盖先到的;移除任意一个效果时,客户端会重新应用栈顶那条(先移除的不会误还原成别人的材质);栈空了自动还原原本材质。
常见问题
Q1:AddModifier 的返回值怎么读?
"added" 新实例成功;"reobtain" 叠加到已有实例(没有新实例);"rejected" 被规则拒绝(非生物 / 不可叠加 / 被拦截 / 达上限);"failed" 系统失败。
Q2:为什么第二次添加同一个预设,没有出现新实例?
因为 Key 相同,自动走了叠加流程:可叠加就是 reobtain,不可叠加就是 rejected;如果勾了「仅同源叠加」且来源不同,则会作为独立实例共存。
Q3:查询为什么查不到刚添加的效果?
服务端立刻就能查到;客户端读的是本地镜像,需要等服务端广播或实体同步到达。判断时以服务端为准。
Q4:场景里预摆放的效果,为什么按 Key 查不到?
预摆放的实例没有 Key。需要按 Key 查找或叠加时,在服务端运行时写 SetAttribute("ModifierKey", ...) 补上。
Q5:改了层数但没变化?
三种可能:① 时间策略是「独立计时」,这种模式下外部改层数无效;② 层数被夹在 [0, MaxStackCount] 之间;③ 客户端的请求还没被服务端处理完。
Q6:效果不消失,或者时间对不上?
先确认 Duration:填 0 是永久,不会自动到期。另外暂停期间时间冻结、恢复会补偿,时间自然对不上——EndTime 是绝对时间戳,UI 显示请用 GetRemainingTimeByKey 取剩余时间。
Q7:材质没有还原,或者被别的效果冲掉了?
材质是客户端按栈处理的:后到的覆盖,移除时重新应用栈顶。请走正常的移除路径(移除接口 / 层数归零 / 到期),不要直接销毁效果单位。
Q8:AttrConfigs 里的属性修改不生效?
它依赖属性系统包,需要先在资源包里引用或导入「复杂属性包」。没引用时这一项会被静默跳过(不报错),所以效果照常生效,只是属性没变。
Q9:拦截为什么没生效?
要同时满足三点:① 在「生物即将获得效果」事件回调里同步调用;② 只对新实例获得有效(叠加刷新不触发);③ 拦截标记用一次即失效。
Q10:一个生物最多挂多少个效果?
99 个实例(叠加后仍按实例数计),超出后新实例会被拒绝。
Q11:DestroyWithModifier 默认是什么?有什么坑?
默认是 true(跟随销毁)。属性面板只会记录「作者改动过」的字段——不勾选不会产生该字段,运行时读到空值也按 true 处理。所以循环特效会跟随效果销毁,不会留在场上。
Q12:包内的默认值能改吗?
可以读,但不建议改。实例上限 99、默认持续时间 5、默认最大层数 999 这些都写在包内配置 common/packages/modifier_system/config.lua 里。单个效果要有差异,请用预设面板或动态添加时传参覆盖,不要去改包内配置。
术语对照
| 中文(本文用词) | 代码 / 编辑器里的名字 |
|---|---|
| 效果系统(包名) | modifier_system |
| 效果 / 效果实例 | modifier |
| 效果预设 | 编辑器预设,类型 modifier |
| 效果 Key | ModifierKey(动态添加时等于预设资产 ID) |
| 拥有者 | owner |
| 效果实体 | 查询接口返回的实例对象 |
| 层数 | CurrCount / StackCount |
| 时间策略 | StackDurationMode |
| 层数策略 | StackCountMode |
| 服务端脚本接口 | ModifierAPI |
| 复杂属性包 | 属性系统所在的那个包;要用「修改属性参数」得先在资源包里引用它 |
当前版本边界
- 没有独立的「效果管理器」节点,全局服务自动管理;
- 效果不需要在场景里预摆放:既可以运行时动态添加,也可以在生物预设下直配(见第 5 节)。
