到目前,各种参于战斗的属性个数已经到了 200 个左右的规模,它们相互依赖,用一些属性计算出另外一些来。这使得在 Lua 中计算这些公式变成了相当大的负担。但另一方面也有一些好消息,就是很多原来预想的需求并没有被具体使用。比如在属性计算这个层次上。策划并不需要做任何查表运算(那是在上一层,Buff 处理中用到的)。这些需要计算的属性虽然量很大,但它们类型单一,可以全部用浮点数表示,并用基本的四则运算就可以搞定。并不需要更复杂的功能。
我想了一下,干脆把这一整块东西封装到 C 模块中。给 Lua 提供一个基于寄存器模式的纯函数式计算器即可。上层只需要构造一个表达式组的对象,输入若干简单的四则运算表达式,由模块本身做拓扑排序后,得到公式间的相互依赖关系。以及每条公式用到的寄存器号。然后,我们就可以通过简单的寄存器访问接口更新指定寄存器的值,就可以自动去做相关的公式运算得到那些最终需要通过计算得到的其它寄存器的值了。
对于 C 模块,它处理的公式中的变量只是一系列的寄存器编号。用 Lua 做一个简单的封装,把原始公式中的可读的属性名转换成内部编号,仅仅是一个简单的文本替换工作。
整个任务需求非常清晰,实现也不难。不需要动用 tcc 这种重量的外部库。只需要把策划提供的表达式转换为规整的逆波兰形式。计算的时候,一遍扫描就可以得到结果。
ps. 我在设计内部数据结构的时候取了一个巧。内部公式中只保存正的浮点常量(负常量可以通过正常量加一个取负的一元运算替代),然后用 union 来保存寄存器变量或操作符。这样,任何一个逆波兰表达式都可以用一个浮点数组来表达了。
作为惯例,我把花了一天时间实现的这个模块开源了。有兴趣的同学可以去 github 上拿到源代码 。
不太需要多解释,如果你运行这样一段程序:
local attrib = require "attrib"
local e = attrib.expression {
"攻击 = 力量 * 10",
"HP = (耐力 +10) *2",
"FOO = 攻击 + HP",
}
local a = attrib.new(e)
a["力量"] = 3
a["耐力"] = 4
for k,v in pairs(a) do
print(k,v)
end
会得到这样的输出:
力量 3
攻击 30
FOO 58
耐力 4
HP 28
做完这个以后,Mike 同学觉得能在底层把游戏中最常用的一个特性顺带实现了比较好。
简单说是这样:
往往,游戏中 “力量”这个值有一个基础数值。另外,各种装备和 Buff 有可能给这个数值加上一个数字,以及乘上一个比例。我们最常改变的是这个递增值,以及递增比例。有时候,还需要把力量设置为一个绝对值,不受前面那些值的影响。也就是说,默认会有一组公式:
力量 = ( 基础力量 + 力量增加 ) * (1 + 力量增比)
if ( exist 力量绝对值) then 力量 = 力量绝对值 end
同理,其它属性,比如智力,敏捷,都有相关的公式。如果能隐藏这些细节会更好一些。
我觉得,这个需求应该在 Lua 层封装好,自动生成“力量增加”这样的中间属性,并给出对应的接口去修改它们。不用在目前的模块中增加代码。至于力量绝对值这种设置,借助一些小技巧。增加一个变量“设置力量”,默认为 0 ,可以改写为 1 。然后:
力量公式 = ( 基础力量 + 力量增加 ) * (1 + 力量增比)
力量 = 力量公式 * (1 - 设置力量) + 力量绝对值 * 设置力量
在不增加新特性的前提下,这也算是一个折中的解决方案了吧。