show_message

这大概是最简单粗暴的办法了。

我们介绍了简单输入输出函数,这些函数的共同特点就是,弹出窗口时会自动暂停游戏。

所以我们可以在 world 的按下 P 键事件(其他什么键都行,也可以在 step 里用函数来代替按键事件)中写上 show_message("Pausing."); 来实现暂停效果。

实例解散法

show_message 法简单,但是局限性很大,几乎不能再做额外的事情。很多游戏的暂停界面都是一个菜单面板,我们也想这么做一个。

这时候我们就需要了解一下实例的解散。(GML 汉化文档 28-29 页)

  • instance_deactivate_all(notme) 解除房间内的所有实例。如果参数 notme 为 true 正在调用的实例不会解除(通常是你想要的效果)。
  • instance_activate_all() 激活房间内的所有实例。

本节中涉及的函数只有这两个,解散实例相关的其他函数道理差不多,敬请自行理解。

什么叫做解散(解除)一个实例?

简单的说,就是暂时地禁用这个实例,这个实例的代码不再会被执行,也不会响应按键和外部调用。但是数据并不会丢失,一旦被解散的实例重新激活,它会接着中断的地方继续执行。如果我们解散了所有的实例,就相当于暂停游戏,之后再把所有实例激活,就相当于继续游戏。

实现思路:

首先看到函数 instance_deactivate_all(notme),正如函数解释中所说,如果 notme 填 1,调用这个函数的实例就不会被解散。我们需要这个实例(本教程使用 world 来调用这个函数)来响应按键,以继续游戏,所以 notme 只能填 1。

但是,解散实例也会禁用实例的绘制,换而言之,解散实例后,屏幕里就只有背景图片了,所有的实例都将进入不可见状态(如果你觉得没关系,可以跳过这一步)。所以,我们需要先想办法把屏幕里的图像先保存下来:

  • background_create_from_screen(x, y, w, h, transparent, smooth, preload) 通过复制给定屏幕区域创建一个背景。这个函数可以用来创建任何你想用的背景。在屏幕上使用绘制函数绘制图像接着从中创建一个。(如果你不在绘制事件中使用,你恰好可以这样做,它不会刷新,所以在屏幕上不可见)其他参数和上面一样。函数返回新背景的索引。这儿必须要有一个操作警告。即使我们说到屏幕,实际是和绘制区域相关。事实是屏幕上有窗口,图像可能在窗口中缩放。

官方文档讲的又臭又长,说白了,就是以当前视野左上角为原点截屏,然后作为一张 background 使用。所以我们使用:

1
backPause = background_create_from_screen(0, 0, view_wview[0], view_hview[0], 0, 0);

来储存当前屏幕的图像。

但是这还没完呢!我们选择用 world 作为解散其他实例而自己保留的主体,但是,world 同时也是记录游戏时间的主体。即使我们解散了所有的实例,world 依然会继续计时,这不符合我们的需求。所以,我们需要一个变量来表示游戏的暂停。

假设我们使用变量 pausing,首先先在 world 的 create 事件初始化 pausing = 0;,然后,暂停时令 pausing = 1;,解除暂停时令 pausing = 0;,最后把 world 的计时代码用

1
2
3
4
if (!pausing)
{
    计时的代码
}

包裹起来。同样的,凡是 world 不应该在暂停期间做的事情,一律使用 if (!pausing) 进行限制。

现在,我们可以开始动工设计我们的暂停面板了。

首先,为 world 创建 draw 事件,draw 事件是专门用来绘图的事件,与绘图相关的事件一般都放在 draw 事件中。在 draw 事件中,我们首先写上 if (pausing){}

  • draw_background(back, x, y) 绘制背景在坐标(x, y)。

把我们之前保存下来的截屏图片画上去:

1
2
3
4
if (pausing)
{
    draw_background(backPause, 0, 0);
}

如果你还要画点别的什么,请参考后续的章节,之后我会花很多笔墨着重讲 draw 事件

接来下就是重点了:在执行函数 instance_deactive_all 之后才创建的实例,不会被影响。也就是说,实例解散函数,只解散执行函数的那一瞬间,游戏里有的实例,并不会持续生效而解散之后才创建的实例。所以我们的面板设计,就可以通过 obj 来实现:暂停游戏之后,通过 world 来创建这些 obj 的实例,结束暂停之前,先把这些实例销毁,再激活所有实例。至于面板要怎么设计,就靠各位自行发挥了。

结束暂停,就是通过函数 instance_activate_all(); 来实现的。但是在结束暂停之前,我们还有一些事情要做,除了销毁面板效果 obj 的实例,让 pausing 变回 0 以外,我们还要销毁刚刚通过截屏保存的 background

  • background_delete(ind) 将背景从内存中删除,释放内存空间。

即,

1
background_delete(backPause);

总结一下流程:

  1. 创建 pausing 变量,并用 pausing 变量控制 world 的计时。
  2. 创建按下暂停键的事件,改变 pausing 为 1。
  3. 截屏保存为 background,然后在 draw 事件画出来。
  4. 解散实例。
  5. 创建构成暂停面板的 obj 的实例。
  6. 结束暂停,建议通过面板 obj 接受按键(如面板有“继续游戏”“重玩本关”“退出游戏”等选项,而玩家选择了继续游戏),然后反馈给 world。
  7. 结束暂停之前,销毁面板 obj 的实例。
  8. 销毁截屏保存的 background。
  9. 改变 pausing 为 0。
  10. 激活所有实例。

跨房间法

在此之前,我们需要介绍一下房间的持续

首先要先提一点,若要制作 RPG,建议使用 RPG Maker(即 RM)而不是 GameMaker。不过这并不是说 GM 不能制作 RPG,或者说没有 RM 做的 RPG 好。RM 是 RPG 特化引擎,集成了大量可以直接现用的 RPG 组件,而 GM 作为一个泛用式游戏引擎,并不对任何类型的游戏提供专用组件,所以要自己一个个去实现。

如果要使用 GM 制作 RPG,那么这一节无疑是重点中的重点。

我们在制作 RPG 的时候,可能会需要这样一个效果:在地图上走动,遇到怪物之后,跳转到战斗房间,战斗结束之后返回之前的地图。那么现在的问题是,GM 跨越房间时,原本的房间会自动清除数据,所以战斗结束后返回原本的房间,原本房间就重头开始了,也就是说,玩家位置,地图信息,全都重置了。

细心的人可能发现了房间是有一个“持续”属性的,它的官方介绍是:切换房间时是否保留本房间的实例(再次进入这个房间的时候所有的实例都会保持离开这个房间时候的样子)。

Persistence

也就是说,你离开时房间是什么样子,再回去的时候还是什么样子,而不会重置。

有一点要注意的是,持续的实例独立于持续的房间。什么意思呢?举个例子,我们的游戏中,world 的实例是一个持续的实例,游戏开始被创建,游戏结束才会被销毁。如果我们离开一个持续的房间,这个房间会记住离开时所有实例的状态与数据,但是不包括 world。也就是说,如果离开这个房间时 world 记录了某个数据为 2,在其他房间变成了 3,再次回到这个持续的房间时,值依然是 3,不会变回 2。

咦,这不就是我们想要的效果吗,那就给每个用作游戏地图的房间都勾上持续不就好了!

然而不好意思,我得泼一盆冷水了。在 GM 中,并不建议给房间永久的持续属性。我们知道,在一般情况下,GM 每次跳转房间,就会清理掉上一个房间的数据,以节约内存。但是,如果房间被勾上了持续属性,那么这个房间的数据就不会被清理,而是被一直保留在内存中,直到游戏结束才释放。一两个房间还好,但如果是八九个,甚至十几二十个房间同时占用内存,玩家就该抱怨这游戏怎么优化这么差了。

另外一个问题是,RPG 一般是由多个地图构成的,离开一个地图,再回去时,地图信息(如怪物信息)应当被刷新重置,而不是保留原本的状态(至于什么任务信息,BOSS 信息,商店信息,那应该保存在存档里)。事实上,我们只是需要在遇敌跳跃到战斗房间时,临时保存一下地图数据就行了。

咳,请耐着性子再等我多废话几句。我们其实只需要一个战斗用房间(代码中用 rBattle 表示),然后通过独立的 world 来进行信息传递。在写基层代码的时候,我们就应该把角色的数据,如 hp,mp,skills 等用 world 来储存而不是让 player 来储存,player 应该只执行移动等操作性的代码,这样既方便了 world 调用脚本 saveGameloadGame 存读档,又能保证跨房间数据不丢失。

接下来就是我们的主角登场了:

  • room_persistent 当前房间是否持久显示。有两个值,0(即 false)或1(即 true),等效于勾不勾选房间的持续属性。

道理很简单,就是让房间默认不持续,在跨房间战斗之前先改成持续,战斗回来之后,再恢复不持续。

首先先给 world 定义一个变量(假设是 goBackMap)用来储存地图的房间,这样战斗完才能准确地回到原地图的房间,以及一个变量(假设是 monster)用来储存是与哪个怪物发生战斗,以传递到 rBattle 房间内。先在 create 事件里初始化一下,goBackMap = -1;monster = -1。至于为啥是初始化为 -1 而不是 0,是因为 GM 很多函数或者变量都把 -1 视作“无”或者“默认”,初始化为 -1 就当是入乡随俗,当然初始化成别的也是没有任何问题的。

然后,给 world 新建两个 alarm 事件,假设为 alarm 0 和 alarm 1,如果已经使用过了,就换成别的 alarm 事件,当然相应的代码也要改。

在 alarm 0 里写:

1
2
room_persistent = 1;
room_goto(rBattle);

在 alarm 1 里写:

1
room_persistent = 0;

当玩家与怪物触发战斗时(通常是碰撞 player 事件中),应该让怪物执行代码:

1
2
3
4
5
world.goBackMap = room;
world.monster = object_index;
world.alarm[0] = 1;
// 如果不摧毁怪物,就会无限触发战斗。
instance_destroy();

object_index 是返回实例属于哪个对象。

战斗房间如何设计就是各位自行发挥了,可以通过 world.monster 来读取是和哪个怪物发生了战斗,形如 if (monster == objDragon){xxx;}

当战斗结束之后,应该让战斗房间的某个 obj 执行:

1
2
3
// 此处填1有可能发生bug,故填2。
world.alarm[1] = 2;
room_goto(world.goBackMap);

回到原本的地图房间,并且取消房间的持续属性。

现在,我们再回使用跨房间法来暂停游戏,是不是思路瞬间明朗了起来?我们只需要创建一个新的房间,专门用来搞暂停面板,用和上面同样的办法,就可以实现暂停与继续的效果了。由于 world 是独立于房间之外的,我们同样可以在暂停前先截图,然后储存到 world 里,通过 world 带到暂停面板的房间中使用。