OFFICIAL Guest Blog: Utilizing 3D Cameras in 2D Games

rmanthorp

YoYo Games Staff
Admin
YYG Staff
Matharoo makes games, tutorials and courses using GameMaker. He has joined us on the blog to share the following quick and exciting use of cameras in GMS2.

https://www.yoyogames.com/blog/552/utilizing-3d-cameras-in-2d-games
There’s an amazing little trick, or rather, feature in GameMaker Studio 2, that you maybe didn’t know about! Let’s take a look at it in this blog.

I was working on the town area for one of my side-projects and was struggling with how to build it. I didn’t want to create a generic, 2D tiled area, with just one layer to fill the background; I wanted it to be a little more complex and three-dimensional, while still being a 2D game...

And you know what GameMaker Studio 2 has? 3D cameras, and depth-based layers.






Let’s learn how this magic can be incorporated into any game!

Side-Note:

  • You can download the project here, and have it open on the side, as you read this blog.
  • There is also a video version of this tutorial (but make sure to also read this post as it covers more than the video):



THE THIRD DIMENSION
If you’ve used the Room Editor, you’ll know that each layer has a “depth” value. So, a layer with a lower depth would be drawn above a layer that has a higher depth.




Three visible layers with depths 0, 100 and 200


What’s interesting about this, is that the depth value actually corresponds to the “z” value in 3D space! So the third dimension, z, would be the distance of a layer from the camera.

This means that we only need to set up our 3D camera properly, and the rest will be handled by the layer system!

3D CAMERAS
Using a 3D camera in GML mainly consists of setting up a projection matrix and a view matrix.

  • The view matrix defines where the camera is, and where it’s looking.

  • The projection matrix defines how the world is rendered; for example, whether the view is orthographic or perspective, what the field of view is, etc.
VIEW MATRIX
The view matrix can be set up using the function “matrix_build_lookat”:

var _viewMat = matrix_build_lookat(cam_x, cam_y, cam_z, look_x, look_y, look_z, 0, 1, 0);

The function name itself explains how it works: it lets you “look at” a point in 3D space, from another point.

The arguments “cam_x, cam_y, cam_z” represent the 3D position of the camera. Then, the arguments “look_x, look_y, look_z” represent the 3D point where the camera is looking.

And finally, the last three arguments “0, 1, 0” (in order: x, y, z) represent the “UP” vector of the camera. It’s basically the camera asking you: “which direction is up?”

Although we are setting up a 3D world, we still want it to retain the old 2D view, where x represents left-to-right movement and y represents up-to-down movement. So to set the UP vector on the y axis, we set the UP vector to “0, 1, 0” (again: x, y, z).

PROJECTION MATRIX
Our projection matrix will be set up using the function “matrix_build_projection_perspective_fov”:

var _projMat = matrix_build_projection_perspective_fov(70, 16/9, 3, 30000);

For the projection matrix, these are the arguments, in order: FOV, Aspect Ratio, ZNear, ZFar

So we pass in the field of view of the camera, and the aspect ratio that it needs to maintain. And then we pass in the ZNear and ZFar values. Anything drawn outside of this z range, whether it’s too close to the camera, or too far away, will not be rendered.

IMPLEMENTATION
Before starting the implementation, make sure you have these things ready inside your room:

  1. Layers, with proper depth order. Additionally, you can use the padlock button next to the layer depth, to set a custom depth value.
  2. A camera view following an object, which can easily be set up in the Room Properties.
    a. If your camera view is set up through code, make sure that it is set up before you run the code below.
This is what I’ll be working with:




A simple 2D game -- without anything “3D”


For managing the 3D camera, we’re gonna use a separate “oCamera” object. Alternatively, you can also do this in a game controller/manager object, if you already have one.

Now let’s set up some variables, in the Create event:

// Camera
camera = view_camera[0];

// 3D camera properties
camDist = -300;
camFov = 90;
camAsp = camera_get_view_width(camera) / camera_get_view_height(camera);

First, we get the camera ID, and store it in camera.

Then we set up some 3D camera properties:

  • camDist: z value where the camera is positioned
  • camFov: Field of view
  • camAsp: Aspect ratio
Now the magic will happen in the Draw Begin event.

Why Draw Begin, you ask? Because it runs before the Draw events, and the 3D camera needs to be updated before anything else is drawn.

// Update 3D camera
var _camW = camera_get_view_width(camera);
var _camH = camera_get_view_height(camera);
var _camX = camera_get_view_x(camera) + _camW / 2;
var _camY = camera_get_view_y(camera) + _camH / 2;

var _viewMat = matrix_build_lookat(_camX, _camY, camDist, _camX, _camY, 0, 0, 1, 0);
var _projMat = matrix_build_projection_perspective_fov(camFov, camAsp, 3, 30000);

camera_set_view_mat(camera, _viewMat);
camera_set_proj_mat(camera, _projMat);

camera_apply(camera);

First, we get the width and the height of our camera, in _camW and _camH. Then we set up the position of the 3D camera, in _camX and _camY. You can see that it points to the center of the camera (by adding half the size to the camera position).

After that we set up the view matrix. The 3D camera is at (_camX, _camY, camDist), and is looking at (_camX, _camY, 0). So between these vectors, only the z value is different.

For the projection matrix, we simply pass in the FOV and aspect ratio using the variables, and then the ZNear and ZFar values.

Then we apply both matrices to the camera, and at the end, use camera_apply. This applies all the changes to the camera immediately, instead of waiting for the next step to update it.






Boom! Our layers now have real depth: you can tell by the neat parallax effect.

But we’re not done yet! Now we only need to have…

SOME FUN!
ROTATION
Let’s take a look at our view matrix:

matrix_build_lookat(_camX, _camY, camDist, _camX, _camY, 0, 0, 1, 0)

The first three arguments represent the 3D position of the camera. So if we offset those coordinates, we can effectively tilt our camera!

So, for example, let’s do this:

matrix_build_lookat(_camX + 100, _camY + 50, camDist, _camX, _camY, 0, 0, 1, 0)

With this offset, and a lower FOV, we get this awesome view:






FOCUS
Let’s say you want to focus on a character, during a cutscene, or to aid in gameplay. You can do that by simply decreasing the camera distance!






Here is a bonus project, that includes code for real-time camera rotation, and simple focusing: http://matharoo.net/projects/2-5D_Platformer_Matharoo_Bonus.yyz

CONCLUSION
3D cameras are fun! We can use this power (creatively) to spice up our games and make them stand out from the crowd.

If you need any further help with GML or want to discuss this technique, feel free to hop into our Discord community. You can also tweet at me for any questions: @itsmatharoo. DMs are open too.

Happy GameMaking!
 

Cpaz

Member
I'll have to keep this bookmarked for when I get back to my platforming project.

Great stuff!

Edit:
Shoot, I also got spritestack the other day, now I need to know of I can use that with any of these methods...
 
Last edited:

FrostyCat

Member
If there's anything this article should be teaching YoYo, it's to stop drawing a line in the sand saying "GMS 2 is a 2D engine, we won't facilitate integration with 3D because of that". There are plenty of elements and concepts in 3D graphics that also make sense in 2D. For example, fleshing out built-in support for matrix, vector and quaternion math would be a step in the right direction.
 

gnysek

Member
I would really appreciate some tutorial for matrix-noobs (I mean showing and explaining every array element with examples). I've done many things in GMS during my life, but that's the thing I don't understand in fact.
 

FrostyCat

Member
I would really appreciate some tutorial for matrix-noobs (I mean showing and explaining every array element with examples). I've done many things in GMS during my life, but that's the thing I don't understand in fact.
I would really appreciate it if GM users (particularly rookies and so-called "intermediates") would start realizing the universality of mathematical and logical competencies in programming, and stop discounting third-party resources like this one just because the language is marginally different.
 

drandula

Member
I would like to have ds_grids more Matrix-like behavior capalities. They can already look a bit like Matrix.

Edit. Blog was good, I was going to make parallax with calculations on global game jam, but this saved my time from it remainding 3D possiblity. Own camera code already supported zooming on Z axis, just had to change ortho to perspective
 
Last edited:

FrostyCat

Member
I would like to have ds_grids more Matrix-like behavior capalities. They can already look a bit like Matrix.
Looking like a matrix is only half the deal. Being optimized and convenient at runtime is another.

As an emulation for a matrix, grids score horribly at both optimization and convenience. They are CPU-bound on the most part, not GPU-bound the way actual matrices are, so they suck at high usage. They also need to be manually cleaned after.

While it would be best if YoYo could expand and better formalize their offerings for hardware-accelerated matrix and vector routines, if it had to be GML-emulated I'd sooner pick arrays over any ds_ data structure. They are garbage-collected, have built-in visual syntax since GMS 2, and their status in GML is not fuzzy. And I walk the talk.

Most sensible people with more than a month's of experience can emulate matrix and vector arithmetic with arrays and the like themselves, and like I demonstrated, there are already stock GML solutions. The problem with GML in particular is how flimsily its built-in hardware-accelerated matrix and vector routines integrate into the GMS 2 engine. Its return values are all over the place, have no formal integration into the language (compare Unity or Godot), and there are no pathways into the built-in vector types without resorting to shader uniforms. It is by far the worst integration I've seen among game engines. If YoYo could address that, it could pave the way for better 2D graphics and less flimsy plane geometry (e.g. fewer problems asking about rotating off-origin or gun/arm placements), and indirectly help 3D integration down the road via community and/or company initiative.
 

Joe Ellis

Member
The depth of each sprite is using the Z axis, and rendered with a shader using directx11, the ONLY key difference is that the projection matrix has no perspective to divide with (its 1) So it still sorts things behind other things, which is 3d, it just doesn't make things smaller the more depth they have.

So following that idea, you can use a 3d camera, all it does is enable perspective so things get smaller\bigger.
 

drandula

Member
Looking like a matrix is only half the deal. Being optimized and convenient at runtime is another.
I know. Writing multilayer neural network with correct backpropagation was a pain, and as there are lot of computations, even slight improvements would do wonders. Mostly those calculations would be done with matrices, but I had to improvise.
 
Top