GameMaker: Smooth pixel-perfect camera

This is a mini-tutorial and an explanation of an approach that allows you to have fluid sub-pixel movements with pixel-perfect cameras in GameMaker!

The idea

Let's suppose that you have a pixel-art game:

(mouseover/click to play GIF) Featuring classic pixel-art game elements such as tiles and a Bright Square

When implementing camera movement, you may find that you can't really have it "smooth" - especially when moving the camera at less than a pixel per frame and/or with acceleration/friction:

(after all, your smallest unit of measurement is a pixel!)

A common solution to this is increasing application_surface size to match output resolution:

This works, but introduces potential for rotated, scaled, misplaced or otherwise mismatched pixels (note the rotating square no longer being pixelated). Depending on your specific game, visual style, and taste, this can vary from being an acceptable sacrifice to An Insult To Life Itself.

The solution is to make the camera 1 pixel wider/taller, keep the camera coordinates rounded, and offset the camera surface by coordinate fractions when drawing it to the screen,

Thus achieving smooth, sub-pixel movement with a pixel-perfect camera!

The code

I have published this entire example project on GitHub, but here's the idea in short:

For this we'll be rendering a view into a surface.

Although it is possible to draw the application_surface directly, adjusting its size can have side effects on aspect ratio and other calculations, so it is easier not to.


Since application_surface will not be visible anyway, we might as well disable it. This is also where we adjust the view dimensions to include one extra pixel.

// game_width, game_height are your base resolution (ideally constants)
game_width = camera_get_view_width(view_camera[0]);
game_height = camera_get_view_height(view_camera[0]);
// in GMS1, set view_wview and view_hview instead
camera_set_view_size(view_camera[0], game_width + 1, game_height + 1);
display_set_gui_size(game_width, game_height);
view_surf = -1;

End Step

The view itself will be kept at integer coordinates to prevent entities with fractional coordinates from "wobbling" as the view moves along.

This is also where we make sure that the view surface exists and is bound to the view.

// in GMS1, set view_xview and view_yview instead
camera_set_view_pos(view_camera[0], floor(x), floor(y));
if (!surface_exists(view_surf)) {
    view_surf = surface_create(game_width + 1, game_height + 1);
view_surface_id[0] = view_surf;

(camera object marks the view's top-left corner here)

Draw GUI Begin

We draw a screen-sized portion of the surface based on fractions of the view coordinates:

if (surface_exists(view_surf)) {
    draw_surface_part(view_surf, frac(x), frac(y), game_width, game_height, 0, 0);
    // or draw_surface(view_surf, -frac(x), -frac(y));

the earlier call to display_set_gui_size ensures that it fits the game window.


Finally, we remove the surface once we're done using it.

if (surface_exists(view_surf)) {
    view_surf = -1;

In GMS1, you'd want to use Destroy and Room End events instead.

And that's all.

Have fun!

Related posts:

13 thoughts on “GameMaker: Smooth pixel-perfect camera

  1. Hello Vadim,
    Nice idea, as usual.
    I’ve already quite scratched my head around my pixel perfect camera myself and because I also setup a 4-player splitscreen, I came to ask if your technic would work with it, before I try to implement it.
    I guess I would need to manually draw 4 surface for each view on top of the 4 surfaces I already draw for the GUI of each player, is it right ?
    I’m a bit concern about performance if it’s the case.

    • Hello! You can do that – performance impact is going to be negligible and you’ll only need to handle drawing each view surface at correct spot.

    • Parallax layers would have to be drawn independently from application_surface (e.g. prior to drawing it in Draw GUI) to keep them smooth.

  2. Hello,
    This looks great! I’m sure it will help with my jittering.
    However, I’m struggling with how to follow an object rather than using the mouse, while centering the view on the object and keeping the view in the room. Any pointers? I’m still new to cameras and surfaces :)

    • Something like

      with (obj_camera) {
      	var vc = view_camera[0];
      	var cw = camera_get_view_width(vc);
      	var ch = camera_get_view_height(vc);
      	var cx = round(obj_player.x) - cw / 2;
      	var cy = round(obj_player.y) - ch / 2;
      	x = clamp(lerp(x, cx, 0.1), 0, room_width - cw);
      	y = clamp(lerp(y, cy, 0.1), 0, room_height - ch);

      Depending on factors, you might want something smarter than that, as trying to keep a player at the exact middle of view (while having them still constrained to view pixels) may have the player jitter instead

  3. I tried playing around with the project for a bit and noticed that mouse_x and mouse_y don’t seem to be able get the mouse position within the room correctly using this system.

    How would one work around that?

  4. So I am got it working but it is still super jittery? Doesn’t really seem any different from the regular way. I have the camera view size set to my maximum resolution + 1 and the gui size set to maximum resolution. Game is 320×180 view port and the resolution is 1920×1080.

    link to see issue ->

    Not exactly sure what is going on? Everything else is the same as your code, just doesn’t look like the examples you posted.
    Thanks in advance!! :D

      • I would recommend trying to move your camera code to the example project first to exclude chance of error in implementation.

  5. Would this system work if the GUI size was scaled to say 1920×1080 instead of the game’s internal resolution? Also are there any caveats to using a camera that uses lerp to follow an object?

    • If you are using a custom GUI resolution, you’ll need to scale the surface to match (via draw_surface_part_ext).

      Lerping the coordinates will not have any negative effect on this.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.