介绍

表达式系统对外表现为一种脚本语言,用户通过编辑一个脚本,调用程序中某些特定的函数,从而实现自己需要的效果。在游戏开发中这个系统通常由程序组开发维护,交由策划组使用,来丰富游戏内容,例如不同英雄的技能逻辑、不同NPC的交互逻辑等,这些工程交给程序来做未免过于精细且往往达不到策划想要的效果,并且这些细节逻辑的变动会牵扯到程序策划两个组的联调,影响工作效率,会徒增不少工作量。所以我们程序选择造个轮子然后把工作量丢给策划,引入表达式系统的好处在于,一方面我们可以让策划自行决定这个行为的具体逻辑,另一方面策划们可以将不同技能的具体逻辑保存到一张Excel表中,每一个技能是什么样的逻辑都可以一目了然。

表达式系统(Express System)是指一种用在大型游戏项目中的系统,这个系统提供了一种脚本语言,具体表现形式一般如下:

if Func(10) then:
    DoSth(EMsgId)
else:
    DoOtherThing(EMsgId)

这个系统常用于给游戏策划编写相关逻辑,从而减轻程序员的维护难度,脚本本身仅支持简单的顺序和分支逻辑,不支持循环,但足够了。

表达式系统的组成较为简单,表达式系统由以下几部分组成:

  1. 程序运行时的上下文 (Context, ctx)

  2. 程序对外暴露的表达式接口 (AtomOperation, atom)

  3. 表达式脚本代码 (Expression, expr)

这个系统的诞生就是为了解决大型游戏中,大量角色技能的逻辑处理等相关问题,如果使用Lua进行同样地操作,则可能遇到Lua解释器运行过慢的问题,而表达式则可以是基于C++构建的一整套系统,因此在运行上有更好的效率。

表达式脚本代码的组成

我们参照C++代码的结构,对表达式系统的脚本代码拆解,代码整体由关键字,方法,公式三种组件组成,如下图所示:

关键字 Keywords

为了保证表达式系统的简洁与可读性,我们仅引入下面的几个关键字:if then else endif Src Tgt ,他们分别用于表示分支逻辑,分支条件,分支结束,表达式上下文中的发起者和目标。

前四个不必多说,我们着重来看后两个SrcTgt ,即Source和Target,分别表示动作的发起者和目标。引入这两个关键字其实与我们的表达式系统原理相关,简单来讲:表达式执行的时机事实上是由程序员决定的,而策划在表达式中需要操作一些对象的数据,以达到需求,因此程序需要将这些对象以关键字的形式暴露给策划以供调用,而程序也需要在执行表达式前,明确表达式的上下文,即明确表达式中SrcTgt分别指代的是哪个对象。

就像上面的图中使用到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 内部视为1false 内部视为00 视为 false ,所有非 0 数都视为 true ,这样就保证了 boolint的隐式转换。在策划使用的时候,他们可以直接用 1 0 表示真假。

属性

属性是公式的一个核心组成部分,对于脚本的用户,也就是策划来说,它相当于一个数,但对于程序来说,它本质上是一个除程序上下文外,没有其他参数的一个有返回值的函数。例如前面提到的TgtMoney 实际上对应的是 int get_tgt_money(EXPR_CTX *ctx) 这样的一个函数,函数返回值即为TgtMoney 的值。

属性不同于程序变量,它是不可变的(即不能使用 TgtMoney = 10 这样的写法),如果想要修改需要通过 DecMoney 等方法实现。

策划看到的所有“名字”都是​​只读的、源自游戏对象状态的值​​,而非可写的存储单元。

常量

常量是公式中另一个提供数据的组分,说白了就是直接写在脚本中的一个具体的数,例如前面的 102

运算符

公式中用于操作数据的组分,用于对属性和常量进行计算或比较,不必多说,一笔带过。

用于计算的符号 (+, -, *, /, %),比较的符号 (==, !=, >, <, >=, <=),逻辑组合的符号 (&&, ||, !),以及括号 ()

七岁打码天才,游戏开发糕手