GUI for Pixel art games - here's how

I wanted to share how I made a pixel art scale GUI, I’m probably not the first to do this and it’s probably one of the intended ways to do this:

My game has a “native” resolution of 480x270 that’s scaled up x4 to fill 1920x1080
I make a GUI screen as normal, but design it in the game’s native 480x270 resolution, let’s call it “Main HUD”. This includes various template elements, eg. buttons, borders, icons, all done with pixel art, pixel art fonts, and all with no scaling.

Then I make another “display space” gui that hosts the “Main HUD” as a template node, and sets the scale to 4. This is the GUI I add to the main collection to render in game.

Now it’s a pixel art HUD that matches the pixel scale of the game, without scaling up individual elements! And I can add other things to the “display space” gui that will render at the display resolution, outside of the game’s native resolution, if I need to.

NOTE: You need to avoid using any “center” or “edge” anchors for nodes, because they will render “off grid” and make the pixels wonky. You need to stick to the corners (north east, north west, etc) and then do the centering yourself in script using whole numbers, to keep everything on the pixel grid.

UPDATE:
Encapsulating all of your GUIs in a wrapping GUI that just scales it by 4 can be cumbersome, so if you don’t need the flexibility of having some GUIs show up at the actual display resolution and want your WHOLE game to be fixed to 4x scaling, you can do it one time in the render script.

You can edit the default.render_script directly or (you should) copy out your own .render and .render_script and point the game to that in the game.project, Bootstrap, Render setting. But either way, just add this 1 line to the render_script (well it’s 2 lines for clarity) marked with BEGIN and END, in the init function:

function init(self)
    self.predicates = create_predicates("tile", "gui", "particle", "model", "debug_text")

    -- default is stretch projection. copy from builtins and change for different projection
    -- or send a message to the render script to change projection:
    -- msg.post("@render:", "use_stretch_projection", { near = -1, far = 1 })
    -- msg.post("@render:", "use_fixed_projection", { near = -1, far = 1, zoom = 2 })
    -- msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })

    local state = create_state()
    self.state = state
    local camera_world = create_camera(state, "camera_world", true)
    init_camera(camera_world, get_stretch_projection)
    local camera_gui = create_camera(state, "camera_gui")
    --init_camera(camera_gui, get_gui_projection)
    --BEGIN: Scale GUI
    local zoom_level = 4
    init_camera(camera_gui, function(camera, state)
        -- Modify the projection function to include scaling
        return vmath.matrix4_orthographic(0, state.window_width / zoom_level, 0, state.window_height / zoom_level, camera.near, camera.far)
    end)
    --END: Scale GUI
    update_state(state)
end
12 Likes