论GameMakerStudio对象实例的执行顺序(二)
一、继承
严格来说这部分内容并不属于对象实例执行顺序范畴内,但由于在后面章节会涉及到此功能,所以我们先做个铺垫。
说到继承,就要提到父与子这两种角色。当一个对象有一个父对象时,它可以继承其所有事件与代码内容。而这个对象就是这个父的子。当然子也可以有自己的子,但一个子不能有多个父。所以继承关系我们可以想象为树状结构即可。
正常情况下,一个对象(子)继承另一个对象(父),而子为空时(无任何事件),子将继承父的所有事件以及其事件内的代码(除sprite、visible、solid、persistent、physics、depth等)。
子可以设置父所没有的事件,相当于青出于蓝而胜于蓝。而当子创建了一个其父所拥有的事件时,子将不继承父在本事件里的代码。除非子在本事件的代码里写上event_inherited()函数,他将继承父的在本事件的所有内容。
关于继承的功能,这里举个简单的例子。
假设有对象obj_A、obj_B、obj_C三个对象。obj_B与obj_C都继承obj_A。
obj_A对象的代码如下:
Create事件:
n = 10;
Step事件:
n += 5;
obj_B对象的代码如下:
End Step事件:
n += 3;
obj_C对象的代码如下:
Create事件:
n = 5; //不继承obj_A在create事件里的代码。
Step事件:
event_inherited();//obj_A在step事件里的代码n += 5;
n += 2;
结果:
在本帧结束时,obj_A的n从10(create事件)到15(step事件)。
obj_B的n从10(继承obj_A的create事件)到15(继承obj_A的step事件),再到18(end step事件)。
obj_C的n从5(create事件的代码。它没有继承obj_A在create事件里的代码)到10(继承obj_A的step事件里的代码),同时再到12(step事件的代码)。
二、with函数
with函数的参数可以为对象(可为父),也可以为某特定实例id。其功能是在指定对象的所有实例或某一特定实例,即刻执行with里面的代码(对每个实例内生效)。
功能看似简单,但实际上可能会很复杂。我们来分析一位网友的with问题。
假设只有一个对象obj_A。我们事先在房间编辑器创建obj_A的三个实例a1、a2与a3。Id从小到大(即为执行顺序):a1 -> a2 -> a3。
obj_A对象的代码如下:
Create事件:
time = 0;
n = 0;
Step事件:
with obj_A
{
if keyboard_check_pressed(vk_space) && time = 0
time += 1;
}
if time = 1
{
n -= 5;
time = 0;
}
结果:
a1的n为-10;a2的n为-10;但a3的n为-5。
问题分析:
我们按照实例的执行顺序,来详细分析下。
从vk_space键按下的那一帧开始算起。由于vk_space键只按一下,所以第二帧不会执行keyboard_check_pressed(vk_space)。
第一帧a1的step事件:
a1的with代码,导致a1,a2,a3的time都为1。
a1执行with下面代码后,此时a1的time为0,n为-5。
第一帧a2的step事件:
a2的with代码,导致a1的time为1(此时a2,a3的time也为1)。
a2执行with下面代码后,此时a2的time为0,n为-5。
第一帧a3的step事件:
a3的with代码,导致a2的time为1。
注意,a1并不执行其with下面代码!原因是a1的time为1。
a3执行with下面代码后,a2的time为0,n为-5。
至此第一帧结束。此时a1的time为1,n为-5;a2的time为1,n为-5;a3的time为0,n为-5。
第二帧a1的step事件:
由于a1的time为1,所以执行了n -= 5;
a1最终time为0,n为-10。
第二帧a2的step事件:
由于a2的time为1,所以执行了n -= 5;
a2最终time为0,n为-10。
第二帧a3的step事件:
由于a3的time为0,所以什么都不执行;
a3最终time为0,n为-5。
以上结果是两帧后的结果。也就是说,当你在一帧里按下虚拟按键vk_space后,通过两帧才执行完成。
至此,网友的问题分析结束。当然,我们不推荐用with来玩嵌套。但如果你能分析清楚这个问题,那么with应该难不倒你了。
关于with还需要注意的是,如果with后面为父对象,那么父与其子都将执行with里面的代码。
举个简单例子。
假设有对象obj_A、obj_B、obj_C、obj_D四个对象。obj_B与obj_C都继承obj_A。
obj_D对象的代码如下:
Step事件:
with obj_A
{
n += 10;
}
结果:
所有obj_A、obj_B与obj_C的实例中,变量n都将加10。
如果我们只想让obj_A的实例中变量n增加10,可以将代码写成:
with obj_A
{
If object_index = obj_A
{
n += 10;
}
}
三、碰撞函数与事件
本质上碰撞事件与其他非draw事件在执行规则方面是相同的。但由于碰撞事件与碰撞函数的区别,以及碰撞多个实例的执行顺序问题,这部分内容我们还得拿出来单独讨论。
其实碰撞事件与碰撞函数还是有很大区别的。
1、碰撞函数以instance_place为例(与碰撞事件最为相像),在碰撞代码处只执行一次。也就是说,当本对象实例碰撞多个目标对象的实例时,在一帧里碰撞函数只会检测其中的一个实例(根据实例执行顺序决定)。并且根据实例的执行顺序逻辑,每帧都只会碰撞检测这个实例,除非改变被碰撞检测对象实例的执行顺序。
我们假设个场景。如果在本帧开始时,执行碰撞函数的实例,同时与多个被检测对象的实例碰撞时,碰撞函数只会检测id最小的(或在房间编辑器里Instance Order修改的实例顺序优先的)实例(与实例所属对象的object_index无关!)。然后再执行碰撞函数下面的其他代码。
举个简单例子。
假设有对象obj_A、obj_B两个对象。对象obj_A有一个实例id:100000;对象obj_B有两个实例:id:100001与id:100002。
obj_A对象的代码如下:
Create事件:
n = 0;
Step事件:
var temp;
temp = instance_place(x,y,obj_B); //只检测id小者
if temp
n = temp.n;
obj_B对象的代码如下:
Create事件:
n = id;
结果:
当实例id:100000(obj_A)同时碰撞id:100001(obj_B)与id:100002(obj_B)两个实例时,id:100000的n为100001。
2、而对于执行碰撞事件的实例,同时与多个被检测对象的实例碰撞时,碰撞事件将执行多次。其次数是被检测对象与之碰撞实例个数的总和。例如,当一个执行碰撞事件的实例,同时与被检测对象5个实例同时碰撞时,碰撞事件将执行5次。检测被撞对象实例的执行顺序,与对象实例执行顺序规则相同(遵循对象object_index与id规则)。
举个简单例子。
假设有对象obj_A、obj_B两个对象。对象obj_A有一个实例id:100000;对象obj_B有两个实例:id:100001与id:100002。
obj_A对象的代码如下:
Create事件:
n = 0;
与obj_B的collision事件:
n = other.n; //执行两次
obj_B对象的代码如下:
Create事件:
n = id;
结果:
当实例id:100000(obj_A)同时碰撞id:100001(obj_B)与id:100002(obj_B)两个实例时,id:100000的n先为100001,然后变为100002。
四、speed相关问题
原本这部分内容应该放到【论GameMakerStudio事件的执行顺序】来讨论。但由于需要理解两个实例的执行顺序,所以我认为放到里较为合适。
首先我们举例来了解与分析问题。
假设有对象obj_A、obj_B两个对象。每个对象各有一个实例。这两个实例的精灵图片为64x64的正方形图片(中心点居中)。我们在房间编辑器里预先创建两个实例,并且让两个实例左右(obj_A在左,obj_B在右)无缝连接起来(两者x坐标差值为64)。
obj_A对象的代码如下:
Step事件:
x = obj_B.x-64;
obj_B对象的代码如下:
Create事件:
hspeed = 10;
结果:
游戏在执行过程中,你会发现obj_A与obj_B的两个实例间有个10像素的缝隙。
分析:
其实原因是在于speed属性和step事件的特点。当对象以speed属性(包括hspeed和vspeed属性)进行移动时,只有在step事件结束时(所有实例都执行完step事件)才会改变其实例的坐标。上面例子中object_B的实例,只有当所有实例执行完step事件后,才会执行x += 10(hspeed = 10的结果)。
所以在object_A的step实例里的x = obj_B.x-64代码中的obj_B.x,其实并没有执行hspeed = 10的结果。也可以理解为object_A一直在跟随object_B上一步的坐标。
要想避免这个情况发生,我们可以将object_A中step事件变成end step事件。
obj_A对象的代码如下:
End step事件:
x = obj_B.x-64;
或者我们干脆删除object_A中所有事件,object_B中不应用hspeed属性,在object_B的step事件里用坐标移动自己与跟随者。
obj_B对象的代码如下:
Step事件:
x += 10;
obj_A.x = x;
至此,对象实例的执行顺序讨论完毕。由于对象实例的执行顺序远远比想象的复杂的多,所以这次讨论绝对不可能涵盖所有可能性。要完善这部分内容,还需要大家多多提出建议。如果可能,我们会再做出一次专题性讨论。
论GameMakerStudio对象实例的执行顺序(一)
对象实例顺序
上次我们共同讨论了事件的执行顺序,但那只能作为我们了解GMS运行机制的基础。要想真正掌握GMS的运行机制,我们还需对对象实例的执行顺序进行深入的研究与探讨。以下规则全部基于GMS1.4.1474版本测试验证。
如果大家想对GMS进行深入研究,欢迎来GameMaker深入研究群:133687276
直接进入正题。由于GMS是以实例为基本单元,所以程序里至少要包含一个实例才能运转。而实例的执行其本质是以事件为基础逐步推进的。正常情况下一个实例在一帧的周期内,所有事件只执行一次(当然也有例外,后面会进行探讨)。并且在一个事件里,所有实例执行结束后才会进入下一事件的执行。那么我们就以一个事件为基础,研究下事件里所有实例的执行顺序。
说到实例的执行顺序,可能很多人会想到id,或者是layer/depth(决定Draw事件的执行顺序)。其实实例的执行顺序并不像我们想象那么简单。除id与layer/depth外,我们还需考虑实例所属对象的这个因素。
在论GMS事件的执行顺序中,我们讨论过对象实例在房间编辑器里预建立,或在游戏过程中建立两种情况。其实对象实例的执行顺序也会受到这两种情况的影响。我们会分类进行讨论。
一、实例在游戏过程中创建
先来个简单点的:多对象单实例场景。说到对象的执行顺序,我们必须了解在GameMaker:Studio中的object_index属性,就是每个对象的编号。它是决定对象在事件中执行顺序的首要因素。这个对象的object_index属性,也就是在IDE中object树的前后顺序。从上至下(无论是否归纳为分组),编号从0到n。下图对象的object_index值:object2为0;object1为1;object3为2;object0为3。
而对象在事件中的执行顺序,我们还需要分为两部分来讨论:非Draw事件与Draw事件。原因是GMS为了让我们更好的掌控Draw事件的先后顺序,可以用layer/depth属性来进行控制。
1、在非Draw事件中,object_index小的先执行,反之。这个很容易理解,对象执行顺序,其实是按照IDE对象树自上而下的顺序(object_index)执行。以上图为例,对象在非Draw事件中的执行顺序为:object2、object1、object3、object0。
2、在Draw事件中,由于它也与layer/depth属性有关,所以还要分为两部分来讨论:layer/depth相同与layer/depth不同。
1)在多个对象的layer/depth属性相同的情况下,object_index不影响Draw事件的执行顺序!
2)当layer/depth属性不同时,layer/depth大(默认为0,可以为负数)的者优先执行,反之。
多对象单实例的情况已讨论完毕。接下来为单对象多实例场景:
1、非Draw事件中,实例的执行顺序完全依赖于id。id小者优先执行,反之。
2、而在Draw事件中,id大者优先执行,反之。
注意:实例在GMS2中draw事件的执行顺序与GMS1相反,还请大家各自试验与验证!
除多对象单实例,和单对象多实例的执行顺序外,还有多对象多实例场景。
1、非Draw事件中,执行顺序遵循object_index优先,实例id次之。
2、而在Draw事件中,执行顺序遵循layer/depth优先,实例id次之。如果layer/depth相同。执行顺序只与实例id有关。id大者优先执行,反之。
举个例子来验证下:
我们在IDE里建立了3个对象: object1(layer/depth 100)、object2(layer/depth 0)、object3(layer/depth 10)。对象的编号如下:
现在房间里有6个实例:id:100000(object1)、id:100001(object1)、id:100002(object3)、id:100003(object3)、id:100004(object2)、id:100005(object2)。
在step事件中,这6个实例的执行顺序为:
id:100004(object2)->id:100005(object2)->id:100000(object1)->
id:100001(object1)-> id:100002(object3)->id:100003(object3)
在draw事件中,这6个实例的执行顺序为:
id:100001(object1)-> id:100000(object1)-> id:100003(object3) ->
id:100002(object3)-> id:100005(object2)-> id:100004(object2)
如果你能够读懂这个例子,那表明你已理解了本部分内容。
二、实例在房间编辑器预创建
当我们在房间编辑器里创建了多个实例,我们可以在房间编辑器的setting选项中的Instance Order里,改变实例的执行顺序。
在Instance Order里,实例按照由上至下的顺序执行。这种行为你可以理解为修改了其id属性(实际id没变),后续的事件循环中,实例将遵循房间编辑器里在Instance Order修改的实例顺序,而不会遵循id原则。
需要注意的是,房间编辑器预建立的实例优先于游戏过程中创建的实例。
至此,对象实例执行顺序的基本原则已经讨论完毕。
简单总结下对象实例执行顺序的规则(“n、”与“n)”编号代表规则的先后顺。如果规则有冲突,以小编号规则为准):
对象执行顺序:
1、非draw事件:
object_index小者优先执行。
2、draw事件:
1)layer/depth大者优先执行。
2)object_index不影响执行顺序。
实例执行顺序:
1、非draw事件:
1)在房间编辑器中的实例的执行顺序,按照Instance Order自上而下的顺序执行。
2)在游戏过程创建的实例,id小者优先执行。
2、draw事件:
1)在房间编辑器中的实例的执行顺序,按照Instance Order自下而上的顺序执行。
2)在游戏过程中创建的实例,id大者优先执行(GMS2执行顺序相反)。
至此,对象实例的执行顺序讨论完毕。
真的结束了吗?其实还差得远呢。如果你觉得不够过瘾,那咱们就再进行更深一步的讨论。
具体见【论GameMakerStudio对象实例的执行顺序(二)】
论GameMaker:Studio事件的执行顺序
游戏制作是一个系统工程,随着规模的扩大,任何一个环节的问题都可能会让游戏崩溃。我们必须要在游戏引擎运行的机制上,尽可能的排除这个干扰项,拒绝黑盒测试。
另外,给GameMaker深入研究群(133687276)做个推广。本群宗旨是对GM于GMS进行深入研究(不闲聊),所以新手还是先打打基础。
注意:以下内容全部基于GMS1.4.1474版本测试并验证。并经在GMS2上初步测试,并没有发现异常。如发现错误,还请及时指出,以免误导大家。
一、说明
首先来了解下GMS的特点:1、事件驱动;2、以实例为基本单元;3、顺序执行。那么我们要了解GMS的内部机制,首先应该研究引擎的执行顺序。
言归正传,由于GMS在每一帧里会按照顺序执行多个事件(事件的内容为程序代码),而执行的基础单元为对象的实例。所以应该把GMS的执行顺序分为两个层面来分析:事件与对象实例的执行顺序。
注意:
1、本次讨论不涉及event_perform函数。我们要研究的内容是GMS事件执行顺序。由于event_perform函数可以随意调用,所以讨论它没有意义。
2、本次讨论不涉及Asynchronous Event。异步事件应属触发式,所以不作为本次讨论的内容。
3、Other Events中No more lives、No more health、Animation end、Close button也不作为本期讨论的内容。原因是用处太少,并且也不会影响游戏整体的执行顺序。
二、正文
由于GMS是以实例为基本单元,没有实例就没事件的执行。其实为了便于我们讨论事件的执行顺序,一个实例显然便于分析与讨论。但实际情况要复杂的多,我们需要至少保证2个实例才能包含所有可能性(具体原因后面再详谈)。
那我们现在正式开始。由于我们要讨论的GMS事件有20多种,为了便于讨论我们分为几个部分来进行。
首先我们看下官网给出的事件(部分事件)执行顺序:
1、 Begin step events
2、 Alarm events
3、 Keyboard, Key press, and Key release events
4、 Mouse events
5、 Normal step events
6、 Collision events
7、 End step events
8、 Draw events
这8个事件逻辑顺序是讨论事件执行顺序的前提。关于事件的执行顺序,我们会以这8个事件作为基础进行展开。
这里要注意的是,GMS 1.3以及后续版本将Draw事件又分成了多个子事件:
l Resize
l Pre Draw Event
l Draw Begin Event
l Draw Event
l Draw End Event
l Post Draw Event
l Draw GUI Begin Event
l Draw GUI Event
l Draw GUI End Event
鉴于这些Draw子事件执行顺序一目了然,可等同于GM、GMS1.1与GMS1.2的Draw事件。所以在执行顺序上,我们将上述子事件都归纳为一个draw事件来对待。
除了上述8个常用事件外,还有无顺序的Destroy事件。由于Destroy事件取决于instance_destroy()函数的位置,属于触发式执行。我们可以把Destroy事件看做为无序可控触发类事件。由于Destroy事件比较常用,所以后面章节会再次进行分析。
当然还有我们经常接触的,最先执行的Create事件。由于Create事件在正常情况下只执行一次(event_perform函数不在讨论范围),并不进行循环执行。我们把这类事件称之为有序一次性事件。
此外,还有不常用的Other Events。但为了了解GMS的执行顺序,其中部分事件也划入到探讨范围内。其中包括Outside room、Intersect boundary、Views、Game start、Game end、Room start、Room end以及End of path事件。
鉴于大家很少接触到这些事件,我直接引用下官方解释:
Outside room: This event happens when the instance lies completely outside the room. This is typically a good moment to destroy it.
Intersect boundary: This event happens when the instance intersects the boundary of the room, that is, it lies (at least) partially outside the room.
Views: Here you find a number of events that are useful when you use views in your rooms. These events test whether the instance lies completely outside a particular view or interesects the view boundary.
Game start: This event happens for all instances in the first room when the game starts. It happens before the room start event (see below) but after the creation events for the instances in the room. This event is typically defined in only one "controller" object and is used to start some background music and to initialize some variables, or load some data.
Game end: The event happens to all instances when the game ends. Again typically just one object defines this event. It is for example used to store certain data in a file.
Room start: This event happens for all instances initially in a room when the room starts. It happens after the creation events.
Room end: This event happens to all existing instances when the room ends.
End of path: This event happens when the instance follows a path and the end of the path is reached.
由于房间的存在,我们还需考虑到房间中涉及执行的两个行为方式:
1、Room Creation Code。这个是房间的初始代码。类似实例的初始事件一样,每个房间都可以有自己独立的初始代码。具体位置见下图左侧。
2、Instance Creation Code of each instance。这个是在房间编辑器里,修改事先在房间添加的对象实例的初始属性。具体位置见下图右侧。
注意:它与对象的Create 事件是两回事。
事件的范围(包括room行为方式)已经确定了。但我们讨论的次序并没有严格的逻辑关系以及顺序,可能大家会感觉到非常混乱。那我们根据事件的特点,一起归纳下上述事件。
我按照事件的特点将上述事件总结出如下3个类别。
1、有序8个循环事件。包括Begin step、Alarm、Keyboard, Key press, and Key release、Mouse、Normal step、Collision、End step以及Draw事件(所有Draw的子事件都归属为一类)。
2、无序可控触发类事件。包括Destroy、Outside room、Intersect boundary、Views、Game end、Room end以及End of path事件。
3、有序一次性事件。包括Create事件、Room Creation Code、Instance Creation Code of each instance(房间编辑器里的实例,与Create事件要区分开)、Game start与Room start事件。
8+5=13(无序触发类事件后面再讨论)。
测试内容暂时只包含一个实例,并且事先在房间编辑器中部署。具体过程我就省略了,直接给大家最终结果。
1. Create events(实例事件)
2. Creation Code of each instance(房间行为方式)
3. Game start events(实例事件)
4. Room Creation Code(房间行为方式)
5. Room start events(实例事件)
6. Begin step events(实例事件)
7. Alarm events(实例事件)
8. Keyboard, Key press, and Key release events(实例事件)
9. Mouse events(实例事件)
10. Normal step events(实例事件)
11. Collision events(实例事件)
12. End step events(实例事件)
13. Draw events(实例事件)
曾经有人提出过这样的问题,为什么在Room Creation Code里写的代码却不能最先执行。通过上述实验得知,它前面还有3个行为事件。
当然,上面讨论的实例是在房间编辑器建立的。如果我们再添加一个非房间编辑建立的实例呢,有序一次性事件(前5个行为事件)会如何执行呢。其实这是一件钻牛犄角的问题,因为我们不应该在两个实例里,都创建Game start与Room start事件,这是毫无意义的行为。但鉴于我们打算深入研究GMS的机制,那就放到一起进行讨论。
本来这部分内容,应该在下期的对象实例执行顺序里讨论。但我认为还是应该提前预热下较好。
既然要添加另外一个实例,那这个实例在什么时候创建,由谁来创建是个关键问题。下面我们会分情况进行讨论。
假设在房间编辑器创建了A实例。那么另外一个在游戏中创建的实例称之为B实例。A与B属于不同对象的实例。需要注意的是,这个B实例的生命周期里是不会包含Creation Code of each instance行为的。还有个重要的前提条件,A所属对象比B所属对象先创建(在IDE编辑器里)。这个重要的原因,我会在对象实例顺序章节再进行讨论。
在A实例的Create事件里创建B实例。A与B的执行顺序:
1. A实例的Create events与B实例的Create events
2. A实例的Creation Code of each instance
3. A实例的Game start events
4. B实例的Game start events
5. Room Creation Code
6. A实例的Room start events
7. B实例的Room start events
其实没什么特别的(A比B先执行的具体原因下章节会详细讨论),除了B实例没有Creation Code of each instance外,都在正常执行。
如果在A实例的Creation Code of each instance里创建B实例。A与B的执行顺序:
1. A实例的Create events
2. A实例的Creation Code of each instance与B实例的Create events
3. A实例的Game start events
4. B实例的Game start events
5. Room Creation Code
6. A实例的Room start events
7. B实例的Room start events
需要注意的是B的Create事件。
注意:任何实例的Create事件的执行,都会在instance_create()函数执行点(当前代码行),立即执行!
如果在A实例的Game start事件里创建B实例。A与B的执行顺序:
1. A实例的Create events
2. A实例的Creation Code of each instance
3. A实例的Game start events与B实例的Create events
4. Room Creation Code
5. A实例的Room start events
6. B实例的Room start events
这个很明显,B实例没有参与Game start事件。原因是B在Game start事件执行后才被创建。
注意:任何实例在当前事件被创立时,除立刻执行自己的Create事件内容外,不会参与本次事件的执行。在下一帧里,被创建的实例会正常执行所有事件。
由于Game start事件属于有序一次性事件,所以在B实例的生命周期里,永远都不会执行Game start事件。哪怕B实例的Game start事件里的确有具体内容。如果B实例是在Room start事件里被创建,那么B实例将正常循环执行8个常用事件。当然,在创建时要立即执行一次Create事件。
关于instance_create函数与Create事件举个例子。
前提是房间里仅有一个obj_A的实例。并且obj_A实例比obj_B实例先执行。
{obj_B对象的代码如下:
Create事件:
global.n=10;
Step事件:
global.n=30;
obj_A对象的代码如下:
Create事件:
global.n=0;
Step事件:
instance_create(100,100,obj_B);//此时obj_B的实例被创建,并立即执行自己的Create事件。但是这个被创建的obj_B实例,在本帧里不会执行Step事件。
global.n=20;
结果:
在本帧结束时,global.n数值先从0,到10,最后为20。
第二帧结束时,global.n数值先从20,到10,再到20,最后为30}
我们对上面内容做个总结。首先是最开始我们提出的13个事件执行顺序,这个是固定不变的。无论实例是在房间编辑器里建立的,还是在中途建立的。其次是有序一次性事件,过期不候。如果是中途建立的实例,在某些有序一次性事件后产生,那么它将永远不会执行那些事件。
剩下就只有无序可控触发类的7个事件了:Destroy、Outside room、Intersect boundary、Views、Game end、Room end以及End of path事件。
先说说Destroy事件。说到Destroy事件,就离不开instance_destroy()函数。这个函数可以在任何事件的任意位置中。如果在某实例的某事件里,执行了instance_destroy(),Destroy事件会在当前代码处立刻执行,然后再执行本事件instance_destroy()函数后的代码,最后在本事件结束后销毁实例。(这里要感谢鸵翼天指出了问题)
再举个instance_destroy()与Destroy事件的例子。
前提是房间里仅有obj_A一个实例。
{obj_A对象的代码如下:
Create事件:
global.n=0;
Step事件:
intance_destroy();//立即执行Destory事件
global.n=20;//执行完Destory事件后还会执行这行代码
//Step事件执行后,销毁本实例
Destory事件:
global.n=10;
End Step事件;
Global.n=30;//由于本实例在Step事件结束后被销毁,这行代码不会执行。
结果:
在本帧结束时,global.n从0,到10,最后为20。}
Outside room、Intersect boundary、Views(包含所有子事件)事件很简单。它会在本帧Step事件执行后执行。
Game end事件会在执行game_end()函数所属的事件执行后执行。而在Game end事件执行后,游戏结束。
Room end事件与Game end事件类似。只是触发Room end事件函数有很多种:room_restart()、room_goto(numb)、room_goto_next()、room_goto_previous()都可以触发。
Room end事件会在函数触发所属事件执行后执行。而在Room end事件执行后,房间会根据触发函数而转变。
End of path事件。这个事件是当你使用路径函数path_start(path,speed, endaction,absolute)才有可能触发。endaction为任何值时都等同于0,走完一次完整path后触发End of path事件。
实验结果表明,End of path事件会在本帧Step事件执行后执行。
注意:path_end()函数不会触发End of path事件。它只会停止正常执行path实例的行为。
至此,GMS事件执行顺序的探讨已经完结。下期我们再研究对象实例的执行顺序。
关于GMS精灵资源的处理机制
一、前言
近期好多人问起GMS1/2的texture page(以下简称纹理页面)与texture group(以下简称纹理组)方面的问题,这里我针对GMS精灵资源处理机制进行次系统性的讨论。
GMS编译的程序除included files和以stream方式读取的音频文件外,全部资源都会在程序执行时加载到内存里。所以很多人认为GMS只能做小游戏,实际情况并不是这样。
内存的占用绝大部分由精灵和音频这两部分组成。
音频方面上,GMS可以通过load/unload音效组或将音效资源以流读取方式(在IDE中将音效资源设置为stream类型)实现按需加载。需要注意的是,流读取方式相对较耗费CPU资源,所以建议只针对游戏背景音乐使用。而相对较短且使用频繁的音效资源(例如开枪音效),最好还是提前加载到内存里(非stream方式并在关卡开始时加载资源)以便提高执行效率。
二、精灵处理机制
关于精灵资源的处理上,YoYoGames结合了便捷、效率与资源等多方面因素进行了折中,选择将所有精灵资源整合成一张或多张纹理页面,然后再归纳到一个或多个纹理组的方式进行处理。其原理是将游戏中所涉及的所有精灵资源(非included files资源),尽可能紧凑有序的排序到一张或多张纹理页面里。目的是为了避免因GPU多次纹理页面swaps(纹理页面交换是要耗费GPU资源寻址的。而一个精灵无论其大小都相当于一张纹理页面,想想你游戏场景里会涉及多少张纹理页面)导致的效率问题。如果将多个精灵整合成一张或少数几张纹理页面的话,swap次数肯定会减少(甚至为1)。关于纹理页面的大小我们是可以设置的,从256x256到8192x8192(看平台支持情况,例如IOS最高只能支持到4096x4096)。这里可能有人会问纹理页面是越大越好啊,这样肯定会减少纹理页面swap的次数。这话说的没错,但纹理所占内存空间(即显存)同样需要注意。关于精灵所占用内存空间,下节会做详细的探讨。
这里要说明下,关于swap对性能的影响主要是在移动平台上,而对win平台direct11影响微乎其微(应该把重点放在像素填充率上)。
三、精灵资源空间
精灵资源内存空间的占用主要分为两部分:资源内存(暂且这么叫)和纹理内存(就是显存)。
1、资源内存方面,GMS是将纹理页面压缩成png格式存储到编译后的游戏执行文件里。就算在游戏过程中不加载纹理页面至显存(就是不绘制任何精灵),纹理页面.png文件也要加载到内存中。除非精灵资源是以included files形式存在(注意,外置精灵文件不会整合到纹理页面里)。
2、而显存方面,YoYoGames为了让GMS绘制无损的2D画面而将texture page解压为bitmap加载到显存中。
我举个简单点的例子。就精灵占用内存而言,比如某游戏只有一张2048x2048的texture page。经过GMS压缩成png后假设占用2MByte内存。当游戏运行开始时并没有绘制任何精灵,但此时游戏中精灵占用的2MByte内存是不会变的。当游戏绘制精灵时,程序会将这整张2048x2048的texture page解压成bitmap格式加载到显存中(即16MByte)。所以此时游戏中精灵所占的内存为2M+16M=18MByte。鉴于多数PC或移动平台的内存与显存都为共享式,所以你完全可以认为精灵占用的内存为这两部分的总和,即18MByte。
另外,目前版本的GMS在编译时可以通过一条简单命令gml_pragma("PNGCrush")来针对纹理页面png文件进行深度压缩,即减少精灵所占用的资源内存(显存还是以bitmap方式存在)。唯一的缺点是会增加工程的编译时间。所以大家在日常测试中可以关闭这个功能,等到在游戏正式发布时再打开也不迟。
关于pngcrush方面的技术细节,大家可以通过网站来了解:https://pmt.sourceforge.io/pngcrush/
四、纹理页面分配
之所以不建议在游戏中用included files直接加载并创建精灵的原因是,included files加载的精灵无论其大小,都会占用一张单独的纹理页面。这样肯定会增加 swaps而产生效率问题。试想你所设计的游戏中某一场景要涉及多少个精灵。主角、敌人、物品、GUI、背景等等,每个精灵的绘制就会产生一次纹理页面交换。而整合到纹理页面(系统自动分配)很大程度会大大减少swaps的数量,但也需要你做好规划与设计。千万不要认为两张纹理页面就只会出现2次swap,两张纹理页面出现的swap数量是无穷大的。举个例子,假设主角、敌人、粒子精灵属于纹理页面A;武器、道具、GUI页面、字库等属于纹理页面B。游戏中绘制顺序可能是:主角(A)->主角武器(B)->敌人(A)->敌人武器(B)->战斗的粒子效果(A)->物品(B)->天气等粒子效果(A)-> GUI页面绘制(B)->字库调用显示(B)。大家看看一帧里出现了多少次纹理页面交换,答案是8次swap。
当然这个例子比较极端,我只是想凸显下多个页面swap问题的严重性。其实我们完全可以通过合理的设计和分配来减少swap次数。总结起来需要做到两点:纹理页面大小与纹理组规划与设计。
注意:默认情况下我都认为一个纹理组里只有一张纹理页面。因为两张以上的话就会出现我刚才所提的情况,不可控也不合理。
1、关于纹理页面大小的设计相对简单,只要尽可能让关卡内所有精灵资源能放到一张或两张纹理页面中即可。但也要在满足你关卡需求的情况下,越小越好(降低显存的占用空间)。一般情况下2048x2048与4096x4096相对较主流。
2、而纹理组的设计方面就相对较复杂了,这里我先谈谈我的原则。我一般将纹理组分为两类:通用类与关卡类。
1)通用类的精灵资源主要为GUI界面,字库,主角以及相关精灵资源等等。总而言之,凡是所有关卡中重复出现的精灵资源全都到放到这个组里,即尽可能让一张纹理页面(一个组一张纸很重要)囊括所有资源。
2)关卡类顾名思义,是将关卡所特有的全资源放到关卡组里。不同特点的关卡或区域都可以设计成单独的纹理组。
听起来比较容易,但你可能没有仔细思考过。如果关卡1的敌人出现在关卡2或关卡3中会怎么样,如果某一关卡要出现前面几个关卡中敌人又如何。
暂且不谈swap对性能的影响,单显存占用就会让你的游戏轻而易举崩溃。要知道一张2048x2048的纹理页面要占用16MByte的显存空间。如果一个关卡中要加载10张纹理页面的话,显存空间就要占用160MByte(还没算上游戏所占的内存空间呢),这对于手游平台来说已经不算小了(例如IpadMini2只有1G内存空间,程序内存占用超过500MByte就会崩溃)。我想这个问题对于策划者来说绝对是头疼的,难道这个问题真的就没法解决吗?答案是否定的。这个问题从未是GMS引擎的障碍,以前不是,以后也绝对不是。
五、进阶处理方法
如果你只想做个小型游戏的话,以下章节可以直接越过。否则不但解决不了您的问题,可能还会增加疑惑。
通过咱们讨论完上述内容后,大家可能会觉得GMS对精灵资源的处理机制很不灵活。例如,工程中所有纹理页面不能按需加载。游戏过程中只能同步加载并创建精灵(因为同步的原因,会引起cpu中断导致游戏短暂卡顿)。本章节我们的目的就是如何避免这两个问题。
我们的目的很明确,就是如何在不影响效率的情况下尽可能占用少的内存资源。首先来说,纹理页面以bitmap的方式加载到显存中是无法避免的,这是GMS内部机制。当然这样做的目的是可以绘制出无损的2D精美画面。所以我们所能做只有按需加载纹理页面(不要与3D引擎中对纹理的压缩处理,真的没可比性),以及如何在游戏过程中异步加载精灵资源这两方面了。
六、精灵按需加载
本章要解决的问题是纹理页面动态组合并加载。理想情况是在进入某关卡前,将本关卡的精灵资源通过included files的方式动态加载,随后通过某种算法将其所有精灵资源组整合成一张纹理页面中,这样我们就不用担心某关卡里避免出现其他关卡的精灵了。
实现方式很简单,15年时YoYoGames社区里一个叫Braffolk的朋友发布了一个Custom Sprite Framework插件。目前插件已发布在marketplace中,并且为免费。下载地址(需注册账户):https://marketplace.yoyogames.com/assets/4543/custom-sprite-framework
此插件的功能非常强大,在这里就不做详细介绍了。下面只举个简单的例子,便于大家认识与理解:
1、系统初始化。游戏执行中只需做一次。
image_system_init();
2、创建纹理页面组
image_group_create("main");
//创建页面组
image_stream_start("main",2048,2048,0);
//设置页面组大小为2048x2048,且精灵之间无间隔像素。
3、加载精灵资源
image_stream_add("main","player","player.png",subimg,xorg,yorg);
//读取精灵。其中subing为帧数,如果设置为2插件会将player.png图片横向等分切开,分成两帧存储。而xorg和yorg是精灵的中心。
image_stream_finish("main");
//读取结束并将所有精灵整合到纹理页面中。
4、存储精灵索引
img_player = image_group_find_image("main","player");
//提取纹理页面中精灵资源。
5、绘制精灵
draw_image(img_player,image_index,x,y);
//绘制精灵。
6、回收资源
image_group_clear("main");
//清除并不删除组。下次可以直接用组名加载精灵资源。
image_group_destroy("main");
//不用时可以删除组。下次需要重新创建。
七、精灵动态加载
当游戏背景不能通过tile展现时,异步加载背景就显得很重要了。再或者对于tile和个性化背景相结合的工程,我们不想在游戏开始时将整个关卡的背景资源一股脑的加载到内存里。不幸的是GMS的sprite_add函数为同步加载函数(只有在加载http资源时为异步),加载时会引擎短暂的卡顿。所以我们需要想办法来实现精灵的异步加载。
我直奔主题,简单介绍下目前可以解决的方法与流程:
1、首先将sprite精灵以bitmap的格式存储为bin文件,然后通过IDE放置到included files里作为外部精灵资源,以便动态加载。
2、当游戏过程中需要绘制外部精灵资源时,首先通过buffer_load_async异步读取资源到buffer里。
3、然后通过buffer_set_surface将buffer转换为surface。
4、最后再利用sprite_create_from_surface将surface转换为精灵。是否将表面转换为精灵看个人需求,也可直接绘制表面。
下面我再举个简单例子介绍下如何实现:
1)先对精灵进行二进制格式转换。
可以将这部分内容写成一个脚本scr_sprite_save(精灵索引,帧数,文件存储位置)。
var width = sprite_get_width(argument0);
var height = sprite_get_height(argument0);
var xoffset = sprite_get_xoffset(argument0);
var yoffset = sprite_get_yoffset(argument0);
var sur = surface_create(width, height);
surface_set_target(sur)
draw_sprite(argument0, argument1, -xoffset, -yoffset);
surface_reset_target()
buff = buffer_create(width * height * 4, buffer_fixed, 1);
buffer_get_surface(buff, sur, 0, 0, 0);
buffer_save(buff, argument2);
surface_free(sur);
buffer_delete(buff);
return buff;
2)异步加载精灵
surf = surface_create(1024,1024);
buff = buffer_create(4194304,buffer_fast,1);
loadid =buffer_load_async (buff,"sprite.bin",0,4194304);
3)生成精灵(异步Save/Load事件)
if ds_map_find_value(async_load, "id") == loadid
{
if ds_map_find_value(async_load, "status") == false
{
buffer_set_surface(buff,surf,0,0,0);
//经测试除WIN与PS4平台外IOS平台也可用(android未测试)
myspr = sprite_create_from_surface(surf,x,y,w,h,removeback,smooth,xorig,yorig);
}
}