« Lua int64 的支持 | 返回首页 | 让多个 Lua state 共享一份静态数据 »

开发笔记(17) : 策划表格公式处理

前段时间为策划提供过一些技术上的支持, 设计过一个简单的 DSL 。但随着更多策划加入团队,我觉得这个思路不能很好的贯彻下去。

策划更喜欢通过 excel 表格来表达他心中的数值关系,而不是通过代码语言。我需要找到另一种更切合实际的方案来将策划的想法转换为可以运行的代码。

研究了一下策划历史项目上的 excel 表格后,我归纳了一下其需求。我们需要把问题分步解决,excel 表格到程序可以利用的数据结构是一项工作,而从表达了数据之间联系的结构到代码又是另一项工作。

对于数值运算,有怎样的需求呢?

我更多看到的是一种声明式的表达,而不是过程式的。比如,策划会定义,“血量”这个属性,实际上是等价于“耐力 * 10”。我们把后者定义为一个公式。

许多表格其实就是在不同的位置表达了这种公式推导关系:一个属性等价于另一些属性组成的表达式。而在运行时,根据人物的一些基础属性,可以通过一系列的公式推导,得到最终的一系列属性值。满足这个需求并不难,读出表格里的对应项,做简单的解析就可以了。(这里涉及到另一个问题,表格里的对应项在哪里,今天暂且不谈)

对于这种声明式表达,程序要做的工作是进行一次拓扑排序,把需要先求值的属性排在前面,有依赖关系的属性求解放在后面。这样就可以转换为过程式的指令。

另一种表格称为查表。其实就是表达一种映射关系。如下表:

法术伤害物理伤害
战士0.51
法师10.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 代码是怎样的:

float table_1[] = {1,0.5}; void main(float input[],float output[]) { float t[7]; t[0]=input[0]; t[2]=input[1]; t[4]=input[2]; t[1]=t[0] + 1; t[3]=t[2] * 2; t[5]=table_1[(int)t[4]]; t[6]=t[3] / t[1] * t[5]; output[0]=t[6]; }

一共有三个输入参数,以及一个输出参数,还有三个中间变量。

我们可以把一共 7 个量看作是 7 个寄存器,在分析之前的公式关系时,无非就是做了一下寄存器分配的工作。除了生成这些表达式运算的代码行外,还提取出了查表需要的表列。这里,通过分析输入,可以知道“职业”这一个属性其实是一个枚举量,能用整数表达。那么把“战士”对应到 0 的这个过程其实是在 lua 代码中查表完成的。而 C 函数则接受的是数字而不是字符串了。

最终我们可以计算出“伤害”属性的值,放在 t[6] 中,最后赋予 output[0]。当然,这段 C 代码的生成还可以进一步优化。这些可以留到以后去做了。

Comments

策划也是分很多类的,例如写任务的和数值就是一个天一个地,我觉得倾向于由对程序架构比较熟悉的数值策划或者主策划主导游戏数据表结构的设计,统一约定一套规则,这样策划也方便调试。 数值策划喜欢用excel的一个原因是excel自带比较方便的vba,有经验的数值策划在设计数值架构的时候会使用vba去生成具体数值和进行测试 程序和数值是不分家,不能片面强调使用程序自己制作的工具,因为无论怎么样,费心费力做出来的编辑器有时候用起来就是不及excel的1/10,我想这个也应该能理解
@大树妖 这篇文字和策划公式本身无关。我瞎凑的几个属性加减乘除随机组合一下而已。
因为今天的机器,还不能做到自举,我们用巨大的工程,使得计算成为可能,有专业的人去做芯片,开发系统,制作工具,
云风同学,我警告你,耐力是可以换算成生命,但从策划的角度,一件装备单独赋于生命这个属性,也是理所当然的要的。耐力加乘生命可能会有个计算上的渐变,但生命这个属性是不会变化的,一级属性和二级属性是两个属性。如果设计需要,也许加十点力量也会加一点生命。这两个属性混一块,你的策划就没法混了。 另外,从来没听说过计算公式要查表的,计算公式直接都写到代码里不好吗。策划的表格只是他们用来计算用的。 PS.我是策划,也许没看明白你的代码,有则改之,无则加勉嘛
某种程度,今天的程序还在做着给计算机打孔输入的工作,关注系统,理解系统,为这种拙劣的人机界面,给系统做翻译
至于程序,将来没有程序,我们将关注逻辑,而不是程序,
游戏开发将越来越上升到更高层次-面向工具, 将游戏抽象化,提供编辑开发的手段, 这样的后果就是游戏开发就像现在3D,后期制作一样,产量制作,创意为主
我觉得将来开发游戏的方式主要以编辑器为主,从暴雪公司游戏趋势可以看到
策划需要的是工具,第一选择当然是非常适合的编辑器,其次就是二维表格了
对于数据驱动可以下面两种方式, 数据->代码->runtime 数据->runtime 你的 PB c库是第二种方式,而这里面的作法是第一种方式,我倒比较愿意在这两个场合用相反的方式
我对网游逻辑程序有好几年经验,但是看这篇文章云里雾里的。不知道是云大侠没完全搞清需求还是我没明白意思。 期待后序
其实你需要的是一个DSVM(domain speciafic VM), 其实有许多lua级别的调用是可以直接掉外部的一些接口或者硬件的加速来实现的,对于这种情况,用tcc就多绕了不少圈子。 另外,stack based vm更容易实现哈,那个,你还记得大名湖畔的forth么
直接做个编辑器会更好,例如星际二编辑器,星际二编辑器编辑完是xml的,其实我们也可以用protobuf http://www.cnblogs.com/egametang/archive/2012/03/28/2421441.html
以前这些东西只是大概知道一点,看了这个获益不少,思路也更清楚了!感谢!
之前的经验是,大部分情况下这些表格可以变成.ini配置文件或者变成Lua脚本里的table,程序省事,写这些表格的策划也省事,就是负责这部分工作的Lua脚本策划辛苦了点……
风哥,怎么你的文章页面越来越窄了呢?

Post a comment

非这个主题相关的留言请到:留言本