Skip to content

效果系统(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 里手动删。

在生物预设下挂「效果预设」子组件 ​

不写代码的做法:

  1. 打开生物预设(要加效果的那个生物);
  2. 在它下面挂载子组件:分类选「服务端脚本」,类型选「效果预设」;
  3. 选中这个子组件,在右侧参数面板里设置参数(持续时间、能否叠加、获得 / 失去表现……逐项见第 5.4 节)。

配好保存后,游戏里该生物就带上了这个效果,不需要任何运行时脚本。

两种用法怎么选 ​

接口动态添加(第 4、6 节)预设直配(本节)
要写脚本吗要不用
生效范围你指定的单位、你指定的时机所有使用该生物预设的生物,生成即生效
时长 / 层数能运行时改吗能只能按面板配好的值走
适合场景条件触发、动态增减、关卡逻辑固定配置、批量行为、快速验证

两种方式可共用

两种方式可以同时用,互不冲突。

效果预设的参数逐项说明 ​

字段名就是运行时读属性的名字,用 GetAttribute("字段名") 读。

分组字段类型默认值说明
基础Duration数字5持续时间(秒)。填 0 表示永久
UgcModifierType枚举20 有益 / 1 有害 / 2 中立
RemoveMode枚举10 无 / 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")

服务端是权威端:查询和修改都直接生效。

函数参数返回说明
AddModifierownerUnit, assetId, addConfig?结果字符串添加效果,返回值含义见下表
RemoveModifiermodifierUnit布尔按效果实例移除(带失活处理)
ClearUnitModifiersownerUnit, modifierKey?整数按 Key 移除全部匹配;Key 为空则清除全部
GetUnitModifiersownerUnit, modifierKey?效果实例列表查效果实例;Key 为空返回全部
IsInModifierownerUnit, modifierKey布尔是否拥有指定 Key;Key 为空时表示「是否有任意效果」
SetModifierStackCountmodifierUnit, count布尔设置层数;设为 0 会触发带失活的移除
AddModifierStackCountmodifierUnit, delta整数增减层数(可为负),返回新层数
AddModifierDurationByInstancemodifierUnit, extra布尔延长持续时间(可为负)
SetModifierRemainTimemodifierUnit, remaining布尔设置剩余时间(秒)
GetModifierOwnermodifierUnit拥有者或 nil取效果实例的拥有者
PauseownerUnit, modifierKey, unitId?布尔暂停:冻结倒计时与叠加计时器
ResumeownerUnit, modifierKey, unitId?布尔恢复:补偿暂停时长并重建倒计时
SetInterruptModifierObtain—true阻止当前这次效果获得;仅「生物即将获得效果」事件内有效
GetRemainingTimeByKeyownerUnit, modifierKey数值剩余时间;永久效果返回 -1
GetSourceByKeyownerUnit, 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")

客户端没有添加接口(添加是服务端的权威操作)。修改类接口全部是发请求给服务端执行,返回值只代表「请求是否已发出」;查询类读的是本地镜像。

函数参数返回说明
RemoveModifiermodifierUnit布尔请求移除效果实例
ClearUnitModifiersownerUnit, modifierKey?整数(恒为 0)请求按 Key 移除 / 清除全部
SetModifierStackCountmodifierUnit, count布尔请求设置层数
AddModifierStackCountmodifierUnit, delta整数(恒为 0)请求增减层数
AddModifierDurationByInstancemodifierUnit, extra布尔请求延长持续时间
SetModifierRemainTimemodifierUnit, remaining布尔请求设置剩余时间
PauseownerUnit, modifierKey, unitId?布尔请求暂停
ResumeownerUnit, modifierKey, unitId?布尔请求恢复
GetUnitModifiersownerUnit, modifierKey?效果实例列表查询(本地镜像)
IsInModifierownerUnit, modifierKey布尔查询(本地镜像)
GetRemainingTimeByKeyownerUnit, 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 的已有实例,按这个顺序判断:

  1. 已有实例不可叠加(Stackable = false)→ 新实例销毁,返回 rejected;
  2. 勾了「仅同源叠加」且来源不同(两边都有来源时)→ 不叠加,作为独立实例共存;
  3. 其他情况 → 叠加:把层数和时间策略应用到已有实例上,新实例销毁,返回 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
效果 KeyModifierKey(动态添加时等于预设资产 ID)
拥有者owner
效果实体查询接口返回的实例对象
层数CurrCount / StackCount
时间策略StackDurationMode
层数策略StackCountMode
服务端脚本接口ModifierAPI
复杂属性包属性系统所在的那个包;要用「修改属性参数」得先在资源包里引用它

当前版本边界 ​

  • 没有独立的「效果管理器」节点,全局服务自动管理;
  • 效果不需要在场景里预摆放:既可以运行时动态添加,也可以在生物预设下直配(见第 5 节)。