Skip to content

技能系统(ability_system) ​

认识技能系统 ​

技能系统是一套数据驱动的技能框架:技能之间的差异用属性面板上的配置表达,通用行为(冷却、施法、蓄力、充能、等级)由包里的共享逻辑统一实现,技能表现(动画、特效、音效、生成物)由锚点按时间轴在指定时机触发。

能力清单 ​

能力说明
装备与管理按槽位组织技能(从 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 里手动删除。

关于平权实现

技能包是完全平权实现的:包里的逻辑都能改,也可以照着它做自己的技能包。

预设分类:三种预设 ​

在预设面板的「技能」分类下新建,共有三种:

预设(编辑器显示名)根节点子节点作用对应壳文件
技能ScriptLocalScript一个具体技能,承载全部配置与时间轴server/packages/ability_system/ability_script.lua
技能背包(即技能管理器)ScriptLocalScript技能容器,挂在生物下管理槽位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 = "" }              -- 子技能一项

装载技能 ​

  1. 在生物上添加子组件:分类选「技能」,类型选「技能背包」;
  2. 选中技能背包,在初始技能列表里配置「技能预设 + 槽位」;
  3. 槽位的作用是和技能按钮 UI 对应,它的编号要与技能槽位 UI 上的 index 一一对应(见第 14 节)。

初始技能列表也可以留空,全部在运行时用 AddAbility 加。

槽位规则速记

槽位从 0 开始、无上限,-1 表示未入槽——这条规则贯穿全部接口,详见第 3.1 节。

编辑技能 ​

  1. 在「技能」分类里新建技能预设;
  2. 技能预设自带一个「本地运行脚本」子节点,负责 UI 等客户端内容逻辑,一般不用动;
  3. 要写技能获得时的逻辑,在技能本体下再挂一个「服务端脚本」子组件,逻辑写在这个子组件里;
  4. 需要注册、初始化时,用服务端脚本接口操作(见第 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
函数参数返回说明
AddAbilityunit, abilityKey, slotIndex?技能脚本 / 失败原因装备技能。不传槽位时从 0 号槽起递增找第一个空槽(低位有空缺先补空缺);同槽同预设会复用现有实例;槽位被别的预设占用 → 返回 nil, err
GetAbilityunit, abilityIndex技能脚本按槽位取技能
GetAbilityByScriptabilityScripthandler按技能脚本反查 handler(进阶用法,见第 13 节)
GetAbilitiesunit技能脚本列表全部已装备技能(按槽位升序)
GetAbilitiesFromIndexRangeunit, startIndex, endIndex技能脚本列表指定槽位区间(含两端)
CastAbilityunit, abilityIndex, releasePoint?, releaseDir?, releaseTarget?是否成功服务端权威施放。返回 false 表示被拒(CD 中 / 施法中 / 无充能 / 外部条件不满足)
StopAbilityunit, abilityIndex是否成功打断施法;若还在蓄力阶段则退出蓄力
AccumulateAbilityunit, abilityIndex是否成功进入蓄力(EnableAccumulate = false、CD 中或充能不满足时返回 false)
SwitchNextAbilityunit, parentSlotIndex是否成功子技能组切到下一个成员(任何切换模式都可以显式调用)
SetAbilityToSlotunit, ability, slotIndex技能脚本把已装备的技能移动到指定槽位
RemoveAbilityunit, slotIndex技能脚本按槽位移除并销毁
RemoveAbilityByKeyunit, 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 只代表请求发出去了,不代表服务端已经同意施放,最终结果要看服务端回推的状态。

函数参数返回说明
GetManagerForUnitunit背包脚本按 AbilityManager 标签反查背包(自身或子节点)
GetAbilityunit, abilityIndex技能脚本按槽位取技能(遍历背包子节点的 Index 属性)
RequestCastmanager, abilityIndex, releasePoint?, releaseDir?, releaseTarget?是否已发出请求请求施放(客户端 → 服务端)
RequestStopmanager, abilityIndex是否已发出请求请求打断 / 停止
RequestAccumulatemanager, abilityIndex是否已发出请求请求进入蓄力
RequestSwitchNextmanager, parentSlotIndex是否已发出请求请求子技能轮转
GetUIManager—UI 管理器取技能栏 UI 管理器单例(懒初始化)
Registerdomain, 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 个) ​

事件名载荷时机
BeforeUseabilityScript使用尝试开始(合法性检查之前,结果未定)
UsedabilityScript本次使用已生效(检查通过、蓄力放行、充能扣减之后)
BeforeCastabilityScript, releasePoint, releaseDir施法前
CastStartabilityScript施法开始
CastEndabilityScript施法完成(正常结束)
CastBreakabilityScript施法被打断(技能被销毁时也会发一次)
CDStartabilityScript进入 CD
CDEndabilityScriptCD 结束
ChargeabilityScript, chargeCount充能层数变化
ChargeFullabilityScript充能已满
AccumulateStartabilityScript蓄力开始
AccumulateBreakabilityScript蓄力被打断(先于 AccumulateEnd 触发)
AccumulateEndabilityScript蓄力结束(任何退出路径都会发)
OnDowngradeabilityScript, newLevel技能降级
AnchorStartabilityScript, groupId锚点点火(技能级镜像,带 groupId)
AnchorBreakabilityScript, groupId锚点中断
AnchorEndabilityScript, groupId锚点结束
AnchorStopabilityScript, groupId锚点终结(AnchorBreak / AnchorEnd 之后必发)

锚点级事件(挂在锚点单位下,共 4 个) ​

事件名相同(AnchorStart / AnchorBreak / AnchorEnd / AnchorStop),但区别于技能级:

  • 主体是锚点自身,载荷只有 abilityScript——监听方不需要用 groupId 过滤;
  • 一个技能有多个同类锚点时,可以逐个锚点单独监听。
lua
local anchor = abilityScript:FindFirstChild("你的锚点节点名")
anchor:FindFirstChild("AnchorStart"):Connect(function(abilityScript)
	-- 这个锚点点火了(不用区分 groupId)
end)

背包级事件(挂在技能背包单位下,共 3 个) ​

事件名载荷时机
AbilityAddedabilityScript, slotIndex技能加入背包
AbilityRemovedabilityScript, slotIndex技能移出背包
AbilityUpgradedabilityScript, newLevel技能升级

子技能组事件(挂在父技能单位下,共 3 个) ​

事件名载荷
OnActivatednewHandler, newIndex
OnDeactivatedoldHandler, oldIndex
OnSwitchNextnewHandler, newIndex

服务端 → 客户端广播 ​

客户端已内部处理这些广播,作者通常无需关心;只有在你自绘技能栏 UI 时才需要知道:

广播名参数
OnCastStartabilityUnitId, releasePoint, releaseDir
OnCastEnd / OnCastBreakabilityUnitId
OnCDStartabilityUnitId, cdTime
OnCDEndabilityUnitId
OnChargeabilityUnitId, newCount
OnSwitchNextownerUnitId, slotIndex, newActiveIndex
ForbidSlotownerUnitId, 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("字段名") 读。

分组字段类型默认说明 / 显隐条件
基础CdTimenumber3.0冷却时间(秒)
CastTimenumber0.5施法时间(秒),到点自动完成施法
升级Upgradablebooleanfalse能否升级
MaxLevelinteger5技能最大等级(Upgradable == true)
LevelRequirementsInt[]{}各等级所需角色等级(下标 = 目标技能等级)
蓄力EnableAccumulatebooleanfalse能否蓄力
MaxAccumulateTimenumber1.0满蓄力时间;蓄力比 = 已蓄时长 ÷ 该值(上限 1)
限制 / 释放UseLimitationInt(枚举)00 无限制 / 1 施法中可用 / 2 失控可用 / 4 滚动可用 / 8 冰冻可用
ReleaseTypeInt(枚举)00 松开施放 / 1 按住持续 / 2 两段式点击 / 3 按下施放
指示器PointerTypeInt(枚举)00 无 / 1 矩形 / 2 圆形 / 3 扇形 / 4 抛物线
ReleaseDistancenumber10.0施法范围(PointerType != 0)
AffectWidthnumber1.0影响宽度(PointerType == 1)
SectorAnglenumber90.0扇形广角(PointerType == 3)
ReleaseRadiusnumber3.0影响半径(PointerType 为 2 或 3)
ParabolaHorizontalSpeednumber10.0水平速度(PointerType == 4)
ParabolaVerticalSpeednumber10.0垂直速度(PointerType == 4)
目标筛选TargetFilterTypeString[]{}目标类型筛选,空 = 不筛选;多选时任一命中即通过("EggyUnit" / "HumanUnit" / "PhysicsUnit")
TargetTypeInt(枚举)10 无目标 / 1 方向 / 2 点 / 3 单位
TargetFilterCampInt(枚举)00 不限 / 1 敌方 / 2 友方
TargetRequiredTagsString[]{}必须标签(全部满足才命中)
TargetIgnoredTagsString[]{}忽略标签(含任一即跳过)
充能IsChargeConsumingbooleanfalse是否消耗充能
ChargeTypeInt(枚举)00 不充能 / 1 不满时充能 / 2 用完后充能
MaxChargeCountinteger5充能上限
ChargeAmountinteger1每次充能增加几次
ChargeIntervalnumber1.0充能间隔(秒)
子技能SwitchModeInt(枚举)00 不切换 / 1 施法后切换 / 2 定时切换 / 3 仅手动
SwitchOrderInt(枚举)00 顺序循环 / 1 洗牌 / 2 随机
SubAbilities结构体列表{}子技能列表(与父技能共用同一槽位)
SwitchIntervalnumber3.0切换间隔(SwitchMode == 2)
SwitchCooldownnumber0.5切换后冷却(SwitchMode != 0)
RevertTimenumber0回归计时(SwitchMode == 1;0 = 不回归)
SwitchRefreshbooleantrue子技能互切是否重置回归倒计时(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() ​

依次判定,任一不满足就禁止:

  1. 不在 CD 中(InCD == false);
  2. 不在施法中(InCast == false);
  3. 如果是消耗充能的技能:ChargeCount > 0;
  4. 所有通过 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 跳过所有校验强制升。

锚点系统 ​

锚点的四个配置 ​

配置说明
Phase1 蓄力阶段 / 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_cdInstant无(读宿主 CdTime)点火即让宿主进 CD,配合第 10.2 节的判重逻辑
cast_face_targetInstant无施法者朝向转到释放方向(仅水平方向)
destroy_abilityInstant无把宿主技能从背包移除
custom_eventInstantABILITY_ANOSTATE_EVENT_NAME触发 World.CustomEvent[名]:Fire()
play_vfxInstantABILITY_ANOSTATE_NEW_SOUND、Volume(50.0)、SoundDuration(1.0)一次性 3D 音效,所有人可听
create_obstacleInstantABILITY_ANOSTATE_NEW_OBJ、Offset(0,0,0)、Scale、ABILITY_ANOSTATE_BIND、ABILITY_ANOSTATE_LIFE_DUR实体自管生命周期
create_landmineInstantOffset(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_teleportballInstantBallDuration、ABILITY_ANOSTATE_BULLET_OBJ、Offset(0,0,0)、Scale、ABILITY_ANOSTATE_BULLET_HSPEED、ABILITY_ANOSTATE_BULLET_VSPEED命中后建传送门并传送施法者;配表时旧的 VSPEED 值请填到 HSPEED
create_bullet_to_castangleInstantABILITY_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_cordInstant + 内部状态机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_modelConstantABILITY_ANOSTATE_USE_PERFAB、Socket("origin")、Offset(0,3,0)挂到骨骼插槽,终结时销毁
gravity_changeConstantABILITY_ANOSTATE_UP_ACCELERATIONDuration 必须 > 0;期间持续施力,使净 Y 加速度≈配置值
speed_addConstantABILITY_ANOSTATE_WALK_SPEED(可以为负)Duration 必须 > 0;终结时恢复原速
play_body_animationConstantABILITY_ANOSTATE_ANIMKEY、_ANIM_HALF、_LOOP、_ANIM_STARTPOINT、_ANIM_SPEED(1)需要施法者带 Animator;半身动画固定 FilterType = 30
play_sfxConstantABILITY_ANOSTATE_NEW_SFX、Offset(0,0,0)、ABILITY_ANOSTATE_SCALE(1.0)、ABILITY_ANOSTATE_BINDBind = true 时必须 Duration > 0,否则无法回收
melee_hitConstantDuration(>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_HITSFXOFFSETDuration 必须 > 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类
PointerType0 无 / 1 矩形 / 2 圆形 / 3 扇形 / 4 抛物线AbilityPointer / RectanglePointer / CirclePointer / SectorPointer / ParabolaPointer
ReleaseStrategy0 松开 / 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 件事

  1. 不要依赖自己的 new——框架不会调用它,实例创建后直接使用 self.ctrl;
  2. 交互走 self.ctrl:_requestCast(point, dir, target) / _requestStop() / _requestAccumulate() / _requestStopAccumulate()。这几个下划线命名的方法虽然看着像内部接口,但它是策略层唯一的通道,内置的 4 个策略也是这么用的;
  3. 如果需要「先点场景选点」(两段式那种):暴露 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_timerCD 进度(节点需支持 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)
PlaceAnimIdofficial://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_systemrequire("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 / SetCanUseOnUncontrolhandler.setLimitation(位, enable)新包统一走限制位
SetCanUseWhenFreezedsetLimitation(冰冻位, 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 属性对照 ​

结论先说:老配置可以直接搬,字段名基本没变,需要改的地方如下。

改名

老新
运行时属性 ReleaseDirectionReleaseDir
结构体 AbilityInfo.AbilityAssetAbilityInfo.AssetId

新增(老包没有)

CastStartTime、AccumulateRatio、AbilityPresetKey、AbilityName(装备时自动写入)、背包 Icon。

默认值不同(老配置搬过来后行为可能变,注意核对)

字段老默认新默认
CdTime1.03.0
CastTime1.00.5
MaxLevel15
MaxChargeCount15
PointerType10
TargetFilterCamp10
ParabolaVerticalSpeed8.010.0
UseLimitation10

老有、新没有的字段

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层级从技能挪到背包
OnCastStartCastStart去掉 On 前缀
OnCastBreakCastBreak同上
OnCastEnd(is_break)CastEnd新包把「被打断」拆成独立事件,CastEnd 只表示正常结束
OnAccumulateStart / OnAccumulateBreak / OnAccumulateEndAccumulateStart / AccumulateBreak / AccumulateEnd去掉前缀
OnUpgrade背包级 AbilityUpgraded层级移动
OnDowngradeOnDowngrade同名 ✅
OnChargeEndChargeFull改名
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。

有差异的

枚举老新
UseLimitationNone=0 / InCast=1 / UnControllable=2 / Roll=4同上,多一位 Freezed=8
SwitchModeNone=0 / AfterCast=1 / Timer=20 不切换 / 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)
end

A.9 迁移检查清单 ​

  1. 把 Packages.ability_system 全部换成 require("...packages.ability_system.api") 的接口;
  2. 客户端施放从 CastAbility 换成 RequestCast(注意传的是背包,不是角色);
  3. 事件监听从 ConnectEvent 换成 FindFirstChild("事件名"):Connect,并按 A.5 改名;
  4. 锚点从「回调字段写逻辑」换成「挂 anchors/<行为>.lua」,事件名改成固定 4 个;
  5. 核对 A.4 的默认值差异,尤其是 CdTime / CastTime / PointerType / TargetFilterCamp;
  6. 确认没用到 A.2 / A.3 里的「没有」项;用到的话按替代方案改写,没有替代方案的由作者自行实现。