Screen resizing

Most games allow the screen to be resized. We can do this in LÖVE by enabling t.window.resizable = true.

function love.conf(t)
    t.version = "12.0"
    t.window.title = "My game"
    t.window.width = 960
    t.window.height = 540
    t.window.resizable = true
end

But what you will notice is that as you resize your game's window, it won't resize the game with it. What you draw on the screen is not influenced by the size of the window. But, with a canvas and basic math, we can make this happen.

Create a new file called screen.lua. In this file we create a new local table. We'll give it a function to initalize a new Canvas and a Transform.

local screen = {}

function screen.init(width, height)
    screen.width = width
    screen.height = height
    screen.canvas = love.graphics.newCanvas(width, height)
    screen.transform = love.math.newTransform()
end

return screen

In your main.lua, require the file, and call screen.init.

local screen = require "screen"

screen.init(960, 540)

The width and height you pass are the width and height of your game. In our case, if the window was at resolution 1920x1080, the game should be drawn twice as big.

Back to our screen.lua we need to write some more functions. We want to capture everything that needs to be resized in our canvas. Finally, after everything has been drawn, we draw our canvas.

function screen.capture()
    -- Push the current state (including the main canvas) onto the stack.
    love.graphics.push("all")
    -- Set our canvas. From now on everything will be drawn on our canvas.
    love.graphics.setCanvas(screen.canvas)
    love.graphics.clear()
end

function screen.draw()
    -- Pop the state, back to the main canvas.
    love.graphics.pop()
    -- Draw our canvas using the transform.
    love.graphics.draw(screen.canvas, screen.transform)
end

return screen

Now all that's left is to change the transform (the scaling and translation) based on the size of our window. Let's create a new function that we will call when the window has resized.

We can resize our window in any way we like, meaning we can have a different aspect ratio than 16:9. Because of this, we need to calculate the smallest ratio. Do we change the scaling of our game based on the width or height of our window?

Next will use this scaling to shift the position of our canvas, because want our game to be drawn in the center of the window. We will apply this information to our transform.

function screen.resize(w, h)
    local scale = math.min(w / screen.width, h / screen.height)
    local x = (w - screen.width * scale) / 2
    local y = (h - screen.height * scale) / 2

    screen.transform:setTransformation(x, y, 0, scale, scale)
end

return screen

Back in main.lua we need to call our new functions.

local screen = require "screen"

screen.init(960, 540)

function love.draw()
    screen.capture()
    -- Draw your game
    screen.draw()
end

function love.resize(w, h)
    -- Upon resizing the window, pass the new width and height.
    screen.resize(w, h)
end

Now our game will be resized along with our window. There is a slight problem, however. When we draw something based on the position of our mouse, you will see that it doesn't match your mouse's position.

function love.draw()
    screen.capture()

    local x, y = love.mouse.getPosition()
    love.graphics.circle("fill", x, y, 10)

    screen.draw()
end

This is because originally it is drawn on our mouse position, but then as the window resizes and shifts, so does the circle we drew. To fix this, we can add a function to get our actual mouse position, or rather what would be the mouse position after the scaling and translating is applied.

function screen.getMousePosition()
    local x, y = love.mouse.getPosition()
    return screen.transform:inverseTransformPoint(x, y)
end

The function inverseTransformPoint inverses the transformation we apply. The x and y it gives us are where we need to draw our circle if we want it to appear at the original x and y after the transformation has been applied.

function love.draw()
    screen.capture()

    local x, y = screen.getMousePosition()
    love.graphics.circle("fill", x, y, 10)

    screen.draw()
end

Now our circle draws on the correct position!

Pixel art #

You might notice that your game looks fuzzy when you scale it. This is especially noticable with pixel art. To fix this, we can change the filtering to nearest. Let's do this based on a third parameter, pixelArt.

function screen.init(width, height, pixelArt)
    screen.width = width
    screen.height = height
    screen.canvas = love.graphics.newCanvas(width, height)
    if pixelArt then
        screen.canvas:setFilter("nearest")
        screen.pixelArt = true -- We will use this in a moment.
    end
    screen.transform = love.math.newTransform()
end

Another thing you might want to add is add flooring to our scaling. This way we will only scale whole numbers (2x, 3x, etc.). This way our pixel art stays looking proper.

function screen.resize(w, h)
    local scale = math.min(w / screen.width, h / screen.height)

    if screen.pixelArt then
        scale = math.floor(scale)
    end

    local x = (w - screen.width * scale) / 2
    local y = (h - screen.height * scale) / 2

    screen.transform:setTransformation(x, y, 0, scale, scale)
end

If we make a pixel art game, we can now init the screen with passing true as third argument.

screen.init(960, 540, true)

Libraries #

For more advanced options, check out the libraries push and shove.

Full code #

local screen = {}

function screen.init(width, height, pixelArt)
    screen.width = width
    screen.height = height
    screen.canvas = love.graphics.newCanvas(width, height)

    if pixelArt then
        screen.canvas:setFilter("nearest")
        screen.pixelArt = true
    end

    screen.transform = love.math.newTransform()
end

function screen.capture()
    -- Push the current state (including the main canvas) onto the stack.
    love.graphics.push("all")
    -- Set our canvas. From now on everything will be drawn on our canvas.
    love.graphics.setCanvas(screen.canvas)
    love.graphics.clear()
end

function screen.draw()
    -- Pop the state, back to the main canvas.
    love.graphics.pop()
    -- Draw our canvas using the transform.
    love.graphics.draw(screen.canvas, screen.transform)
end

function screen.resize(w, h)
    local scale = math.min(w / screen.width, h / screen.height)

    if screen.pixelArt then
        scale = math.floor(scale)
    end

    local x = (w - screen.width * scale) / 2
    local y = (h - screen.height * scale) / 2

    screen.transform:setTransformation(x, y, 0, scale, scale)
end

function screen.getMousePosition()
    local x, y = love.mouse.getPosition()
    return screen.transform:inverseTransformPoint(x, y)
end

return screen