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.

Create

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.

application_surface_enable(false);
// 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.

Cleanup

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

if (surface_exists(view_surf)) {
    surface_free(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:

25 thoughts on “GameMaker: Smooth pixel-perfect camera

  1. This is awesome!
    How would you handle parallax scrolling with background layers?
    After implementing this, my background layers still have a jitter to them.
    Thanks!

    • Untying parallax backgrounds from game pixels means having to draw them separately from game surface (which would now have to be transparent), and that’s some work. I think at that point I’d reconsider whether it’s better to use this trick or just round coordinates (and maybe use a shader like this one if you need pixel-perfect rotations)

  2. Really no reason to use an extra surface or the GUI drawing stuff.

    Just use: application_surface_draw_enable(false);

    Then in the post draw event draw the application surface with blending disabled. Then you don’t have to work around not having the use of the application_surface. Plus, if someone does use the begin draw GUI event for other things the draw order could cause issues.

    GUI size is often different than the internal rendering size too, which is just another reason this is a post draw solution.

    • I suppose you could do that, though ultimately it will not spare you of having to deal with mouse/surface coordinate mismatches – could use a new GM function for that.

  3. Great tutorial! After having fixed so the camera follows my player object, I’ve noticed they started jittering. A few other objects in the room also jitter, but only at certain movement speeds. How would one work around that?

    • Depends on kind of jitter, but usually means that things are being drawn on fractional coordinates, at which point it’s up to GPU to decide what to do (usually rounding/flooring the coordinates)

  4. Hi yal! I made a simple way to zoom this camera by scaling the surface and wanted to share. It works great for what I need and might save someone some time.

    var _scale = 2;

    draw_surface_part_ext(
    view_surf,
    frac(x),
    frac(y),
    game_width,
    game_height,
    0 - (game_width*_scale - game_width)/2,
    0 - (game_height*_scale - game_height)/2,
    _scale ,
    _scale ,
    c_white,
    1
    );

  5. 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.

    • Had this issue since it became quite jarring having background scroll in low resolution whilst having a sub-pixel camera.
      I managed to keep parallax smooth by creating separate surfaces for each background layer and drawing them to GUI before view_surf. You also have to disable all backgrounds/background color to avoid view_surf drawing over the background. There might be issues with this solution but it´s worked for me so far.

  6. 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

  7. 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?

  8. 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 -> https://imgur.com/gallery/jxwhxnz

    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.

  9. 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.