Lua, LÖVE, and the Global Monstrosity
This is written with my currently still-brief experience in using Lua. I had decided to use Lua to code my first ever game. And me, being very oblivious into how LÖVE works, decided that I can first focus on the game logic (backend) before integrating it with the front end. Essentially, I had thought this should be relatively simple. My experience in GUI is very little, but I have once tinkered with pygame, tkinter, and javafx. So I had thought that it would comes down to me writing a sort of infinite while loop and call drawing functions one by one. In fact, the core of my game logic is an infinite loop where the user moves from one event to another and would later enter a phase of clicking. Very simple, the backend!
Having finished the game logic, I originally intended to write the game contents. But I was stuck with writer's block, and decided to write the GUI first. So I finally got into LÖVE. Frankly I hate the idea of GUI programming, so I decided to ask AI to write out the code to display the interface I want. It went well! But of course, the code is not extensible, so it's up to me to properly refactor it.
In LÖVE, you don't write while loops, you don't even run commands, you simply write functions that Love will interpret to be the window creation, drawing, updating, event listeners, etc. Something like this:
function love.load()
love.window.setsize(800, 600)
end
function love.draw()
love.graphics.rectangle(x,y, x length, y length)
end
function love.mousepressed()
etc. etc...
end
As you can see, we cannot control the flow proper by doing the function calls ourself. We must essentially force our entire codebase into the LÖVE interface, where everything becomes munched by the draw function. You ended up with something like this. Suppose you have a namespace level
and mainmenu
. To call the game proper, you must do something like this:
mainmenu = require("gui/mainmenu")
level = require("gui/level")
state = mainmenu
function love.load()
love.window.setsize(800, 600)
-- etc..
end
function love.draw()
state.draw()
end
function love.mousepressed()
state.mousepressed()
end
function love.onscroll()
state.onscroll()
end
In this respect, your namespaces level
, mainmenu
, etc. needs their own implementation of various love.graphics()
functions to handle drawing objects for their current view session. To change between mainmenus or level, you need to implement a function in each state to change states.
What? What does it mean?
It means that state
as defined at the top of the code is a global variable. You can implement some mousepressed function in mainmenu.lua such that they do something like state = level
, and this means that when state
is mainmenu
as called in main.lua
, when you call state.draw()
and then state.mousepressed()
and the proper button is pressed, then it's going to edit state
as defined in the global namespace!
Am I making sense here? This design insanity forces you to use globals like hell!! And the problem is that you get a codebase that becomes exponentially difficult to follow as your game becomes more complex!!
This is a real world example from github.com/challacade/cavern
function getGlobals()
require("source/libraries/slam")
require("source/startup/loadFonts")
require("source/startup/loadSprites")
require("source/startup/loadSounds")
require("source/libraries/Tserial")
require("source/global/utilities")
require("source/global/collision_classes")
require("source/global/gameState")
gameStateInit()
require("source/global/saveGame")
require("source/global/saveUtil")
require("source/global/soundManager")
getCollisionClasses()
Camera = require("source/libraries/hump/camera")
vector = require("source/libraries/hump/vector")
flux = require("source/libraries/flux")
anim8 = require("source/libraries/anim8")
...
end
This function goes on for over 50 lines!!! How is this even legal? You have literally dozens (for complexer games, possible hundreds) namespaces called into the global scope, and each of this require
lines can call 5-10, hell even also dozens of other variables and put them in the global scope.
It's impossible to tell what's going on here in the main function, or even how do we get from one screen to another. If we dig deep into each of those draw functions we're going to see uninitialized variables used out of nowhere, which are assumed to exists as the function is supposed to use them from the global namespace. This is from blackscreen.lua
All of this globals complement the obscurity of the love.draw()
function:
function love.draw()
cam:attach()
-- Handles most drawing for the game
local drawGameplay = require("source/draw")
drawGameplay()
-- Draw the colliders for all physics objects
-- Commented out for final game, used for debugging
if drawPhysics then
love.graphics.setLineWidth(2)
world:draw(150)
gravWorld:draw(150)
end
cam:detach()
menuDraw()
textBox:draw()
intro:drawInterrupt()
saveUtil:drawMessage()
blackScreen:draw()
flash:draw()
--[[
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print(debug, 0, 100)
love.graphics.print(debug2, 0, 120)
]]
end
It's impossible to tell what's going on here in the main function, or even how do we get from one screen to another. If we dig deep into each of those draw functions we're going to see uninitialized variables used out of nowhere, which are assumed to exists as the function is supposed to use them from the global namespace. This is from blackscreen.lua
:
-- 0.5 * scale zooms the camera out. Start at player's position
cam = Camera(player.physics:getX(), player.physics:getY(), 0.5*scale)
cam.x = 0
cam.y = 0
function cam:update(dt)
-- player has died, don't update the camera
if player.state < 0 then
return
end
local lookX = player.physics:getX()
local lookY = player.physics:getY()
-- Camera can't pan outside the room
if mapdata.room ~= nil then
...
This, by the way, is the actual line 1-17 in the source file. What the hell is Camera? Where was it defined? Duh, it's a global variable. And what the hell is player? Same thing. The way these global variables are called properly is that in the getGlobals()
function above, Camera = require("source/libraries/hump/camera")
is run before require("source/ui/blackScreen")
!!!
I'm not sure if this is fully Lua's fault by design. But it sure as hell is the problem of LÖVE's way of forcing the programmer to use global namespaces extensively. And that's the biggest problem. It seems extremely difficult or unwieldy to avoid using globals at all, or even minimizing it. It looks to me that LÖVE is forcing the programmer to adopt the radical mindset of embracing global variables, and I do not love it. This is such an awfully insane and monstrous way of structuring your code, and I can't even begin to imagine coding like this as my project would become larger and larger.
First upload: 23 March 2025