主题
技能系统 API 文档
- 快速上手
- 心智模型与数据流
- 编辑器侧:三种预设
- 服务端 API 速查
- 客户端 API 速查
- handler 接口(进阶)
- 事件系统(怎么监听)
- 属性速查
- 状态机语义(必须知道的规则)
- 锚点系统
- 子技能组(槽位共享)
- 扩展:自定义指示器 / 释放策略
- 技能栏 UI 节点约定
- 道具箱(item_box,可选)
- 附录:常见坑 FAQ
快速上手
步骤 1 · 建「技能」预设
推荐走"按预设类型新建":在预设(新建)面板里选类型 「技能」,编辑器会按该类型的模板生成一个已经接好的预设: Script(装全部配置的壳源码)+ 本地运行脚本 子节点。 属性面板会按壳源码自动生成可编辑字段(CD 时间、施法时间、指示器、充能、子技能……见 §技能预设配置属性)。 保存后得到该技能的 AssetId(map://preset/...) —— 后续装备技能用这个字符串。
手工创建方式
也可以手工建:新建 Script 单位,把 server/packages/ability_system/ability_script.lua 的源码贴进去。手工方式要自己补齐子节点(本地运行脚本等),所以优先用类型新建。
步骤 2 · 建「技能背包」预设
同样按预设类型新建:选类型 「技能背包」,生成的预设含 Script(背包壳源码,装 InitAbilities 等配置)+ 本地运行脚本。 在 InitAbilities 列表里填「技能预设 + 槽位」。也可以留空,全部运行时用 AddAbility 加。
步骤 3 · 把技能背包挂到角色下
技能背包是角色(Character)的子单位。挂上后服务端会自动:
- 解析归属(写
OwnerId/PlayerId); - 按
InitAbilities逐个装备技能; - 打
AbilityManager标签,AddAbility类接口靠它反查; - 若技能配置了子技能列表,自动建子技能组。
步骤 4 · 装备 / 施放 / 监听(服务端)
lua
-- 服务端脚本
local AbilityAPI = require("server.packages.ability_system.api")
local abilityScript = AbilityAPI.AddAbility(unit, "map://preset/你的技能预设key") -- 不传槽位=自动找空槽
if not abilityScript then
print("[demo] 装备失败")
return
end
-- 监听技能事件:事件单位是技能单位下的 BindableEvent 子单位
abilityScript:FindFirstChild("CastStart"):Connect(function(script)
print("[demo] 施法开始,槽位 =", script:GetAttribute("Index"))
end)
abilityScript:FindFirstChild("CDEnd"):Connect(function(script)
print("[demo] CD 结束")
end)
-- 监听锚点(第二参数是锚点组 Id)
abilityScript:FindFirstChild("AnchorStart"):Connect(function(script, groupId)
print("[demo] 锚点点火 groupId =", groupId)
end)
-- 主动施放(服务端权威)
AbilityAPI.CastAbility(unit, 0) -- 按预设指示器/策略施放
AbilityAPI.StopAbility(unit, 0) -- 打断 / 停止
AbilityAPI.AccumulateAbility(unit, 0) -- 进入蓄力(需 EnableAccumulate=true)步骤 5 · 用锚点做时间轴动作
在该技能预设下建「锚点」子节点(见 §锚点系统),选一种锚点行为(近战攻击 / 创建特效 / 激活冷却…), 把 anchors/<行为>.lua 挂上,然后在对应事件里写反应即可。大部分常见动作不需要自己写逻辑, 先用现成的 16 种锚点行为(§16 种锚点行为)拼装。
心智模型与数据流
| 概念 | 一句话 |
|---|---|
| 技能(ability) | 一张配置表。属性面板里填的每个字段都会变成运行时属性,用 script:GetAttribute("字段名") 读 |
| 技能背包(ability_manager) | 槽位容器。只管"哪个槽位装哪个技能",不含施法逻辑 |
| 状态机 | CD / 施法 / 蓄力 / 充能 四套状态机由包驱动,作者只负责填配置和监听事件写反应 |
| 锚点(anchor) | 时间轴上的动作,自带 4 个事件(开始 / 打断 / 结束 / 停止)。服务端运行,按 Phase 在蓄力或施法窗口内点火 |
| 事件 | 全是挂在单位下的 BindableEvent 子单位,用 单位:FindFirstChild("<事件名>"):Connect(fn) 监听 |
[客户端输入] Controller → Strategy(释放策略) → Pointer(指示器)
│ RemoteEvent "AbilitySystem"
│ 单参数 table:{ action, ownerId, index, args = {...} }
▼
[服务端权威] ability_logic(CD/施法/蓄力/充能)
├─→ Anchor 调度器(按 StartTime/Duration 点火)
│ └─→ anchors/<行为>.lua(现成动作)
└─→ 事件单位 Fire(技能级 / 锚点级 / 管理器级)
│ RemoteEvent s→c 广播(OnCastStart / OnCDEnd / OnCharge / OnSwitchNext / ForbidSlot …)
▼
[客户端表现] ability_local_script(属性变化 → AbilityUIHooks)→ UIManager(技能栏 UI)权威性:施法、CD、充能、伤害结算全在服务端。客户端只发"请求"(RequestCast 等), 服务端 canUse() 通过才真正施放。所以客户端界面转好了不代表服务端放行。
编辑器侧:三种预设
两种创建方式
| 方式 | 做法 | 结果 |
|---|---|---|
| ① 按预设类型新建(推荐) | 在预设(新建)面板选类型:技能 / 技能背包 / 锚点 | 出来的预设已接好:根 Script(装壳源码)+ 子 本地运行脚本,只需改名 + 调参 |
| ② 手工创建 | 新建 Script 单位,把对应壳文件源码贴进 SourceCode | 需要自己补齐子节点(如 本地运行脚本),容易漏 |
三种类型与对应壳文件
| 预设类型(编辑器显示) | 根节点 | 子节点 | 作用 | 对应壳文件 |
|---|---|---|---|---|
| 技能 | Script | LocalScript | 一个具体技能,承载全部配置与时间轴 | server/packages/ability_system/ability_script.lua |
| 技能背包 | Script | LocalScript | 技能容器,挂在角色下管理槽位 | server/packages/ability_system/ability_manager_script.lua |
| 锚点 | Script | — | 技能时间轴上的一个动作 | server/packages/ability_system/anchor_script.lua + anchors/<行为>.lua |
预设与包的关系
也就是说:新建出来的预设本身不写逻辑,它的根 Script 只声明可编辑属性 + require(...).Attach(script);
真正的行为在包里(ability_logic.lua / ability_manager_logic.lua / anchor_logic.lua + anchors/<行为>.lua)。
换/升级包版本时,已建好的预设无需改动。
内置结构体(common/packages/ability_system/data.lua):
lua
ability_system.AbilityInfo = { AssetId = "", Index = 0 } -- 初始技能一项
ability_system.SubAbilityInfo = { AssetId = "" } -- 子技能一项槽位规则(贯穿全部 API)
- 槽位 0 基、无上限:
0是合法槽位,1/2/3...同理,不存在"最大槽位数"。 nil= 不指定(AddAbility会自动找第一个空槽)。-1(AbilityConstants.SLOT_INDEX_UNASSIGNED)= 未入槽哨兵:子技能休眠态、未分配槽位的过渡值。 它不是合法槽位,客户端也因此不会给它绑 UI。
服务端 API 速查
服务端 API 引入
local AbilityAPI = require("server.packages.ability_system.api")
模块加载时(服务端)会自动注册客户端请求监听(幂等),作者不需要手动调 RegisterEvents。
| 函数 | 签名 | 返回 | 说明 |
|---|---|---|---|
AddAbility | (unit, abilityKey, slotIndex?) | abilityScript?, err? | 装备技能。不传槽位自动找空槽;同槽同预设复用现有实例;同槽被别的预设占用 → nil, err |
GetAbility | (unit, abilityIndex) | abilityScript? | 按槽位取技能脚本单位 |
GetAbilityByScript | (abilityScript) | handler? | 按技能脚本反查 handler |
GetAbilities | (unit) | abilityScript[] | 全部已装备技能(按槽位升序) |
GetAbilitiesFromIndexRange | (unit, startIndex, endIndex) | abilityScript[] | 指定槽位区间(含两端) |
CastAbility | (unit, abilityIndex, releasePoint?, releaseDir?, releaseTarget?) | boolean, err? | 服务端权威施放。返回 false 表示被拒(CD/施法中/无充能/外部条件) |
StopAbility | (unit, abilityIndex) | boolean, err? | 打断施法;若还在蓄力阶段则退出蓄力 |
AccumulateAbility | (unit, abilityIndex) | boolean, err? | 进入蓄力(EnableAccumulate=false 或 CD/充能不满足时返回 false) |
SwitchNextAbility | (unit, parentSlotIndex) | boolean, err? | 子技能组切换到下一个成员(任意 SwitchMode 都可显式调用) |
SetAbilityToSlot | (unit, ability, slotIndex) | abilityScript? | 把已装备技能移动到指定槽位 |
RemoveAbility | (unit, slotIndex) | abilityScript? | 按槽位移除并销毁 |
RemoveAbilityByKey | (unit, abilityKey) | — | 按预设 Key 移除该单位所有对应技能 |
RegisterEvents | () | — | 内部用;模块加载时自动调用(幂等) |
unit 参数是什么? 是拥有技能背包的单位(角色),也可以是技能背包本身。 实现里先按 AbilityManager 标签在 unit 自身及其子节点上找管理器,所以两种传法都成立。
失败信息:所有错误返回值都带 [ability_system] 前缀,例如 "[ability_system] AddAbility: slot is occupied by another ability"、"[ability_system] CastAbility: ability not found in slot"。
完整示例:装备 + 常驻监听 + 动态换槽
lua
local AbilityAPI = require("server.packages.ability_system.api")
local M = {}
function M.setup(unit)
-- 1) 指定槽位装备(0 基)
local shield = AbilityAPI.AddAbility(unit, "map://preset/盾技能预设key", 2)
-- 2) 自动找空槽装备
local dash = AbilityAPI.AddAbility(unit, "map://preset/冲刺预设key")
-- 3) 升级(前提:该技能 Upgradable=true 且未到 MaxLevel,且满足 LevelRequirements)
local handler = AbilityAPI.GetAbilityByScript(dash)
if handler and handler.canUpgrade() then
handler.upgrade(1)
print("新等级 =", handler.getLevel())
end
-- 4) 换槽:把 2 号槽的盾挪到 5 号槽
AbilityAPI.SetAbilityToSlot(unit, shield, 5)
-- 5) 遍历所有技能
for _, script in ipairs(AbilityAPI.GetAbilities(unit)) do
print(script:GetAttribute("Index"), script:GetAttribute("CdTime"))
end
end
return M客户端 API 速查
客户端 API 引入
local AbilityClientAPI = require("client.packages.ability_system.api")
客户端只能"发请求",真正的施放判定在服务端。
| 函数 | 签名 | 返回 | 说明 |
|---|---|---|---|
GetManagerForUnit | (unit) | managerScript? | 按 AbilityManager 标签反查管理器(自身或子节点) |
GetAbility | (unit, abilityIndex) | abilityScript? | 按槽位取技能(遍历管理器子节点的 Index 属性) |
RequestCast | (manager, abilityIndex, releasePoint?, releaseDir?, releaseTarget?) | boolean, err? | 请求施放(client→server) |
RequestStop | (manager, abilityIndex) | boolean, err? | 请求打断/停止 |
RequestAccumulate | (manager, abilityIndex) | boolean, err? | 请求进入蓄力 |
RequestSwitchNext | (manager, parentSlotIndex) | boolean, err? | 请求子技能轮转 |
GetUIManager | () | UIManager | 取技能栏 UI 管理器单例(懒初始化) |
Register | (domain, key, value) | — | 注册自定义指示器 / 释放策略(见 §自定义指示器与释放策略) |
StartClientLifecycle | () | — | 引擎内部:等 LocalPlayer、监听重生、自动挂接管理器(幂等)。作者通常无需调用 |
参数差异
参数差异要注意:服务端 API 收 unit(角色),客户端 API 收 manager(技能背包单位)。写反了会直接返回失败。
lua
-- 客户端:自绘快捷栏点击施法
local Players = game:GetService("Players")
local AbilityClientAPI = require("client.packages.ability_system.api")
local manager = AbilityClientAPI.GetManagerForUnit(Players.LocalPlayer.Character)
if manager then
AbilityClientAPI.RequestCast(manager, 0) -- 0 号槽:走预设的指示器/策略
AbilityClientAPI.RequestAccumulate(manager, 1) -- 1 号槽:进入蓄力
AbilityClientAPI.RequestStop(manager, 1) -- 松开:停止/施放
AbilityClientAPI.RequestSwitchNext(manager, 2) -- 2 号槽:子技能轮转
endhandler 接口(进阶)
handler 是"技能实例在服务端的操作句柄",从注册表反查:
lua
local AbilityRegistry = require("common.packages.ability_system.registry")
local handler = AbilityRegistry.getAbilityHandler(abilityScript) -- 技能 handler
local managerHandler = AbilityRegistry.getManagerHandler(managerScript) -- 技能背包 handler
local manager2 = AbilityRegistry.findManagerForPlayer(player) -- 按玩家反查背包 handlerhandler 调用方式
这些方法内部已闭包持有脚本引用,直接用点调用 handler.getLevel() 即可(用冒号也不会报错,多余参数被忽略)。
下划线开头的方法(_getRawAttribute / _tryStartCharge …)属内部实现,不保证跨版本稳定。
技能 handler
| 分类 | 方法 | 说明 |
|---|---|---|
| 基础 | getId() / getScript() / getManager() | UnitId / 技能脚本 / 所属背包 |
getSignals() | 取该技能的事件单位表(signals.CastStart 等) | |
ConnectEvent(eventName, func) | 便捷监听:handler.ConnectEvent("CastEnd", fn) → 连接句柄或 nil | |
_getRawAttribute(name) | 读原始属性(内部/筛选器用) | |
| CD | enterCD(cdTime?) | 进入 CD(不传则用自身 CdTime) |
getLeftCD() / getCdTime() / setCurrentCD(cdTime) | 剩余 CD / CD 总时长 / 改 CD 并立即重进 | |
| 施法 | canUse() | 是否可施放(CD、施法中、充能、外部条件综合判定) |
startCast(releasePoint?, releaseDir?, releaseTarget?) | 开始施法(返回 boolean) | |
endCast(broken) / breakCast() | 结束 / 打断施法(作者一般用 breakCast) | |
| 蓄力 | startAccumulate() / endAccumulate(broken) / isAccumulating() | 进/退/查蓄力 |
getAccumulateRatio() | 当前蓄力比 0~1(startCast 时会被捕获进 AccumulateRatio 属性) | |
| 充能 | getChargeCount() / getMaxChargeCount() / getChargeInterval() | 当前 / 上限 / 间隔 |
changeChargeCount(delta) | 增减充能(自动夹到 [0, MaxChargeCount],满了发 ChargeFull) | |
onChargeTick() | 手动 +1 次充能 | |
getIsInCharge() / getChargeLeftTime() / setChargeLeftTime(time) | 是否充能中 / 本周期剩余 / 改剩余(0 = 立刻完成本次充能) | |
setChargeConfig(enable, maxCount?, chargeType?, interval?, amount?) | 运行时改充能配置 | |
| 等级 | getLevel() / getMaxLevel() / canUpgrade() | 查询 |
upgrade(delta?) / increaseLevel(delta?) | 升级(前者校验 canUpgrade,后者强制)/ 均会向背包发 AbilityUpgraded | |
decreaseLevel(delta?) / setLevel(l) / setMaxLevel(m) | 降级 / 设置(会自动夹取范围) | |
| 释放参数 | getReleasePoint() / getReleaseDirection() / getReleaseTarget() | 本次释放的点 / 方向 / 目标 |
setReleaseDistance(d) / setReleaseRadius(r) | 改施法距离 / 半径 | |
| 使用限制 | getLimitation(type) / setLimitation(type, enable) | 位掩码读写(见 §状态机语义) |
| 外部条件 | addUseCondition(key, func) / removeUseCondition(key) | 注入自定义"能不能放"的条件(返回 false 即禁止) |
| 其它 | removeSelf() / canPick() | 从背包移除自己 / 当前恒返回 true |
lua
-- 用外部条件实现"只有血量 > 50% 才能放"
local hp = 1.0
handler.addUseCondition("hp_gate", function()
return hp > 0.5
end)
-- 之后 handler.canUse() 会连带判定;不再需要时 handler.removeUseCondition("hp_gate")技能背包 handler
| 方法 | 说明 |
|---|---|
getManager() / getOwner() / getOwnerId() | 背包脚本 / 拥有者单位 / 拥有者玩家名 |
getAbilities() / getAbilitiesInSlotRange(a, b) | 技能列表(按槽位升序)/ 区间列表 |
getAbility(slotIndex) | 槽位 → handler |
getFirstAvailableSlot() | 第一个空槽索引 |
addAbility(abilityScript, slotIndex) / removeAbility(slotIndex) | 装 / 卸(一般用 AbilityAPI.AddAbility 更省事) |
moveAbility(fromIndex, toIndex) | 交换两个槽位(含 Index 属性与广播) |
setSlotAbility(slotIndex, abilityScript) | 整体替换槽位成员(子技能切换用) |
stopAbility(slotIndex) | 打断指定槽位施法 |
forbidSlot(slotIndex, isForbid, grayout) | 禁用/启用槽位(广播给所有客户端,UI 灰显) |
notifySwitchNext(slotIndex, newActiveIndex) | 广播子技能切换 |
common 注册表
| 分类 | 方法 | 说明 |
|---|---|---|
| 技能 | getAbilityHandler(script) / registerAbility / unregisterAbility | 技能 handler 反查/注册 |
| 背包 | getManagerHandler(script) / findManagerForPlayer(player) … | 背包 handler 反查 |
| 组 | findGroup(parentScript) / findGroupByChild(childScript) | 子技能组反查 |
| 域 | register(domain, key, value) / get(domain, key) / list(domain) | 开放域("PointerType" / "ReleaseStrategy") |
| 标签 | addTag(unit, tag) / hasTag(unit, tag) / removeTag(unit, tag) / clearTags(unit) | 标签读写(引擎组件缺失时走 Lua 兜底) |
| 键 | unitKeyOf(scriptUnit) | 统一键推导(优先 UnitId,异常时回退字符串) |
事件系统(怎么监听)
事件单位模型
- 事件 = 挂在某个单位下的
BindableEvent子单位,名字就是事件名。 - 监听:
单位:FindFirstChild("<事件名>"):Connect(function(...) end)。 - 生命周期不依赖父子级联销毁:所有者销毁时框架会显式销毁事件单位(技能 / 锚点 / 管理器都已处理)。 自建事件单位请在销毁时自行清理。
- 事件是本端本地广播:服务端触发只对服务端监听者有效;客户端表现靠属性同步 + s→c 广播。
技能级事件(挂在技能单位下,共 18 个)
| 事件名 | 载荷 | 时机 |
|---|---|---|
BeforeUse | (abilityScript) | 使用尝试开始(合法性检查前,结果未定) |
Used | (abilityScript) | 本次使用已生效(检查通过、蓄力放行、充能扣减之后) |
BeforeCast | (abilityScript, releasePoint, releaseDir) | 施法前 |
CastStart | (abilityScript) | 施法开始 |
CastEnd | (abilityScript) | 施法完成(正常结束) |
CastBreak | (abilityScript) | 施法被打断(技能销毁时也会发一次) |
CDStart | (abilityScript) | 进入 CD |
CDEnd | (abilityScript) | CD 结束 |
Charge | (abilityScript, chargeCount) | 充能层数变化 |
ChargeFull | (abilityScript) | 充能已满 |
AccumulateStart | (abilityScript) | 蓄力开始 |
AccumulateBreak | (abilityScript) | 蓄力被打断(先于 AccumulateEnd) |
AccumulateEnd | (abilityScript) | 蓄力结束(任何退出路径必发) |
OnDowngrade | (abilityScript, newLevel) | 技能降级 |
AnchorStart | (abilityScript, groupId) | 锚点点火(技能级镜像,带 groupId) |
AnchorBreak | (abilityScript, groupId) | 锚点中断 |
AnchorEnd | (abilityScript, groupId) | 锚点结束 |
AnchorStop | (abilityScript, groupId) | 锚点终结(Break/End 之后必发) |
锚点级事件(挂在锚点单位下,4 个)
名字相同(AnchorStart / AnchorBreak / AnchorEnd / AnchorStop),但:
- 主体是锚点自身,载荷只有
(abilityScript)—— 监听方不需要用groupId过滤; - 优势:一个技能有多个同类锚点时,可以逐个锚点单独监听。
lua
local anchor = abilityScript:FindFirstChild("你的锚点节点名")
anchor:FindFirstChild("AnchorStart"):Connect(function(abilityScript)
-- 这个锚点点火了(不需要区分是哪个 groupId)
end)管理器级事件(挂在技能背包单位下)
| 事件名 | 载荷 | 时机 |
|---|---|---|
AbilityAdded | (abilityScript, slotIndex) | 技能加入背包 |
AbilityRemoved | (abilityScript, slotIndex) | 技能移出背包 |
AbilityUpgraded | (abilityScript, newLevel) | 技能升级 |
子技能组事件(挂在父技能单位下)
| 事件名 | 载荷 |
|---|---|
OnActivated | (newHandler, newIndex) |
OnDeactivated | (oldHandler, oldIndex) |
OnSwitchNext | (newHandler, newIndex) |
服务端 → 客户端广播(RemoteEvent "AbilitySystem")
客户端已内部处理这些广播,作者通常无需关心;仅在你自绘技能栏 UI 时才需要知道:
| action | args |
|---|---|
OnCastStart | { abilityUnitId, releasePoint, releaseDir } |
OnCastEnd / OnCastBreak | { abilityUnitId } |
OnCDStart | { abilityUnitId, cdTime } |
OnCDEnd | { abilityUnitId } |
OnCharge | { abilityUnitId, newCount } |
OnSwitchNext | { ownerUnitId, slotIndex, newActiveIndex } |
ForbidSlot | { ownerUnitId, slotIndex, isForbid, grayout } |
AbilityAdded / AbilityRemoved / AbilityMoved | 槽位增删与移动的兜底通道 |
客户端 UI Hook(自绘 UI 用)
不想自己重写技能栏 UI、只想改某些表现时,可覆盖 UI hook:
lua
local AbilityUIHooks = require("common.packages.ability_system.ui_hooks")
AbilityUIHooks.setUIHooks({
onInCDChange = function(abilityScript, isInCD, cdFinishTime) end,
onChargeCountChange = function(abilityScript, chargeCount, maxChargeCount) end,
onAccumulateChange = function(abilityScript, startTime) end,
onPointerTypeChange = function(abilityScript, pointerType) end,
onReleaseTypeChange = function(abilityScript, releaseType) end,
onIndexChange = function(abilityScript, newIndex) end,
onAbilityAdded = function(managerScript, abilityScript, slotIndex) end,
onAbilityRemoved = function(managerScript, slotIndex) end,
})可覆盖的 hook 全集(共 11 个):onInCDChange、onInCastChange、onChargeCountChange、 onAccumulateChange、onPointerTypeChange、onReleaseTypeChange、onIndexChange、onOwnerIdChange、 onAbilityAdded、onAbilityRemoved、onSwitchNext。 setUIHooks 只替换已存在且是函数的键,传未知键会被忽略。
未覆盖 hook 的默认表现
未覆盖的 hook 仍走内置技能栏 UI 的默认表现。
属性速查
技能预设配置属性(编辑器可配)
来源:server/packages/ability_system/ability_script.lua。属性名 = 字段名,运行时用 script:GetAttribute("字段名") 读。
| 分组 | 字段 | 类型 | 默认 | 说明 / 显隐条件 |
|---|---|---|---|---|
| 基础 | CdTime | number | 3.0 | 冷却时间(秒) |
CastTime | number | 0.5 | 施法时间(秒),到点自动完成施法 | |
| 升级 | Upgradable | boolean | false | 能否升级 |
MaxLevel | integer | 5 | 技能最大等级(Upgradable==true) | |
LevelRequirements | Int[] | {} | 各等级所需角色等级(下标=目标技能等级)(Upgradable==true) | |
| 蓄力 | EnableAccumulate | boolean | false | 能否蓄力 |
MaxAccumulateTime | number | 1.0 | 满蓄力时间;蓄力比 = 已蓄时长 / 该值(上限 1)(EnableAccumulate==true) | |
| 限制/释放 | UseLimitation | Int(枚举) | 0 | 0 无限制 / 1 施法可用 / 2 失控可用 / 4 滚动可用 |
ReleaseType | Int(枚举) | 0 | 0 松开施放 / 1 按住持续 / 2 两段式点击 / 3 按下施放 | |
| 指示器 | PointerType | Int(枚举) | 0 | 0 无 / 1 矩形 / 2 圆形 / 3 扇形 / 4 抛物线 |
ReleaseDistance | number | 10.0 | 施法范围(PointerType!=0) | |
AffectWidth | number | 1.0 | 影响宽度(PointerType==1) | |
SectorAngle | number | 90.0 | 扇形广角(PointerType==3) | |
ReleaseRadius | number | 3.0 | 影响半径(PointerType in {2,3}) | |
ParabolaHorizontalSpeed | number | 10.0 | 水平速度(PointerType==4) | |
ParabolaVerticalSpeed | number | 10.0 | 垂直速度(PointerType==4) | |
| 目标筛选 | TargetFilterType | String[] | {} | 目标类型筛选(空=不筛选,多选任一命中即通过,如 "EggyUnit"/"HumanUnit"/"PhysicsUnit") |
TargetType | Int(枚举) | 1 | 0 无目标 / 1 方向 / 2 点 / 3 单位 | |
TargetFilterCamp | Int(枚举) | 0 | 0 不限 / 1 敌方 / 2 友方 | |
TargetRequiredTags | String[] | {} | 必须标签(全部满足才命中) | |
TargetIgnoredTags | String[] | {} | 忽略标签(含任一即跳过) | |
| 充能 | IsChargeConsuming | boolean | false | 是否消耗充能 |
ChargeType | Int(枚举) | 0 | 0 不充能 / 1 不满时充能 / 2 用完后充能 | |
MaxChargeCount | integer | 5 | 充能上限 | |
ChargeAmount | integer | 1 | 每次充能增加次数 | |
ChargeInterval | number | 1.0 | 充能间隔(秒) | |
| 子技能 | SwitchMode | Int(枚举) | 0 | 0 不切换 / 1 施法后切换 / 2 定时切换 / 3 仅手动 |
SwitchOrder | Int(枚举) | 0 | 0 顺序循环 / 1 洗牌 / 2 随机 | |
SubAbilities | List<ability_system.SubAbilityInfo> | {} | 子技能列表(与父技能共用同一槽位) | |
SwitchInterval | number | 3.0 | 切换间隔(SwitchMode==2) | |
SwitchCooldown | number | 0.5 | 切换后冷却(SwitchMode!=0) | |
RevertTime | number | 0 | 回归计时(SwitchMode==1;0=不回归) | |
SwitchRefresh | boolean | true | 子技能间互切是否重置回归倒计时(SwitchMode==1) |
技能运行时属性(服务端写入,客户端同步可见)
只读使用,不要手动改(改了不会触发状态机):
| 属性 | 含义 |
|---|---|
Index | 槽位号;-1 = 未入槽(子技能休眠) |
Level | 当前等级(初始 1) |
InCD / CdFinishTime | 是否 CD 中 / CD 结束的服务器时间戳 |
InCast / CastStartTime | 是否施法中 / 施法开始的服务器时间戳 |
AccumulateStartTime | >0 = 蓄力中;退出蓄力清零 |
AccumulateRatio | 本次施法捕获的蓄力比 0~1(在 CastStart 钩子里读) |
ChargeCount | 当前充能层数(充能型初始 = MaxChargeCount,否则 0) |
ReleasePoint / ReleaseDir / ReleaseTarget | 本次释放的点 / 方向 / 目标 |
OwnerId | 归属玩家名(由技能背包写入;"" = 尚未解析) |
AbilityName / AbilityPresetKey | 装备时写入的技能名 / 预设 Key |
客户端 ability_local_script 只对以下 8 个属性的变化做响应式绑定(自绘 UI 可参考): InCD、InCast、ChargeCount、PointerType、ReleaseType、Index、OwnerId、AccumulateStartTime。
从视角读 UI 需要的量(客户端的通用做法):
lua
local leftCD = (script:GetAttribute("CdFinishTime") or 0) - World:GetServerTime()
local ratio = World:GetServerTime() > 0 and (World:GetServerTime() - script:GetAttribute("AccumulateStartTime")) / script:GetAttribute("MaxAccumulateTime") or 0技能背包属性
| 属性 | 编辑器可配 | 说明 |
|---|---|---|
InitAbilities | 是 | List<ability_system.AbilityInfo>,初始技能列表(每项 = 技能预设 AssetId + 槽位) |
OwnerId / PlayerId / ManagerReady | 否 | 运行时由服务端写入(归属玩家名 / UserId / 就绪标记) |
状态机语义(必须知道的规则)
能不能放:canUse()
依次判定,任一不满足即禁止:
- 不在 CD(
InCD == false); - 不在施法中(
InCast == false); - 若是充能消耗型:
ChargeCount > 0; - 所有通过
addUseCondition注入的外部条件都返回真。
施放被拒的处理
被拒时 startCast 直接返回 false,并且如果此时正处于蓄力态,会兜底退出蓄力。
CD 起点与"锚点优先、框架兜底"
- 正常结束施法(
CastEnd)时才由框架自动进 CD;被打断不进 CD(可由作者在自己的反应里决定)。 - 如果本轮使用中已经有锚点(如
【锚点】激活冷却)调用了handler.enterCD(), 框架的兜底 CD 不会再重复进一次。 - 这解决的是"CD 走完又走一次 / UI 环转好了还要等一会儿才能再放"的问题。 结论:想让 CD 由锚点精确控制 → 挂
active_cd锚点;想用统一 CD → 不挂,用CdTime兜底。
蓄力(Accumulate)
- 按住 →
startAccumulate()(需EnableAccumulate=true,且canUse()通过); - 蓄力比实时读
handler.getAccumulateRatio(); - 转入施法时,框架把这个比值写进
AccumulateRatio属性 → 在CastStart里读它做威力缩放; - 退出路径:
AccumulateBreak(被打断,先发)→AccumulateEnd(任何路径必发)。
充能(Charge)
ChargeType:1不满时充能 /2用完后充能 /0不充能;- 每次施法扣 1 层(在
canUse通过之后扣); - ⚠️
ChargeType=0用完就永久不能放了(不可逆),要"能回复"必须把ChargeType设成1或2。
使用限制(位掩码)
UseLimitation 是位掩码:1 施法中可用 / 2 失控可用 / 4 滚动可用 / 8 冰冻可用。 可用 handler.getLimitation(type) 查询、handler.setLimitation(type, enable) 动态改。
升级
- 需
Upgradable=true,且Level < MaxLevel; LevelRequirements[下一等级]若存在,则要求owner.CustomProperty.LevelRuleLevel >= 该值;upgrade会向背包发AbilityUpgraded;increaseLevel跳过所有校验强制升。
锚点系统
锚点语义
| 配置 | 说明 |
|---|---|
Phase | 1 蓄力阶段 / 2 施法阶段(锚点只在其所属窗口内生效) |
StartTime | 窗口开始后延迟多少秒点火(0 = 立即) |
Duration | 持续时长(秒);0 = 保持到窗口结束;>0 = 到点自动结束 |
TrackIndex | 时间轴轨道序号(编辑器排版用) |
事件路径(服务端):
Phase=2施法阶段:CastStart→ 按StartTime点火 →CastEnd(正常收尾)/CastBreak(打断);Phase=1蓄力阶段:AccumulateStart→ 点火 →AccumulateEnd/AccumulateBreak;- 点火发
AnchorStart;正常结束发AnchorEnd;被打断发AnchorBreak;之后必发AnchorStop(统一收尾信号)。 - 因此:要"做了就一定要撤销"的效果,写在
AnchorStop里最稳(End / Break 两条路都会走到它)。
锚点预设模板(双层挂载)
按类型 「锚点」 新建时,生成的预设已经把锚点壳源码放在 Script 上;把壳源码最后一行换成你要用的锚点行为模块即可。 锚点预设的源码就是这套结构(两层:框架调度 + 行为逻辑):
lua
---@type number 开始时间
StartTime = 0.0
---开始时间
---@type number 持续时长
Duration = 0.0
---持续时长
---@type Int
---@style enum
---@enum [[1, "Accumulate", "蓄力"],[2, "Cast", "施法"]]
---@title 生效阶段
Phase = 2
---所属阶段
---@type integer 轨道序号
TrackIndex = 0
local RunService = game:GetService("RunService")
if RunService:IsServer() then
local AnchorLogic = require("server.packages.ability_system.anchor_logic")
AnchorLogic.Attach(script) -- ① 框架:排程 + 创建锚点级 4 事件
require("server.packages.ability_system.anchors.melee_hit").Attach(script) -- ② 行为:换成本锚点类型的模块
end锚点单位必须挂在技能单位之下(script.Parent = 技能脚本),否则框架会跳过并打印日志。
16 种锚点行为、配置键、硬约束
现成行为模块位于 server/packages/ability_system/anchors/。下表配置键即属性名(在锚点预设上填); ABILITY_ANOSTATE_* 是编辑器里自动生成的配置键,Offset/Scale/Socket 等为普通键。
| 锚点 | 类型 | 关键配置键(默认值) | 硬约束 / 备注 |
|---|---|---|---|
active_cd | Instant | 无(读宿主 CdTime) | 点火即让宿主进 CD;配合 §CD 起点与框架兜底 判重逻辑 |
cast_face_target | Instant | 无 | 施法者朝向转到释放方向(仅水平 yaw) |
destroy_ability | Instant | 无 | 把宿主技能从背包移除 |
custom_event | Instant | ABILITY_ANOSTATE_EVENT_NAME | 触发 World.CustomEvent[名]:Fire() |
play_vfx | Instant | ABILITY_ANOSTATE_NEW_SOUND、Volume(50.0)、SoundDuration(1.0) | 一次性 3D 音效;CampRoleId 固定 0(所有人可听) |
create_obstacle | Instant | ABILITY_ANOSTATE_NEW_OBJ、Offset(0,0,0)、Scale、ABILITY_ANOSTATE_BIND、ABILITY_ANOSTATE_LIFE_DUR | 实体自管生命周期(Instant 无终结语义) |
create_landmine | Instant | Offset(0,0,0)、ABILITY_ANOSTATE_NEW_OBJ、Scale、ABILITY_ANOSTATE_LIFE_DUR、ABILITY_ANOSTATE_NEW_SFX、ABILITY_ANOSTATE_AREA、ABILITY_ANOSTATE_HITPOWER | 非 Static 需转 Dynamic 才能碰撞;生成后 1s 才注册碰撞、0.8s 后解除免碰撞对 |
create_teleportball | Instant | BallDuration、ABILITY_ANOSTATE_BULLET_OBJ、Offset(0,0,0)、Scale、ABILITY_ANOSTATE_BULLET_HSPEED、ABILITY_ANOSTATE_BULLET_VSPEED | 命中后建传送门并传送施法者;配表时旧的 VSPEED 值填 HSPEED |
create_bullet_to_castangle | Instant | ABILITY_ANOSTATE_NEW_SFX、ABILITY_ANOSTATE_AREA、ABILITY_ANOSTATE_HITPOWER、Offset(0,1,0)、Ability.TargetType==1 分支、ABILITY_ANOSTATE_BULLET_OBJ、Scale、ABILITY_ANOSTATE_BULLET_HSPEED/VSPEED/DUR、ABILITY_ANOSTATE_COLLISION_CASTER_TIME、ABILITY_ANOSTATE_IGNORE_GRAVITATION_TIME、ABILITY_ANOSTATE_ONLYDESTROYBOOM、ABILITY_ANOSTATE_BULLET_HITDESTROY、ABILITY_ANOSTATE_ONLYHITEGGY | 子弹/爆炸击退;不要自己调 bullet:Destroy()(框架已在销毁回调里处理,双销毁会崩) |
bungee_cord | Instant + 内部状态机 | SPRING_ROPE_REST_LENGTH、SPRING_ROPE_STIFFNESS、SPRING_ROPE_DAMPING_COEFFICIENT、SPRING_ROPE_TENSION_FADE_IN_DURATION、SPRING_ROPE_MAX_PER_TENSION、SPRING_ROPE_DRAG_MAX_DURATION、SPRING_ROPE_TOTAL_MAX_LIFETIME、SPRING_ROPE_SPAWN_FORWARD_OFFSET、SPRING_ROPE_SPAWN_UP_OFFSET、SPRING_ROPE_MODEL_ROTATION_CORRECTION、SPRING_ROPE_PARABOLA_GRAVITY_ACC;宿主 ParabolaHorizontalSpeed(10)/ParabolaVerticalSpeed(8) | 绳必须转 Dynamic,否则永不碰撞;SPRING_ROPE_MAX_PER_TENSION 没有兜底默认值,必须填 |
add_bind_diy_model | Constant | ABILITY_ANOSTATE_USE_PERFAB、Socket("origin")、Offset(0,3,0) | 挂到骨骼插槽,终结时销毁 |
gravity_change | Constant-only | ABILITY_ANOSTATE_UP_ACCELERATION | Duration 必须 > 0;期间持续施力使净 Y 加速度≈配置值 |
speed_add | Constant-only | ABILITY_ANOSTATE_WALK_SPEED(可为负) | Duration 必须 > 0;终结时恢复原速 |
play_body_animation | Constant | ABILITY_ANOSTATE_ANIMKEY、ABILITY_ANOSTATE_ANIM_HALF、ABILITY_ANOSTATE_LOOP、ABILITY_ANOSTATE_ANIM_STARTPOINT、ABILITY_ANOSTATE_ANIM_SPEED(1) | 需要施法者 Animator;半身动画固定 FilterType=30 |
play_sfx | Constant | ABILITY_ANOSTATE_NEW_SFX、Offset(0,0,0)、ABILITY_ANOSTATE_SCALE(1.0)、ABILITY_ANOSTATE_BIND | Bind=true 时必须 Duration>0,否则无法回收 |
melee_hit | Constant | Duration(>0)、ABILITY_ANOSTATE_HITBOX_OFFSET(0,1,0)、ABILITY_ANOSTATE_HITBOX_SCALE(2,2,2)、ABILITY_ANOSTATE_BULLET_DAMAGE、ABILITY_ANOSTATE_HITPOWER、ABILITY_ANOSTATE_USE_PERFAB、ABILITY_ANOSTATE_ANIMKEY、ABILITY_ANOSTATE_BINDSOCKET("origin")、ABILITY_ANOSTATE_OFFSET、ABILITY_ANOSTATE_QUD、ABILITY_ANOSTATE_SCALE、ABILITY_ANOSTATE_FACE_SYNC(true)、ABILITY_ANOSTATE_HIT_SFX/_SCALE/_ROTATION、ABILITY_ANOSTATE_BULLET_HITSFXOFFSET | Duration 必须 > 0(否则只打 WARNING 直接 return);命中盒与武器随锚点生命周期回收 |
锚点类监听差异
Instant 类锚点只监听 AnchorStart(点火即完成);Constant 类锚点监听 AnchorStart + AnchorEnd/AnchorBreak/AnchorStop(保证可逆状态被撤销)。
写自己的锚点行为
约定(与现有 16 个模块完全一致):
lua
-- server/packages/ability_system/anchors/my_action.lua
local M = {}
function M.Attach(anchor_script)
local RunService = game:GetService("RunService")
if not RunService:IsServer() then
return
end
-- 1) 点火:监听锚点级 AnchorStart
local start_sig = anchor_script:FindFirstChild("AnchorStart")
if not start_sig then
return
end
start_sig:Connect(function()
local ability_script = anchor_script.Parent -- 宿主技能
local owner = ability_script and ability_script.Parent and ability_script.Parent.Parent -- 施法者
local my_param = anchor_script:GetAttribute("MY_PARAM") or 0
-- 你的表现/结算逻辑
end)
-- 2) 需要可逆效果时,额外监听 AnchorEnd / AnchorBreak / AnchorStop
for _, name in ipairs({ "AnchorEnd", "AnchorBreak", "AnchorStop" }) do
local sig = anchor_script:FindFirstChild(name)
if sig then
sig:Connect(function() end) -- 收尾/清理
end
end
end
return M要点:
- 入口固定为
M.Attach(anchor_script),模块最后return M; - 配置键通过
anchor_script:GetAttribute("<键>")读取(编辑器里在锚点预设上填); - 需要 handler 时用
AbilityRegistry.getAbilityHandler(ability_script); - 需要可逆状态(加移速、加力、挂模型)就必须监听收尾三事件。
子技能组(槽位共享)
配置方式(编辑器,零代码):在技能预设上设 SwitchMode != 0 且 SubAbilities 非空即可 —— 装备技能时会自动创建子技能并建组。
语义:
- 子技能与父技能共用同一槽位:切换时旧的
Index置-1(休眠,不打断进行中的施法),新的占据槽位; - 技能栏 UI 会自动跟随(走槽位增删广播);
- 建组时子技能全部休眠(
Index=-1); SwitchMode:1 AfterCast(活跃成员施法结束轮转)/2 Timer(定时)/3 Manual(仅手动);SwitchOrder:0 Sequential(顺序)/1 Shuffle(每轮洗牌,轮间尾头不重复)/2 Random(不重复上次);SwitchCooldown:切换后新成员进 CD(回归路径不触发);RevertTime(仅 AfterCast):切到非首成员后计时,到点自动回父技能;SwitchRefresh控制子技能互切是否重置倒计时。
监听与手动切换
lua
local AbilityRegistry = require("common.packages.ability_system.registry")
local abilityScript = AbilityAPI.GetAbility(unit, 0)
-- 组级事件(挂在父技能单位下)
abilityScript:FindFirstChild("OnActivated"):Connect(function(newHandler, newIndex)
print("激活成员", newIndex)
end)
abilityScript:FindFirstChild("OnDeactivated"):Connect(function(oldHandler, oldIndex)
print("失活成员", oldIndex)
end)
-- 手动轮转(Manual 模式或任意模式显式触发)
AbilityAPI.SwitchNextAbility(unit, 0)
-- 直接拿到组对象做更细的操作
local group = AbilityRegistry.findGroup(abilityScript) or AbilityRegistry.findGroupByChild(abilityScript)
if group then
group.activateNext() -- 轮转
group.switchTo(2) -- 切到第 2 个成员(1 基)
print(group.getActiveIndex(), #group.getMembers())
end组对象接口(SubAbilityHandler.create 返回):activateNext()、switchTo(index)、getActiveIndex()、getActiveHandler()、getMembers()、getSignals()、destroy()。
手动建组(进阶)
require("server.packages.ability_system.sub_ability").create(parentHandler, parentScript, childHandlers, switchMode, switchOrder, timerDelay, options),options 支持 shareSlot / slotIndex / switchCooldown / revertTime / switchRefresh / destroyWithMembers。
扩展:自定义指示器 / 释放策略
注册入口(唯一):
lua
local AbilityClientAPI = require("client.packages.ability_system.api")
AbilityClientAPI.Register("PointerType", <整数key>, 你的类) -- 指示器
AbilityClientAPI.Register("ReleaseStrategy", <整数key>, 你的类) -- 释放策略内置映射(key 与 AbilityConstants 对齐):
| 域 | key | 类 |
|---|---|---|
PointerType | 0 无 / 1 矩形 / 2 圆形 / 3 扇形 / 4 抛物线 | AbilityPointer / RectanglePointer / CirclePointer / SectorPointer / ParabolaPointer |
ReleaseStrategy | 0 松开 / 1 按住 / 2 两段式 / 3 按下 | ReleaseStrategy / HoldStrategy / TwoStageStrategy / PressStrategy |
查不到 key 时会静默回退到基类(AbilityPointer / ReleaseStrategy),不会报错。
自定义指示器(Pointer)
lua
local AbilityPointer = require("client.packages.ability_system.pointer.AbilityPointer")
local AbilityClientAPI = require("client.packages.ability_system.api")
local MyPointer = {}
setmetatable(MyPointer, AbilityPointer)
MyPointer.__index = MyPointer
MyPointer.super = AbilityPointer
-- 必须实现:接受 (技能脚本, Controller),由框架调用 cls.new(ability, uiController)
function MyPointer.new(ability, uiController)
local obj = AbilityPointer.new(ability, uiController)
obj.effectInfo = { "official://preset/7185", "official://preset/7186", 20.0 } -- {特效, 取消特效, 基础长度/半径}
obj.distanceEffectInfo = { "official://preset/7187", "official://preset/7188", 10.0 }
setmetatable(obj, MyPointer)
return obj
end
-- 可选覆写:Create(dirInfo) / Refresh(dirInfo) / UpdateEffect() / GetReleasePoint(dirInfo) / Clear() / Destroy()
-- 若覆写 Create 仍想要默认特效与选中环,请先调用 self.super.Create(self, dirInfo)
AbilityClientAPI.Register("PointerType", 5, MyPointer)
return MyPointer指示器会读的技能属性:PointerCount、ReleaseDistance、PointerOffset、PointerOffsetAngle、PointerIntervalAngle、 ReleaseRadius(圆形)、ParabolaHorizontalSpeed / ParabolaVerticalSpeed(抛物线)。
自定义释放策略(Strategy)
lua
local BaseStrategy = require("client.packages.ability_system.strategy.BaseStrategy")
local AbilityClientAPI = require("client.packages.ability_system.api")
local MyStrategy = setmetatable({}, BaseStrategy)
MyStrategy.__index = MyStrategy
-- 可选实现:OnTouchBegin(eventData) / OnTouchMove(eventData) / OnTouchEnd(eventData) / Destroy()
-- self.ctrl 是 Controller 实例(框架注入),用它与服务端交互:
function MyStrategy:OnTouchBegin(eventData)
self.ctrl:_requestAccumulate() -- 按下即蓄力
end
function MyStrategy:OnTouchEnd(eventData)
self.ctrl:_requestCast(nil, nil, nil) -- 松开即施放
end
AbilityClientAPI.Register("ReleaseStrategy", 4, MyStrategy)
return MyStrategy必须知道的 3 件事:
- 不要依赖自己的
new—— 框架不会调用它;实例创建后直接使用self.ctrl。 - 触发请求用
self.ctrl:_requestCast(point, dir, target)/_requestStop()/_requestAccumulate()/_requestStopAccumulate()(下划线是内部命名,但这是策略层唯一通道,内置 4 个策略也这么用)。 - 若需要"点场景选点"(两段式那样):暴露
isAiming字段与OnSceneClick(screenPos),用SceneInput.Connect(self, game:GetService("UserInputService"))接场景点击。eventData字段为BeganPosition/MovedPosition/EndedPosition(Vector2);从按钮点击回调进来时可能是零向量,需自行容错。
技能栏 UI 节点约定
技能栏 UI 由客户端 UIManager 自动发现并绑定,识别完全基于节点自定义属性(custom_kv),不依赖节点名字:
| 节点用途 | 需要的 custom_kv | 说明 |
|---|---|---|
| 技能槽位 | UIType = "AbilitySlot" + Index = <槽位号> | Index 必须是数字且与技能槽位一致;0 也是合法值 |
| 蓄力进度节点 | UIType = "AbilityAccumulateNode" | 单例,不需要 Index |
| 取消区域 | UIType = "AbilityCancelArea" | 可摆多个;不需要 Index |
槽位节点的子节点名(必须拼写一致,缺谁就少哪个功能,不会报错):
| 子节点名 | 作用 |
|---|---|
ImageMoveRange | 拖拽范围指示(也会被当作取消态变色对象) |
ImageTouchPoint | 摇杆点 |
ProgressTimer 或 progress_timer | CD 进度(需支持 SetPercent) |
CdText | 剩余秒数(%.1f) |
CdMask | 灰显遮罩(槽位禁用/灰显时显示) |
ChargeBackground | 充能背景 |
ChargeProgressTimer 或 charge_progress_timer | 充能进度(需支持 SetPercent) |
ChargeCount | 充能层数文本 |
蓄力节点的子节点:Icon(图标);进度条可以就是节点自身(只要它支持 SetPercent)。
自绘 UI 可直接用 UIManager:
lua
local UIManager = require("client.packages.ability_system.ui.UIManager")
local um = UIManager.GetInstance() or UIManager.new(manager) -- 单例
um:BindAbility(abilityScript) -- 手动绑定一个技能(正常由事件自动触发)
um:UnBindAbility(slotIndex) -- 解绑
um:SetManager(manager) -- 换背包(重生/换角色后)
um:ResetBindings() -- 清空所有 UI 绑定(切背包时)
um:SetCancelAreaVisible(true) -- 显示/隐藏取消区
um:IsInCancelArea(position) -- 判断某点是否落在取消区道具箱(item_box,可选)
给技能加"道具箱表现"(技能被拾取/放置时的头顶模型、放置动画、摘除技能栏 UI 等)。
用法(两步):在技能预设下挂一个子 Script 节点,把 server/packages/ability_system/item_box/item_box_shell.lua 的源码贴进该子节点的 SourceCode,然后在属性面板按需填写字段。不需要道具箱表现的技能不挂这个子节点即可。
| 字段 | 默认 | 说明 |
|---|---|---|
HeadPrefab | 无(必填) | 头顶模型预设;不填会报错 |
HeadOffset | (0, 2, 0) | 头顶模型偏移 |
HeadRotation | (0, 0, 0) | 欧拉角(度) |
HeadScale | (1, 1, 1) | 缩放(零分量会被兜底为 1) |
PlaceAnimId | official://animation/25205 | 放置动画 |
行为概要:监听宿主技能的 CastStart / CastEnd / CastBreak(事件单位为技能单位下的 BindableEvent), 切换头顶模型的显示;技能可用次数耗尽时摘掉技能栏 UI(把 Index 置 nil)并在 50 秒后销毁技能单位; 举横批动画 official://animation/25862 硬编码;头顶模型只作展示、不参与碰撞。
附录:常见坑 FAQ
Q1:AddAbility / CastAbility 里的 unit 传角色还是技能背包?
服务端 API 传角色(拥有者);客户端 API 传技能背包。两者都能靠 AbilityManager 标签反查,传错会返回失败信息。
Q2:CastAbility 返回 true,但没看到表现?
服务端只负责状态机,表现要靠锚点或你自己的事件反应(CastStart 里播特效/动画)。
Q3:槽位 -1 是什么?
"未入槽"哨兵(子技能休眠态)。它不是合法槽位,客户端不会为它绑 UI。合法槽位从 0 开始,无上限。
Q4:AddAbility 同槽同预设会怎样?
直接复用现有实例并返回它,不会重复创建。同槽被别的预设占用才返回 nil, err。
Q5:技能在 CD / 正在施法,CastAbility 静默失败?
是的,canUse() 不通过就返回 false。先查 InCD / InCast / ChargeCount / 外部条件。
Q6:充能技能放完几次就再也放不出来了?
ChargeType=0(不充能)时用完即死锁,这是配置语义。要回复必须把 ChargeType 改成 1 或 2。
Q7:锚点没反应?
依次查:① 锚点是否挂在技能单位下(不是挂在管理器/角色下);② Phase 是否与实际窗口一致(蓄力锚点在施法阶段不会点火); ③ Constant 类锚点 Duration 是否 > 0;④ 服务端日志里是否有 [AnchorLogic] 锚点已挂载(没有则说明框架没跑到)。
Q8:AnchorStop 和 AnchorEnd / AnchorBreak 什么区别?
End = 正常收尾,Break = 被打断,Stop = 两者之后必发的统一收尾信号。要"包清理"就写 Stop。
Q9:事件监听拿不到事件单位(FindFirstChild 返回 nil)?
事件单位由服务端在技能 Attach 时创建;如果你在技能刚创建、还没 Attach 时就去取,会拿不到。 建议在 AddAbility 的返回值上取,或用 ChildAdded 等待。锚点事件单位由锚点运行时创建 —— 锚点单位本身也要先就绪。
Q10:客户端 UI 不显示槽位?
槽位节点要有 UIType="AbilitySlot" 与正确的 Index;技能要真正"入槽"(Index >= 0); 另外 OwnerId 为空字符串时客户端是放行的(未解析状态),若非空则要求与本地玩家一致。
Q11:子技能切换后技能栏没更新?
切换走的是"槽位替换"通道(先移除后加入的广播),UI 会自动跟随;OnSwitchNext 只是给作者钩子用的通知。
Q12:handler 的方法要用冒号还是点?
点调用即可(用冒号也能跑,多余的 self 参数会被忽略)。
