木匣子

Web/Game/Programming/Life etc.

通用等级经验表配置

最近翻资料的时候看到一篇很早以前写的内部分享文章,关于等级-经验表的配置设计。对游戏开发来说,这项配置简单到不能再简单了,但是如果处理不好会增加许多沟通成本,甚至闹 Bug —— 没人喜欢 Bug. 所以我打算拿出来分享一下。

在游戏开发中,等级与经验表是一个非常重要的配置。在我参与的第一个项目的开发实践中因为没有确定一个比较好的规范,导致了一些分歧,所以本文将对等级经验表配置的合理性进行探索。

一些需要考虑的事情

  • 配置表中哪些参数是必须的?
  • 等级由 0 级开始还是由 1 级开始?
  • 升级所需经验是由程序取出两行经验相减,还是生成配置表的时候计算好?
  • 服务端如何存储等级和经验?以及如何处理经验上限?
  • 界面上如何表现经验?

这些问题如果没有一个明确的答案,开发过程中就会出现策划每新增一个功能,做出来的表都跟之前的不一样。然后接下来就是程序的恶梦了,沟通一不留神,就埋个 Bug.

此外需求总是多变的,例如等级由 0 还是 1 开始,在不同的功能模块中,可能就是个变量。还有界面的表现,有时希望进度条每级清零,有时又希望显示当前经验占总经验的百分比。

那么有没有省心一点的标准答案呢?其实只要给经验表增加一些冗余,就会发现其实问题也没有那么复杂。

例表 Level - Exp Table

lv0    lv1    start    end    interval
0      1      0        99     100
1      2      100      299    200
2      3      300      599    300
...
98     99     7000     9999   3000
99     100    10000    14999  5000

细看这个表,可以发现有许多字段是多余的:只须 end 或 interval 列即可推算出其它所有列。如果策划只提供 end 或 interval 中的一列,那么程序最好能自行生成其它四列。所幸经验表只是个小表,一点冗余并不会有多少负担,但是带来的收益是非常大的。它们的存在能够非常快地简化程序。该表在设计的时候做了如下考虑:

  • DO - 等级 0 与等级 1 同时填写,需要用哪种形式就取哪个,可以直观看出不同形式的上限等级(供强迫症策划参考);
  • DO - 预先计算每一等级的「起始经验」、「结束经验」和 「经验区间」,方便程序直接查询,必避在用的时候进行跨行计算,否则要多很多逻辑去小心处理边界问题;
  • DON’T - 千万不要用每一等级的「起始经验」作为关键数据(去推算其它列),否则你会在如何确定等级上限跟策划争论不休,尤其是当你没有满足上一条的时候。

扩展

这个配置表实际上可以用于描述任何跟经验有关的配置,不只是人物等级也可以是 VIP 等级-充值 之类的关系,附带属性只需增加相应的字段来存放额外的信息就行了,而程序检索起来也非常方便。

List: sample data

var exp_table = [{
    level0: 0,
    level1: 1,
    start_exp: 0,
    end_exp: 99,
    interval_exp: 100,
    extra_data: "a"
}, {
    level0: 1,
    level1: 2,
    start_exp: 100,
    end_exp: 299,
    interval_exp: 200,
    extra_data: "b"
}, {
    level0: 2,
    level1: 3,
    start_exp: 300,
    end_exp: 599,
    interval_exp: 300,
    extra_data: "c"
}];

代码

核心程序只有这三个,将错误的可能性降至最小:

List: core functions

function getConfigFromExp(config_table, total_exp) {
    var row;
    for (row = 0, len = config_table.length; row < len; row++) {
        if (config_table[row].end_exp >= total_exp) {
            return config_table[row];
            break;
        }
    }
    return config_table[row - 1]; // top level
}

function getConfigFromLevel0(config_table, level0) {
    return config_table[level0];
}

function getConfigFromLevel1(config_table, level1) {
    return config_table[level1 - 1];
}

使用方法:

List: usage

> getConfigFromExp(exp_table, 0).level0
0

> getConfigFromExp(exp_table, 0).level1
1

> getConfigFromExp(exp_table, 0).extra_data
"a"

> getConfigFromExp(exp_table, 999).level0
2

> getConfigFromExp(exp_table, 999).level1
3

> getConfigFromExp(exp_table, 999).end_exp
599

> getConfigFromLevel0(exp_table, 0)
Object {level0: 0, level1: 1, start_exp: 0, end_exp: 99, interval_exp: 100…}

> getConfigFromLevel1(exp_table, 1)
Object {level0: 0, level1: 1, start_exp: 0, end_exp: 99, interval_exp: 100…}

关于数据存储

除此之外,配置变更是游戏运营过程中的家常便饭。所以应该如何存储玩家的等级与经验,也是一个需要考虑的因素。

  1. 存储玩家的总经验,在游戏中换算成等级;
  2. 存储玩家的等级,以及当前等级获得的经验;

对于方案一,当经验配置变更时,玩家看到的等级也会随着变化。这可能不是一个很好的情况; 所以方案二可能是比较好的选择,具体情况视需求而定。

方案一相关公式

已知当前总经验 totalExp ;

取当前等级相关配置:

var config = getConfigFromExp(exp_table, totalExp);

计算升级进度:

var percent = (totalExp - config.start_exp) / config.interval * 100;

判断能否升级(假定等级从 1 开始算):

var hasNextLevel = !!getConfigFromLevel1(exp_table, config.level1 + 1);
var canLevelUp = hasNextLevel && exp >= config.end_exp; 

方案二相关公式

已知当前等级 level ; 已知当前等级获得经验 exp ;

取当前等级相关配置(假定等级从 1 开始算):

var config = getConfigFromLevel1(exp_table, level);

计算当前总经验:

var totalExp = config.start_exp + exp;

计算升级进度:

var percent = exp / config.interval * 100;

判断能否升级:

var hasNextLevel = !!getConfigFromLevel1(exp_table, level + 1);
var canLevelUp = hasNextLevel && exp >= config.interval; 

妈妈再也不用担心我和策划撕 B 了~