介绍
表达式系统对外表现为一种脚本语言,用户通过编辑一个脚本,调用程序中某些特定的函数,从而实现自己需要的效果。在游戏开发中这个系统通常由程序组开发维护,交由策划组使用,来丰富游戏内容,例如不同英雄的技能逻辑、不同NPC的交互逻辑等,这些工程交给程序来做未免过于精细且往往达不到策划想要的效果,并且这些细节逻辑的变动会牵扯到程序策划两个组的联调,影响工作效率,会徒增不少工作量。所以我们程序选择造个轮子然后把工作量丢给策划,引入表达式系统的好处在于,一方面我们可以让策划自行决定这个行为的具体逻辑,另一方面策划们可以将不同技能的具体逻辑保存到一张Excel表中,每一个技能是什么样的逻辑都可以一目了然。
表达式系统(Express System)是指一种用在大型游戏项目中的系统,这个系统提供了一种脚本语言,具体表现形式一般如下:
if Func(10) then:
DoSth(EMsgId)
else:
DoOtherThing(EMsgId)
这个系统常用于给游戏策划编写相关逻辑,从而减轻程序员的维护难度,脚本本身仅支持简单的顺序和分支逻辑,不支持循环,但足够了。
表达式系统的组成较为简单,表达式系统由以下几部分组成:
程序运行时的上下文
(Context, ctx)
程序对外暴露的表达式接口
(AtomOperation, atom)
表达式脚本代码
(Expression, expr)
这个系统的诞生就是为了解决大型游戏中,大量角色技能的逻辑处理等相关问题,如果使用Lua
进行同样地操作,则可能遇到Lua解释器运行过慢的问题,而表达式则可以是基于C++构建的一整套系统,因此在运行上有更好的效率。
表达式脚本代码的组成
我们参照C++代码的结构,对表达式系统的脚本代码拆解,代码整体由关键字,方法,公式三种组件组成,如下图所示:
关键字 Keywords
为了保证表达式系统的简洁与可读性,我们仅引入下面的几个关键字:if
then
else
endif
Src
Tgt
,他们分别用于表示分支逻辑,分支条件,分支结束,表达式上下文中的发起者和目标。
前四个不必多说,我们着重来看后两个Src
和Tgt
,即Source和Target,分别表示动作的发起者和目标。引入这两个关键字其实与我们的表达式系统原理相关,简单来讲:表达式执行的时机事实上是由程序员决定的,而策划在表达式中需要操作一些对象的数据,以达到需求,因此程序需要将这些对象以关键字的形式暴露给策划以供调用,而程序也需要在执行表达式前,明确表达式的上下文,即明确表达式中Src
和Tgt
分别指代的是哪个对象。
就像上面的图中使用到Src
来表示这个方法操作的对象是动作的发起者,这句话可能的意思为“执行表达式的NPC将在10秒后前往超市”。
公式 Formula
公式是表达式系统中进行数值计算和逻辑判断的核心单元。它由属性和常量通过运算符组合而成,最终计算出一个具体的值(数字、布尔值等)。策划无需关心底层实现,只需像写数学算式一样组合这些元素。前面的例子中提到的10
就是最简单的一个公式。
我们再来举一个例子,我们需要将对方的钱转一半给自己,那我们可以这么写:
IncMoney(Src, TgtMoney / 2) # Src 增加 (TgtMoney / 2) 的值
DecMoney(Tgt, TgtMoney / 2) # Tgt 减少 (TgtMoney / 2) 的值
在这里, TgtMoney / 2
是一个公式。其中 TgtMoney
是属性,2
是常量,/
是运算符。我们假设 TgtMoney
当前是 200
,则公式计算结果为 100
。那么这里的方法调用等同于 IncMoney(Src, 100)
和 DecMoney(Tgt, 100)
。
由此可见,一个公式就是一段计算表达式,当脚本执行到包含公式的地方时,系统会立即计算这个公式的结果。
公式的类型
公式得到的值有两种类型,一种是数,一种是布尔值。
为了程序实现更方便,也为了让策划更易懂,我们在这里以C/C++风格定义:true
内部视为1
,false
内部视为0
,0
视为 false
,所有非 0
数都视为 true
,这样就保证了 bool
和 int
的隐式转换。在策划使用的时候,他们可以直接用 1
和 0
表示真假。
属性
属性是公式的一个核心组成部分,对于脚本的用户,也就是策划来说,它相当于一个数,但对于程序来说,它本质上是一个除程序上下文外,没有其他参数的一个有返回值的函数。例如前面提到的TgtMoney
实际上对应的是 int get_tgt_money(EXPR_CTX *ctx)
这样的一个函数,函数返回值即为TgtMoney
的值。
属性不同于程序变量,它是不可变的(即不能使用 TgtMoney = 10
这样的写法),如果想要修改需要通过 DecMoney
等方法实现。
策划看到的所有“名字”都是只读的、源自游戏对象状态的值,而非可写的存储单元。
常量
常量是公式中另一个提供数据的组分,说白了就是直接写在脚本中的一个具体的数,例如前面的 10
和 2
。
运算符
公式中用于操作数据的组分,用于对属性和常量进行计算或比较,不必多说,一笔带过。
用于计算的符号 (+
, -
, *
, /
, %
),比较的符号 (==
, !=
, >
, <
, >=
, <=
),逻辑组合的符号 (&&
, ||
, !
),以及括号 ()
。