开篇废话
这文档适合已经会使用其他编程语言(C#、C\C++、java、python等)的游戏开发爱好者学习 (逛CSDN怎么会有不会编程的人呢) ,如果不会其他语言也可以看个大概,毕竟字不多,已经入门 GML 的新人也可以看看这个文档,或许能发现一些有趣的东西。
这份文档主要基于 GM 8.0,理论上大部分适用于GMS、GMS2,还请GMS、GMS2用户以实践为准。
文档写的略急促,如果发现有问题或讲错了敬请指出。
作者联系方式:QQ 2293840045
作者有话说:
本文之前是放在CSDN的,但由于 CSDN 经常在未经我同意,且没有告知我的情况下,自动将本文设置成
VIP 免费阅读
,同时没有提供取消 VIP 阅读的设置。因此文章迁移至此。
GM软件的基本の基本
软件的左边一列(GMS2默认在右边)的文件夹是游戏的资源,例如游戏的图片(sprites
)、声音(Sounds
)、脚本(Scripts
)等,把准备好的素材导入到这里即可。
每一项资源可以再细分文件夹,这点随开发者喜好随意整就行。
资源经常要在代码中调用,所以资源的名称要符合标识符命名规则,例如数字不能开头不能包含特殊符号等。
[!tip]
初学 GML 的时候可以建立一个游戏物体,添加 create (创建)事件,在这里面写代码(GM8和GMS要在右边找到代码小方块,拖到左边空白区域才行),然后创建一个房间,把这个物体放到房间里面。
觉得上面那个麻烦也可以新建房间,然后进房间设置,有个“房间创建代码”的按钮点开可以写代码,这里就像程序的 main 方法一样,一运行就执行。
GM中的面向对象
上面介绍的资源中,有一个 Objects ( GMS2 中文翻译可能叫“物体”)最为重要。
Object 可以直接理解为其他语言的 class ,而游戏中每生成一个物体,就是 new 了一次这个 class 的实例。
继承
GML 没有封装和多态(也可以说到处都是多态),但可以使用继承,在 object 物体编辑的面板上指定父物体即可。
这里的继承父物体是 class 继承父类的意思,而不是 Unity 中的物体父子级关系。
要注意,GML的继承和传统继承区别很大。
继承可以使子物体拥有父物体的变量,可以使子物体没有实现的事件由父物体实现。
很特殊的一点,传统的继承中,每 new 一个子类都会产生一个父类,而GM当中的父类可以永远不被实例化,但这并不影响子类继承父类成员。
重写
例如父物体在 step 事件中有x += 1
代码,他的子物体如果没有自己的 step 事件,则这个子物体也会在 step 事件触发时运行x += 1
。
但如果这个子物体存在自己的 step 事件,则不会再运行x += 1
了,如果想运行子物体的某个事件后(或前)再运行父物体的相同事件,则可以在子物体中需要运行父物体事件的位置调用event_inherited()
方法,就等同于 java 的super.xxx()
或者 C# 的 base.xxx()
。
即便子物体的 step 事件代码为一行注释,那么它也算作“子物体存在自己的 step 事件”。
至于为什么是一行注释而不是空代码呢,因为GM会自动删除没有代码(或注释)的事件,所以如果想“顶掉”父物体事件,写一行注释即可
//
基本的语法
GML的基础语法和C\C++、C#、Java这类语言极度相似,且作为一个专为游戏而生的语言,有相当多的特性用起来非常简单、舒服。
GML语言使用花括号{}
表示代码块,语句结尾可以加分号;
表示结束也可以直接换行。
和大部分语言一样使用点.
引用成员,例如Player.hp=0
就可以干掉玩家。
变量
GML是弱类型的语言,就是说使用变量无需指定数据类型。
本地变量
可以向下面这样直接使用变量
name = "黎明"
show_message("这是" + name) // 显示 这是黎明
show_message() 是gm内置方法,可以让游戏弹出一个窗口并显示内容
直接赋值产生的变量我习惯叫做本地变量
,这个变量作用在当前实例上,就类似在 java、C# 的 class 中直接定义的 public String name;
,所以这个方法一般用来定义属性,例如血量、名字等等。
临时变量
如果是为了临时计算而使用变量,一般会在声明变量时加个var前缀:
// GM8
var a,b,c; // 这是 GM8 语法的一大缺陷,这里的 var 语句结束必须要加分号
a = 233 // 并且不能边声明变赋值,只能单独写一行
// GMS2
var a,b = 123,c // GMS 就没有上面的问题了
var 的变量我习惯叫做临时变量
,这个变量仅仅作用于当前的事件或脚本中,这就和在其他语言的方法中int a = 123;
一样,方法结束变量就会消失(忽略闭包啥的)
全局对象变量
除此之外,还有个全局对象变量
,用起来大概这样:
// 用起来就像给一个叫做 global 的对象属性赋值一样,所以我叫它“全局对象变量”
global.a = 123
这种变量就和他名字一样,在全局任意位置都可以使用,作用于贯穿整个游戏程序的生命周期,从声明,也就是第一次赋值,持续作用到游戏程序关闭。
global 可以理解成 GM 帮咱实现好的一个单例,用就完事了。
为啥不直接叫“全局变量”呢,因为GM8中已经有这个东西了,语法格式和 var 一样,需要把 var 替换成 globalvar ,然而这东西缺点挺多,最新的GMS2中已经移除了这个语法,所以不再说明。
数据类型
就两种类型
GM的数据类型不去深挖,其实就两种:数字、字符串。
数字就是数字,不分整数小数,字符串就不用说了。
不过有个不太舒服的地方就是 GM 没有隐式类型转换,就是说"Hello " + 233
会报错,因为字符串不能和数字相加,需要调用string()
方法转换一下:"Hello" + string(233)
。
字符串定界符使用双引号或单引号都行
true和false
如果运行show_message(true)
会直接显示一个数字 1 出来,所以可以猜到换成 false 就是显示数字 0,这里和 C 语言处理 bool 值好像挺像的。
目前我们知道了 true => 1,false => 0,但有意思的是 GM 处理逻辑运算时,会把所有的正整数当做 true,其他整数当做 false 处理,所以show_message(123 and 111)
结果显示 1 ,show_message(-1 and 0)
显示一个 0。
上面只说了整数情况,小数情况比较奇怪,我在GM8中的测试结果显示,>= 0.5的小数当做 true,< 0.5 是 false,我也不清楚为什么 0.5 是分界线
noone
GM 中使用 noone 关键字来表示空,就像其他语言的 null 关键字一样。
实际上 noone == -4
id 与资源
那问题来了哈,如果说 GM 只有数字和字符串两种类型,那么引用的资源是什么类型呢?引用的实例呢?
GM 中每个实例都有一个 id,这个 id 类似其他语言中一个实例的内存地址,是一个实例的引用。可以
show_message(id)
看到当前实例的id
不卖关子了,估计都能猜到,id与资源都是数字,并且都是正整数。
利用正整数 => true 的特点可以直接判断某个id或资源是否存在:
// 假设 target 是个实例引用
if (target){ // 如果没有这个if,那么下面那句可能报错
target.y -= 100 // 如果target存在,就让它向上移动
}
数据结构
GM本身也有一个数组,但是很难用,甚至都不能获取数组长度:
arr[0] = 1
arr[1] = 11
arr[2] = 111
arr[3] = 1111
var i;
for(i = 0 ; i < 4 ; i += 1){
show_message("这是第" + string(i+1) + "个元素,值是" + string(arr[i]))
}
不过好在 GML 提供了列表、字典等经典数据结构,操纵它们的函数都是ds_
开头,具体可以查阅官方文档,我只是在这里提醒一下有这些东西。
这些数据结构还都自带序列化方法,可以方便的做网络传输或者数据保存。
结构语法
IF
if 语法和 java 啥的一模一样:
if(xxx){
}else if(xxx){
// 可以很多else if
}else
show_message("可以省略花括号只写一句代码")
python来的同学一定注意,没有 elif 语法,并且所谓的"可以省略花括号只写一句代码",也不是python的那种省略花括号
这里 if 后面的圆括号可以省略,像 python 一样,但是个人感觉怪怪的。
WHILE
while 也和 java 啥的一模一样:
while(true){
show_message("你的弹窗永远也点不完")
}
python来的同学又要注意,没有while … else … 语法
FOR
for 也也和 java 啥的一模一样:
var i; // 难受,GM8 不能把这句话写到 for 那一行
for(i = 0 ; i < 10 ; i += 1){ // 难受*2,GM8 没有++,不过 GMS 有
show_message(i) // 只显示数字不用 string 转换
}
python来的同学可能一脸懵逼(
应该没),这段代码翻译成python是:for i in range(10): print(i)
REPEAT
这个东西是为了方便开发游戏 GM 自己造出来的,语法大概这样:
repeat(3){
show_message("重要的事情说三遍 ! ")
}
python同学猛地站起来说:这个我熟啊!
for i in range(3): print("重要的事情说三遍 ! ")
…… 可惜这个语句不怎么实用 ……
总结
基本上语法和java、C#那些差不多,还有个 switch 游戏机语法也一样,不过因为用得不多我就不列举了。
并且break
和continue
语句也一样用
运算符
数学运算符就是 +
-
*
/
不用说,还有个 mod
求余和 div
整除,没了。
a = 10 mod 3 // 结果是1,mod等同于其他语言的%
b = 10 div 3 // 结果是3,div等同于某些语言的\
比较关系用>
<
>=
<=
!=
不等于 ==
等于,应该也不用说
比较有意思的是在进行逻辑运算时,等于写成单等号也行,不会变成赋值,但比较奇怪,个人不喜欢。
逻辑运行可以用常见的!
&&
||
形式,也可以用not
and
or
形式。
个人喜欢字母形式,因为有代码着色,看起来舒服一些
赋值运算有=
+=
-=
*=
/=
没什么好说的
连接字符串用加号就行+
,记得把数字转换成字符串,用string()
方法
脚本
脚本是GML中定义方法的方式,在GML的面向对象当中方法不属于某个实例,而是直接放在全局资源里面了,谁调用都可以。
脚本编写
新建脚本就是在资源那一栏的Scripts
直接创建脚本就行,然后起个名字,这个名字就是调用时用的方法名了。
参数定义在哪里呢,,,,没有,对。不用定义参数,GML 的脚本是可以随意接受任何参数的,但最多不能超过16个。
因为参数问题,GML显然不支持重载,并且GM8没有文档注释写法,所以调用函数的时候一般比较蒙蔽,建议养成在脚本开头用注释写文档的好习惯。
GMS2支持文档注释,可以给方法加参数提示了。
在脚本中引用参数要使用argument
内置变量,这是一个数组,里面每个位置存了一个参数。例如写个脚本实现计算加法
// 这是脚本里面
return argument[0] + argument[1]
// 假设那是Add脚本,调用它就是
res = Add(1,2) // 返回一个3
奇特的脚本调用机制
在脚本中调用本地变量时,调用的变量是执行脚本的那个实例的变量。
例如写了一个控制移动的脚本:
x += 1 // 碰壁移动,永不回头
那么谁调用了这个脚本,谁就会向右移动。
这点还是挺有意思的,好像弥补了 GML 的残废继承和没有多态的缺点。
比如写了一个“敌人死亡”的脚本,脚本里面实现了死亡动画、玩家加分、掉落物品等功能,那么每个敌人死亡时都可以直接调用这个脚本,脚本中也可以很方便地读取到敌人实例的属性。
在脚本中疯狂用 var !
估计刚才看脚本参数的时候各位都吐槽了一句:“这么麻烦?”,然后聪明的各位一定想到了这种写法:
name = argument[0]
level = argument[1]
show_message("你就是"+name+"啊,只有"+string(lvevl)+"级呢")
这样写相当 nice,但是一定不要忘了第一行加上一行:
var name,level;
因为不写 var 的话,这个 name 和 level 是会当做本地变量永远存储在调用者实例上面的,吃点内存还好说,万一变量名冲突了就麻烦了,所以脚本中产生的变量一定要var var var。