N28S(No more global functions in scripts)

N28S(No more global functions in scripts)

This repository demonstrates how to use n28s (No More Global Functions in Scripts) inside a Defold project. The helper module (n28s.lua) lets you keep your gameplay logic encapsulated in a regular Lua table instead of spreading Defold lifecycle callbacks (init, update, on_input, etc.) over the global scope. It is a tiny drop-in utility inspired by Lerg’s approach and trimmed down for simplicity.

Keeping the script APIs on a table also makes auto-completion and type hints work better in editors such as VS Code or JetBrains IDEs—the tooling understands methods on explicit tables far more reliably than anonymous global callbacks with implicit self.

Defold injects a userdata self into callbacks by default, but storing your state on a plain Lua table and passing that around keeps all lookups inside Lua and is measurably faster than going through the userdata wrapper.

What the sample does

  • Loads n28s.lua from the project root.
  • Defines a script table in main/main.script, registers it with N28S.register, and delegates the Defold callbacks through that table.
  • Shows how to acquire input focus, set a fixed-fit projection, and react to the touch input action without leaking globals.
local N28S = require "n28s"

local Script = {}

function Script:init(go_self)
	self.go_self = go_self
	msg.post(".", "acquire_input_focus")
	msg.post("@render:", "use_fixed_fit_projection", { near = -1, far = 1 })
end

function Script:on_input(action_id, action)
	if action_id == hash("touch") and action.pressed then
		print("Touch!")
	end
end

N28S.register(Script)

Using n28s in your own scripts

  1. Copy n28s.lua into your project (keep it somewhere in the Lua module search path, e.g., the project root or libs/).
  2. require "n28s" inside any script file.
  3. Place lifecycle functions (init, update, fixed_update, on_message, on_input, on_reload, final) as methods on a Lua table.
  4. Call N28S.register(MyScriptTable) once. The helper asserts that no global callbacks are already defined, ensuring a single source of truth per script file.

This pattern keeps state localized, enables reuse across game objects, and makes Lua tooling (linters, unit tests, IDE completion) easier to apply since the script logic is regular table methods instead of hidden global functions.

Folder layout

  • main/main.script – example game object script that uses n28s.
  • n28s.lua – the helper module described above.
  • game.project, assets, input – standard Defold desktop template files.

Feel free to extend the main script or replace it with your own gameplay logic. As long as you register your table with N28S.register, Defold will keep calling into your encapsulated script without cluttering the global namespace.

7 Likes

Nice; two salient points you make:

(1) “Defold injects a userdata self into callbacks by default, but storing your state on a plain Lua table and passing that around keeps all lookups inside Lua and is measurably faster than going through the userdata wrapper.”

Please can you give a link to where in the Defold engine source code this is done.

(2) “gameplay logic encapsulated in a regular Lua table instead of spreading Defold lifecycle callbacks over the global scope.”

Does the following approach also do this and how does it differ functionally from your code?:

-- ignore the self parameter, use this instead:
local go_self = {}

function init(self)
  go_self.dt = 0.0
end

function update(self,dt)
  go_self.dt = go_self.dt + dt
  print(go_self.dt)
end

My motivation for asking is improvement of my understanding of the what and how. Thanks for the code and post.

1 Like
---self is userdata
function init(self)

end

Looks same is i do. But you still have global function. I use n28s because vscode and idea don’t like and don’t understand global functions in scripts.

--i just move table functions to global. So for engine when it load script file it will see global functions. But in vscode it will be looked as lua module
function M.register(script)
	assert(not _G.init, "global init already exist")
	assert(not script.__n28s_inited, "script already inited")
	script.__n28s_inited = true
	if script.init then _G.init = function (go_self) script:init(go_self) end end

	if script.update then
		local f = script.update
		_G.update = function (_, dt) f(script, dt) end
	end

My question was unclear, I am wondering where in the Defold C++/C code that the callbacks are. No worries as I should be able to work it out for myself, when I get a chance to look.

Sorry i don’t understant.

Here is c++ code where global function is handled by defold

1 Like