更新日志
This commit is contained in:
260
战斗/写代码的诗人/战斗系统:状态配置管理(技能配置管理).md
Normal file
260
战斗/写代码的诗人/战斗系统:状态配置管理(技能配置管理).md
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
阅读提醒:
|
||||
|
||||
1. 本篇虽然总是以技能为例,但在一切皆状态的架构下,所有的状态效果配置都是相同的。
|
||||
|
||||
2. 关于表格设计,可阅读《[配置:表格设计](https://mp.weixin.qq.com/s?__biz=Mzk0MjcwNTY4MA==&mid=2247483826&idx=1&sn=2d3cc9ac988cb20e60fb8fca1405bf52&scene=21#wechat_redirect)》
|
||||
|
||||
|
||||
---
|
||||
|
||||
## **Excel之殇**
|
||||
|
||||
作者的第一个项目和第二个项目,都是使用Excel来配置技能的,那复杂度简直爆炸 —— 好几张表,每张表又是几十个字段。回过头看,我们的战斗策划的工作真是太艰难了,策划里面加班最多的,就是数值策划和战斗策划,可是真的需要那么加班吗?
|
||||
|
||||
我在《[配置:吐槽时间](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzk0MjcwNTY4MA%3D%3D%26mid%3D2247483826%26idx%3D2%26sn%3Dd4aacd587f734bdd25d1d82a220f7a60%26scene%3D21%23wechat_redirect)》中提到:**因为程序不配表,体会不到策划配表的痛苦,因此缺少反思**。程序很少去思考自己设计的正确性,或是想着能用就行,不考虑团队其它成员的工作效率,这对整个项目来说是非常有害的。
|
||||
|
||||
程序,就是用来解放生产力的。如果你的程序不仅没有减少他人的工作量,反而增加他人工作量,那这个程序就是不合格的。
|
||||
|
||||
## **[状态总表](https://zhida.zhihu.com/search?content_id=255785060&content_type=Article&match_order=1&q=%E7%8A%B6%E6%80%81%E6%80%BB%E8%A1%A8&zhida_source=entity)**
|
||||
|
||||
我在前篇《[战斗系统:什么是数据驱动?](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzk0MjcwNTY4MA%3D%3D%26mid%3D2247484217%26idx%3D1%26sn%3D8fe2c017ab84501915d99ec79c3371c3%26scene%3D21%23wechat_redirect)》中提到,对于技能这种复杂的数据,应该舍弃Excel,使用Json或Lua等来配置。那我们是否还需要一张状态总表?
|
||||
|
||||
我的建议是需要,它有几个作用:
|
||||
|
||||
1. 状态总览:我们可以知晓一共有哪些状态,以及状态的一些基础数据
|
||||
2. 有效性控制:只有在表格中存在的状态才是有效的,而不是所有扫描到的配置都有效
|
||||
3. 简化配置:虽然行为相关的参数不适合配置在Excel,但其它参数配置在表格更加方便
|
||||
|
||||

|
||||
|
||||
PS:关于状态表的字段设计,可阅读《[战斗系统:状态管理](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzk0MjcwNTY4MA%3D%3D%26mid%3D2247484197%26idx%3D1%26sn%3D6e069ad2972f1222cf96eaa118116c37%26scene%3D21%23wechat_redirect)》。
|
||||
|
||||
## **[技能总表](https://zhida.zhihu.com/search?content_id=255785060&content_type=Article&match_order=1&q=%E6%8A%80%E8%83%BD%E6%80%BB%E8%A1%A8&zhida_source=entity)**
|
||||
|
||||
同理,技能我们也需要一张总表,用于控制技能的有效性。不过,**技能表中的ID必须在状态表中存在**,这有几个作用:
|
||||
|
||||
1. 可通过技能ID查询关联的State配置 —— 约定大于配置
|
||||
2. _脚本中不需要区分状态ID和技能ID_,可以避免诸多麻烦
|
||||
|
||||

|
||||
|
||||
Q:哪些需要配置在数据脚本,哪些需要配置Excel表?
|
||||
|
||||
A:与执行相关的数据都配置在数据脚本(通过编辑器配置),其它则配置在Excel中。换句话说,Excel配置的主要是与Player养成功能相关的数据。
|
||||
|
||||
**技能类型**
|
||||
|
||||
技能的分类也是多维度的,不过没有状态那般复杂,所以可以展开为多列 —— 但建议仍采用Tag分类法。
|
||||
|
||||
被动技能也采用标签表达。
|
||||
|
||||
|
||||
|
||||
**技能Tips和状态Tips**
|
||||
|
||||
技能的Tips仍然配置在技能表,状态的Tips配置在状态表,虽然技能ID一定在状态表存在,但技能的Tips不能在状态表。
|
||||
|
||||
**技能的Tips属于预览数据的Tips,而状态的Tips属于运行时数据Tips**(真实数据的Tips)。
|
||||
|
||||
直接说,可能大家不太明白。我举个例子:DNF的武神步,其技能描述如下:
|
||||
|
||||

|
||||
|
||||
最终加了多少力量,在技能界面是看不见的,是动态计算的。
|
||||
|
||||
PS:DNF的武神玩家都是多动症玩家...
|
||||
|
||||
|
||||
|
||||
**施展条件**
|
||||
|
||||
有些项目是将技能的施展条件放在了技能表中的,比如我以前的项目;施展条件确实不属于运行时数据,但条件是存在组合和分层的,用Excel很难表达(很不直观)。有几种方式:
|
||||
|
||||
1. 手写Json(或其它格式)
|
||||
2. 为Excel制作编辑器,可视化编辑Json
|
||||
3. 转移到数据脚本,通过编辑器编辑
|
||||
|
||||
为Excel做编辑器需要巨大的工作量才能做得好,但做好了收益巨大;不过,对于中小团队而言,教会策划写Json更具性价比。
|
||||
|
||||
在有通用数据编辑器的情况下,将这部分配置转移编辑器也是个方案;但数据一部分在表格,一部分在编辑器导出的Json文本,可能会带来一些不便。
|
||||
|
||||
不过,由于技能的执行数据是在编辑器中配置的,所以将施展条件等转移到编辑器不会有额外的副作用,所以很合适。
|
||||
|
||||
---
|
||||
|
||||
## **数值成长问题**
|
||||
|
||||
在我将[行为树](https://zhida.zhihu.com/search?content_id=255785060&content_type=Article&match_order=1&q=%E8%A1%8C%E4%B8%BA%E6%A0%91&zhida_source=entity)引入到战斗系统时,技能的所有参数都直接配置在了行为树的节点上,这面临着几个问题:
|
||||
|
||||
1. 角色技能是支持升级的,升级以后数值会变化,总不能让策划每一级画一棵树吧?
|
||||
2. 参数散落在行为树的节点上,客户的技能Tips该怎么做?
|
||||
3. 表现层和逻辑层的逻辑可能不同,客户端和服务器可能使用两棵树,但部分参数是相同的。
|
||||
|
||||
其实第一个问题还好,因为技能升级,其数值通常都是按公式计算的,所以我们可以直接在编辑器中支持公式,就像这样:
|
||||
|
||||
```csharp
|
||||
public class Value {
|
||||
public double value;
|
||||
public int deltaLv; // 每多少级执行一次成长
|
||||
public double k;
|
||||
public double b;
|
||||
public double min;
|
||||
public double max;
|
||||
}
|
||||
```
|
||||
|
||||
PS:这里仅以线性成长函数为例。
|
||||
|
||||
至于客户端技能Tips,一种常见的方式是:策划将格式文本(Fromat)和数值分离,并冗余定义一套变量,由程序根据公式计算每一级的数值,然后再根据Format计算最终的展示文本 —— 使用name占位符。
|
||||
|
||||
这种方案虽然能解决问题,但我向来对**增加策划工作量的设计**表示否定,因为手工维护的东西(冗余数据)越多,工作效率越低,出错的概率也越高;而程序处理问题,通常是一次性的,对就是对,错就是错;如果担心运行时的性能,可以通过开发期的工具解决。
|
||||
|
||||
第三个问题,自然也可以通过冗余配置来解决,但这也是会增加策划工作量的,所以不好。
|
||||
|
||||
正确的方案是:**将变量定义在行为树外部**(集中定义),行为树中的节点通过name引用关联的字段。就像这样:
|
||||
|
||||

|
||||
|
||||
数据结构定义如下:
|
||||
|
||||
```csharp
|
||||
// 状态变量:和上面的Value结构类似,只是多了name
|
||||
public class StateVar {
|
||||
public string name; // 变量名
|
||||
public double value; // value = k * lv +b
|
||||
// ...
|
||||
}
|
||||
// 状态脚本配置--运行时会缓存在State对象上
|
||||
public class StateTaskCfg {
|
||||
// 状态变量
|
||||
public List<StateVar> stateVars;
|
||||
// 技能施展条件
|
||||
public ICondition castCond;
|
||||
// 服务器脚本
|
||||
public Task<Blackboard> stask;
|
||||
// 客户端脚本
|
||||
public Task<Blackboard> ctask;
|
||||
}
|
||||
// 变量引用
|
||||
public class VarRef {
|
||||
public string name; // 变量名,非空表引用
|
||||
public double value; // 常量值
|
||||
public int index = -1; // 查询缓存
|
||||
}
|
||||
```
|
||||
|
||||
简单文本示例:
|
||||
|
||||
```as3
|
||||
// 技能配置 - Dson文本格式
|
||||
{@{StateTaskCfg}
|
||||
skillId: 1001,
|
||||
skillVars: [
|
||||
{name: attack_01, value: 1.5, min: 1.5, max: 3},
|
||||
{name: attack_02, value: 1.2, min: 1.2, max: 2},
|
||||
],
|
||||
castCond: {},
|
||||
stask: {},
|
||||
ctask: {}
|
||||
}
|
||||
// 伤害节点示例:攻击力系数为变量,元素类型为固定值
|
||||
{@{DamageAction}
|
||||
attack: {name: attack_01},
|
||||
element: {value: 1}
|
||||
}
|
||||
```
|
||||
|
||||
我们将技能、Buff中的**所有数值都定义为VarRef类型** —— 包括触发间隔、触发次数、技能CD等。如果变量名不为空字符串,则表示变量引用,否则表示常量值。
|
||||
|
||||
### **定制等级属性**
|
||||
|
||||
首先,数值的成长最好是可预测的,这对玩家来说更容易理解,所以并不建议如此做。
|
||||
|
||||
如果真的有这种需求,是可以支持的,我们只需要将成长公式分等级段即可。
|
||||
|
||||
```csharp
|
||||
public class Value {
|
||||
// 成长公式配置
|
||||
List<ValueUpgradeCfg> upgradeCfgs;
|
||||
}
|
||||
public class ValueUpgradeCfg {
|
||||
public int minLv; // 公式适用等级
|
||||
public int maxLv;
|
||||
|
||||
public double value; // 该段的初始值
|
||||
public int deltaLv; // 每多少级执行一次成长
|
||||
public double k;
|
||||
public double b;
|
||||
public double min;
|
||||
public double max;
|
||||
}
|
||||
```
|
||||
|
||||
## 数值传递问题
|
||||
|
||||

|
||||
|
||||
技能加Buff是非常常见的需求,如果需要在技能的Tips界面,看见最终的Buff数值,如持续时间,属性加成等,应该怎么做?
|
||||
|
||||
最直接的办法,就是**保持两个脚本具有相同的变量** —— name和成长公式相同。这样,只要技能等级和Buff等级一样,那么算出的数值也就一样,也就不需要传递了。
|
||||
|
||||
另一种方案是运行时解决,**允许指定状态的变量值**,即覆盖变量,代码如下:
|
||||
|
||||
```csharp
|
||||
public sealed class State {
|
||||
public StateCfg cfg;
|
||||
public int lv; // 状态等级
|
||||
public StateTaskCfg taskCfg; // 变量配置在这里
|
||||
public List<double> values; // 最终值
|
||||
// ...
|
||||
// 查询变量值
|
||||
public double GetValue(VarRef varRef) {
|
||||
if (string.IsEmptyOrWhitespace(varRef.name)) {
|
||||
return varRef.value; // 常量
|
||||
}
|
||||
if (varRef.index < 0) { // 索引缓存
|
||||
varRef.index = taskCfg.GetIndex(varRef.name);
|
||||
}
|
||||
return values[varRef.index];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
将状态由等级计算的数值全部缓存下来,然后允许外部修改。
|
||||
|
||||
**那怎么选呢?**
|
||||
|
||||
自然是全都要。**技能的数值并不完全由等级计算,还会受到技能修改器影响**,因此保持公式一致并不能完全保证数值的一致性,所有后者是必须的。
|
||||
|
||||
而前者也是必须的,因为Buff并非总是由主动技能添加,也可能由装备的被动技能触发,我们需要保持在无修改器的情况下,等级相同时属性一致。如果担心冗余配置出错,可以通过[数据检查工具](https://zhida.zhihu.com/search?content_id=255785060&content_type=Article&match_order=1&q=%E6%95%B0%E6%8D%AE%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7&zhida_source=entity)来校验 —— 数据检查工具是不可或缺的。
|
||||
|
||||
---
|
||||
|
||||
## **舍弃子弹表**
|
||||
|
||||
我们通常有一个GameObject的总表,配置着逻辑层用到的所有GameObject;然后,为了减少总表的列数,部分项目会将子类型才拥有的字段会拆分到子表中,子弹表就是比较常见的。
|
||||
|
||||
游戏中的子弹有许多特殊的职责 —— 这个我们后面的文章讲,因此子弹表通常有非常多的字段,而且主要是跟逻辑相关的。
|
||||
|
||||
在一切皆状态的框架下,子弹表是可以完全省略的,并不需要额外的配置;但如果大家使用的自己的框架,我建议将子弹的配置改为Json或Lua,也采用数据驱动的方式配置,这可以大幅提供系统的可维护性和可扩展性。
|
||||
|
||||
---
|
||||
|
||||
## **选择一种好的文本格式**
|
||||
|
||||
最后,我们应当选择一种好的文本格式来记录数据,它需要满足以下基本条件:
|
||||
|
||||
1. 能精确表达数据
|
||||
2. 易于阅读和手工编写
|
||||
3. 易于维护(修改)
|
||||
4. 支持注释
|
||||
5. 良好的格式
|
||||
|
||||
作者在处理序列化问题的时候,设计了自己的数据文本格式《[Dson文本格式](https://link.zhihu.com/?target=https%3A//mp.weixin.qq.com/s%3F__biz%3DMzk0MjcwNTY4MA%3D%3D%26mid%3D2247483975%26idx%3D1%26sn%3D9f20abc69e456cbea798be54ca227135%26scene%3D21%23wechat_redirect)》,有兴趣的读者可以看一看。
|
||||
|
||||
---
|
||||
|
||||
## **结语**
|
||||
|
||||
除了技能和Buff外,其实我们的副本流程这些也需要脚本化,其表格和数据脚本的设计,规则是相似的。
|
||||
Reference in New Issue
Block a user