I’ve seen a lot of questions regarding cameras, so I’m aiming to make this into a sort-of definitive guide for using them! Hence, if I’ve got something wrong or there are better ways of doing something, tell me in the comments and I’ll update it!
I will leave a (dd/mm/yy) formatted date here, so you know when this guide was last updated: 12/06/2020 (Added basic splitscreen/multiview set-up code)
I’ll also update the guide if there a better methods that I haven’t thought of/found out about yet, or if it turns out I’m using some function totally inappropriately.
GM Version: IDE: 2.2.4.451, RUNTIME: 2.2.4.357
Target Platforms: ALL
Download: Built Project
Summary:
This guide aims to give you a basic insight into the new camera system, and using it with views. Since cameras are totally new to GMS2, I’m going to write this from the PoV of someone very new to views, but also understands basic GML.
The included project, which uses information from all of these guides, looks like this:
Not spectacular, but demonstrates camera creation + assigning, view moving, zooming and interpolation.
The example controls for the project are simply “click to focus” and “scroll to zoom”
Relevant Documentation Links:
Tutorial:
Firstly, I’m going to point something out – you don’t actually need a camera to have a view – the room editor still has views which work in very much the same way as before. However, they are now far less flexible – you can no longer change view_[x,y,h,w]view through code anymore – unless you set up a camera to do more advanced tweaking.
If you are strictly no-code, then you will have to use the views with the room editor anyway, as there are no Drag&Drop camera functions currently.
Important things to know about this guide
- I will be referring to “GameMaker Studio 2” as “GMS2” throughout the guide, because I am too lazy to keep writing it in full.
- I’ll be going through cameras in GML – There are no D&D camera functions – so it helps if you understand code
- If you want to know more about a function, or something is confusing you, remember to check the manual! You can find out more about most built-in functions, variables or constants by clicking on them in the code! It will open the relevant documentation page in a new tab.
- If you have any other questions or corrections, please let me know!
Here are the things we’ll cover in this guide:
- Setting up a view (to use with cameras)
- Creating and deleting a Camera and Assigning cameras to views
- Setting a camera to follow an object through code (using the standard object tracking system)
- The other way to draw a camera without binding it to a view
- Camera scripts – update, begin and end
- Simple camera position interpolation
- Simple Camera Zooming
- Clamping the camera to the room bounds
- Other camera functions and what they do
Setting up a view (to use with cameras)
Okay, you may want to skip this one, depending on whether or not you already know this stuff. This is literally just going to be enabling a view in a room either through the editor, or through code.
There are two ways you can set up views in your game. One is to use the view tools in the room editor, the second is to set them up with code.
I personally prefer the latter, as it means you do not have to set up views in every single room. You can just run the same code on room start, and it’s done. However, the code version will not resize the game window based on viewport, where as the room editor set up will, assuming it is the first room in the game – sometimes referred to as the “boot room”. If you need to change window size later for whatever reason, there are “window_*” functions for dealing with this.
For both methods, we’ll be setting up view 0 with a view port and size of 640×480, as a throwback to pre-GM:S.
How to set up a view in the room editor
First, open up your room. Conveniently, GMS2 now starts with a room already created, named room0. This is located in the resource tree. You can either double click the room, or drag it into the workspace to open it.
Now, navigate to the “Viewports and Cameras”. By default, this is located at the bottom left of the room editor. Click to open the settings up:
Next, you’ll need to check “Enable Viewports”
Open up the settings for “Viewport 0”, and edit the settings to appear as follows:
Okay, done! Basic view set up.
Note that we’ve not changed view borders, view speed or “Object Following”. Even though the focus here is on cameras, I’m going to quickly explain what they are and what they do:
More Information
The “Object Following” makes the camera follow a chosen object.
The view border is how close the object needs to be to the bounds of the view (in pixels) before the camera follows it.
View speed is how fast the camera will follow the object when it moves past the border. Setting speeds of -1 makes the view move instantly to contain the object.
How to set up a view through code
This code is best fitted to go in the “Room Start” event. You could probably put it in room creation code, however you can write the code only once in a persistent instance this way, saving a little time. (Although, using the new room parenting system, creating default views in the editor is now easier then before)
Instead of writing a whole paragraph here, I’m just going to show you the commented code:
//Enable the use of views
view_enabled = true;
//Make view 0 visible
view_set_visible(0, true);
//Set the port bounds of view 0 to 640x480
view_set_wport(0, 640);
view_set_hport(0, 480);
Setting these bounds here will not automatically resize the game window to fit – even in a boot room. The boot room must be (or have an enabled view of) the size you wish your game window to be to do this automatically.
You can still manualy resize the window to fit, using the window_set_* functions. However, calling window_set_size()
and window_center()
in the same event doesn’t work, as the window only resizes AFTER the current event.
If you wish to resize the game window and center it all at once, add this quick snippet:
//Resize and 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]);
This will resize the window to the view port of view 0, and resize the application_surface
to this size too.
We haven’t actually set a view size, location, target object, border or speed through code – this is because we have to do it with cameras. This is explained in the next part.
From this point on, it’s all code!
Creating and deleting a Camera and Assigning cameras to views
If you are used to views, cameras are going to be a little different. Instead of thinking that they are “just there” and usable (like with views), it’s useful to understand that they are now more like instances – you have to create a camera, assign information to it, and delete it when you are done with it.
This section also shows you how to set view parameters through code, since they are now managed by cameras.
When creating a camera, there are two functions:
camera_create()
and camera_create_view()
Both functions return a unique camera ID to be used with all other camera related functions.
camera_create()
creates a camera that is effectively a blank canvas – you need to specify location and size manually before it can be used properly in 2D – I say this because you can use it in 3D without specifying these.
camera_create_view()
is much better in regards to 2D development as it forces you to specify all the view parameters on creation.
We will be using camera_create_view()
for the purpose of this guide, as we’re working in the context of 2D. If you want to see basic use of camera_create()
with 3D, check out my guide on “Getting Started with 3D in GMS2“.
Here’s how to create the camera. Again, 640×480 size, position will be 0, and we’ll set the object following parameters to the default:
//Camera creation
//Build a camera at (0,0), with size 640x480, 0 degrees of angle, no target instance, instant-jupming hspeed and vspeed, with a 32 pixel border
camera = camera_create_view(0, 0, 640, 480, 0, -1, -1, -1, 32, 32);
If you read the above, you’ll see that we can also define an “angle” on view creation – this partially replaces view_angle[0..7]. Other view angle functions are camera_get_view_angle
and camera_set_view_angle
So, that covers camera creation. To destroy a camera you are finished using, use:
camera_destroy(camera);
Alright, so that’s creating and destroying cameras covered.
Now we need to look at binding a camera to a view. This is done rather simply with this little bit of code:
//Set view0 to use the camera "camera"
view_set_camera(0, camera);
From here on, when wanting to update a view, we should now reference a bound camera instance with view_camera[view_index]
(in this case, view_camera[0]
. This is simply because we want to guarantee that the camera we are updating is the camera assigned to the view, which may not always be the instance that was returned by camera_create
**I have observed cameras working when using “view_camera[X] = camera”. While this works, I still feel that it is probably safer and more reliable to use “view_set_camera()”**
We don’t actually have to bind a camera to a view to use it – I will cover how that works shortly – but binding makes life a lot easier as it grants access to automatic camera “update/begin/end” scripts, which I will also be covering.
You can get the ID of camera a view is using with both view_camera[0..7]
and view_get_camera(view)
.
Setting a camera to follow an object through code (using the standard object tracking system)
If you set up a view through the room editor, you don’t even have to worry about this. However, if you want to set up the view to follow an object through code, you have 2 options:
The first is to set up the standard parameters in “camera_create_view()” to follow an object. e.g
/*
Build a camera at (0,0), with size 640x480, 0 degrees of angle,
targeting instance "objPlayer", instant-jupming hspeed and vspeed, with a 32 pixel border
*/
camera = camera_create_view(0, 0, 640, 480, 0, objPlayer, -1, -1, 32, 32);
Your next option is to build each parameter yourself – this also allows changing the fields on the fly e.g. increasing border size, focusing on an enemy object etc.
Setting up to be exactly the same as above looks like this:
//Basic set up
camera_set_view_pos(view_camera[0], 0, 0);
camera_set_view_size(view_camera[0], 640, 480);
//Setting up object target information
camera_set_view_target(objPlayer);
camera_set_view_speed(view_camera[0], -1, -1);
camera_set_view_border(view_camera[0], 32, 32);
And that’s it! All you need to do to set up a camera to act like the default views when following an instance.
The other way to draw a camera without binding it to a view
If, for whatever reason, you do not want your camera applied to a view (or want to use it without even creating a view), we have the “camera_apply” function.
The code is just this:
//Apply camera settings for drawing
camera_apply(camera);
If you want the camera to be applied only to certain views (without binding) you just need to wrap it in a “view_current” test, like so:
After applying, just run the draw code as normal.
To reset a camera to default after applying, just call this:
camera_apply(camera_get_default());
This resets the camera view to the GameMaker default camera – the one it uses when no views are enabled. Note that you can set a default camera with camera_set_default
, though I can’t think of (nor have I tested) a reasonable usage case yet.
The one major issue with using camera_apply
over binding a camera to a view is that it does not automatically call camera “update/begin/end” scripts – which are rather useful, for reasons I will explain soon. Though, if you made your scripts to suit the apply method, you could call them separately anway
Camera scripts - update, begin and end
Okay, we’re finally on to one of may favorite things about the new camera functions – scripts!
There are three types of camera script – “update”, “begin” and “end”. By looking at the manual, we get this information:
- The cameras for all visible and active view ports have their update script called
- Then, for each individual view:
- The begin script for the camera for that view is called
- The draw events are executed for that view (includes draw begin and draw end)
- The end script for the camera is called
So, what’s nice about this? Well, first of all, the scripts are only called for visible cameras assigned to views – this means you can put code in these scripts, and it won’t waste time running if the view isn’t being drawn – this is good when skipping frames. You also no longer have to have a High-Depth object run setup code in draw begin either.
These scripts only seem to run automatically for cameras that are bound to views, which makes a lot of sense. If your script isn’t runnning, check that it is properly bound!
Okay, these functions all work and are used in similar ways – you can tell WHEN they work from the above. In this case, I assign a script to the “begin” function of our camera for view 0.
The script, which is called update_script
, contains the following – a simple, yet very bad screenshake implementation:
//Shake the camera assigned to the current view
camera_set_view_pos(view_camera[view_current], random_range(-6,6), random_range(-6,6));
We use “view_camera[view_current]” to make sure we can bind this script to any view camera, and it will just work.
So, that’s the function. now let’s bind it to a view. We just use this code to bind to “camera_begin”:
//Bind the bad screenshake script "update_script" to the desired cameras "begin"
camera_set_begin_script(view_camera[target_view],update_script);
In order to bind to the other camera events, we can just use one of these two:
camera_set_end_script(view_camera[target_view],update_script);
camera_set_update_script(view_camera[target_view],update_script);
NOTE: As of writing, there is no way to retrieve which camera is running the current update script from within the script (just use “view_camera[view_current]” or “camera_get_active()” to identify in “begin” and “end” scripts), unless you set it up all the variable connections rigidly. I’ve submitted a bug report, but I don’t really know if it’ll be considered a bug (since drawing hasn’t actually started)
To clear a script from a camera, just run the functions as normal, but use “-1” for the script argument instead.
Each of these script setting functions has a “getter” equivelant, which returns what script is currently bound to a camera.
Another Note: You cannot perform any drawing within camera scripts. This stuff must still be done in the draw event (or onto surfaces)
That just about covers the camera script feature.
Simple camera position interpolation
For this bit, we’re just going to make a camera interpolate from its current position, to center wherever the user clicks. This code can be easily adapted e.g. for tracking objects, but this is a nice, easy thing to start off with.
Things we’ll assume here – you’ve already created a 2D camera, and it is assigned to view 0. If you don’t know how to do this, check the guides above.
Firts, let’s initialize two variables in the create event:
click_x = 0;
click_y = 0;
these will be used to store the last position the mouse was clicked at. Because of this, when we first launch the project it will center the camera on the origin, as I have set these values to “0”.
The next bit of code lies entirely in the step event (of the same object)
//Check if the mouse is clicked. If so, update the click position.
if(mouse_check_button_pressed(mb_left))
{
click_x = mouse_x;
click_y = mouse_y;
}
//Get target view position and size. size is halved so the view will focus around its center
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;
//The interpolation rate
var rate = 0.2;
//Interpolate the view position to the new, relative position.
var new_x = lerp(vpos_x, click_x - vpos_w, rate);
var new_y = lerp(vpos_y, click_y - vpos_h, rate);
//Update the view position
camera_set_view_pos(view_camera[0], new_x, new_y);
As a summary – this code checks if the left mouse button is pressed. If it is, it updates the “clicked position”.
Then, regardless of anything else, the view is interpolated to ceter around the last position the mouse was pressed.
How is this done?
- We retrieve the current (top-left) position and size of the view.
- We half the view size
- We establish the variable “rate”, which specifies the rate of interpolation (it makes changin it later easier)
- We determine the new position of the view by lerping the current position to the click position. The halved width and heights of the view are removed from the click position, making the view focus it intothe center
- The new coordinates are set to the view, ending the process
And that’s a simple view interpolation demo!
Camera Zooming
Camera zooming works similarly to before – you change your view width and height, but maintain port size. The only difference is that now you need a camera and to use some functions.
Things we’ll assume here – you’ve already created a 2D camera, and it is assigned to view 0. If you don’t know how to do this, check the guides above.
To get started, lets just define zoom variables in the create event (after a camera has been created:
zoom_level = 1;
//Get the starting view size to be used for interpolation later
default_zoom_width = camera_get_view_width(view_camera[0]);
default_zoom_height = camera_get_view_height(view_camera[0]);
You’ll then need this code in the step event (of the same object)
//Move the zoom level based on mouse scrolling. Clamp the value so stuff doesn't get too silly.
zoom_level = clamp(zoom_level + (((mouse_wheel_down() - mouse_wheel_up())) * 0.1), 0.1, 5);
//Get current size
var view_w = camera_get_view_width(view_camera[0]);
var view_h = camera_get_view_height(view_camera[0]);
//Set the rate of interpolation
var rate = 0.2;
//Get new sizes by interpolating current and target zoomed size
var new_w = lerp(view_w, zoom_level * default_zoom_width, rate);
var new_h = lerp(view_h, zoom_level * default_zoom_height, rate);
//Apply the new size
camera_set_view_size(view_camera[0], new_w, new_h);
What does this do? Let’s break it down:
- Adjust the zoom level, based on how the mouse is currently scrolling – mouse up zooms in while mouse down zooms out. The zoom level is also clamp to prevent over-zooming
- Get the current size of the view
- Set to interpolation rate – makes it easier to change later
- Figure out what the new sizes should by interpolating the current view size to the original size, multiplied by the zoom level
- Update the view size
And that’s how that works.
Remember though, the view size changes from the upper-left corner, not the center. If you want the view to stay centered, you’ll need this code:
//Get the shift necessary to re-align the view.
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;
//Update the view position
camera_set_view_pos(view_camera[0],shift_x, shift_y);
This uses the difference in view width and height in order to determine how to translate the view to re-center it, and then applies the new position
That wraps everything up for that!
Clamping a Camera to the Room Bounds
I was asked about this on Twitter, so I thought it might be worth me quickly adding this little snippet of code:
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) )
);
This code basically gets the position of the camera with the ID stored in “Camera_ID” and locks its position between (0,0) and the room bound minus the camera size.
If you’ve already calculated or have the camera size and position ready, you can use those values instead of the “camera_get_view_*” functions.
Other camera functions and what they do
Well, after the rest of the guide, you should know enough about the new system to make some magic happen. Regardless, here’s a few other handy functions:
- We can change viewport related stuff with view_set_[x,y,w,h]port() functions
- Views can be told to draw to a surface instead, using
view_set_surface_id()
camera_get_active()
returns the unique ID of the currently active camera (the one that is being drawn to)- There are two functions that use matrices –
camera_set_view_mat()
andcamera_set_proj_mat()
. These are used primarily to set up 3D projections camera_set_proj_mat()
takes a projection matrix, usually built with one of three matrix functions –matrix_build_projection_ortho
,matrix_build_projection_perspective
ormatrix_build_projection_perspective_fov
camera_set_view_mat()
takes a view matrix, usually built withmatrix_build_lookat
- Fun fact – I go over basic usage of these matrix functions in my other guide “Getting Started in 3D with GMS2“
- All of the “setter” functions discussed in the guide come with “getters” too.
It is really worth me also stating that you can draw stuff relative to a camera view with the “camera_get_view_[x/y/width/height]()”, though it is probably far better to draw HUD elements using the draw GUI events.
Multiple Viewports/Splitscreen Setup
I was asked if I could add a section on splitscreen views/multiple viewports, so I will! I’s really useful information to know, especially if you want to make couch multiplayer games.
As before, we’ll go through both possible methods – building the view in the room editor, and through code.
The Room Editor Method
This is very similar to creating a view in the room editor normally. In fact, it is so similar that you just need to repeat the steps outlined in “Setting up a view (to use with cameras)“, and change a couple of settings. The settings that matter are under “Viewport Properties”. These are “X Pos”, “Y Pos”, “Width” and “Height”. For the simplest results, I change “Y Pos” to 481 – this is because this sets the Y position of the viewport to the height of each view, plus one pixel for a border. You can then change the target object
Now, you may want to adjust sizes and positions to your personal taste, but I think it is quite simple to do so. The main disadvantage to this method is that it doesn’t allow a dynamic number of viewports as easily, but you can still show and hide viewports through code to achieve this to an extent.
The Code Method
This is also very similar to creating a single view. In fact, you can just copy most of the code. The important thing is that each view should have its own camera, otherwise both views will show the same thing as they will both set the projection from the same camera.
To keep things simple, let’s copy the important code from the other sections, and adjust it to set up a second camera bound to View 1, instead of View 0:
//Make view 1 visible
view_set_visible(1, true);
//Set the port bounds of view 1 to 640x480
view_set_wport(1, 640);
view_set_hport(1, 480);
//Build a camera at (0,0), with size 640x480, 0 degrees of angle, no target instance, instant-jupming hspeed and vspeed, with a 32 pixel border
camera_view1 = camera_create_view(0, 0, 640, 480, 0, -1, -1, -1, 32, 32);
//Set view 1 to use the camera "camera_view1"
view_set_camera(1, camera_view1);
This code can be put in the same place as the code that enables the first camera. Though you may want to set this up so that the extra views and camera are created and enabled only when in a splitscreen mode.
Now we just need to set the position of viewport of View 1, otherwise both views will be drawn in the same location, by default. This can make it seem like one view isn’t there at all, as they end up drawn one on top of the other.
The important functions for changing the position of the viewport are view_set_xport
and view_set_yport
which set the x and y positions of the view relative to the game window. I want to make the views stack vertically, so I will set the yport of view1 to 481 (the height of the views, plus an extra spacing pixel that will appear as a border):
//Set the yport of view1 so that it appears lower than view0
view_set_yport(1, 481);
We may also want to change window_set_rectangle
and the size of the application surface to account for the total size of both views so that they don’t scale or stretch to fit the window. For example:
//Set the application surface and window width + height
//This works because we know that view1 is lower than view0, so it's y + height will be as big as the window needs to be.
//Dynamically positioned views will need to be iterated over to determine the actual window bounds.
var window_width = view_get_wport(0);
var window_height = view_get_yport(1) + view_get_hport(1);
window_set_rectangle((display_get_width() - window_width) * 0.5, (display_get_height() - window_height) * 0.5, window_width, window_height);
surface_resize(application_surface, window_width, window_height);
While this is useful for this example, it may be preferable to adjust the viewport sizes to match the window and not this way around in full games so that players can keep control of window sizes. For example, set the viewport height to window_get_height() * 0.5
, but how you decide to do this does depend on how you want to scale multiple viewports and is affected by whether or not you want to maintain aspect ratios, too.
I may make an extended example on this, since one of my games makes a lot of use of dynamically scaled and positioned splitscreen, so I might show how that’s done when I re-implement it.
So that’s that. That should be enough information to get you going with the new cameras, but if you need something more – some help, another guide with context – let me know, and I’ll see what I can do!
First off, thank you for this explanation and the download. I have studied it in much detail. For a single camera, it works like a charm.
As soon as I add a 2nd viewport, the first viewport stops working. Therefore, I’m curious if you have an example showing 2 viewports side by side (split-screen) using GML code only (not using IDE). I’ve used the IDE and got it to work, but have difficulty translating the IDE settings to GML only.
Thank you again for the example–it has been really, really helpful!
Best Regards,
JQ