Module mess - how do you structure your projects?

I disagree with my own stance from 2021. I wrote that before I started building anything more than small projects.

It’s still true that generalizable code should be moved into modules, but I admit that the rest of what I said about keeping code in script files and using go.property() for sharing data was mostly wrong and will lead to problems when a project grows large enough. @Vow’s take becomes more and more vital as a project grows.

Of course, with very small projects, it’s no big deal either way.

6 Likes

I am not a code architecture expert, so don’t take my words too seriously.

I really like @Vow’s technical journal in general and the last entry on modules / scripts in particular. In my humble opinion, some interactions with the engine (messages, creation / deletion of game objects) need to be done in a script.

For example, a factory.create in a module function only works if the factory url is in the same collection as the script that called the module function. (At least, I understand it that way). It is difficult to respect this restriction when writing code in modules.

Therefore, although it is tempting to move most of the code into modules (in a not-so-small project), care must be taken when dividing the code into scripts/modules.

4 Likes

Yeah, that’s a good point to bring up. I do occasionally run into the same problem.

Sockets are one of the reasons to add another script to the project. Drivers should know which functions are safe to call for the socket they live in, which is at least resonable based on the fact that driver code will never be called by an unexpected module function in the context of the wrong socket.

4 Likes

I’d like to share some insights based on my experience with Lua modules and script components in Defold. There was also an interesting discussion on Twitter about scripts vs single entry point script in Unity.
https://x.com/keenanwoodall/status/1842587722260898282?t=BbNKQAudwbctz952v482FA&s=19

In my experience, you typically only need one script component per collection proxy. Messages should be used to send commands and data to the GUI or other proxy collections. Don’t make logic on messages, it is hard to understand and debug asynchronous logic

The main disadvantage is that you may need to create an editor for your game or use code to describe levels.

This is how looks controller.script in my games.

local N28S = require "libs.n28s"
local INPUT = require "libs.input"
local SM = require "libs.sm.scene_manager"
local CONTEXTS = require "libs.contexts_manager"
local GAME = require "game.game_world"
local DEFS = require "game.balance.defs.defs"
local POINTER = require "libs.pointer_lock"
local HASHES = require "libs.hashes"
local CONSTANTS = require "libs.constants"
local SOUNDS = require "libs.sounds"

---@class ScriptGame
local Script = {}

function Script:init()
	CONTEXTS:register(CONTEXTS.NAMES.GAME, self)
	INPUT.acquire(self, nil, SM:get_scene_by_name(SM.SCENES.GAME))
	self.fixed_updates = 0


	if (html_utils) then
		-- Player see loading. So we can collect garbage and user can't see it.
		collectgarbage("collect")
		timer.delay(0, false, function ()
			html_utils.hide_bg()
			html_utils.focus()
		end)
	end
	GAME:change_location(DEFS.LOCATIONS.BY_ID.JAM_1.id)
	SOUNDS:liveupdate_load_game_sound()
end

function Script:fixed_update(dt)
	if self.fixed_updates > 4 then --0,1,2,3
		return
	end
	self.fixed_updates = self.fixed_updates + 1
	GAME:fixed_update(dt)
end

function Script:update(dt)
	self.fixed_updates = 0
	self:check_pointer_lock()
	GAME:update(dt)
end

function Script:check_pointer_lock()
	if CONSTANTS.IS_MOBILE_DEVICE then return end
	if SM:is_working() then return end
	if GAME.state.editor_visible then
		POINTER.unlock_cursor()
	 end
	if SM:get_top()._name == SM.SCENES.GAME then
		if html5 and not POINTER.need_locked and not POINTER.locked and not GAME.state.editor_visible then
			SM:show(SM.MODALS.SETTINGS)
		end
	else
		POINTER.unlock_cursor()
	end
end

function Script:final()
	INPUT.release(self)
	CONTEXTS:unregister(CONTEXTS.NAMES.GAME)
	GAME:final()
end

function Script:on_input(action_id, action)
	if (action_id == HASHES.INPUT.ESCAPE and action.pressed) then
		if (not SM:is_working() and SM:get_top()._name == SM.SCENES.GAME) then
			SM:show(SM.MODALS.SETTINGS)
		end
	end
end

N28S.register(Script)


5 Likes