主题
技能系统(ability_system)
- 认识技能系统
- 包结构
- 核心概念
- 快速上手
- 编辑器使用
- 服务端脚本接口
- 客户端脚本接口
- 事件监听
- 属性速查
- 关键规则
- 锚点系统
- 子技能组
- 扩展:指示器与释放策略
- 技能栏 UI 节点约定
- 道具箱
- 常见问题
- 术语对照
- 附录 A:从老技能 API 迁移
认识技能系统
技能系统是一套数据驱动的技能框架:技能之间的差异用属性面板上的配置表达,通用行为(冷却、施法、蓄力、充能、等级)由包里的共享逻辑统一实现,技能表现(动画、特效、音效、生成物)由锚点按时间轴在指定时机触发。
能力清单
| 能力 | 说明 |
|---|---|
| 装备与管理 | 按槽位组织技能(从 0 开始、无上限);支持增删、换位、按预设 Key 移除;背包预设可配初始技能,角色生成后自动装载 |
| 四套状态机 | CD、施法、蓄力、充能,全由包驱动,作者只填配置 + 监听事件写反应 |
| 等级体系 | 可升级开关、等级上限、各等级的角色等级要求;支持升降级 |
| 使用限制 | 位掩码:施法中 / 失控 / 滚动 / 冰冻(可组合),还能注入自定义可用条件 |
| 目标筛选 | 目标类型、阵营、必须标签、忽略标签;施法距离 / 半径 / 扇形角度 / 作用宽度 |
| 表现层 | 释放策略(松开 / 按住 / 两段式 / 按下)、瞄准指示器(矩形 / 圆形 / 扇形 / 抛物线)、技能栏 UI、蓄力进度、取消施法区;可注册扩展 |
| 锚点时间轴 | 16 种现成锚点行为(动画 / 特效 / 音效 / 子弹 / 地雷 / 障碍 / 传送球 / 弹簧绳 / 近战命中 / 重力 / 移速 / 进 CD / 删技能 / 自定义事件 / 骨骼模型 / 转向),也支持作者自行编写 |
| 子技能组 | 一组技能按策略轮转(施法后 / 定时 / 手动)×(顺序 / 洗牌 / 随机),支持回归时间与切换冷却,用来做连招 |
| 道具箱表现 | 可选:持有态举箱动画 + 头顶模型,技能用完自动摘除技能栏 UI |
包结构
三个目录是什么意思
技能包按 common / server / client 三层组织,三层含义固定:
| 目录 | 含义 | 作者视角 |
|---|---|---|
common/ | 双端共用:枚举常量、事件定义、注册表、目标筛选、锚点调度 | 查枚举、注册扩展、写锚点行为 |
server/ | 服务端:权威逻辑,CD / 施法 / 充能 / 结算都在这里 | 主要写代码的地方 |
client/ | 客户端:输入、指示器、释放策略、技能栏 UI | 自绘 UI、自定义指示器 |
一句话记住权威性:施法、CD、充能、伤害结算全在服务端;客户端只发「请求」,服务端判定通过才真正施放。客户端界面转好了,不代表服务端放行。
目录与文件职责
LuaSource_作者版技能系统/ 技能包(ability_system)
│
├─ client/
│ ├─ main.lua 空壳入口,无业务逻辑
│ └─ packages/ability_system/
│ ├─ api.lua ★ 客户端公开接口(发请求 + 查询 + 注册扩展)
│ ├─ ability_local_script.lua 单个技能:属性监听 → 转发 UI 钩子
│ ├─ ability_manager_local_script.lua 技能背包:槽位镜像 + 服务端通知分发
│ ├─ component_scripts/box_component_fix.lua 垫脚石/弹板类组件的本端位置修正
│ ├─ pointer/ 瞄准指示器
│ │ ├─ AbilityPointer.lua 基类(落点计算 / 特效节点 / 刷新 / 清理)
│ │ ├─ RectanglePointer.lua 矩形
│ │ ├─ CirclePointer.lua 圆形
│ │ ├─ SectorPointer.lua 扇形
│ │ └─ ParabolaPointer.lua 抛物线
│ ├─ strategy/ 释放策略
│ │ ├─ BaseStrategy.lua 基类
│ │ ├─ ReleaseStrategy.lua 松开施放(含蓄力、取消区判定)
│ │ ├─ HoldStrategy.lua 按住持续施法
│ │ ├─ TwoStageStrategy.lua 两段式(先瞄后点)
│ │ └─ PressStrategy.lua 按下瞬发
│ └─ ui/ 技能栏 UI
│ ├─ UIManager.lua 按自定义属性找节点、注入 UI 钩子
│ ├─ Controller.lua 槽位控制器(建策略、绑节点输入)
│ ├─ eui_adapter.lua EUI 节点读写适配层
│ ├─ scene_input.lua 场景点击路由(瞄准时接管点击)
│ └─ nodes/
│ ├─ AbilitySlot.lua 技能槽位节点(CD 遮罩 / 倒计时 / 充能 / 禁用态)
│ ├─ AccumulateNode.lua 蓄力进度节点
│ └─ CancelArea.lua 取消施法区
│
├─ common/
│ └─ packages/ability_system/
│ ├─ editor.lua 预设类型注册(技能 / 技能背包 / 锚点)
│ ├─ data.lua 结构体定义(AbilityInfo / SubAbilityInfo)
│ ├─ constants.lua 全部枚举与常量(事件名 / 策略 / 指示器 / 限制…)
│ ├─ event_defs.lua 通信通道 + 事件单位工厂
│ ├─ registry.lua 反查注册表 + 扩展注册口(指示器 / 策略)
│ ├─ target_filter.lua 目标筛选(距离 / 类型 / 标签)
│ ├─ ui_hooks.lua UI 钩子占位层(默认全部空实现)
│ ├─ Anchor.lua 锚点调度器(点火 / 终结 / 中断 / 事件派发)
│ └─ util.lua 内部工具
│
└─ server/
├─ main.lua 空壳入口,无业务逻辑
└─ packages/ability_system/
├─ api.lua ★ 服务端公开接口(装备 / 查询 / 施法 / 枚举)
├─ ability_script.lua ★ 技能预设的「壳脚本」:声明全部可编辑字段
├─ ability_logic.lua 技能共享逻辑(CD / 施法 / 蓄力 / 充能 / 等级 / 限制)
├─ ability_manager_script.lua 技能背包的「壳脚本」:声明 InitAbilities
├─ ability_manager_logic.lua 背包逻辑(槽位 / 增删移 / 通知 / 初始装载)
├─ anchor_script.lua 锚点的「壳脚本」:StartTime / Duration / Phase / TrackIndex
├─ anchor_logic.lua 锚点运行时框架(阶段订阅 + 调度)
├─ anchors/ 16 个内置锚点行为,一个行为一个文件
├─ sub_ability.lua 子技能组运行时(切换 / 回归 / 冷却)
├─ sub_ability_plugin.lua 子技能组装配器(按配置建子技能并建组)
├─ component_scripts/box_component_create.lua 施法时在脚下生成组件
└─ item_box/ 可选的「道具箱表现」
├─ item_box_shell.lua 表现壳(头顶预制 / 动画 / 偏移)
└─ item_box_logic.lua 表现逻辑(持有态 / 施法态 / 耗尽销毁)几个要点
- ★ 标记的是作者要用的入口:两份
api.lua。 - 「壳脚本」是什么:新建预设时贴上去的那段脚本(
ability_script.lua/ability_manager_script.lua/anchor_script.lua,锚点还要加anchors/<行为>.lua)。它只做两件事——声明有哪些可编辑字段、把自己注册进包里。真正的行为都在包内。所以升级包版本时,你已经建好的预设不需要改动。 anchors/里的 16 个文件就是 16 种现成锚点行为,用哪个就贴哪个,不需要作者自行编写逻辑(见第 11 节)。- 包里还有引擎脚手架文件(入口空壳、触发器占位、资源描述等),与玩法无关,未列出。
核心概念
| 概念 | 一句话解释 |
|---|---|
| 技能(ability) | 一张配置表。属性面板里填的每个字段都是运行时属性,用 script:GetAttribute("字段名") 读 |
| 技能背包(ability_manager) | 槽位容器,只管「哪个槽位装哪个技能」,不含施法逻辑。它挂在角色下面 |
| 状态机 | CD / 施法 / 蓄力 / 充能四套状态机由包驱动,作者只负责填配置 + 监听事件写反应 |
| 锚点(anchor) | 时间轴上的一个动作,自带 4 个事件(开始 / 中断 / 结束 / 终结)。挂在技能单位下,按阶段在蓄力或施法窗口内点火 |
| 事件 | 全是挂在单位下的可监听事件子单位,用 单位:FindFirstChild("事件名"):Connect(fn) 监听 |
| 槽位 | 技能的坐标,从 0 开始、没有上限。API 大多按「单位 + 槽位」定位 |
槽位规则(贯穿全部接口)
- 槽位从 0 开始、没有上限:
0是合法槽位,1/2/3...同理,不存在「最大槽位数」; nil= 不指定:AddAbility不传槽位时,会从 0 号槽开始往后(索引递增)找第一个空槽,返回第一个空位的编号。低位有空缺就先补空缺,不跳到末尾追加;-1= 未入槽哨兵(子技能休眠态、未分配槽位时的过渡值)。它不是合法槽位,客户端也不会给它绑 UI。
节点长什么样
角色(Character)
└─ 技能背包(ability_manager)
├─ 技能 A(ability,Index = 0)
│ ├─ 本地脚本(LocalScript)
│ └─ 锚点(anchor)× N ← 按时间轴触发表现
├─ 技能 B(ability,Index = 1)事件单位会挂在技能单位 / 锚点单位 / 背包单位下面,名字就是事件名。
快速上手
步骤 1 · 建「技能」预设
在预设(新建)面板选类型「技能」,编辑器会按模板生成一个已经接好的预设:根节点 Script(装全部配置的壳源码)+ 子节点 本地运行脚本。属性面板会按壳源码自动生成可编辑字段(CD 时间、施法时间、指示器、充能、子技能……见第 9 节)。
保存后拿到该技能的预设 Key(形如 map://preset/...),后续装备技能用这个字符串。
手工创建兜底
也能手工建(新建 Script 单位、把壳源码贴进去),但要由作者自行补齐子节点,容易漏。优先用「按预设类型新建」。
步骤 2 · 建「技能背包」预设
同样按预设类型新建,类型选「技能背包」。它自带 Script(背包壳源码)+ 本地运行脚本。在 InitAbilities 列表里填「技能预设 + 槽位」;也可以留空,全部运行时用 AddAbility 加。
步骤 3 · 把技能背包挂到角色下
技能背包必须是角色的子单位。挂上后服务端会自动:
- 解析归属(写
OwnerId/PlayerId); - 按
InitAbilities逐个装备技能; - 打
AbilityManager标签,AddAbility这类接口靠它反查背包; - 技能若配了子技能列表,自动建子技能组。
步骤 4 · 装备 / 施放 / 监听(服务端)
lua
local AbilityAPI = require("server.packages.ability_system.api")
local abilityScript = AbilityAPI.AddAbility(unit, "map://preset/技能预设") -- 不传槽位 = 从 0 号槽起递增找第一个空槽
if not abilityScript then
print("[demo] 装备失败")
return
end
-- 事件就是技能单位下的可监听事件子单位
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 · 用锚点拼时间轴动作
在技能预设下建「锚点」子节点(三个挂载入口见第 5.5 节),选一种现成行为(近战攻击 / 播特效 / 激活冷却 / 加移速……),把对应的 anchors/<行为>.lua 贴上去,然后在锚点上写这一段逻辑片段、用时间轴排执行顺序即可。大部分常见动作不需要作者自行编写逻辑,先用现成的 16 种锚点行为拼装(见第 11 节)。
编辑器使用
先搞清楚:引用还是导入
技能包有两种拿到手的方式,选哪种决定了你后面能不能改:
| 引用 | 导入 | |
|---|---|---|
| 怎么操作 | 新建地图会默认引用技能包;也可以手动点击引用 / 取消引用 | 先取消引用,再执行导入 |
| 代码和预设会随包体更新吗 | 会 | 不会 |
| 能在包内预设上直接改数值吗 | 不能,要先复制或新建预设再改 | 能,直接在地图里编辑并保存 |
| 能看到 Lua 逻辑吗 | 看不到 | 能,逻辑就在地图的 lua 里 |
| 能反悔吗 | 能,随时取消引用 | 不能,导入是一次性操作 |
选择建议:只想用、想一直跟着官方更新,就引用;想改逻辑、想二创,就导入。导入后想把这套逻辑去掉,需要在地图 lua 里手动删除。
关于平权实现
技能包是完全平权实现的:包里的逻辑都能改,也可以照着它做自己的技能包。
预设分类:三种预设
在预设面板的「技能」分类下新建,共有三种:
| 预设(编辑器显示名) | 根节点 | 子节点 | 作用 | 对应壳文件 |
|---|---|---|---|---|
| 技能 | 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 单位,把对应壳文件源码贴进去),但要由作者自行补齐子节点(比如「本地运行脚本」),容易漏——留作兜底。
三种预设的运行时类型都是 Script,靠预设类型标记区分。新建出来的预设本身不写逻辑:根脚本只声明可编辑属性,然后 require(...).Attach(script) 把行为挂进包里。所以升级包版本时,已建好的预设无需改动。
内置结构体(common/packages/ability_system/data.lua):
lua
ability_system.AbilityInfo = { AssetId = "", Index = 0 } -- 初始技能一项
ability_system.SubAbilityInfo = { AssetId = "" } -- 子技能一项装载技能
- 在生物上添加子组件:分类选「技能」,类型选「技能背包」;
- 选中技能背包,在初始技能列表里配置「技能预设 + 槽位」;
- 槽位的作用是和技能按钮 UI 对应,它的编号要与技能槽位 UI 上的
index一一对应(见第 14 节)。
初始技能列表也可以留空,全部在运行时用 AddAbility 加。
槽位规则速记
槽位从 0 开始、无上限,-1 表示未入槽——这条规则贯穿全部接口,详见第 3.1 节。
编辑技能
- 在「技能」分类里新建技能预设;
- 技能预设自带一个「本地运行脚本」子节点,负责 UI 等客户端内容逻辑,一般不用动;
- 要写技能获得时的逻辑,在技能本体下再挂一个「服务端脚本」子组件,逻辑写在这个子组件里;
- 需要注册、初始化时,用服务端脚本接口操作(见第 6 节)。
挂锚点
挂载锚点有三个入口:
- 在技能预设上「添加子组件」,选「锚点」;
- 时间轴左侧的添加入口;
- 时间轴右键,选新建。
挂上之后:在锚点上写这一段技能的逻辑片段;拖拽时间轴调整执行顺序;可以分别为施法阶段和蓄力阶段写不同逻辑(对应锚点的 Phase 参数,见第 11 节)。
设置技能按钮
界面编辑器 → 「自定义」分类下有技能相关的三种 UI 预设,详见第 14 节:
| UI 预设 | 作用 |
|---|---|
| 技能槽位 | 按钮和技能的对应关系:右侧自定义属性里的 index 与生物下技能背包的槽位编号一一对应 |
| 蓄力图标 | 显示技能预设本体上的蓄力相关参数 |
| 技能取消区域 | 运行时在技能瞄准状态下,手指滑到取消区域即可取消施放 |
服务端脚本接口
lua
local AbilityAPI = require("server.packages.ability_system.api")模块加载时(服务端)会自动注册客户端请求监听(重复加载也安全),作者不需要手动调 RegisterEvents。
表里「返回」列写两个值(中间用斜杠隔开的),是 Lua 的多返回值:第一个是主结果,第二个是失败原因字符串。
- 成功时:主结果有值(技能脚本 /
true),失败原因是nil; - 失败时:主结果为空(
nil或false),失败原因是出错信息,形如[ability_system] AddAbility: slot is occupied by another ability。
只写一个值的,就是单返回值,不用去接第二个。用起来是这样:
lua
local abilityScript, err = AbilityAPI.AddAbility(unit, key)
if not abilityScript then
print(err) -- [ability_system] AddAbility: slot is occupied by another ability
end| 函数 | 参数 | 返回 | 说明 |
|---|---|---|---|
AddAbility | unit, abilityKey, slotIndex? | 技能脚本 / 失败原因 | 装备技能。不传槽位时从 0 号槽起递增找第一个空槽(低位有空缺先补空缺);同槽同预设会复用现有实例;槽位被别的预设占用 → 返回 nil, err |
GetAbility | unit, abilityIndex | 技能脚本 | 按槽位取技能 |
GetAbilityByScript | abilityScript | handler | 按技能脚本反查 handler(进阶用法,见第 13 节) |
GetAbilities | unit | 技能脚本列表 | 全部已装备技能(按槽位升序) |
GetAbilitiesFromIndexRange | unit, startIndex, endIndex | 技能脚本列表 | 指定槽位区间(含两端) |
CastAbility | unit, abilityIndex, releasePoint?, releaseDir?, releaseTarget? | 是否成功 | 服务端权威施放。返回 false 表示被拒(CD 中 / 施法中 / 无充能 / 外部条件不满足) |
StopAbility | unit, abilityIndex | 是否成功 | 打断施法;若还在蓄力阶段则退出蓄力 |
AccumulateAbility | unit, abilityIndex | 是否成功 | 进入蓄力(EnableAccumulate = false、CD 中或充能不满足时返回 false) |
SwitchNextAbility | unit, parentSlotIndex | 是否成功 | 子技能组切到下一个成员(任何切换模式都可以显式调用) |
SetAbilityToSlot | unit, ability, slotIndex | 技能脚本 | 把已装备的技能移动到指定槽位 |
RemoveAbility | unit, slotIndex | 技能脚本 | 按槽位移除并销毁 |
RemoveAbilityByKey | unit, abilityKey | — | 按预设 Key 移除该单位身上所有对应技能 |
RegisterEvents | — | — | 内部用,模块加载时自动调用,无需手动 |
unit 传什么:传拥有技能背包的单位(角色),也可以直接传技能背包本身。实现是先在 unit 自身及其子节点上按 AbilityManager 标签找背包,所以两种传法都成立。
失败信息:错误返回值统一带 [ability_system] 前缀,例如 "[ability_system] AddAbility: slot is occupied by another ability"。
完整示例:装备 + 监听 + 换槽 + 升级
lua
local AbilityAPI = require("server.packages.ability_system.api")
local M = {}
function M.setup(unit)
-- 1) 指定槽位装备(0 基)
local shield = AbilityAPI.AddAbility(unit, "map://preset/盾技能预设", 2)
-- 2) 不指定槽位:从 0 号槽起递增找第一个空槽
local dash = AbilityAPI.AddAbility(unit, "map://preset/冲刺预设")
-- 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客户端脚本接口
lua
local AbilityClientAPI = require("client.packages.ability_system.api")客户端只能发请求,真正的施放判定在服务端。
表里的「是否已发出请求」是单返回值:true 只代表请求发出去了,不代表服务端已经同意施放,最终结果要看服务端回推的状态。
| 函数 | 参数 | 返回 | 说明 |
|---|---|---|---|
GetManagerForUnit | unit | 背包脚本 | 按 AbilityManager 标签反查背包(自身或子节点) |
GetAbility | unit, abilityIndex | 技能脚本 | 按槽位取技能(遍历背包子节点的 Index 属性) |
RequestCast | manager, abilityIndex, releasePoint?, releaseDir?, releaseTarget? | 是否已发出请求 | 请求施放(客户端 → 服务端) |
RequestStop | manager, abilityIndex | 是否已发出请求 | 请求打断 / 停止 |
RequestAccumulate | manager, abilityIndex | 是否已发出请求 | 请求进入蓄力 |
RequestSwitchNext | manager, parentSlotIndex | 是否已发出请求 | 请求子技能轮转 |
GetUIManager | — | UI 管理器 | 取技能栏 UI 管理器单例(懒初始化) |
Register | domain, key, value | — | 注册自定义指示器 / 释放策略(见第 13 节) |
StartClientLifecycle | — | — | 引擎内部用:等本地玩家、监听重生、自动挂接背包(重复调用安全)。作者通常不需要调用 |
参数别传反
参数别传反:服务端接口收 unit(角色),客户端接口收 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 号槽:子技能轮转
end事件监听
事件模型
- 事件 = 挂在某个单位下的可监听事件子单位,名字就是事件名;
- 监听写法:
单位:FindFirstChild("事件名"):Connect(function(...) end); - 生命周期不依赖父子级联销毁:所有者销毁时框架会显式销毁事件单位(技能 / 锚点 / 背包都已处理)。自建的事件单位需由作者自行在销毁时清理;
- 事件是本端本地广播:服务端触发只对服务端的监听者有效;客户端表现靠属性同步和 s→c 广播驱动。
拿不到事件单位
拿不到事件单位(FindFirstChild 返回 nil)?说明技能还没完成挂载。在 AddAbility 的返回值上取,或用 ChildAdded 等待。
技能级事件(挂在技能单位下,共 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 | 锚点终结(AnchorBreak / AnchorEnd 之后必发) |
锚点级事件(挂在锚点单位下,共 4 个)
事件名相同(AnchorStart / AnchorBreak / AnchorEnd / AnchorStop),但区别于技能级:
- 主体是锚点自身,载荷只有
abilityScript——监听方不需要用groupId过滤; - 一个技能有多个同类锚点时,可以逐个锚点单独监听。
lua
local anchor = abilityScript:FindFirstChild("你的锚点节点名")
anchor:FindFirstChild("AnchorStart"):Connect(function(abilityScript)
-- 这个锚点点火了(不用区分 groupId)
end)背包级事件(挂在技能背包单位下,共 3 个)
| 事件名 | 载荷 | 时机 |
|---|---|---|
AbilityAdded | abilityScript, slotIndex | 技能加入背包 |
AbilityRemoved | abilityScript, slotIndex | 技能移出背包 |
AbilityUpgraded | abilityScript, newLevel | 技能升级 |
子技能组事件(挂在父技能单位下,共 3 个)
| 事件名 | 载荷 |
|---|---|
OnActivated | newHandler, newIndex |
OnDeactivated | oldHandler, oldIndex |
OnSwitchNext | newHandler, newIndex |
服务端 → 客户端广播
客户端已内部处理这些广播,作者通常无需关心;只有在你自绘技能栏 UI 时才需要知道:
| 广播名 | 参数 |
|---|---|
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 钩子(自绘 UI 用)
不想重写整个技能栏 UI、只想改某些表现时,可以覆盖 UI 钩子:
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,
})可覆盖的钩子共 11 个:onInCDChange、onInCastChange、onChargeCountChange、onAccumulateChange、onPointerTypeChange、onReleaseTypeChange、onIndexChange、onOwnerIdChange、onAbilityAdded、onAbilityRemoved、onSwitchNext。
setUIHooks 只会替换已存在且是函数的键,传未知的键会被忽略;没覆盖的钩子仍走内置技能栏 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[] | {} | 各等级所需角色等级(下标 = 目标技能等级) | |
| 蓄力 | EnableAccumulate | boolean | false | 能否蓄力 |
MaxAccumulateTime | number | 1.0 | 满蓄力时间;蓄力比 = 已蓄时长 ÷ 该值(上限 1) | |
| 限制 / 释放 | UseLimitation | Int(枚举) | 0 | 0 无限制 / 1 施法中可用 / 2 失控可用 / 4 滚动可用 / 8 冰冻可用 |
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 为 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 | 结构体列表 | {} | 子技能列表(与父技能共用同一槽位) | |
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 |
客户端只对 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 | 是 | 初始技能列表(每项 = 技能预设 Key + 槽位) |
OwnerId / PlayerId / ManagerReady | 否 | 运行时由服务端写入(归属玩家名 / 用户 ID / 就绪标记) |
关键规则
能不能放:canUse()
依次判定,任一不满足就禁止:
- 不在 CD 中(
InCD == false); - 不在施法中(
InCast == false); - 如果是消耗充能的技能:
ChargeCount > 0; - 所有通过
addUseCondition注入的外部条件都返回真。
被拒时 startCast 直接返回 false;如果此时正处于蓄力态,还会兜底退出蓄力。
CD 从哪开始|「锚点优先、框架兜底」
- 正常结束施法(
CastEnd)时才由框架自动进 CD;被打断不进 CD(要不要进由作者自行决定); - 如果本轮使用中已经有锚点(比如挂了「激活冷却」锚点)调用了进 CD,框架的兜底 CD 不会重复再进一次;
- 这解决的是「CD 走完又走一次 / UI 环已经转好却还要等一会儿才能再放」的问题。
CD 用锚点还是兜底
结论:想让 CD 由锚点精确控制 → 挂 active_cd 锚点;想用统一 CD → 不挂,用 CdTime 兜底。
蓄力
- 按住 →
startAccumulate()(需要EnableAccumulate = true,且canUse()通过); - 蓄力比实时读
handler.getAccumulateRatio(); - 转入施法时,框架把这个比值写进
AccumulateRatio属性 → 在CastStart里读它做威力缩放; - 退出路径:
AccumulateBreak(被打断,先发)→AccumulateEnd(任何路径都会发)。
充能
ChargeType:0不充能 /1不满时充能 /2用完后充能;- 每次施法扣 1 层(在
canUse通过之后扣); - 注意
ChargeType = 0用完之后就永久不能放了(不可逆)。要能回复,必须把ChargeType设成1或2。
使用限制(位掩码)
UseLimitation 是位掩码,可以相加组合:1 施法中可用 / 2 失控可用 / 4 滚动可用 / 8 冰冻可用。运行时可以用 handler.getLimitation(type) 查询、handler.setLimitation(type, enable) 修改。
限制位掩码的差异
官方老 API 只有前三位,作者版多了 8(冰冻可用)。
升级
- 需要
Upgradable = true,且Level < MaxLevel; LevelRequirements[下一个等级]若存在,则要求拥有者的角色等级达到该值;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/。下表「关键配置键」就是属性面板上的字段名。
| 锚点 | 类型 | 关键配置键(默认值) | 硬约束 / 备注 |
|---|---|---|---|
active_cd | Instant | 无(读宿主 CdTime) | 点火即让宿主进 CD,配合第 10.2 节的判重逻辑 |
cast_face_target | Instant | 无 | 施法者朝向转到释放方向(仅水平方向) |
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 音效,所有人可听 |
create_obstacle | Instant | ABILITY_ANOSTATE_NEW_OBJ、Offset(0,0,0)、Scale、ABILITY_ANOSTATE_BIND、ABILITY_ANOSTATE_LIFE_DUR | 实体自管生命周期 |
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 才能碰撞;生成后 1 秒才注册碰撞、0.8 秒后解除免碰撞 |
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、_AREA、_HITPOWER、Offset(0,1,0)、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 | ABILITY_ANOSTATE_UP_ACCELERATION | Duration 必须 > 0;期间持续施力,使净 Y 加速度≈配置值 |
speed_add | Constant | ABILITY_ANOSTATE_WALK_SPEED(可以为负) | Duration 必须 > 0;终结时恢复原速 |
play_body_animation | Constant | ABILITY_ANOSTATE_ANIMKEY、_ANIM_HALF、_LOOP、_ANIM_STARTPOINT、_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)、_HITBOX_SCALE(2,2,2)、_BULLET_DAMAGE、_HITPOWER、_USE_PERFAB、_ANIMKEY、_BINDSOCKET("origin")、_OFFSET、_QUD、_SCALE、_FACE_SYNC(true)、_HIT_SFX / _HIT_SFX_SCALE / _HIT_SFX_ROTATION、_BULLET_HITSFXOFFSET | Duration 必须 > 0(否则只打警告并直接返回);命中盒与武器随锚点生命周期回收 |
Instant 与 Constant 的差异
Instant 与 Constant 的监听差异:Instant 类锚点只在点火时生效;Constant 类锚点会一直持续到收尾,因此需要保证可逆状态被撤销——这类锚点的实现里都监听了收尾事件。
自己写一个锚点行为
约定与现有 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) 点火
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) 会留下可逆状态时,额外监听收尾事件
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 时用
require("common.packages.ability_system.registry").getAbilityHandler(ability_script); - 需要可逆状态(加移速、施力、挂模型)就必须监听收尾三个事件。
子技能组
配置方式(编辑器里配,零代码):在技能预设上把 SwitchMode 设成非 0,且 SubAbilities 非空即可——装备这个技能时会自动创建子技能并建组。
语义
- 子技能与父技能共用同一个槽位:切换时旧的
Index置-1(休眠,不会打断正在进行的施法),新的占据槽位; - 技能栏 UI 会自动跟随(走槽位增删广播);
- 建组时子技能全部处于休眠态(
Index = -1); SwitchMode:1施法后切换 /2定时切换 /3仅手动;SwitchOrder:0顺序 /1洗牌(每轮洗牌,轮间首尾不重复)/2随机(不重复上一次);SwitchCooldown:切换后新成员进 CD(回归路径不触发);RevertTime(仅施法后切换):切到非首成员后开始计时,到点自动切回父技能;SwitchRefresh控制子技能互切是否重置这个倒计时。
监听与手动切换
lua
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)
AbilityAPI.SwitchNextAbility(unit, 0) -- 手动轮转(任何模式都可以显式调用)
-- 需要更细的操作时,直接拿组对象
local AbilityRegistry = require("common.packages.ability_system.registry")
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组对象接口:activateNext()、switchTo(index)、getActiveIndex()、getActiveHandler()、getMembers()、getSignals()、destroy()。
手动建组(进阶)
需要手动建组(进阶)时:require("server.packages.ability_system.sub_ability").create(...),可选参数支持 shareSlot / slotIndex / switchCooldown / revertTime / switchRefresh / destroyWithMembers。
扩展:指示器与释放策略
注册入口只有一个:
lua
local AbilityClientAPI = require("client.packages.ability_system.api")
AbilityClientAPI.Register("PointerType", <整数 key>, 你的类) -- 指示器
AbilityClientAPI.Register("ReleaseStrategy", <整数 key>, 你的类) -- 释放策略内置映射(key 与包内常量表对齐):
| 域 | key | 类 |
|---|---|---|
PointerType | 0 无 / 1 矩形 / 2 圆形 / 3 扇形 / 4 抛物线 | AbilityPointer / RectanglePointer / CirclePointer / SectorPointer / ParabolaPointer |
ReleaseStrategy | 0 松开 / 1 按住 / 2 两段式 / 3 按下 | ReleaseStrategy / HoldStrategy / TwoStageStrategy / PressStrategy |
查不到 key 时的行为
查不到 key 时会静默回退到基类(AbilityPointer / ReleaseStrategy),不会报错。
自定义指示器
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
-- 必须实现:框架会用 (技能脚本, UI 控制器) 调 cls.new
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 / Refresh / UpdateEffect / GetReleasePoint / Clear / Destroy
-- 若覆写了 Create 但仍想要默认特效与选中环,请先调用 self.super.Create(self, dirInfo)
AbilityClientAPI.Register("PointerType", 5, MyPointer)
return MyPointer指示器会读这些技能属性:ReleaseDistance、ReleaseRadius(圆形)、SectorAngle(扇形)、ParabolaHorizontalSpeed / ParabolaVerticalSpeed(抛物线)。
自定义释放策略
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 / OnTouchMove / OnTouchEnd / Destroy
-- self.ctrl 是框架注入的控制器实例,用它和服务端交互
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),并用场景输入模块接点击。eventData的字段是BeganPosition/MovedPosition/EndedPosition(二维坐标);从按钮点击回调进来时可能是零向量,需要由作者自行容错。
技能栏 UI 节点约定
界面编辑器的「自定义」分类下已经提供了三种技能相关 UI 预设,直接拖出来用即可:技能槽位、蓄力图标、技能取消区域,各自的作用见第 5.6 节。
如果你是自己拿普通节点拼技能栏,客户端同样能识别,但要手动打上标识属性——技能栏 UI 由客户端自动发现并绑定,识别完全基于节点的自定义属性,不看节点名字:
| 节点用途 | 需要的自定义属性 | 说明 |
|---|---|---|
| 技能槽位 | UIType = "AbilitySlot" + Index = <槽位号> | Index 必须是数字,且与技能槽位一致;0 也是合法值 |
| 蓄力进度节点 | UIType = "AbilityAccumulateNode" | 单例,不需要 Index |
| 取消区域 | UIType = "AbilityCancelArea" | 可以摆多个,不需要 Index |
槽位节点的子节点名(必须拼写一致,缺哪个就少哪个功能,不会报错):
| 子节点名 | 作用 |
|---|---|
ImageMoveRange | 拖拽范围指示(同时作为取消态变色对象) |
ImageTouchPoint | 摇杆点 |
ProgressTimer 或 progress_timer | CD 进度(节点需支持 SetPercent) |
CdText | 剩余秒数(保留 1 位小数) |
CdMask | 灰显遮罩(槽位禁用 / 灰显时显示) |
ChargeBackground | 充能背景 |
ChargeProgressTimer 或 charge_progress_timer | 充能进度(节点需支持 SetPercent) |
ChargeCount | 充能层数文本 |
蓄力节点的子节点只有 Icon(图标);进度条可以直接用节点自身(只要它支持 SetPercent)。
自绘 UI 也可以直接用 UI 管理器:
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) -- 判断某个点是否落在取消区道具箱
给技能加「道具箱表现」——技能被拾取 / 放置时的头顶模型、放置动画、以及技能用完时自动摘掉技能栏 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,切换头顶模型的显示;技能可用次数耗尽时摘掉技能栏 UI(把 Index 置空),并在 50 秒后销毁技能单位;举箱动画硬编码;头顶模型只作展示、不参与碰撞。
常见问题
Q1:AddAbility / CastAbility 里的 unit 传角色还是技能背包?
服务端接口传角色(拥有者),客户端接口传技能背包。两者都能靠 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。建议在 AddAbility 的返回值上取,或用 ChildAdded 等待。锚点的事件单位由锚点运行时创建,锚点单位本身也要先就绪。
Q10:客户端 UI 不显示槽位?
三个条件:① 槽位节点要有 UIType = "AbilitySlot" 且 Index 正确;② 技能要真正入槽(Index >= 0);③ OwnerId 为空字符串时客户端是放行的(未解析状态),非空则要求与本地玩家一致。
Q11:子技能切换后技能栏没更新?
切换走的是「槽位替换」通道(先移除后加入的广播),UI 会自动跟随。OnSwitchNext 只是给作者用的通知。
Q12:handler 的方法要用冒号还是点?
点调用即可(用冒号也能跑,多余的 self 参数会被忽略)。
术语对照
| 中文(本文用词) | 代码 / 编辑器里的名字 |
|---|---|
| 技能系统(包名) | ability_system |
| 技能 | ability |
| 技能背包 | ability_manager |
| 锚点 | ability_anchor / anchor |
| 槽位 | Index / slotIndex |
| 未入槽 | -1(SLOT_INDEX_UNASSIGNED) |
| 技能实例句柄 | handler |
| 服务端脚本接口 | AbilityAPI(AbilityServerAPI) |
| 客户端脚本接口 | AbilityClientAPI |
| 事件单位 | BindableEvent 子单位 |
| 释放策略 | ReleaseStrategy / ReleaseType |
| 瞄准指示器 | Pointer / PointerType |
附录 A:从老技能 API 迁移
关于这份对照
给谁看:以前用官方 ability_system(Packages.ability_system)做过技能的作者。
本文里的「老」= 官方包,「新」= 本页这份作者版技能包。绝大多数概念是一一对应的,主要变化是调用方式和事件监听方式,配置字段基本同名。
A.1 六处根本差异
| 维度 | 老(官方包) | 新(作者版包) |
|---|---|---|
| 引入方式 | 全局 Packages.ability_system | require("server.packages.ability_system.api") / require("client.packages.ability_system.api") |
| 技能 / 背包是什么 | SDK 内建的单位类型 | 自定义预设类型,运行时是普通 Script 单位 |
| 配置怎么存 | 元数据 JSON | 面板字段 = 运行时属性,用 GetAttribute 读 |
| 操作技能实例 | ability:方法() | handler.方法()(点调用,见第 13 节) |
| 监听事件 | ability:ConnectEvent("名字", fn) | 单位:FindFirstChild("名字"):Connect(fn) |
| 跨端通信 | 每个动作一个命名通道 | 单通道 + 动作路由;客户端调 RequestXxx 发请求 |
A.2 包 API 对照
| 老写法 | 新写法 | 说明 |
|---|---|---|
AddAbility(unit, id, index) | AbilityAPI.AddAbility(unit, key, slot) | 参数位置一致;同槽同预设会复用现有实例 |
GetAbility(unit, index) | 同名 | ✅ |
GetAbilities(unit) | 同名 | ✅ |
GetAbilitiesFromIndexRange(...) | 同名 | ✅ |
RemoveAbility(unit, index) | 同名 | ✅ |
RemoveAbilityByKey(unit, key) | 同名 | ✅ |
CastAbility(unit, index, point, dir, target) | 同名 | ✅ 参数完全一致 |
StopAbility(unit, index) | 同名 | ✅ |
AccumulateAbility(unit, index) | 同名 | ✅ |
SetAbilityToSlot(unit, ability, slot) | 同名 | ✅ |
GetAbilityByPrefab(unit, prefabKey) | 没有 | 替代:GetAbilityByScript(abilityScript) 反查 |
ReplaceAbility(unit, slot, newPrefabKey) | 没有 | 替代:先 RemoveAbility 再 AddAbility(unit, key, slot) |
ForbidSkillSlot / ActiveSkillSlot | 没有包接口 | 替代:用背包 handler 的 forbidSlot(slotIndex, isForbid, grayout) |
ForbidAbility(markAbility, unit, targetAbility, duration, showGreyOut, recoverOnCastEnd) | 没有同名接口 | 替代:技能级禁止用 handler.addUseCondition(key, function() return false end),解除用 removeUseCondition(key);要连 UI 一起灰显就用背包 handler 的 forbidSlot(slotIndex, true, true);时长和「施法结束恢复」需由作者自行用定时器或 CastEnd 事件实现 |
StopAccumulateAbility / BreakAccumulateAbility | 没有独立包接口 | 替代:handler.endAccumulate(broken) |
| — | GetAbilityByScript | ➕ 新增 |
| — | SwitchNextAbility(unit, slot) | ➕ 新增(老的子技能切换在实例方法上) |
客户端对照:老的客户端 CastAbility 会直接发请求,新的拆成了语义更明确的 RequestCast / RequestStop / RequestAccumulate / RequestSwitchNext;GetUIManager、Register 与老包同名同义。
A.3 实例 API 对照
老包是 ability:方法(),新包是 handler.方法()。大部分方法同名同义,主要有这几处不同:
| 老方法 | 新写法 | 说明 |
|---|---|---|
GetOwner() | script.Parent.Parent | 技能 → 背包 → 拥有者 |
GetName() | script.Name | — |
GetPrefabKey() | 属性 AbilityPresetKey | 从方法改成读属性 |
GetDescription() / GetIcon() | 没有 | 需由作者自行用运行时属性 AbilityName 维护 |
ConnectEvent(name, fn) | 同名 handler 方法,或直接监听事件单位 | 机制不同 |
EndCast(is_break) | handler.endCast(broken) | 参数语义一致 |
SetDisableOnRolling / SetCanUseOnUncontrol | handler.setLimitation(位, enable) | 新包统一走限制位 |
SetCanUseWhenFreezed | setLimitation(冰冻位, enable) | ➕ 新增的限制位 |
IsTargetType(type) | 没有 | 需由作者自行判断(可借助目标筛选模块) |
GetReleasePointList() / GetReleaseDirectionList() | 没有 | 需由作者自行实现 |
PickTargetInReleaseRange(point)(客户端) | 没有 | 需由作者自行实现 |
SetParentAbilitySlot / SetChildAbilitySlot / SwitchAbilityOnce | 组对象的 switchTo / activateNext | 见第 12 节 |
同名同义、可以照搬的:CD 系列(EnterCD / SetCurrentCD / GetLeftCD / GetCdTime / IsInCD)、施法系列(CanUse / StartCast / BreakCast)、蓄力系列(StartAccumulate / EndAccumulate / BreakAccumulate / GetAccumulateRatio)、充能系列(GetChargeCount / SetChargeCount / SetChargeConfig 等)、等级系列(Upgrade / CanUpgrade / SetLevel / SetMaxLevel / IncreaseLevel / DecreaseLevel)、限制系列(GetLimitation / SetLimitation)、外部条件(AddUseCondition / RemoveUseCondition)、释放参数(Get/SetReleasePoint、Get/SetReleaseDirection、Get/SetReleaseDistance、Get/SetReleaseRadius、Get/SetAffectWidth)、RemoveSelf、IsValidTarget、GetValidTargetsInRange。
A.4 属性对照
结论先说:老配置可以直接搬,字段名基本没变,需要改的地方如下。
改名
| 老 | 新 |
|---|---|
运行时属性 ReleaseDirection | ReleaseDir |
结构体 AbilityInfo.AbilityAsset | AbilityInfo.AssetId |
新增(老包没有)
CastStartTime、AccumulateRatio、AbilityPresetKey、AbilityName(装备时自动写入)、背包 Icon。
默认值不同(老配置搬过来后行为可能变,注意核对)
| 字段 | 老默认 | 新默认 |
|---|---|---|
CdTime | 1.0 | 3.0 |
CastTime | 1.0 | 0.5 |
MaxLevel | 1 | 5 |
MaxChargeCount | 1 | 5 |
PointerType | 1 | 0 |
TargetFilterCamp | 1 | 0 |
ParabolaVerticalSpeed | 8.0 | 10.0 |
UseLimitation | 1 | 0 |
老有、新没有的字段
Name / Desc / Description / Icon(技能自身信息,新包用运行时属性 AbilityName 代替)、AccumulateFinishTime、多指示器的 PointerCount / PointerOffset / PointerOffsetAngle / PointerIntervalAngle、背包的 IndexCounter。
没有等价物的字段
AccumulateFinishTime、多指示器的 4 个字段、背包的 IndexCounter 在当前包里没有等价物,需要时由作者自行实现(找空槽可以用背包 handler 的 getFirstAvailableSlot())。
其余 33 个技能配置字段(含子技能组一整组)新旧同名,详见第 9.1 节。
A.5 事件对照
改名 / 换层级的
| 老事件 | 新事件 | 说明 |
|---|---|---|
OnAbilityAdded / OnAbilityRemoved | 背包级 AbilityAdded / AbilityRemoved | 层级从技能挪到背包 |
OnCastStart | CastStart | 去掉 On 前缀 |
OnCastBreak | CastBreak | 同上 |
OnCastEnd(is_break) | CastEnd | 新包把「被打断」拆成独立事件,CastEnd 只表示正常结束 |
OnAccumulateStart / OnAccumulateBreak / OnAccumulateEnd | AccumulateStart / AccumulateBreak / AccumulateEnd | 去掉前缀 |
OnUpgrade | 背包级 AbilityUpgraded | 层级移动 |
OnDowngrade | OnDowngrade | 同名 ✅ |
OnChargeEnd | ChargeFull | 改名 |
ABILITY_ANCHOR_BEGIN/BREAK/END/STOP_<组Id>(动态名) | AnchorStart / AnchorBreak / AnchorEnd / AnchorStop(固定名,载荷带 groupId) | 机制不同,见第 11.2 节 |
新增事件:BeforeUse、BeforeCast、Used、CDStart、CDEnd,以及背包级和子技能组的一整组事件。
老包里的锚点事件
老包里的 OnAnchorStart / OnAnchorBreak / OnAnchorComplete / OnAnchorEnd 这组事件,在新包中由上面那 4 个固定名事件承担。
A.6 锚点对照
| 维度 | 老 | 新 |
|---|---|---|
| 锚点是什么 | 数据节点,不是独立预设类型 | 独立预设类型,挂在技能单位下 |
| 逻辑怎么写 | 在锚点的配置回调字段里写(开始 / 打断 / 结束 / 完成) | 用现成的 16 种行为模块,或由作者自行编写 M.Attach(anchor_script) |
| 现成动作 | 无,全要靠作者自己写 | 16 种内置行为(近战 / 子弹 / 地雷 / 弹簧绳 / 加移速 / 进 CD……) |
| 事件名 | 动态名 ABILITY_ANCHOR__<组Id> | 固定名 AnchorStart/Break/End/Stop,载荷带 groupId |
| 配置阶段 | 1 蓄力 / 2 施法 | 一致 ✅ |
迁移建议:老项目里自己写的锚点回调逻辑,可以照第 11.5 节的模板改成 M.Attach 形式;如果只是常见动作(播特效、加移速、进 CD 等),直接换成对应的内置锚点即可。
A.7 枚举对照
同名同值、可以直接用:PointerType、TargetType、ReleaseType、ChargeType、SwitchOrder、TargetFilterCamp。
有差异的
| 枚举 | 老 | 新 |
|---|---|---|
UseLimitation | None=0 / InCast=1 / UnControllable=2 / Roll=4 | 同上,多一位 Freezed=8 |
SwitchMode | None=0 / AfterCast=1 / Timer=2 | 0 不切换 / 1 施法后切换 / 2 定时切换 / 3 仅手动 |
TargetFilterType | 枚举(固定几个值) | 自由字符串数组,不再受枚举约束 |
A.8 迁移速查
老写法
lua
local ability = Packages.ability_system.AddAbility(unit, assetId, 0)
ability:ConnectEvent("OnCastStart", function(self) print("开始施法") end)
ability:EnterCD(2.0)
ability:SetCanUseOnUncontrol(true)
Packages.ability_system.CastAbility(unit, 0)新写法
lua
local AbilityAPI = require("server.packages.ability_system.api")
local abilityScript = AbilityAPI.AddAbility(unit, presetKey, 0)
abilityScript:FindFirstChild("CastStart"):Connect(function(script)
print("开始施法")
end)
local handler = AbilityAPI.GetAbilityByScript(abilityScript)
handler.enterCD(2.0)
handler.setLimitation(2, true) -- 2 = 失控可用
AbilityAPI.CastAbility(unit, 0)客户端老写法 → 新写法
lua
-- 老:Packages.ability_system.CastAbility(unit, 0)
-- 新:
local AbilityClientAPI = require("client.packages.ability_system.api")
local manager = AbilityClientAPI.GetManagerForUnit(Players.LocalPlayer.Character)
if manager then
AbilityClientAPI.RequestCast(manager, 0)
endA.9 迁移检查清单
- 把
Packages.ability_system全部换成require("...packages.ability_system.api")的接口; - 客户端施放从
CastAbility换成RequestCast(注意传的是背包,不是角色); - 事件监听从
ConnectEvent换成FindFirstChild("事件名"):Connect,并按 A.5 改名; - 锚点从「回调字段写逻辑」换成「挂
anchors/<行为>.lua」,事件名改成固定 4 个; - 核对 A.4 的默认值差异,尤其是
CdTime/CastTime/PointerType/TargetFilterCamp; - 确认没用到 A.2 / A.3 里的「没有」项;用到的话按替代方案改写,没有替代方案的由作者自行实现。
