GMS2摄像机系统指南

作者:highway★
2018-03-10
16 10 2

写在前面

原作者:Madde Michael

译者:highway★ 

译注:由于 GMS1的 view 已经被干掉,新接触2代的用户对摄像机系统并不太熟悉。在游戏的前期(prototype 或 demo)开发中,摄像机处于3C(Character, Control, Camera)中一个非常重要的核心位置,一个设计合理的摄像机系统会让您的游戏体验起来更 juicy,我觉得花些时间翻译一下这篇指南很值得,希望能帮助到更多 GMS2用户更了解新的摄像机系统。话了,请您继续看。

  • 版本: IDE: 2.0.2.44
  • RUNTIME: 2.0.1.27
  • 导出平台: ALL
  • 工程下载: Built Project

译注:部分图片由于原文是 beta 版,IDE 有些命名与现版本不同,已经修正为现有版本。

概要

这篇指南提供了有关新的摄像机系统的基本信息,以及 views 的使用方法。由于摄像机系统跟 GMS1时相比是全新的,这篇指南面向那些对 gml 有基本了解并对 views 有点儿懵逼的初学者。

项目工程示意:

效果没那么屌,但是实现了摄像机创建、分配,view 的移动、缩放、和插值。对应操作:“点击聚焦”和“滚轮缩放”。

相关链接:

教程

首先,我要说明一下 - 实际上你不用摄像机也能获取一个视图(view) - 场景(Room)编辑器的视图(view)跟 GMS1的工作方式基本一样。但是……它现在可没原来那么灵活了 - 你不能像以前那样在代码里修改 view_[x,y,h,w]view 了(译注:如果你没接触过 GMS1,那么请不要在意这段) - 除非你先设置一个摄像机才能进行高级的操作。

注意:

  • Gamemaker Studio 2缩写为 GMS2
  • 请用 GML,英雄!
  • 困了惑了,请随时按 F1查手册,英雄!
  • 发现了错误,或者有什么问题请 email 我,英雄!(去这里找原作者)

(译注:视图对应view,摄像机对应camera,视图端口对应viewport

这篇指南会讲到这些内容:

  • 设置一个视图(用来使用摄像机)
  • 创建/删除摄像机并分配给视图
  • 通过代码设置摄像机跟随对象(object)
  • 除了绑定视图之外,用其他方法绘制摄像机
  • 摄像机脚本 - 更新、开始和结束
  • 比较简单的摄像机位置插值
  • 比较简单的摄像机缩放
  • 根据场景边界限定摄像机位置
  • 其余的摄像机函数和功能

设置一个视图(用来使用摄像机)

OK,你可能想跳过这里……我知道,你要是知道这玩意的话就跳过吧。实际上就是在一个 Room 内,通过编辑器或者代码来启用视图。

在你的游戏里有两种方法可以设置视图,一个是用 Room 编辑器里的视图工具,第二就是用代码。嗯……我又重复了一遍。

我个人推荐后者,这意味着你无须在每个单独的 Room 里设置视图(译注:如果你的关卡很多,肯定很烦人,而且说不定哪一个 room 就忘了)。你只需要在 Room start 的时候运行代码,完事儿~ 但是,代码并不会改变游戏的窗口(基于视图端口),但是 Room 编辑器里就可以改变游戏窗口,假如它是游戏中的第一个房间 - 有时候称为“启动房间”。如果之后出于一些原因需要改变窗口的尺寸的话,windows_*函数可以帮你搞定。

两种方法,我们都可以设置: 视图0对应的视图端口尺寸为640*480,跟 GMS 一样。

这里是在 Room 编辑器中设置视图的方法:首先,在 Resource 树窗口打开你的房间,GMS2在默认情况下就为你创建好了一个名为 room0的房间。可以双击,也可以拖到工作区来打开。

然后在左侧下方,找到"Viewports and Cameras"。来进行设置:

接下来,在“Enable Viewports”前打钩

打开 Viewport 0的设置区,向下面这样填:

OK,我们搞定了基本的视图设置。

注意我们并没有改变视图边界(border)、视图速度(speed)还有跟随对象(Object Following)。这里解释一下他们的功能:

  • 跟随对象(Object Following):如字面意思,让摄像机跟随选中的对象。
  • 视图边界(View Border):摄像机跟随对象之前,需要靠近视图边界的距离(以像素为单位)。
  • 视图速度(Camera Speed):当越过边界时,摄像机跟随对象移动的速度。速度设为-1会让视图即时移动来显示跟随对象。

下面是以代码来设置 View 的方法:这段代码最好写在“Room Start”事件中。你也可以把它放到创建房间的代码中(room creation code,在 room 编辑器里可以找到),但你可以把代码写在一个永久存在(persistent)的实例中,来节省一些时间。(尽管使用新的房间父子系统,在 Room 编辑器中创建默认视图要比以前更容易)。

这里我没有写整段代码,只是展示注释过的部分

代码:

//启用视图
view_enabled = true;

//设置 view 0为可见
view_set_visible(0, true);

//设置 view 0的视图端口尺寸为640x480,边界为0
view_set_wport(0, 640);
view_set_hport(0, 480);

在这里设置这些边界不会自适应游戏窗口尺寸 - 即使是在启动房间。启动房间(或者有一个启用的视图)的尺寸要和你希望你的游戏的显示窗口尺寸一样,这样才会自动适应。

你也可以手动调整窗口尺寸来适应,用 window_set 函数。但是,在同一个事件中调用 window_set_size()和 window_center()是无效的,因为窗口调整要在当前的事件结束之后才能工作。

如果你想要同时调整游戏窗口并居中,看一下下面的示例:

代码:

//调整尺寸并居中
window_set_rectangle((display_get_width() - view_wport[0]) * 0.5, (display_get_height() - view_hport[0]) * 0.5, view_wport[0], view_hport[0]);
surface_resize(application_surface,view_wport[0],view_hport[0]);

这样就可以调整 view 0对应的视图端口的窗口尺寸,并将"application_surface"的尺寸设为相同。

我们还没有通过代码来设置视图的尺寸、位置、目标对象、边界或者速度 - 因为我们要通过摄像机来进行设置,在后面我会解释如何操作。

从这里开始,开始全是代码了,英雄!

创建和删除摄像机并分配给视图

如果你习惯了 GMS1的视图,那么 GMS2的摄像机会有些不同。你可以将其理解为……嗯,有点像实例(instance) - 你要创建一个摄像机,然后赋予它一些信息,当你用过之后可以删了它。

这一部分将帮助你通过代码来设置视图的参数,从这开始这些属性就都由摄像机来管理了。

当创建摄像机时候,有两个函数我们要了解:

"camera_create()" 和 "camera_create_view()"两个函数都返回唯一的摄像机 ID,通过 ID 我们可以用来使用其他所有的摄像机相关函数。

"camera_create()"用来创建一个实际上是空白画布的摄像机 - 你需要手动指定位置和尺寸,然后才能在2D 中正确使用 - 我这样说是因为你可以在3D 中使用它而无需指定这些参数。"camera_create_view()"更适合2D 开发来使用,因为它强制你在创建时就要指定所有的视图参数。

我们将使用“camera_create_view()”,因为这篇指南的目的是帮助那些意图开发2D 游戏的英雄们~如果你要关于3D 方面的“camera_create”,那可以看我另一篇文章 "Getting Started with 3D in GMS2"。

下面是创建摄像机的方法。再磨叽一遍,640x480的尺寸,位置是0,然后跟随对象参数设为默认:

//创建摄像机
//在(0,0)的位置创建一个摄像机,640x480,角度为0,没有跟随实例,摄像机水平和垂直速度为即时跳转,边界为32像素
camera = camera_create_view(0, 0, 640, 480, 0, -1, -1, -1, 32, 32);

上面代码中我们指定一个角度,这里取代了 GMS1的 view_angle[0..7]。其余关于视图角度的函数为 "camera_get_view_angle" 和 "camera_set_view_angle"。

这就是摄像机创建了,用完摄像机要销毁的话:

camera_destroy(camera);

下面我们要将摄像机绑定到视图,比较简单:

//设置 view0来使用摄像机 "camera"
view_set_camera(0, camera);

从这开始,当我们要更新视图时,我们就要使用"view_camera[view_index]"引用一个绑定的摄像机实例(本例中为 view_camera[0])。这是因为我们要保证我们正在更新的摄像机是分配给视图的那个。

用 view_camera[x] = camera 也可以正常工作,但我感觉用 view_set_camera()更安全更可靠。

我们实际上并不需要将摄像机绑定到视图来使用它 - 我会简单的介绍它的工作原理 - 但绑定会更方便,因为它可以访问自动摄像机“更新/开始/结束”脚本,我将对此进行介绍。

你可以通过这两个函数"view_camera[0..7]"和"view_get_camera(view)",来获取视图正在使用的摄像机的 ID。

通过代码设置摄像机跟随对象(object)

如果你用 Room 编辑器来设置视图,就不用管这个了。但是,如果用代码的话,你有2个选择:第一,在 camera_create_view()里设置标准参数,比如:

//创建一个摄像机,位置(0,0), 尺寸640x480, 角度0, 跟随的目标实例"objPlayer", 即时跳转的水平垂直速度,边界为32像素
camera = camera_create_view(0, 0, 640, 480, 0, objPlayer, -1, -1, 32, 32);

另一个选择是手动的创建每一个参数 - 这允许动态的改变属性,比如增加边界尺寸、聚焦在敌人身上等等。

就像这样设置:

//基础设置
camera_set_view_pos(view_camera[0], 0, 0);
camera_set_view_size(view_camera[0], 640, 480);

//设置目标对象的信息
camera_set_view_target(objPlayer);
camera_set_view_speed(view_camera[0], -1, -1);
camera_set_view_border(view_camera[0], 32, 32);

搞定!

除了绑定视图之外,用其他方法绘制摄像机

如果,你不想将摄像机指定给某个视图(或者想使用摄像机,但不创建视图的话),我们就要用到 camera_apply 函数了。

//应用摄像机并以此来绘制
camera_apply(camera);

如果你只想要将摄像机应用于某些视图(无需绑定),只需要将其包装在 view_current 测试中,就像这样写:

if(view_current == 0)
{
camera_apply(camera);
...
}
...

这样弄之后,照常允许绘制代码。

之后要将摄像机重置为默认值,只要这样调用:

camera_apply(camera_get_default());

这会将摄像机视图重置为 GMS2默认的摄像机 - 当没有视图启用的情况下所使用的默认摄像机。注意你要用 camera_set_default 来操作,虽然我还没想到过合理的使用案例,也没测过。

当使用 camera_apply 时有个比较大的问题是,并不会自动调用摄像机“更新/开始/结束”脚本 - 这个脚本非常好用,后面我会详细解释。当然,如果你自己写脚本来配合 apply 方法来使用的话,你要分开调用它们。

摄像机脚本 - 更新、开始和结束

好了,终于到了新摄像机函数里有趣的地方之一了,脚本!请看手册,英雄!这里有三种摄像机脚本,更新/开始/结束。信息如下:

所有可见和活跃视图端口的摄像机都会调用其更新脚本。

然后对于每个独立的视图:

  • 视图对应的摄像机会调用开始脚本
  • 绘制事件为该视图执行(包括绘制开始 draw begin 和绘制结束 draw end 事件)
  • 调用摄像机的结束脚本

那么,这有什么好处呢?嗯,首先,脚本只会被分配给视图的可见摄像机调用 - 这意味着你可以把代码放到这些脚本中,如果视图没开始绘制,它不会浪费时间来运行 - 当跳帧时这就很好。你也不必在绘制开始时使用高深度对象运行设置代码。

这些脚本好像只能自动为绑定到视图的摄像机运行。如果你的脚本没有运行,检查一下是不是没有正确绑定!

OK,这些函数都可以正常工作,并以类似的方式使用 - 你可以告诉它们什么时候开始工作。在这里,我要将脚本赋予视图0的摄像机的开始函数。

这个名为 Update_script 的脚本,包含下面的内容 - 简单但是比较挫的震屏效果:

//对分配给当前视图的摄像机进行震动
camera_set_view_pos(view_camera[view_current], random_range(-6,6), random_range(-6,6));

我们用 view_camera[view_current]来确保我们将此脚本绑定到任意视图摄像机。

这是震屏功能,现在我们将其绑定到一个视图,用下面的代码绑定到 camera_begin:

//将很挫的震屏脚本 update_script 绑定到所需的摄像机"begin"
camera_set_begin_script(view_camera[target_view],update_script);

为了绑定到其他摄像机事件,我们可以使用以下两种之一:

camera_set_end_script(view_camera[target_view],update_script);
camera_set_update_script(view_camera[target_view],update_script);

注意:在写代码的时候,无法从脚本中获知哪台摄像机正在运行当前的更新脚本(只使用 view_camera[view_current] 或 camera_get_active()来标识"开始"和"结束"脚本),除非你严格的设置所有的变量连接。我已经提交了一个 bug 报告,但我不知道它是否会被 yoyo 看做是个 bug,因为绘制实际上还没有开始。(译注:由于作者写指南的时候是 BETA 版,现在这个问题是否存在我还没有测试过)

要清除摄像机中的脚本,只需按照正常运行函数,而不是使用脚本参数中的“-1”。每个脚本设置函数都有一个“getter”equivelant(我不确定这玩意汉语到底该叫什么=_= sry~),它返回当前绑定到摄像机的脚本内容。

再注意:你不能在摄像机脚本中执行任何绘制。这些玩意必须在绘制事件中完成(或者在 surface 上)。

OK,脚本内容先聊到这儿。

比较简单的摄像机位置插值

对于这一点,我们只是将摄像机从当前位置插入到用户点击的任意位置。这个代码适合...比如,追踪一个对象,这很简单,从简单的东西开始做,是个好选择~

假设 - 你已经创建了一个2D 摄像机,分配给了视图0。如果你忘了怎么操作,翻到上面重新看看。

首先:我们在 create 事件中初始化2个变量:

click_x = 0;
click_y = 0;

它们用来存储鼠标最后一次点击的位置。因此,我们首次启动项目时,它会将摄像机对准原点,因为我们已经将其赋值为0了。

下面这些代码都写在 step 事件(在同一个 object 里,命名叫什么随你,英雄)

//检测鼠标左键是否点击,是的话,更新点击位置
if(mouse_check_button_pressed(mb_left))
{
click_x = mouse_x;
click_y = mouse_y;
}

//获取目标视图位置和尺寸,尺寸减半所以视图将聚焦在中心
var vpos_x = camera_get_view_x(view_camera[0]);
var vpos_y = camera_get_view_y(view_camera[0]);
var vpos_w = camera_get_view_width(view_camera[0]) * 0.5;
var vpos_h = camera_get_view_height(view_camera[0]) * 0.5;

//插值率
var rate = 0.2;

//将视图位置插入新的对应位置
var new_x = lerp(vpos_x, click_x - vpos_w, rate);
var new_y = lerp(vpos_y, click_y - vpos_h, rate);

//更新视图位置
camera_set_view_pos(view_camera[0], new_x, new_y);

总结一下,这段代码检测是否按下鼠标左键,是的话,更新摄像机到“点击位置”。 然后不管任何情况,视图都会插入到鼠标按下的最后位置的中心。

这是如何完成的?

  • 我们获取当前位置(即左上角)和视图的尺寸
  • 将视图尺寸减半
  • 我们建的变量 rate,用来指定插值的速率(这样后面改起效果来会方便一些)
  • 我们用 lerp 函数将当前视图的位置定位到新的点击位置,视图的宽高减半会从点击位置移除,让视图聚焦在 中心
  • 将新的位置坐标赋予视图,过程结束

一个简单的视图插值 demo 完成了~

摄像机缩放

这跟之前的工作原理类似 - 你要修改视图的宽和高,但保持视图端口的尺寸。唯一的区别是你需要一个摄像机才能使用一些函数。

假设 - 你已经创建了一个2D 摄像机,分配给了视图0。如果不幸你又忘了怎么操作,翻到上面重新看看。

我们要在 create 事件中创建一些缩放相关的变量(在摄像机创建之后):

zoom_level = 1;
//获取初始视图尺寸,后面要用于插值
default_zoom_width = camera_get_view_width(view_camera[0]);
default_zoom_height = camera_get_view_height(view_camera[0]);

下面这些代码都写在 step 事件(在同一个 object 里,命名叫什么随你,英雄)

//根据鼠标滚轮来缩放,用 clamp 函数可以让效果看起来没那么蠢
zoom_level = clamp(zoom_level + (((mouse_wheel_down() - mouse_wheel_up())) * 0.1), 0.1, 5);

//获取当前视图尺寸
var view_w = camera_get_view_width(view_camera[0]);
var view_h = camera_get_view_height(view_camera[0]);

//设置插值率
var rate = 0.2;

//通过插值计算当前和目标缩放尺寸获取新视图尺寸
var new_w = lerp(view_w, zoom_level * default_zoom_width, rate);
var new_h = lerp(view_h, zoom_level * default_zoom_height, rate);

//应用新尺寸
camera_set_view_size(view_camera[0], new_w, new_h);

这些是干嘛的? 我们一步步拆开来看:

  • 根据鼠标滚轮当前的滚动方式调整缩放级别 - 向上放大向下缩小。缩放级别也用 clamp 函数处理,防止过度缩放
  • 获取当前的视图尺寸
  • 设定插值率(这样后面改起效果来会方便一些)
  • 通过当前视图尺寸插入到原始尺寸乘以缩放级别来确定新尺寸的大小
  • 更新新的视图尺寸

这就是缩放的处理。请记住,视图尺寸从左上角开始变化,而不是中心。如果你想让视图保持居中,需要加上这些:

//获取重新对齐视图所需的位移值
var shift_x = camera_get_view_x(view_camera[0]) - (new_w - view_w) * 0.5;
var shift_y = camera_get_view_y(view_camera[0]) - (new_h - view_h) * 0.5;

//更新视图位置
camera_set_view_pos(view_camera[0],shift_x, shift_y);
这将使用视图的宽高的差异来确定如何移动视图以此来重新居中,然后再应用新的位置。

这一章节结束~

根据场景边界限定摄像机位置

我在 twitter 上被问到过这个问题,所以我觉得下面这段代码有必要讲一下:

camera_set_view_pos(Camera_ID,
clamp( camera_get_view_x(Camera_ID), 0, room_width - camera_get_view_width(Camera_ID) ),
clamp( camera_get_view_y(Camera_ID), 0, room_height - camera_get_view_height(Camera_ID) ));

这行代码通过存储在 Camera_ID 变量中的 ID 来获取摄像机的位置,然后将其位置控制在(0,0)和房间界限减去摄像机尺寸之间。

如果你已经计算好或者已经知道摄像机尺寸和位置,你可以不用 camera_get_view_*这个函数,直接用值就可以(译注:如果能用变量的话,后期查阅或者修改代码的时候会比单纯用值来写的可读性更好)。

其余的摄像机函数和其功能

上面说了那么多之后,我想你应该对新的摄像机系统有了一定的了解,用它们来创造奇迹吧~英雄!当然,还有点东西最好说一下,很方便的几个函数:

  • 我们可以用 view_set_[x,y,w,h]port()函数来改变视图端口相关的东西
  • 可以通过 view_set_surface_id()告诉视图绘制到 surface
  • camera_get_active()返回当前活跃摄像机的唯一 ID(正在绘制的摄像机)
  • 有两个函数使用了矩阵 - camera_set_view_mat()和 camera_set_proj_mat()。这些主要用于设置3D 投影(译注:如果你有兴趣扩展学习,可以去 Shaun Spaulding 的 youtube 频道看看,他比较喜欢用这个,有几个视频专门讲了 GMS2的摄像机)
  • camera_set_proj_mat()采用投影矩阵,通常用三个矩阵函数之一 - matrix_build_projection_ortho,matrix_build_projection_perspective 或 matrix_build_projection_perspective_fov
  • camera_set_view_mat()采用视图矩阵,通常用 matrix_build_lookat 构建
  • 我在另一篇指南“GMS2的3D 开发起步”讲了这些矩阵函数的基本用法
  • 本指南中讨论的所有 setter 函数都对应的有 getter 函数

值得一提的是,你可以用"camera_get_view_[x/y/width/height]()"来绘制与摄像机视图相关的东西,尽管使用 Draw GUI 事件来绘制 HUD 会好的多。

(译注:HUD,即 head-up display,西方游戏行业对游戏界面的定义,从飞机的抬头显示系统拿来的名词,国内游戏业更喜欢叫 UI,如果你希望查阅国外游戏的资料,建议还是更改习惯,称之为 HUD。)

就写到这吧,你现在应该可以去玩这个新的摄像机系统了,如果还有什么你想了解的,或者有什么问题,需要什么帮助的话,联系我,我看看能不能帮到你~

扩展阅读 : [GDC2015] Scroll Back - 2D 卷轴游戏的摄像机理论与实例

写在最后

这篇文章是 Itay Keren(Mushroom11的开发者,IGN9分游戏,貌似 IOS 上过推荐)在2015年的游戏开发者大会上的演讲的图文版,超长超屌,理论研究+实例分析~ 如果你在开发2D 卷轴游戏,那你一定要精读精读精读这篇文章,里面涉及到很多行业前辈带来的超级精彩精心设计的摄像机系统。在看过文章之后,再去回顾以前玩过的游戏,或者新玩到的游戏,就会更加体会到摄像机系统对游戏体验的强力加分或者……折扣……。废话不多说了,请去 GMS2实验吧~ 期待您的新成果~~~~~ 一起加油!

本文为用户投稿,不代表 indienova 观点。

近期点赞的会员

 分享这篇文章

您可能还会对这些文章感兴趣

参与此文章的讨论

  1. Oncle 2018-03-10

    真干货系列

  2. meme 2018-03-13

    支持干货

您需要登录或者注册后才能发表评论

登录/注册