前言
原贴写于饥荒游戏贴吧,为了使文章针对性更强,将原文切割并精简。此贴主要为编程0基础的modder讲解一些编程的基础知识。至于说有关饥荒框架的介绍,则会放在另一篇文章里讲解。
编程0基础的人,要想学习制作MOD,难度是比较大的,因为缺乏一些基本的编程概念,只懂得复制别人的代码或者在它们的基础上稍加改变,遇到稍微复杂一点的代码,就束手无策了。对于MOD崩溃或错误,也几乎没办法自行处理。但我也不推荐先去学一门编程语言之后再来学习MOD代码,这样做花费的时间精力都过多,又缺乏足够的正反馈,很容易半途而废。事实上饥荒MOD里用到的基本编程知识都比较简单,所使用的lua语言相比c之类的强类型语言,也已经做了许多简化。单纯想要做MOD的话,只需要了解一些基本知识和概念就可以了。
以下内容全部基于lua语言。
标识符
一个名字
给常量,变量,函数,类一个名字,这样我们才能通过名字来使用它。一般使用英文字母、数字和下划线的组合来命名。
推荐命名规则
- 常量:全大写,单词之间以下划线隔开。
- 变量、函数、文件名:全小写,单词之间以下划线隔开。
- 类:所有单词首字母大写。
这个只是个人推荐的命名规则,读者可以根据自己的喜好决定命名规则。
变量
可以通过符号'='赋值改变的量。
典型代表:人物的饥饿值。这个值在游戏里几乎每时每刻都在不停变化着,这样一来,我们就可以根据不同的变化,设置不同的效果,比如沃尔夫冈不同的饥饿值会有不同的形态,这个就是通过检测饥饿值来实现的。
常量
程序运行时,不会被改变的量
实际上,lua语言里没法自己定义常量。但是,对于某些量,我们不需要在游戏运行的过程中改变,又需要引用它。比如说长矛攻击力,在游戏过程中不需要改变,但官方所做的所有的武器的攻击力都是长矛攻击力的某倍数,这又需要引用它进行计算。这时候,不妨就把长矛攻击力看作是一个常量,用一个变量将其定义下来(SPEAR_DAMAGE,这个定义在tuning.lua里)
作用域
变量的生效区域。
在作用域以外的域内,如果你引用这个变量,又没有域内的同名变量,就会造成出错。
lua中对一个变量的作用域只有两个选项:local 和global。默认不加local修饰的变量为global(全局)变量,加了local的为局部变量。全局变量的作用域为整个程序。局部变量的作用域则在所定义的域中。一个选择控制结构内,一个函数体内,或者一个文件内,都是一个域。
这个作用域的主要价值在于,使得系统免于混乱。比如说,在被攻击的时候,需要计算所受到的伤害,为了方便进行多次计算,我们把这个数值设置为一个变量。但是,面对的敌人的攻击力会有变化,自身的防具减伤也会有变化,这时候我们就希望,计算结束,变量被引用到血量变化之后,这个变量能消失掉,不会影响我们下一次计算其他的伤害。这就是局部变量的重要作用。当然,局部变量还有另一个好处就是读取它的数据,要比全局变量快一些,不过,提高MOD性能不是本篇教程的重点,就不详细展开了。总而言之,全局变量,不到必不得已的情况,应当尽可能少用。
定义和声明##
定义,就是告诉系统,我设置了的这个变量/常量是什么。
声明,就是告诉系统,我设置了一个变量/常量,你给我记好了。
定义和声明是不一样的,但常常会混在一起。如果你只是想写写mod的话,不了解他们的区别也没关系,只认为是定义就够了。
引用##
引用就是告诉系统,我要用这个变量/常量来做某某事。
比如说用于某个表达式的计算,系统就会帮你读取储存在其中的数据。
赋值##
赋值就是告诉系统,往这个变量/常量里存入你给出的数据。
注意,此处的数据,不仅仅指数字,可以是lua语言允许的任何类型,比如说一段文字(字符串),一个布尔值(真或假)等等。在某些语言比如C语言中,定义和赋值是可以分开的。但在饥荒MOD的脚本语言lua中,这两者是连在一起的。对一个变量的第一次赋值,就是对它的定义了。
数据类型
对变量赋值,就是给它写入数据,那就涉及到了数据类型的问题。在初次赋值给一个变量时,所给予的值的数据类型,就是变量的数据类型。此后再给这个变量赋值,就必须赋予同一数据类型的值,如果值不同,就会导致系统崩溃(除了nil)。这里不展开细讲,只针对lua语言,简单地列出MOD中常用的几种类型
- nil:表示无效值,可以给任何数据类型的变量赋这个值。实际效果相当于删除这个变量
- boolean:包含两个值:false和true(假和真)
- string:字符串,用一对双引号或单引号括起来
- function:函数,这个会在下面讲
- table:表,这个概念会在后面讲
函数
这是编程里的一个非常重要的概念。
函数与变量的区别,可以做这样的类比:一个变量,就好比是一个属性,你可以给一个客体以某个属性,让它可以被描述,比如说,属性:可以被烧毁。而函数,则是一种操作方法,你让一个客体拥有一个函数,就是让它有某种操作。比如说,操作方法:被烧毁的具体步骤和操作。
函数由函数名,参数表和函数体组成。函数也和变量一样,能被引用,也有作用域。不过与变量不同的是,函数需要单独定义,在不同的编程语言中,函数的定义格式不一样,但都少不了上面所说的三个基本组成。在lua中,函数也可以看成是变量,可以被赋值。另外,函数可以有返回值,也就是把计算的结果返回,供另一个函数或者表达式使用。
Lua中,函数定义的基本格式如下为:
function 函数名(参数表)
函数体
end
如果希望函数的作用域是局部的,则在function 前面添加local。这样,你将无法在其作用域之外调用该函数。
函数是怎样工作的呢?首先,你需要明白,定义函数并不会让函数工作。只有执行了函数语句才会让它工作。还是拿计算伤害来做例子。你定义了怎么计算伤害的函数,参数为攻击者的攻击力和防御者的护甲。这个函数在定义之后,本身并不会立刻工作。只有你设定了一系列的流程,让函数在出现攻击状态的时候触发,才能算是执行了这个函数。函数执行的时候,输入了两个参数:攻击者的攻击力和防御者的护甲。在函数体中,经过一系列的计算,得到了结果,利用return返回来,由变量接收或者加在各种表达式里使用。需要注意的是,即使是没有参数的函数,在执行时,也必须写成这样的形式: 函数名(实际参数表)
代码例子:
--定义了函数caldamage,但没有执行
function caldamage(attack,armor)
return attack-armor
end
local damage = caldamage(10,8) --这里执行了函数caldamage,并把计算的结果返回,赋值给damage
函数的作用是什么呢?就是使得你的编程显得更有逻辑,模块化,还能减少代码的使用量。定义好一个函数之后,就不再需要管这个函数里面详细的执行过程(也就是函数体写了什么),我们只需要知道这个函数的名字,参数表和返回值,和这个函数有什么作用。因为在饥荒的MOD中,大量的函数是没有返回值的(也就是返回值为nil),执行这样的函数,目的在于使用它的功能。
函数是做饥荒MOD时最重要的东西。我们做MOD,主要的目标就是修改或者向游戏添加函数。了解这些函数在什么时候会触发,需要哪些参数,有什么功能,返回值是什么,是非常重要的。
我们是在原游戏的基础上做MOD,也就是说,有很多已经定义好了的函数可以供我们使用。打个比方,饥荒这个游戏,就好比一部车,函数就是这部车上面的零件,它让这部车能够拥有某些功能:启动,刹车等等。我们现在觉得这部车不能满足我们的需要了,那么,很显然的,做适当的改装,要比重新造一部车容易。做MOD,就好比是做一些改装。既然是改装,那你就有必要了解到,你所需要改装的部分,需要哪些零件。有些核心零件是非要弄清楚不可的。
现在饥荒MOD本身的结构是非常开放的,但官方没有给出详细的说明文档,当我们想要实现一项功能的时候,我们不知道官方有没有给出来,怎么办呢?我的建议是,思考一下游戏里的各种功能,以及他人已经发布的MOD,有没有和你的需求类似的,去参考一下相应的代码。易宁修改也是一个很好的参考,但易宁修改毕竟是直接修改游戏的核心文件,与MOD还是有一些区别的,所以要使用的话,前提是理解其含义。
表
表不是一门编程语言的必须概念,但这个概念是lua内置的核心数据结构,在饥荒MOD里使用得非常频繁。游戏的整个框架,也非常依赖于表。表的重要作用也和函数一样,是为了让你的编程显得逻辑清晰。比方说,现在有4个个体,a,b,c,d,有多项属性描述:health、sanity、hunger、damage、armor、attack_period、walkspeed、runspeed。这些属性,对于4个个体来说,有的有,有的没有,我们要怎么组织起来呢?用多张表连起来,就是一个好主意。首先,我们来给属性分一下类,health、sanity、hunger是饥荒中的三大基本属性,各自单独成一类,damage、armor、attack_period是和战斗有关的,分类为combat,至于walkspeed、runspeed则是和移动有关的,分类为locomotor。那么,我们就有多张表了:
一张总表:
个体属性表
属性 | a | b | c | d |
---|---|---|---|---|
health | a的血 | b的血 | c的血 | d的血 |
sanity | a的精神 | b的精神 | c的精神 | d的精神 |
hunger | a的饥饿 | b的饥饿 | c的饥饿 | d的饥饿 |
combat | a的战斗属性 | b的战斗属性 | c的战斗属性 | d的战斗属性 |
locomotor | a的移动属性 | b的移动属性 | c的移动属性 | d的移动属性 |
上表中的每一个元素,都是一张表,现在不妨取a的战斗属性表出来,是这样的:
战斗属性表
damage | armor | attack_period | |
---|---|---|---|
数值 | 20 | 50 | 3 |
那么,我们想要引用a1的damage的时候,怎么办呢?先在总表第一横栏中找到a,然后在竖栏中找到combat,这样我们就得到了提示:转去找a1的战斗属性表。然后在战斗属性表中,我们在横栏中找到了damage,这时候竖栏中只有一项,就不必再查找了。我们在查找过程中,寻找的a,combat,damage 就是所谓的索引。我们按先横后竖的顺序查找的,a为1级索引,combat为2级,damage为3级。按顺序最后找到damage的具体的值(20),就是所谓的值。这个值不仅仅是数值,比如说在总表中找到的a的战斗属性表,也可以称为值。再拓展一些,如果说战斗属性表中的竖表不只有一项,而是有两项:max,min,此时我们想要查damage最大值,该如何呢?那就要增加一个四级索引max。当你想要引用a的伤害最大值时,在编程里的调用语句,你就可以写a.combat.damage.max
结合饥荒Mod编程,我们常常会看到类似这样的一条语句
inst.components.sanity:DoDelta(-10)`
这句话的意思是当前对象的精神减10。
具体是怎么操作的呢?首先,游戏里这么多个体,要在茫茫人海中找到你,必须要有个名字,这个名字就是inst,然后,inst下有很多属性类,我们需要的精神值,归类为components,也就是组件。组件这个概念,是饥荒为了编程上的逻辑清晰而创造出来的一个概念,会在介绍饥荒编程框架的文章里详细说明。然后我们继续在components表里找包含着精神值和操作精神值的函数的表,就是sanity。这时候你可以看到sanity后面是冒号:而不是之前的那些点号. 这是因为,我们是希望执行这个函数。如果是想要引用这个函数做其他操作的话,还是要用点号. 的。学过C++的人都会了解类的概念,对这个肯定不陌生。没学过的人呢,看我在下面关于类的解释。
类和面向对象编程
类这个概念,就是在面向对象编程的思想上发展起来的。为什么要使用类呢?就是因为面向对象编程显得逻辑结构清晰,易于实现、互动和维护。那么,什么是一个类呢?在编程上,可以理解为一些变量和函数的集合。这个集合是封装起来的,其中有一些变量和函数你可以访问和引用,称为公有变量和公有函数,还有一些是你访问不到,也无法使用的,就是私有变量和私有函数。需要注意的是,类本身只是一个逻辑结构,并不是实体。用现实的东西举个例子,自行车就是一个类的概念,它有很多基本属性:颜色,材质等等,也有很多操作:骑、前进、刹车等等。属性就是变量,操作就是函数。颜色、材质,是你能够看到的,就是公有变量,而内部的转盘的颜色你看不到,就是私有变量。而骑和刹车的操作,是你能够决定的,就是公有函数。而前进这个操作,你没法直接进行,你必须要反复踩踏板,才能让自行车前进,所以前进就是私有函数。在头脑中想到自行车这个概念,就是类。而想到你的自行车,就是一个具体的实体。
结合到饥荒MOD里,sanity这就是一个类,这里面有很多属性:当前精神值,最大精神值等等,也有很多操作:精神增加/减少,设置当前精神值,设置最大精神值等等。而具体到一个人物的sanity,那就是这个类的实体了。
实际上,在lua里,只有表,没有类这个概念。但是饥荒的游戏制作者为了编程方便,还是用某种手段,在表的基础上,类这个概念创造出来了。我们只需要认识到,怎样使用一个类就可以了。
引用类中的变量,操作方法就和引用表中元素一样。如果想要调用函数,则需要将点号.改成冒号:,并且在函数名后面添加(函数参数表)。
比如说人物的当前精神值,人物的最大精神值等等,同时也有一些操作函数,比如上面举例的DoDelta。我之前说过了,在lua里,函数也可以看成变量。如果你想要引用这个函数,比如说引用去给一个函数赋值,那么,上面的冒号:就要改成点号.,而且后面的"(-10)"也要去掉。如果你想要执行这个函数,那就要用冒号: 并且添加相应的参数。