GameMaker8.0 新手教程 Part 14 -制作游戏(四)-

分类栏目:gamemaker教程

482

GameMaker8.0 新手教程 Part 14 -制作游戏(四)-

14、游戏的暂停

①show_message法
这大概是最简单粗暴的办法了。
第六章第五节中,我们介绍了简单的输入输出函数,这些函数的共同特点就是,弹出窗口时会自动暂停游戏。
所以我们可以在world的按下P键事件(其他什么键都行,也可以在step里用函数来代替按键事件)中写上show_message("Pausing.");来实现暂停效果。
②实例解散法
show_message法简单,但是局限性很大,几乎不能再做额外的事情。
很多游戏的暂停界面都是一个菜单面板,我们也想这么做一个。
这时候我们就需要了解一下实例的解散。(GML汉化文档28-29页)
instance_deactivate_all(notme) 解除房间内的所有实例。如果参数 notme 为 true ( 1 )正在调用的实例不会解除(通常是你想要的效果)。
instance_activate_all() 激活房间内的所有实例。
本节中涉及的函数只有这两个,解散实例相关的其他函数道理差不多,敬请自行理解。
什么叫做解散(解除)一个实例?
简单的说,就是暂时地禁用这个实例,这个实例的代码不再会被执行,也不会响应按键和外部调用。但是数据并不会丢失,一旦被解散的实例重新激活,它会接着中断的地方继续执行。
如果我们解散了所有的实例,就相当于暂停游戏,之后再把所有实例激活,就相当于继续游戏。
思路:
首先看到函数instance_deactivate_all(notme),正如函数解释中所说,如果notme填1,调用这个函数的实例就不会被解散。我们需要这个实例(本教程使用world来调用这个函数)来响应按键,以继续游戏,所以notme只能填1。
但是,解散实例也会禁用实例的绘制,换而言之,解散实例后,屏幕里就只有背景图片了,所有的实例都将进入隐形状态(如果你觉得没关系,可以跳过这一步)。所以,我们需要先想办法把屏幕里的图像先保存下来:
background_create_from_screen(x,y,w,h,transparent,smooth,preload)  通过复制给定屏幕区域创建一个背景。这个函数可以用来创建任何你想用的背景。在屏幕上使用绘制函数绘制图像接着从中创建一个。(如果你不在绘制事件中使用,你恰好可以这样做,它不会刷新,所以在屏幕上不可见)其他参数和上面一样。函数返回新背景的索引。这儿必须要有一个操作警告。即使我们说到屏幕,实际是和绘制区域相关。事实是屏幕上有窗口,图像可能在窗口中缩放。
官方文档讲的又臭又长,说白了,就是截屏,然后作为一张background使用。
所以我们使用:

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

来储存当前屏幕的图像。
但是这还没完呢!我们选择用world作为解散其他实例而自己保留的主体,但是,world同时也是记录游戏时间的主体。即使我们解散了所有的实例,world依然会继续计时,这不符合我们的需求。所以,我们需要一个变量来表示游戏的暂停。
假设我们使用变量pausing,首先先在world的create事件初始化pausing = 0;,然后,暂停时令pausing = 1;,解除暂停时令pausing = 0;,最后把world的计时代码用

if(!pausing)
{
  计时的代码
}

包裹起来。同样的,凡是world不应该在暂停期间做的事情,一律使用if(!pausing)进行限制。
现在,我们可以开始动工设计我们的暂停面板了。
首先,为world创建draw事件,draw事件是专门用来绘图的事件,与绘图相关的事件一般都放在draw事件中。
在draw事件中,我们首先写上if(pausing){}。
draw_background(back,x,y) 绘制背景在坐标(x,y)。
把我们之前保存下来的截屏图片画上去:

if(pausing)
{
  draw_background(backPause, 0, 0);
}

如果你还要画点别的什么,请参考后续的章节,之后我会花很多笔墨着重讲draw事件。
接来下就是重点了:在执行函数instance_deactive_all之后才创建的实例,不会被影响。
也就是说,实例解散函数,只解散执行函数的那一瞬间,游戏里有的实例,并不会持续生效而解散之后才创建的实例。
所以我们的面板设计,就可以通过obj来实现:暂停游戏之后,通过world来创建这些obj的实例,结束暂停之前,先把这些实例销毁,再激活所有实例。
至于面板要怎么设计,就靠各位自行发挥了。(其实是我懒得写一个例子)
结束暂停,就是通过函数instance_activate_all();来实现的。但是在结束暂停之前,我们还有一些事情要做,除了销毁面板效果obj的实例,让pausing变回0以外,我们还要销毁刚刚通过截屏保存的background。
background_delete(ind) 将背景从内存中删除,释放内存空间。

即,background_delete(backPause);。
总结一下流程:

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

③跨房间法
参看下一节(咳咳,别扔菜叶子臭鸡蛋)。

15、房间的持续

首先要先提一点,若要制作RPG,建议使用RPG Maker(即RM)而不是GameMaker。
不过这并不是说GM不能制作RPG,或者说没有RM做的RPG好。RM是RPG特化引擎,集成了大量可以直接现用的RPG组件,而GM作为一个泛用式游戏引擎,并不对任何类型的游戏提供专用组件,所以要自己一个个去实现。
好了,进入正题。如果要使用GM制作RPG,那么这一节无疑是重点中的重点。
我们在制作RPG的时候,可能会需要这样一个效果:在地图上走动,遇到怪物之后,跳转到战斗房间,战斗结束之后返回之前的地图。那么现在的问题是,GM跨越房间时,原本的房间会自动清除数据,所以战斗结束后返回原本的房间,原本房间就重头开始了,也就是说,玩家位置,地图信息,全都重置了。
细心的人可能发现了房间是有一个“持续”属性的,它的官方介绍是:
切换房间时是否保留本房间的实例(再次进入这个房间的时候所有的实例都会保持离开这个房间时候的样子)。
GameMaker8.0 新手教程 Part 14 -制作游戏(四)-
也就是说,你离开时房间是什么样子,再回去的时候还是什么样子,而不会重置。
有一点要注意的是,持续的实例独立于持续的房间。什么意思呢?举个例子,我们的游戏中,world的实例是一个持续的实例,游戏开始被创建,游戏结束才会被销毁。如果我们离开一个持续的房间,这个房间会记住离开时所有实例的状态与数据,但是不包括world。也就是说,如果离开这个房间时world记录了某个数据为2,在其他房间变成了3,再次回到这个持续的房间时,值依然是3,不会变回2。
咦,这不就是我们想要的效果吗,那就给每个用作游戏地图的房间都勾上持续不就好了!
然而不好意思,我得泼一盆冷水了。在GM中,并不建议给房间永久的持续属性。我们知道,在一般情况下,GM每次跳转房间,就会清理掉上一个房间的数据,以节约内存。但是,如果房间被勾上了持续属性,那么这个房间的数据就不会被清理,而是被一直保留在内存中,直到游戏结束才释放。一两个房间还好,但如果是八九个,甚至十几二十个房间同时占用内存,玩家就该抱怨这游戏怎么优化这么差了。
另外一个问题是,RPG一般是由多个地图构成的,离开一个地图,再回去时,地图信息(如怪物信息)应当被刷新重置,而不是保留原本的状态(至于什么任务信息,BOSS信息,商店信息,那应该保存在存档里)。事实上,我们只是需要在遇敌跳跃到战斗房间时,临时保存一下地图数据就行了。
咳,请耐着性子再等我多废话几句。我们其实只需要一个战斗用房间(代码中一律用rBattle表示),然后通过独立的world来进行信息传递。在写基层代码的时候,我们就应该把角色的数据,如hp,mp,skills等用world来储存而不是让player来储存,player应该只执行移动等操作性的代码,这样既方便了world调用脚本saveGame和loadGame存读档,又能保证跨房间数据不丢失。
接下来就是我们的主角登场了:
room_persistent 当前房间是否持久显示。
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]里写:
room_persistent = 1;
room_goto(rBattle);
在alarm[1]里写:
room_persistent = 0;
当玩家与怪物触发战斗时(通常是碰撞player事件中),应该让怪物执行代码:
world.goBackMap = room;
world.monster = object_index;
world.alarm[0] = 1;
//如果不摧毁怪物,就会无限触发战斗。
instance_destroy();
object_index是返回实例属于哪个对象。
战斗房间如何设计就是各位自行发挥了,可以通过world.monster来读取是和哪个怪物发生了战斗,形如if(monster == objDragon){xxx;}
当战斗结束之后,应该让战斗房间的某个obj执行:
//此处填1有可能发生bug,故填2。
world.alarm[1] = 2;
room_goto(world.goBackMap);
回到原本的地图房间,并且取消房间的持续属性。
现在,我们再回到上一节所说的,使用跨房间法来暂停游戏,是不是思路瞬间明朗了起来?我们只需要创建一个新的房间,专门用来搞暂停面板,用和上面同样的办法,就可以实现暂停与继续的效果了。由于world是独立于房间之外的,我们同样可以在暂停前先截图,然后储存到world里,通过world带到暂停面板的房间中使用。