GML Smooth camera movement in pixel-perfect games

YellowAfterlife

ᴏɴʟɪɴᴇ ᴍᴜʟᴛɪᴘʟᴀʏᴇʀ
Forum Staff
Moderator
GM Version: GMS1, GMS2
Target Platform: All
Download: https://github.com/YAL-GameMaker/pixel-perfect-smooth-camera
Links: https://yal.cc/gamemaker-smooth-pixel-perfect-camera/

Summary:
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!

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.

GML:
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.

GML:
// 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:
GML:
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.
GML:
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.
 

samspade

Member
That makes sense. Mostly. The part that doesn't is you're also making the view 1 px wider and taller which would seem to negate the benefit, but I'll have to experiment with it to understand it better (as it obviously works).
 

YellowAfterlife

ᴏɴʟɪɴᴇ ᴍᴜʟᴛɪᴘʟᴀʏᴇʀ
Forum Staff
Moderator
That makes sense. Mostly. The part that doesn't is you're also making the view 1 px wider and taller which would seem to negate the benefit, but I'll have to experiment with it to understand it better (as it obviously works).
Try drawing some lines to the camera surface before it gets drawn in Draw GUI to understand how it works.
 
Top