技能系统数据关联全解(新手策划入门)
面向新策划的一站式指南:从客户端配表 → 服务端 SkillBase → 代码执行, 以及 NpcBase 中技能字段如何与 SkillBase 挂钩,一篇文章全看懂。
适用对象:技能策划、数值策划、新手策划 关联文件:
tbl/SkillBase.xml、tbl/SkillBase.tbl2、tbl/NpcBase.xml、tbl/NpcBase.tbl2、tbl_xml/ExcelMakePack.xml、base/zDatabase.h、ScenesServer/Skill.cpp、ScenesServer/SkillStatusManager.cpp、tools/gen_server_skillbase.py
一、数据表全景图
┌─────────────────────────────────────────────────────────────┐
│ 客户端(策划设计器导出) │
│ │
│ skillbase.xml — 技能基础表(每技能一行) │
│ skilllevelbase.xml — 技能等级表(每技能ID+等级一行) │
│ │ │
│ │ tools/gen_server_skillbase.py 自动映射 │
│ ▼ │
│ SkillBase.xml / SkillBase.tbl2(服务端) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 24 列,基础字段自动填,仅「效果」需人工配置 │ │
│ │ 技能ID + 技能等级 联合唯一 │ │
│ └──────────┬───────────────────────────────────────┘ │
│ │ 按 (技能ID-1)*10+等级 哈希索引 │
│ ▼ │
│ skillbm(服务端技能数据管理器) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ zSkillB 结构,解析后包含: │ │
│ │ - 技能基础属性(等级/消耗/间隔/前提等) │ │
│ │ - skillStatus 向量(「效果」字段解析结果) │ │
│ │ - weaponlist(需要武器列表) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ NpcBase.xml / NpcBase.tbl2(服务端NPC表) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 「技能」列 → NPC基础攻击技能ID │ │
│ │ 「使用技能」列 → 结构化技能配置串 │ │
│ └──────────────────────┬───────────────────────────┘ │
│ │ 技能ID引用 │
│ ▼ │
│ skillbm 中的 zSkillB(按技能ID+等级查找) │
└─────────────────────────────────────────────────────────────┘
二、客户端表与服务端 SkillBase 的关系
2.1 客户端的两张表
客户端策划设计器导出两份技能表:
| 表 | 文件名 | 粒度 | 作用 |
|---|---|---|---|
| 技能基础表 | skillbase.xml | 每个技能一行(不含等级) | 技能的基本设定:名称、图标、特效 |
| 技能等级表 | skilllevelbase.xml | 每个「技能ID+技能等级」一行 | 每个等级的具体数值:消耗、间隔、伤害等 |
关键点:
skilllevelbase.xml已经按「技能ID + 技能等级」逐行展开, 这与服务端 SkillBase 的存储方式完全一致。
2.2 服务端 SkillBase 的 24 列结构
服务端 tbl/SkillBase.xml / tbl/SkillBase.tbl2 是按「技能ID + 技能等级」逐行展开的 24 列配置表。
字段顺序(来自 tbl_xml/ExcelMakePack.xml 服务端配置):
| 序号 | 字段名 | 类型 | 说明 |
|---|---|---|---|
| 0 | 技能ID | DWORD | 技能唯一ID,和技能等级联合确定一行 |
| 1 | 技能名称 | string(32) | 技能显示名称 |
| 2 | 技能等级 | DWORD | 技能等级 1~10 |
| 3 | 技能系别 | DWORD | 技能所属职业/系统 |
| 4 | 技能树别 | DWORD | 技能树分支 |
| 5 | 需要本线技能点数 | DWORD | 学习该等级需要投入的技能点数 |
| 6 | 前提技能一 | DWORD | 前置技能1的ID |
| 7 | 前提技能一等级 | DWORD | 前置技能1所需等级 |
| 8 | 前提技能二 | DWORD | 前置技能2的ID |
| 9 | 前提技能二等级 | DWORD | 前置技能2所需等级 |
| 10 | 前提技能三 | DWORD | 前置技能3的ID(或角色等级要求) |
| 11 | 前提技能三等级 | DWORD | 前置技能3所需等级 |
| 12 | 间隔时间 | DWORD | 技能冷却时间(毫秒) |
| 13 | 攻击方式 | DWORD | 技能类型(1=主动/2=被动触发/3=被动常驻) |
| 14 | 能否骑马使用 | DWORD | 0=不可骑马使用 1=可以 |
| 15 | 需要物品 | DWORD | 学习所需物品ID |
| 16 | 需要武器 | string(128) | 使用该技能所需武器类型,冒号分隔 |
| 17 | 消耗体力值 | DWORD | 使用消耗的体力 |
| 18 | 消耗法术值 | DWORD | 使用消耗的法力 |
| 19 | 消耗生命值 | DWORD | 使用消耗的生命 |
| 20 | 伤害加成 | DWORD | 额外伤害加成 |
| 21 | 效果 | string(1024) | 技能逻辑核心,详见下文 |
| 22 | 消耗物品类型 | DWORD | 使用消耗的物品ID |
| 23 | 物品消耗数量 | DWORD | 使用消耗的物品数量 |
2.3 客户端 → 服务端的字段映射
通过 tools/gen_server_skillbase.py 将客户端的 skilllevelbase.xml 一键映射为服务端 SkillBase:
| 服务端字段 | 客户端字段 | 备注 |
|---|---|---|
| 技能ID / 技能名称 / 技能等级 | 同名 | 硬性一致,不可改名/错号 |
| 需要本线技能点数 | 需要本树技能点数 | 字段名归一化 |
| 前提技能一/二/三 | 前提技能1/2/3 | 字段名归一化 |
| 前提技能一/二/三等级 | 前提技能1/2/3级别 | 字段名归一化 |
| 能否骑马使用 | 允许骑马 | 字段名归一化 |
| 需要武器 | 适用武器类型 | 字段名归一化 |
| 消耗物品类型 | 消耗物品 | 字段名归一化 |
| 物品消耗数量 | 消耗数量 | 字段名归一化 |
| 效果 | (客户端无此字段) | 需策划人工配置 |
| 需要物品 | (客户端无此字段) | 需策划按需填写 |
注意:客户端表中还有「目标」「距离范围」「攻击方式」「施放方式」等字段, 它们不直接进入服务端 SkillBase 的独立列,而是作为配置「效果」字段时的参考信息 出现在
SkillBase_效果待填清单.csv中,辅助策划判断该技能的施法方式和目标类型。
三、服务端 SkillBase 如何被加载和索引
3.1 哈希索引规则
服务端将所有技能按 skill_hash(技能ID, 技能等级) 索引存储:
#define skill_hash(type, level) ((type - 1) * 10 + level)
即:技能ID=1, 等级=1 → hash = (1-1)*10+1 = 1
技能ID=1, 等级=10 → hash = (1-1)*10+10 = 10
技能ID=2, 等级=1 → hash = (2-1)*10+1 = 11
3.2 加载到内存
服务端启动时,将 tbl/SkillBase.tbl2 的每一行读入 skillbm(一个全局技能数据管理器)。
每一行被解析为 zSkillB 结构,包含:
- 所有基础字段(技能ID、等级、消耗、间隔等)
skillStatus向量(「效果」字段的解析结果)weaponlist(需要武器列表)
3.3 运行时使用
当角色/NPC使用技能时,通过 skill_hash 在 skillbm 中查找到对应的 zSkillB,
创建 zSkill 运行时对象,然后通过 zSkill::action() 执行技能逻辑。
四、NpcBase 中的技能字段
4.1 两个关键字段
tbl/NpcBase.xml / tbl/NpcBase.tbl2 中有两个与技能相关的字段:
| 字段 | 类型 | 用途 |
|---|---|---|
| 技能 | DWORD | NPC 的基础攻击技能ID,单个技能 |
| 使用技能 | string(1024) | NPC 的主动技能配置,结构化字符串,可配多个 |
4.2 「技能」字段(基础攻击)
- 这是 NPC 的普攻技能。
- 当 NPC 进行近战/远程普通攻击时,代码将该技能ID作为
wdMagicType发送。 - 若此字段为
0,近战攻击使用SKILLNORMAL(技能ID=377),远程攻击使用NORMALBOW(技能ID=378)。 - 该技能ID引用
SkillBase.tbl2中对应技能ID的技能,使用zSkill::create(this, skill_id, npc->level)加载。
实际数据示例:
编号=277 名称=王城守卫 技能=8001
编号=12106 名称=虚空凶灵 技能=304
即「王城守卫」的基础攻击使用技能8001,「虚空凶灵」使用技能304(神罚)。
4.3 「使用技能」字段(主动技能)
这个字段是一个结构化字符串,格式为:
技能类型:技能ID-需求等级-触发概率-系数,技能ID-需求等级-触发概率-系数;技能类型:...;
各字段含义:
| 位置 | 含义 | 说明 |
|---|---|---|
| 技能类型 | NPC AI 分类 | 1=伤害 2=恢复 3=增益Buff 4=减益Debuff 5=召唤 6=复活 |
| 技能ID | 技能表ID | 引用 SkillBase.tbl2 中的 技能ID |
| 需求等级 | 使用门槛 | NPC等级需≥此值才可用(当前AI代码中未强制检查,预留) |
| 触发概率 | 施放概率 | 0~100 的百分比,AI判定是否施放 |
| 系数 | 伤害/数值系数 | 预留字段,当前AI代码未使用 |
多个技能以 , 分隔,多个类型以 ; 分隔。
实际数据示例:
编号=401 名称=禁卫军 使用技能=1:77-0-100-1;
编号=277 名称=王城守卫 使用技能=1:993-0-100-1;
编号=30093 名称=南方增长天王 使用技能=1:979-0-100-1;5316:10000:1-1;5315:10000:1-1;...
解析逻辑(zNpcB::parseSkills):
- 按
;分割 → 得到每个技能类型组 - 按
:分割 → 第一段是技能类型,第二段是该类型的技能列表 - 按
,分割 → 得到每个独立技能 - 按
-分割 → 得到技能ID、需求等级、概率、系数
4.4 NPC 技能如何被 AI 使用
NpcBase.tbl2 → parseSkills() → skillMap{类型: [npcSkill数组]}
│
NPC初始化时加载所有技能到 usm
│
AI 按类型随机选一个技能
│
检查触发概率 (selectByPercent)
│
useSkill(target, skill.id)
│
zSkill::action() → 执行技能效果
NPC AI 中按类型调用:
| 场景 | 调用类型 |
|---|---|
| 普通攻击 | getRandomSkillByType(SKILL_TYPE_DAMAGE, skill) |
| 治疗自己 | getRandomSkillByType(SKILL_TYPE_RECOVER, skill) |
| 给自己上Buff | getRandomSkillByType(SKILL_TYPE_BUFF, skill) |
| 给敌人上Debuff | getRandomSkillByType(SKILL_TYPE_DEBUFF, skill) |
五、「效果」字段详解
5.1 什么是「效果」字段
SkillBase 表的第 21 列「效果」,是一行技能全部逻辑效果的浓缩文本。
服务端在加载时解析为两层结构:
一个技能 = 若干「状态组」(SkillStatus)
一个状态组 = 若干「效果元素」(SkillElement)
5.2 四级分隔符
| 分隔符 | 作用 | 层级 |
|---|---|---|
. | 分隔多个状态组 | 第1层 |
: | 分隔「状态定义」和「元素列表」 | 第2层 |
; | 分隔多个效果元素 | 第3层 |
- | 分隔字段内部数值 | 第4层 |
5.3 状态组 SkillStatus(7个字段)
A-B-C-D-E-F-G
id target center range mode clear isInjure
| 位置 | 字段 | 含义 | 取值 |
|---|---|---|---|
| 1 | id | 状态组标识 | 通常为0 |
| 2 | target | 目标类型(位掩码) | 1=自身 2=友方 4=敌方 8=NPC 16=地表 32=宠物 64=召唤物,可叠加 |
| 3 | center | 施法中心 | 1=鼠标点 2=自身 |
| 4 | range | 攻击范围 | 1=单体,其他值=范围 |
| 5 | mode | 飞行模式 | 0=近身 1=飞行 |
| 6 | clear | 能否被清除 | 0/1 |
| 7 | isInjure | 是否触发伤害判定 | 0=仅状态 1=伤害结算 |
5.4 效果元素 SkillElement(5个字段)
A-B-C-D-E
id percent value time state
| 位置 | 字段 | 含义 | 说明 |
|---|---|---|---|
| 1 | id | 效果状态ID | 对应 SkillStatus_XX() 处理函数,决定效果类型 |
| 2 | percent | 触发百分比 | 0~100,100=必定触发 |
| 3 | value | 效果数值 | 具体加成/伤害值 |
| 4 | time | 持续时间 | 秒 |
| 5 | state | 状态表现 | 对应客户端 USTATE 状态ID |
5.5 实际解析示例
以技能ID=993「固伤弓箭30%」的效果为例:
0-1-2-1-0-0-0:23-100-1-1-0;.0-12-1-1-1-0-1:73-100-30-1-0;133-0-0-0-0;.
状态组1 0-1-2-1-0-0-0:23-100-1-1-0;
- 状态定义:
target=1(自身) center=2(以自身为中心) range=1(单体) mode=0 - 效果元素:
SkillStatus_23= 物理攻击力+1,100%触发,持续1秒
状态组2 0-12-1-1-1-0-1:73-100-30-1-0;133-0-0-0-0;
- 状态定义:
target=12(敌方+NPC) isInjure=1(触发伤害) - 效果元素1:
SkillStatus_73= 降低目标生命值30%,100%触发 - 效果元素2:
SkillStatus_133= 标记本次攻击为物理伤害
整体含义:该技能对自身附加物攻+1的短暂增益,同时攻击目标并造成其生命值30%的伤害。
六、数据关联全景(最核心)
客户端 skilllevelbase.xml ──→ 服务端 SkillBase.tbl2 ──→ skillbm(内存)
(技能ID+等级展开) (gen脚本映射,效果人工填) (skill_hash索引)
│
├──→ 玩家使用技能
│ zSkill::action()
│ → 解析skillStatus
│ → SkillStatus_XX()执行
│
NpcBase.tbl2 ────────────────────────────────────────────→ │
「技能」字段(基础攻击)──────→ skillbm查找──────→ NPC普攻
「使用技能」字段(主动技能)──→ skillbm查找──────→ NPC AI施法
配技能需要关心的三个层次:
- 客户端:策划在设计器中配置技能的展示信息(名称、图标、特效)和基础参数
- 服务端 SkillBase:策划配置技能的所有数值和「效果」逻辑
- NpcBase:策划将技能挂载到 NPC 上,决定 NPC 何时用什么技能
七、新手策划配置流程速查
7.1 添加新技能
- 在客户端设计器配置技能的基础参数(名称、图标、冷却等)
- 导出
skilllevelbase.xml - 运行
gen_server_skillbase.py生成服务端 SkillBase 表 - 在「效果待填清单」中为每个技能编写「效果」字段
- 合并回填后,将最终
SkillBase.xml替换到tbl/目录 - 重新生成
SkillBase.tbl2(通过 ExcelMakePack 打包)
7.2 给NPC配置技能
- 确认技能已在
SkillBase.tbl2中存在 - 打开
NpcBase表 - 「技能」列填 NPC 基础攻击技能ID
- 「使用技能」列按格式填写:
1:技能ID-0-100-1;2:技能ID-0-80-1;
- 类型1=伤害 2=恢复 3=Buff 4=Debuff
- 概率填触发百分比
- 重新打包
NpcBase.tbl2
7.3 常见问题排查
| 问题 | 可能原因 |
|---|---|
| NPC放不出技能 | 技能ID不在SkillBase表中;NPC等级不满足;概率填0 |
| 技能没有效果 | 「效果」字段未填或格式错误;效果ID超出 SKILLSTATENUMBER(400) |
| 技能不生效但客户端显示正常 | 服务端SkillBase「效果」列被截断(tbl2已修复此问题) |
| 技能伤害不对 | 检查效果元素的 value、percent 是否正确;检查状态组的 isInjure 是否为1 |
| NPC不会治疗/加Buff | 「使用技能」中类型填错,AI按类型查找 |