这一篇文章是GDScript精要部分的最后一篇。这里主要讲一些前面没有讲到的、不方便归类的内容。但是其中一些特性是很常用、很方便的,因此依然建议阅读。
枚举
试想一下,你要用一个变量来表示一周的每一天(星期几)。
自然而然地可以想到用整数来表示。但是合法的星期几只能是1-7,而一个int类型的变量取值范围要大得多。我们无法限制取值范围。
这里可以通过定义枚举(enum)类型来提供更明确的约束。枚举类型定义了一组合法的值,一个枚举类型的变量应当取这些值中的一个(GDScript的枚举目前无法限制枚举类型的变量只取枚举中定义的值)。有时候枚举类型也被视作”有名字的常量“(named constants)。
在GDScript中,使用enum关键字定义一个枚举类型。enum后是枚举类型的名称,然后是一对花括号。括号中是枚举类型可用取值的列表,以逗号隔开:
定义枚举类型后,可以像这样使用枚举类型:
枚举可以作为类型在类型标注处出现,枚举的某个取值也用点语法获得。
你此时可能会说,诶,那我需要获得它对应的数字怎么办呢,你这没用数字表示啊。实际上枚举定义时已经暗含了这一点。前面已经提到枚举相当于一系列常量,定义枚举时如果不给他们指定值,默认就会从整数1开始按出现顺序给它们设置值。你可以试着print一下Monday和Sunday看它们是不是分别为1和7。
实际上GDScript中的枚举类型很难说是真正的枚举类型。在Godot 3.x中它甚至不能作为类型使用。定义枚举类型时enum后面的名字可以省略。此时它等价于一系列常量定义:
这样写有一个问题就是可能会造成命名冲突,因为我们直接就能访问到这些常量。而有名字的枚举需要通过枚举类型的名称来访问具体的选项。
不过不管那种方式定义枚举,有一个好处就是如果你要定义一系列连续编号常量,那么只需要指定枚举中某一个项目的取值,后续的取值都会自动加一:
如果像这样定义,星期三后面的取值会自动编号为4、5、6、7。但是要注意的是,星期三前面不会自动减一。此时Monday为0,Tuesday为1。当然你也可以分别为各个选项指定值而不按顺序。
GDScript的枚举类型的值只能是整数。
有名字的枚举类型,实际上就是一个字典。这就意味着字典的所有方法对于一个枚举类型来说也是可用的:
顺带一提,C#中星期几就是用一个枚举类型来表示的。不过由于习惯的不同,星期天对应的是0。
特别的字典语法
除了像之前那样提到的用花括号和冒号来写字典字面量,还有另一种语法来初始化字典:
这种写法是Lua式的——Godot文档中也是这样称呼这种写法的。Lua是另一门编程语言,它的设计很有特点,并且在历史的不同阶段也是游戏开发的热门语言之一。Lua提供了——且只提供了一种内置数据结构那就是表(table),它实际上就是类似于字典的一种数据结构。这种Lua式的写法也用花括号,但是各键值对的键和值用等号连接。此外这种写法中键必须是合法的变量名,也就是说不能以数字开头。
另一方面,字典可以直接用点语法来访问键和值:
当然这种语法糖并不局限于使用Lua式写法的字典。只要字典中有这样的字符串键,且这个字符串可以作为合法的变量名,那它就可以用这种语法访问。并且不要求字典中所有的键都满足这一约束。
格式化字符串
前面提到可以用加号和字符串提供的各种转换函数来根据某变量的值来构造字符串。实际上还有更方便的方法来往字符串中填入某些变量的值。
例如按照之前的方法,在需要根据名字来输出问候语时,我们需要这样:
这种写法单纯拼接两个字符串还好,如果要拼的字符串太多就很麻烦,尤其是变量出现在一串字符中间的时候,要多写几个加号。
利用GDScript中提供的格式化字符串来写,就是这样:
模板字符串中的%s相当于占位符。它可以被填入各种各样的值。在格式化字符串后面用%运算符加上具体的值来替换占位符后就可以得到具体的字符串。也可以写上多个占位符,然后在百分号后用数组来填充占位符:
格式化字符串中的占位符支持多种格式。%s里面的s代表simple,它会直接把传入的表达式转换为字符串。不过GDScript格式化字符串的其他格式主要是改变数字和向量的显示格式,因此并不算常用。有兴趣可以看文档。
当然还有一个问题就是,如果我就是要在字符串里包含百分号怎么办?比如我要输出一个百分数。这个时候需要给百分号转义(escape),用两个连续的百分号来表示百分号本身:
除了格式化字符串以外,还有一种格式化字符串的方法,那就是String.format实例方法。字符串模板中的占位符需要用花括号围起来,然后把对应的字典或者数组作为参数传递给format方法:
format的第二个参数(可选)用于指定占位符,默认是花括号。如果你的字符串中包含花括号就可以指定其他符号作为占位符。
注意format方法会返回一个新的字符串而不会就地修改字符串本身。
实现转换为字符串的方法
在print和格式化字符串中,Godot会利用某个方法来将对象转换为字符串。内置类型实际上都能够很好地转换为字符串。但是对于我们自定义的类型,如果不编写额外的代码print出来就是一些让人难懂的东西。比如之前的一个Item:
输出的是:
让人摸不着头脑。实际上这是它的基类和它的对象id——但是这对于一般的开发者和开发工作来说没啥用。
重写_to_string方法之后,它就可以按照我们指定的方法来将它转换为字符串。在Item的定义中定义如下函数:
实际上写这个函数的时候编辑器会自动补全,它是知道你可能在重写基类方法的。这样之后再print就能看到我们需要的信息了:
print的更多用法
前面的示例代码中只讲到直接用print输出单个值(表达式)。实际上print可以接受任意多个参数:
不过这种写法不是特别有用,因为它会直接把几个参数连在一起,上面这行代码输出的是11.5?。
另外目前GDScript不支持自己编写支持可变数量参数(vararg)的函数。但是C#支持。
prints和printt和print类似,也接受任意多个参数,但是它们会在输出时给个参数之间分别加上空格(space)和制表符(tab)。
print输出完毕后自带换行,如果你不需要换行,就可以用printraw函数来输出。
在字符串中还有一些特殊的以反斜杠加字母表示的转义字符。这些特殊字符是不可见的,所以要用转义字符来表示。最常用的\n表示换行:
输出:
完整的转义字符列表参见文档。
push_error和printerr类似,都会输出红色的错误信息。但是只有push_error会在调试面板的错误页面中加入一个错误。并且这两者都不会中断游戏运行。类似的push_warning则是输出黄色的警告信息。
此外print_rich支持BBCode格式的富文本。
断言
前面的文章中我们在需要测试某个东西时往往都是直接用print输出来检查。在编程过程中可以用断言(assert)来验证某个布尔表达式是否为真,并在表达式为假时直接报错终端程序运行。
GDScript中用assert函数来进行断言。它可以接受两个参数,第一个参数是条件;第二个参数是可选的,它是断言失败时输出的错误信息。
这行代码在游戏运行到这里时会直接报错中断。
断言也是一种调试(debug)常用技巧。在后续文章中我会适时讲解更多有关调试的内容。
代码规范
在编写代码中我们必须要给很多不同的东西命名,因此有必要讲一下命名规范及其重要性。
在开发过程中,我们很可能需要和他人合作。符合规范的代码能够让大家更容易理解,能够让协作更顺畅。首要的一条是要尽可能使用有意义的名字来命名各种元素。如今很多开发工具都支持自动补全且能按首字母进行提示,因此应当尽量避免缩写,用完整的单词来命名。
当然,包括GDScript在内的很多编程语言都支持用英文字母以外的字符来作为代码符号的名称,因此我们也不一定要用英文命名。但是无论如何,你的代码在团队中应当有一以贯之的规范。顺带一提中文命名有一个小问题就是,很多开发工具支持英文首字母自动补全,但是不支持汉字的拼音首字母补全,所以频繁切换输入法有点麻烦。有一个通用的小窍门是在汉字名称前面加上拼音首字母缩写,这样就可以让编辑器识别到。当然这样一来就不好看了,因此究竟采用何种规范还需要开发者自行定夺。
由于GDScript和Python有很高的相似性,因此官方的规范也有很多地方和Python重合。
命名方面变量名、方法名采用snake_case。意味着所有字母都小写,多个单词用下划线连接。比如GDScript内置的二维向量类中的:
常量全部字母大写,单词用下划线连接(CONSTANT_CASE):
类型名首字母大写,多个单词不用符号连接(PascalCase):
对于枚举类型来说,枚举的类型名应该是PascalCase,但是枚举的可选值应当是CONSTANT_CASE。
此外文档在组织文件中的内容时,前后的空格、换行等格式上也有一些建议,完整的官方代码风格指南参见文档。
当然,这些规范都是“君子协定”,它们并不属于GDScript语法要求的一部分(除了用来界定代码块的缩进),不遵循这些规范也不会报错。你也可以有自己团队认可的不同的规范。
函数也是对象
没错,函数也是对象。这就意味着函数也可以放在变量里、也可以作为函数参数传递:
函数的类型在GDScript中叫Callable(可调用对象)。不过GDScript中没法显式表明函数的签名,也没法在引用函数的变量上直接用常规语法调用引用的函数。这里需要用Callable的call方法来调用:
在call中传入引用的函数所需要的参数即可。
如果一定要说为什么要这么做,原因在于它能够分离函数调用者和函数实现者。比如送奶工给订户送牛奶。送牛奶的才知道什么时候送,送怎么样的牛奶,而需要牛奶的订户只关心消费牛奶而不知道什么时候送来。所以”送牛奶“这个函数要交给送奶的调用,而牛奶送到之后具体怎么做,是订户自己的事情。
我们会在后续的文章中见到这个特性的具体用例。
花式注释
注释除了单纯的笔记其实还有更多用处。
在注释开头写上特定的单词可以让注释以不同的颜色显示,让它更醒目:
这些单词的完整列表参见这里。
有时候代码写得太多,而有的代码暂时又不需要管,很碍眼。可以用注释来定义一个区域,让编辑器可以把这部分代码收起来。
注释的井号后面跟上region(不能有空格)表示可收起区域的开头。region后面可以跟上任意内容,方便在收起之后依然能够明白这一段写的啥。在要结束区域的地方写上井号和endregion,这就算定义了一个可收起区域了。此时开头的地方左边会出现一个箭头表示可以收起。
限定类型的数组
前面提到GDScript的容器类型可以容纳不同类型的元素。但是如果你确实只需要在数组中保存某一种类型的元素,也可以给数组加上类型标注来提高安全性。要指定数组元素的类型,需要像这样写:
这样一来,GDScript就会对这个数组进行静态分析,在游戏运行前就会检测出任何违反类型约束的操作。也就是说你没法往里面加入其他类型的元素,也不能让这个变量引用其他类型的数组。
这种操作看起来有点像模板或者泛型。但是目前GDScript并没有这类特性。
match语句
如果你有其他编程语言的使用经验(特别是语法和C/C++接近的),可能会想GDScript怎么没有switch语句呢(当然Python也没有)。传统的switch语句主要是为了在某个表达式取不同的值时执行不同的代码。一方面,多个if语句确实可以替代switch,另一方面传统switch语句中的case只支持常量(字面量),这就限制了它的能力。比如我们没法用switch语句去测试一个对象中的各字段是否满足某个条件。
在相对现代的编程语言(以及一些语言的较新版本中)都衍生出了所谓”模式匹配“(pattern matching)的语法,它的作用之一就是解决switch的局限性。模式匹配可以检查表达式的值是否满足某种模式,而模式可以是多种多样的,可以是类型,可以是某个常量,也可以是对某类对象的各属性取值的要求。
match语句以match关键字开头,后面是一个表达式,然后是一个代码块。代码块中又有若干代码块,它们分别代表一种模式以及该模式能够匹配时执行的代码。match有多种模式可以出现在分支中:
字面量模式很简单,直接检查是否相等。字典和数组模式除了常规的检查各个元素,还支持一个通配符。这个通配符的意思就是只要匹配了明确给出的元素就行,其他的就不管了。
绑定模式(var n)实际上就是让一个变量绑定到整个模式或者模式中的某一个部分,这样在处理这个模式的代码中就可以直接用它而不用再通过match后面的表达式来访问。
还有一个终极的通配符,就是一个下划线。它是作为兜底的模式,正常情况下只应该作为最后一个模式出现在match中。它能够匹配任何的值,相当于switch中的default。C#的模式匹配语法中也有这个东西,给它的名字叫弃元(discard)。但要注意在数组模式中它和两点的区别。下划线只能任意匹配一个,而两点可以任意匹配多个元素。
when分句很好用。它可以在模式匹配之后进行额外的约束。when后面是一个布尔类型的表达式,when前面的模式匹配上之后,when后面的表达式必须为true才能视作整个模式匹配成功。例如我们可以先用绑定模式,然后再让确认它的某个属性是否为真。
match的语法看起来有点复杂,但是合理运用可以减少很多乱七八糟的if-else。在文档中可以看到更详细的例子。
撒花
好了GDScript精要部分就算结束了!下一篇文章开始就要运用前面所学的知识开始做些更像做游戏做的事了!