GameMaker: Supporting high-density screens in HTML5 games

While developing HTML5 games using GameMaker Studio, it may eventually come to your attention that the game does not render at native resolution, depending on the device, looking variously blurry as result. This small post is about fixing that.

Why does it happen

For reasons that can be promptly described as "the user may desire to be able to read text", higher-density screens will lie about screen dimensions and element sizes to have existing pages behave largely-correctly by default.

This separates the concepts of "screen pixels" and "page pixels".

How to fix it

devicePixelRatio variable exists for this purpose and reports the multiplier for page-to-screen pixels. Making use of it in GameMaker is not very hard, and, in fact, almost identical to the first code snippet on MDN.

First you would want to create a new extension (via context menu on Extensions section in GameMaker resource tree) and add a JS file to it. Then open that file in extension's file directory via an external editor.

You would need exactly two functions here - one to report devicePixelRatio, and one to change canvas.style.width / canvas.style.height (as the built-in window_set_size will only change canvas.width / canvas.height).

function browser_get_device_pixel_ratio() {
    return window.devicePixelRatio || 1;
}

function browser_stretch_canvas_ext(canvas_id, w, h) {
    var el = document.getElementById(canvas_id);
    el.style.width = w + "px";
    el.style.height = h + "px";
}

Then you would want to expose the functions by adding them via GameMaker extension UI:
(browser_get_device_pixel_ratio needs 0 arguments while browser_stretch_canvas_ext needs 3)


(note: argument/return types don't matter for GML and JS extensions)

Then you would want to add a GML file to the extension. The only reason why this is needed at all is to avoid hardcoding the canvas ID that our JS function takes (so far it's always been "canvas", but you never know).

#define browser_stretch_canvas
/// (width, height)
return browser_stretch_canvas_ext(window_handle(), argument0, argument1);

Similarly, you would want to expose that single 2-argument script via the extension UI.

With that done, the helper extension is complete and ready to use.

How to use

The flow is pretty simple and, again, not unlike the MDN example:

  • Get scaling factor via browser_get_device_pixel_ratio.
  • Calculate true size by multiplying screen size by scaling factor.
  • Set your room/view/application_surface/window sizes to true size.
  • Force correct screen size by passing it to browser_stretch_canvas_ext.

Here's an example of that:

var w = browser_width;
var h = browser_height;

// find screen pixel dimensions:
var rz = browser_get_device_pixel_ratio();
var rw = w * rz;
var rh = h * rz;

// update room/view size:
room_width = rw;
room_height = rh;
camera_set_view_size(view_camera[0], rw, rh);
view_wport[0] = rw;
view_hport[0] = rh;

// resize application_surface, if needed
if (application_surface_is_enabled()) {
    surface_resize(application_surface, rw, rh);
}

// set window size to screen pixel size:
window_set_size(rw, rh);

// set canvas size to page pixel size:
browser_stretch_canvas(w, h);

Downloads

Downloadable versions of the extension and a small sample project can be found on itch.io.

There is obviously more to good mobile scaling than just correct DPI, but this offers a good starting point while that's a story for another day.

Have fun!

Related posts:

15 thoughts on “GameMaker: Supporting high-density screens in HTML5 games

  1. I inserted these codes in room creation code panel.
    but I have a issue

    if I comment this part “browser_stretch_canvas(w, h);” my app run properly but most items size are not right,
    but if I don’t comment “browser_stretch_canvas(w, h);” , the app crash and return error
    “””
    _Nv
    :
    “unable to call function undefined typeof=undefined”
    _Ov
    :
    “unable to call function undefined typeof=undefined”
    _P7
    :
    true
    _Pv
    :
    (7) [‘function _Fi(“unable to call function undefined typeof=undefined”)\n’, ‘function _M7([undefined])\n’, ‘function gml_Room_rm_play_Create([instance], [instance])\n’, ‘function _V52(1, [unknown])\n’, ‘function _mj3(1)\n’, ‘function __i3()\n’, ‘function _ri3(414.725)\n’]
    _Qv
    :
    “”
    _Rv
    :
    -1
    function _Fi(text) {
    var index;
    var _Sv = _Tv();
    var _Uv = new _Hv(text,text,_Vv(_Fi.caller.name),-1,_Sv);
    throw _Uv
    }

    “””

  2. A very helpful article! Thanks!
    But, it is worth adding that the pixel ratio value may be non-integer.
    For example, in my display settings (on Windows), “Change the size of text, apps and other items” is set to 125%, so browser_get_device_pixel_ratio() returns 1.25 and using example code from article I’ve got a slightly blurry image.

    So, to get rid of blur, I’ve added these lines between rz,rw,rh definition and “// update room/view size:” comment:
    var dw = floor(rw) – (floor(rw) mod 2);
    var dh = floor(rh) – (floor(rh) mod 2);
    w *= (dw/rw);
    h *= (dh/rh);
    rw = dw;
    rh = dh;

  3. Hi.
    I am happy to finally find a solution to make the game use the full browser client area, but…
    While this works technically to “get the game maximized in the browser’s client area”, it is not really usable for existing projects.
    I don’t think it’s a good idea to change the room size.
    the room may be PLANNED to be like 4×3 screens wide in a scrolling game. same for camera and viewport. should not change, should SCALE. respecting aspect ratio (with black bars and all the stuff, the windows runtime does)
    this approach here forces a total rewrite of room logic and a second code base, the normal code running on windows and other platforms can’t be used here as the room’s size gets changed all the time, including all negative effects of aspect ratio destruction and so on.

    • That is not what this post is about – if you need to stretch the game to browser bounds while maintaining aspect ratio, you’ll need a little math, like this.

      • Hello! we have managed to apply the extension to 100% the game looks beautiful but I have a problem :(

        it seems the point x & y of the mouse don’t work

        something like that :
        if mouse_check_button_pressed(mb_left) {
        var mx = device_mouse_x_to_gui(0);
        var my = device_mouse_y_to_gui(0);

        //var mx = (window_mouse_get_x()/browser_get_device_pixel_ratio());
        //var my = (window_mouse_get_y()/browser_get_device_pixel_ratio());

        if point_in_rectangle(mx,my,600,660,680,725)
        show_message(“Click”);
        }

        draw_set_color(c_red);
        draw_rectangle(600,660,680,725,1);

        Note: I have this drawn in the Draw gui
        and in the first room : display_set_gui_size(1280,720);

        If I remove the extension and try on the mobile and on windos it clicks well

        create : canvas_init(1280,720);

        step :
        https://gyazo.com/4118a445ed7b4b09fcd6b208d407e1e1.png

        Thank you!

  4. Very nice!
    Are we supposed to need some mouse position scaling/adjusting with this?
    Also is there any fix to Ricky’s get_string_async problem?
    This is very useful, always wondered why games were blurry on some browsers but not others. This helps a lot with that!

    • So long as you are using an up-to-date version of GM (GMS2 or last few versions of GMS1), you should not need extra hacks for mouse position.

      You’d probably want to overwrite popup position using CSS instead (via !important), or use a custom popup (see browser_input_capture).

  5. Awesome post!

    window_get_width, window_get_height (and maybe other related functions) will return incorrect values when the canvas is scaled this way and devicePixelRatio > 1, though.

    It is easy to deal with by dividing the value of the function by browser_get_device_pixel_ratio().
    window_get_width()/browser_get_device_pixel_ratio() will be enough.

    Other problem I noticed is the input box created by get_string_async(). Its position also get totally messed up. I couldn’t find a fix for this one, as it seems like the coordinates values are overwritten every frame by the runner.
    I wonder if other things also get messed up…

    Ideally, GMS2 should offer options to deal with browser dpi and handle it all internally.

    • Yeah, GMS2 definitely should. This is a pretty fundamental and necessary feature in 2020

      Have you reported a bug to them? I’ll do it too and they will most likely fix it at some point

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.