编者按
indienova 会员青铜的幻想为希望了解学习 GameMaker: Studio 的中文读者专门撰写了本系列教程,本文为第五期,本期将完成上一篇教程提到的游戏视野(摄像机)的设置,并且通过重构来完善游戏。
欢迎读者朋友在文章后留言,以便作者能够针对性地安排接下来的教程内容。本栏目已经有了专门的专题页面,请参看这里。
教程目标
和之前一样,这个系列教程的项目/代码及原始美术素材全部都在 GitHub 项目库。这个教程的内容基于目录 GMS_TUT_03
下的内容开始,完成后的内容放在目录 GMS_TUT_04
。
房间的视野设置
在 GMS 中,视野(views)的概念等同于 3D 游戏中的摄像机,它决定了当前窗口的分辨率以及所能看到的位置和范围。房间的视野设置面板如下图所示:
它的主要选项包括:
启用视野(Enable the use of Views)
是否使用视野。这个选项默认是关闭的,在不使用视野的情况下,GMS会以尽可能大的分辨率显示整个房间的内容。可以举几个例子来说明。情形A,场景大小为 1440x900(目前游戏的场景大小),显示器分辨率为 1920x1200,那么这种情况下游戏运行的窗口大小将会是 1440x900,从而无缩放的显示整个场景。情形B,我们如果将场景大小增加到 3840x2400(长宽分别为显示器分辨率的2倍),那么此时运行游戏时,GMS 将会全屏显示整个场景,但因为场景大小超过了显示器的分辨率,所以这时场景是以二分之一的尺度显示的。因此,通常当我们设定的场景大小超过了一个屏幕所能显示的内容时,我们都不希望使用默认的这种设置。
用窗口颜色清除背景(Clear Background with Window Colour)
这个选项是默认开启的,它的作用是在游戏每一帧绘制以前,都用一个颜色打底,然后在这个基础上再画背景、场景等等。这个选项几乎不用去管,除非你想看看下面这个神奇的效果。
它的原理是,如果我们关掉这个选项,同时再把房间的背景色和背景图片去掉,那意味着游戏的每一帧里场景中的物品和人物都是直接绘制在上一帧的物品和人物上的。这样的结果就是人物移动时的重影和具有 alpha 通道的灯光的叠加。
接下来是一个视野的列表,GMS 提供了最多 8 个视野,它可以让你设置不同的视野参数,然后在游戏运行时根据需要切换。例如 RPG 中常见这样的情形,在主角进入一个场景时,起初视野是跟随主角的,但在场景的某个地方触发了一个特殊事件以后(例如触发机关让某个洞口打开了),摄像机会迅速切换至事件发生的地点,待事件结束以后再回到主角身上。这时就可以通过设置多个视野参数,并即时切换来实现。
进入房间时可见(Visible when room starts)
这个选项相当于对每个视野的开关。值得一提的每个视野的开关是独立的,并不是同一时间只能打开一个视野。于是这个功能可以很方便的实现各种画中画的效果:
在这个动图中,我同时开启了两个视野。一个是全局的视野,另一个视野的范围放置在了血池旁边,同时将这个视野的内容渲染到游戏窗口的左上角。这样就仿佛添加了一个监控血池的隐秘摄像机,稍微脑洞一下感觉可以利用这个功能添加一些新的游戏特性呢。
房间中的视野(View in room)及屏幕上的窗口(Port on screen)
前者所定义的是一个在房间中的矩形范围,后者定义的是在游戏显示屏幕上的一个矩形范围,其中的X和Y用于指定矩形的左上角坐标,W 和 H 分别为宽度和高度。当这个视野启用后,GMS 就会把前者矩形范围中的内容渲染到后者所定义的矩形范围当中。
物体跟随(Object following)
这里用于指定视野所跟随的物体,以及跟随的边界和跟随速度。所谓跟随的边界如下图所示:
如果我们将人物伊瑟拉作为视野的跟随对象,那么当她走到图中黄色所示的方框边界时,视野开始跟随她移动。而定义方框位置的两个参数 Hbor
和 Vbor
是指方框两边距离窗口边缘的水平距离和垂直距离。另外两个参数 Hsp
和 Vsp
则是视野移动的速度,如果设置为 -1 则表示视野的移动完全紧贴人物移动。下面两个动图即为 Hsp 设置为 1 和 -1 时的对比:
在了解了这些参数的含义之后,就可以很容易的根据自身的需要来设置它们了。下面是我对于这个范例项目的参数设置:
应该都很好理解,或者你也可以自己尝试改变这些参数的取值,然后测试效果。
项目的重构
好吧,我先说明这一章的内容可能不那么有趣,因为它并没有给项目增添任何新的内容或功能。但如果你不仅仅是拿 GMS 来玩,而是真正想要做出完整的游戏,这个步骤应该是必不可少的。重构也是敏捷开发中关键的一步,它属于上次教程中所提到的开发-完善 - 反馈循环中的完善过程,它的存在是为了让以后的开发更加顺利,防止随着开发的进展,整个项目逐渐变成一团乱麻。
讲重构的书籍或文章有很多,我作为一个处女座(猛烈地黑起来吧),一直在尝试想要从中获取真经来与混乱不堪的项目做斗争,直到有一天……我放弃治疗了 _(:3」∠)_
想要写出完美的代码是不现实的,特别是需求三天两头变化的游戏行业,但为了你和项目成员的美好生活,你需要至少坚持这一件事——消除重复。
让我们先来看看上一次的教程内容里,都存在哪些重复的操作和代码:
- 伊瑟拉与木桶、雕像、水池和血池的碰撞设置
- 对雕像、水池及血池在创建时执行代码
depth = -y;
(初始化深度) - 对伊瑟拉和木桶,在每一帧更新时执行代码
depth = -y;
- 值得注意的是,所谓的重复操作,是指完全一模一样的操作。有些步骤,例如对各种物体设置碰撞形状,虽然是对每个物体都需要进行的操作,但因为每个物体的碰撞形状都可能是不一样的,因此这样的操作没办法通过程序来简化。(其实,而且 GMS 并不支持物理属性的继承,所以没办法消除物理属性这部分的重复内容)
对于重复的操作和代码,我们需要利用“面向对象”的设计原则来改进。面向对象的设计是通过“继承”来消除重复,即为具有相同行为的物体类抽象出一个父类(GMS使用 Parent 一词,因此后文将不加区分地使用 Parent 和父类两个名词),为这个父类定义一系列行为,然后所有该父类的子类共享这些行为。如果这样说太抽象,那么可以结合示例项目来说明:
- 伊瑟拉与木桶、雕像、水池和血池均发生碰撞,那么木桶、雕像、水池和血池这四种物体,我们可以归纳起来给个名字叫做“场景中的基本物体”(
obj_scene_base
)。这样的话,我们只要设置伊瑟拉会与“场景中的基本物体”碰撞就够了,然后只要有需要与伊瑟拉发生碰撞的物体,把它的Parent设置成obj_scene_base
就好了。 - 雕像、水池及血池需要在被创建时初始化深度,那么结合这几个物品的特点,它们应该属于一类叫做“场景中的静态物体”(obj_scene_static)。
- 伊瑟拉和木桶需要每一帧都更新深度信息,你已经知道我要起什么名字了吧——“场景中的动态物体”(obj_scene_dynamic)。
因此根据上面三点分析,我们建立出了一个这样的类别体系:
值得注意的是,这只是为了教程演示的目的而建立的一个简单继承关系,而这个关系是会随着项目的进展不断演化的。例如在游戏加入了“血池”的加血功能以后,它就会从一个静态物体变成一个可交互物体,那么“可交互物体“会作为”静态物体“的一个子类插入到这个树形结构当中去。
虽然 GMS 所采用的脚本语言 Game Maker Language(简称GML)并非一门完全面向对象的编程语言,但 GMS 提供设置 Parent 机制很好地实现了大部分面向对象的特性。
- 封装将数据与数据的行为封装成抽象的类;
- 继承允许基于已有类来扩展新类;
- 多态确保接口的重用性。
更多信息请参阅专门介绍面向对象编程的书籍。
下面,我们就利用这个 Parent 的机制来在 GMS 中具体实现上面所说的重构内容。
GMS 的 Parent 机制
首先,我们在 GMS 中建立三个新的 Object,分别命名为 obj_scene_base
、obj_scene_static
和 obj_scene_dynamic
。这三个 Object 只是作为那些具体的物体的一个模板,而自身并不会出现在场景中,因此并不需要设置它们的 Sprite。然后将 obj_scene_static
和 obj_scene_dynamic
的 Parent 设置为 obj_scene_base
:
接下来,我们分别设置下列Object的Parent:
obj_ysera
(伊瑟拉): obj_scene_dynamic
obj_barrel
(木桶): obj_scene_dynamic
obj_statue
(雕像): obj_scene_static
obj_blood_pool
(血池): obj_scene_static
obj_pool
(水池):obj_scene_static
Parent 体现了“XXX是一个XXX”的含义。例如“猫”的 Parent 是“哺乳动物”,我们可以说猫是一种哺乳动物。这里我们介绍两个下面要用到的 Parent 的特性:
Parent 的 Parent——“哺乳动物”的 Parent 是“动物”,那么也同时意味着猫是一种动物。
Parent 的事件脚本将自动对子类执行——哺乳动物所具有的特征和行为,猫也都有。
消除重复
然后我们就可以开始着手消除之前列出的重复的地方了:
伊瑟拉设置碰撞处的重复
之前的 obj_ysera
的 Event 里是这样的:
下面罗列了 4 种物品,可以想象,如果以后添加更多种类的物品,将会很难管理。现在我们把这4种物品删除掉,换成与父类 obj_scene_base
的碰撞事件:
这样的话,以后只要是属于这种类别的物品(根据上节提到的 Parent 的 Parent 的特点,只要该 Object 的任意层级的 Parent 是 obj_scene_base
,那么都可以说它是属于这种类别的物品),都将会与人物伊瑟拉发生碰撞。
静态物体雕像、水池及血池的深度初始化
我们在教程四中讲过,深度初始化是通过在 Create 事件时,执行以下脚本语句实现的:
depth = -y;
现在我们为 obj_scene_static
加上这个事件和脚本:
然后我们依次查看雕像、水池及血池的事件脚本,会发现 obj_statue
和 obj_blood_pool
的 Create 事件里唯一的内容就是这句脚本。根据上节提到的子类会自动执行 Parent 的事件脚本,因此已经没有必要再留下来了,所以我们把 obj_statue
和 obj_blood_pool
的 Create 事件直接删除掉。
而对于水池来说,它的 Create 事件中有两句脚本:
image_speed = 0.1; depth = -y;
其中上面一句是因为它是一个具有动画的物体,我们需要设置它的动画速度。此时不能直接删除 Create 事件,而是把它的脚本:
depth = -y;
替换成:
event_inherited();
这个语句的意思是调用它从父类那里继承的事件脚本,即 obj_scene_static
的 Create 脚本。
动态物体伊瑟拉和木桶的深度动态更新
这里与上面所做的事情完全一致,即首先为 obj_scene_dynamic
添加事件 Step 并在此执行脚本:
depth = -y;
删除掉 obj_barrel
的Step事件,并将 obj_ysera
的 Step 事件中的这一句替换为:
event_inherited();
这里也是因为 obj_ysera
的 Step 事件里还调用了脚本 YseraStep,因此不能直接删除 Step 事件。
那么这就是我们对完善项目所做出的努力,这时运行一下游戏进行测试,你会发现一切都没发生变化。这样就对了,重构的目标就是重构前和重构以后游戏的行为完全一致。
重构以后的便利
我们可以通过下面这个改动来体会重构带来的便利,将墙壁 obj_wall
改成一个玩家可以发生碰撞的静态物体。我们先来看看现在墙壁是什么样的:
现在墙壁有两个问题:
- 深度信息不对,导致与人物之间的没有正确的遮挡关系
- 没有与人物的碰撞检测
我们就按顺序来修复这两个问题,解决第一个问题需要做的是正确的设置墙壁 Sprite(spr_wall
)的原点,在房间中修正因为原点改动后墙壁位置的变动,然后将 obj_wall
的 Parent 设置为 obj_scene_static
:
注意在设定墙壁的原点的时候依然遵循上一次教程中提到的设置在物体底部中心点的原则。那么在这样的改动后,再次运行游戏:
这次可以人物可以“正确”穿过墙壁了:)好吧,虽然穿过墙壁是不对的,但当人物走到墙壁后方,墙壁会遮挡住人物,这点的确是我们想要达到的效果。试想如果这面墙壁并非封闭,而是仅有一半,那么人物确实是可能走到墙后的。
接下来再解决墙壁的碰撞检测,这需要我们打开 obj_wall
的物理选项,再为其添加碰撞的形状:
最后切记将密度(density)设置为0(否则墙壁会被人物推走…… -_-!)。
最后再运行游戏测试一下:
OK,一切顺利了!欧耶~
下周我们将会在教程中讲解由网友投票选出的最感兴趣的内容,再会~
附录:教程资源链接
该系列教程的项目/代码及原始美术素材全部更新至 GitHub 项目库。
赞一个!关于上次调查的内容我都想学习,希望能逐步更新,至于优先的我选择:GMS的UI介绍,希望比GM有所提高。
good! 一直关注,初接触GMS使用,谢谢作者及时更新
最近由 flywuya 修改于:2016-08-18 10:54:31前排好点赞! 等有时间了一定从头开始拜读实践!
功德无量!
这两天工作之余,抽空已经把5集教程全实现了。
一直跟着教程走,大赞~
好东西啊
学完打卡
这篇完成之后,墙壁突出的地方理论上应该是挡住玩家的 但是还是会无碰撞
不知道青铜大大能不能讲讲怎么在第三种碰撞图形上添加点,我最多就只有3个
还有就是怎么通过组合完成复杂的碰撞
@wizderdota2:第三种碰撞模式,左键碰撞线加点,右键删除点;组合完成复杂碰撞请参考第7章的hit box绑在敌人身上的步骤.
有一个问题请教下~
最近由 forxidian 修改于:2016-10-06 18:28:30这里一共有4种属性的物品:
A.多帧的物体
B.深度最前的物体
C.可跟人物碰撞的物体
D.随帧更新深度的物体
其中B可以作为C和D的父类,但是pool同时属于A、B、C,导致不能把A抽象出一个类来,还必须在每个obj里进行动画速度的初始化。
GML好像不能多继承父类?
@forxidian:请教一下您提到的“随帧更新深度”如何实现?
最近由 Denny 修改于:2016-10-19 20:34:35比如这个例子里的木桶,当主角把木桶推离原竖向位置后,就不符合应有的遮挡关系了。怎样改正呢?
@Denny:你看一下你的人和木桶的depth=-y是不是放在step event里面的,只有在step里面才会在游戏过程中不断更新
是不是在子类里有同样event下执行的代码就必须要加event_inherited()?
@tzh1990:如果子类的event里面只有一条和父类一样的代码,直接删掉这个event就好了;如果子类event里面的除了和父类一样的代码还有自己的其他代码,就把要继承的代码改为event_inherited();
博主你好,我勾选Enable the use of Views,运行之后黑屏了是怎么回事?帧数还是能正常的统计,角色每帧都还能触发
@Mydust:可以把Viewport里的visible打开
UT里的宅龙偷窥器是这么做的吗!!waaaaaa和toby同款工具感觉自己好幸福