开发笔记(17) : 策划表格公式处理
前段时间为策划提供过一些技术上的支持, 设计过一个简单的 DSL 。但随着更多策划加入团队,我觉得这个思路不能很好的贯彻下去。
策划更喜欢通过 excel 表格来表达他心中的数值关系,而不是通过代码语言。我需要找到另一种更切合实际的方案来将策划的想法转换为可以运行的代码。
研究了一下策划历史项目上的 excel 表格后,我归纳了一下其需求。我们需要把问题分步解决,excel 表格到程序可以利用的数据结构是一项工作,而从表达了数据之间联系的结构到代码又是另一项工作。
对于数值运算,有怎样的需求呢?
我更多看到的是一种声明式的表达,而不是过程式的。比如,策划会定义,“血量”这个属性,实际上是等价于“耐力 * 10”。我们把后者定义为一个公式。
许多表格其实就是在不同的位置表达了这种公式推导关系:一个属性等价于另一些属性组成的表达式。而在运行时,根据人物的一些基础属性,可以通过一系列的公式推导,得到最终的一系列属性值。满足这个需求并不难,读出表格里的对应项,做简单的解析就可以了。(这里涉及到另一个问题,表格里的对应项在哪里,今天暂且不谈)
对于这种声明式表达,程序要做的工作是进行一次拓扑排序,把需要先求值的属性排在前面,有依赖关系的属性求解放在后面。这样就可以转换为过程式的指令。
另一种表格称为查表。其实就是表达一种映射关系。如下表:
法术伤害 | 物理伤害 | |
战士 | 0.5 | 1 |
法师 | 1 | 0.5 |
如果公式中提到了“法术伤害”这个属性值时,需要根据“职业”这个属性,去这张表里查到对应项。职业可以是“战士”或“法师”,对应了不同的“法术伤害”值。从 C 语言的角度看,“职业”是一个枚举量,而法术伤害可以定义为一个一维数组,以枚举值为索引,便可以查到对应值。若是动态语言,更适合的方式是做一个字典,性能低一些,但更加灵活。
但无论是表达式求值,还是查表求值,目的都是一个:用一组属性通过某种变换得到新的属性值。每一张表格都提供了一组变换关系,最终可以从一组基础属性推导出各种需求的值来。
把策划这个需求解决好并不太难,尤其是对于动态语言来说更为简单。难点在于性能。尤其对于即时战斗的 MMO 来说,每次攻击都需要做一系列的运算,这些运算若是依赖大量的表格推演,性能很难保证。我想,最为彻底的方案是把这些属性数值之间的关系编译成目标机器上的原生代码。为此,我选择了 tcc 这个工具。这样,我只需要用 lua 去一次性生成 C 代码,然后交给 tcc 编译就好了。
ps. tcc 最新版本的 git 仓库在这里 。
我首先定义了 lua 和 tcc 交互的接口。最简单的方式是只支持一种数据类型,float 。一个 tcc 内的函数可以接受若干个 float 输入,产生若干个 float 输出。这样的 C 函数的原型看起来是这样:
void func(float input[] , float output[]);
交互接口很容易实现,为 lua 写一个 C 扩展即可。
接下来是实现一个 lua 模块利用 tcc 生成需要的运算函数。
这个一个模块,提供一个公式对象,然后可以把一系列的表达式以及查表规则设入对象,最后调用编译方法得到编译好的运算函数。
f = require "formula" test = f() test:table("属性表", { "战士" , "法师" } , { ["物理伤害"] = { 1 , 0.5 } , ["法术伤害"] = { 0.5 , 1} }) test:expression("攻击强度", "力量 * 2") test:expression("血量", "耐力 + 1") test:expression("伤害", "攻击强度 / 血量 * 物理伤害") test:lookup("物理伤害","属性表","职业") test:compile { "伤害" } print(test { ["力量"] = 2 , ["耐力"] = 3.5 , ["职业"] = "战士" })
这里,table 方法可以把一个查询表置入对象。分别是表名,key 表,和数据表。
expression 可以设置属性间的换算表达式。
lookup 可以设置一种查找规则。
最后只需要把基础属性传给 compile 得到的 C 运算函数(传入前做一个替换工作,把字典替换成循序的参数),得到运算结果。
下面看一下,内部生成的 C 代码是怎样的:
一共有三个输入参数,以及一个输出参数,还有三个中间变量。
我们可以把一共 7 个量看作是 7 个寄存器,在分析之前的公式关系时,无非就是做了一下寄存器分配的工作。除了生成这些表达式运算的代码行外,还提取出了查表需要的表列。这里,通过分析输入,可以知道“职业”这一个属性其实是一个枚举量,能用整数表达。那么把“战士”对应到 0 的这个过程其实是在 lua 代码中查表完成的。而 C 函数则接受的是数字而不是字符串了。
最终我们可以计算出“伤害”属性的值,放在 t[6] 中,最后赋予 output[0]。当然,这段 C 代码的生成还可以进一步优化。这些可以留到以后去做了。
Comments
Posted by: 祝川 | (16) May 24, 2012 04:02 PM
Posted by: Cloud | (15) April 18, 2012 11:47 PM
Posted by: lichking | (14) April 18, 2012 11:26 PM
Posted by: 大树妖 | (13) April 18, 2012 11:25 PM
Posted by: lichking | (12) April 18, 2012 11:23 PM
Posted by: lichking | (11) April 18, 2012 11:19 PM
Posted by: lichking | (10) April 18, 2012 11:16 PM
Posted by: lichking | (9) April 18, 2012 10:25 PM
Posted by: lichking | (8) April 18, 2012 10:19 PM
Posted by: lichking | (7) April 18, 2012 10:13 PM
Posted by: mayao11 | (6) April 18, 2012 09:34 PM
Posted by: geek42 | (5) April 18, 2012 07:20 PM
Posted by: 唐诗 | (4) April 18, 2012 06:59 PM
Posted by: 摸象 | (3) April 18, 2012 05:10 PM
Posted by: 低调的马甲 | (2) April 18, 2012 04:34 PM
Posted by: 独酌逸醉 | (1) April 18, 2012 03:48 PM