Module mess - how do you structure your projects?

That’s a neat trick and makes perfect sense. It’s not the only reason I’m overusing modules but it’s one piece of the puzzle!

2 Likes

Hmm. In your crew/ship situation, I would not expect the two modules to actually depend on each other. Obviously the crew needs to interact with the ship, but presumably those interactions would work no matter what kind of ship it was, right? (Or have sensible failure checking…“well duh, this is a robot ship with no living quarters, you can’t house crew in it.”) In more specific terms, I would expect the crew functions to be passed in everything they need. If they need to interact with the ship, then the function will be sent a ship object, or whatever ship data it needs.

Especially with modules, try to keep your functions as “pure” as possible, like math functions. A function should only do what it says on the tin.

4 Likes

I use one module that require all game modules.
world.lua

Link to world passed in constructor, or world after require another module, set world var in module.

module.world = self

When ship need crew.
ship.world.crew
When crew need ship.
crew.world.ship

3 Likes

I felt tempted to “defend” my design here by explaining in more depth how my game works, but there’s no point. It boils down to:

And:

What I’ve started doing is, rather than moving generalised code into modules, is group (an excessive amount of) code into modules based on the part of the game (crew, ships, enemies, etc) it relates to. What I’m getting from your replies is that my specific code should reside in a .script file and only the generalised parts should be in a module. The example of a math module is a good one.

I need to think about whether a big refactor is in order for my current project, but certainly it’s something I should keep in mind from the beginning for my next one.

2 Likes

Haha, but I can’t think of a more fun way to waste time than by having a lengthy debate on game/code design!

It is hard to be sure when the Return-On-Investment of refactoring becomes worth it. I usually do it when I realize that I’m avoiding doing something because the code is too annoying to work with. In my case refactoring sometimes means “dumbing down” and just copy-pasting more stuff, rather than trying to fit everything into a big, perfect system.

3 Likes

To be clear, I meant “there’s no point” not as “because you wouldn’t understand” but as “I’m probably wrong anyway”. :stuck_out_tongue:

Broadly speaking I’m actually quite happy with how it’s working right now. It’s just when I run into the case of wanting to cross-require that I realise I’m doing something naughty. It’s probably not worth a big refactor right now… Let’s let future Alex worry about that (when it’s inevitably going to be an even bigger mess!).

3 Likes

My modules tend to be self contained and specialized but don’t know anything about data. They take an input and return a result.
All the data lives in other lua files, but it’s only data, no functions.

3 Likes

When I started out with Lua I created a lot of stateful Lua modules. My modules had state and functions mixed. This felt natural as I came from an OO background. Nowadays I have unlearnt some of my bad habits and I always try to keep the data separate from modules. The data is passed to a function, the function makes some kind of operation on the data and optionally returns some data back.

3 Likes

Do you think this module is good?

1 Like

Is the purpose of the param.lua module to store different kinds of parameters used by several scripts?

I need to better understand what you are proposing. Let’s start with the basic example where the cat.script does a msg.post to the dog.script. What is the purpose of the message? And in the same example using param.lua the cat and dog script access param.pos? Is one of them setting and the other reading the value?

2 Likes

The purpose of the param.lua module is to manage all the variables.
In this example, the dog or GUI reacts to the cat’s position, items, and button presses.
I have failed many times with the left side method.
However, I have also failed with the right side method.
I want to achieve object orientation…

1 Like

Perhaps what you need is a combination? You can keep the application state in param.lua and use message passing to notify listeners/interested parties when something changes.

  • The cat starts moving and lets the dog know through a message
  • The cat has updated it’s inventory and sends a message to tell the dog that the inventory has changed
  • The cat presses a button and sends a message to the dog that the button has been pressed
2 Likes

Thank you very much.
I need to understand more message passing.

1 Like

This examples are good for one direction messages.
But what if dog wants to know cat’s name.
In OO style it would be almost as simple as cat.name which is synchronous and doesn’t split the logic in half, with messages you can get it only with (or am I missing something?)

-- somewhere in the dog.script
function half_of_the_logic(self. params)
   -- some logic here, then it needs cat's name to continue
    msg.post(cat_url, "say_ur_name_say_ur_name")
end

function on_message(self, message_id, message)
    if message_id == hash("hi_my_name_is") then
        -- another half of the logic is here. How to trace it? How to wait for it?
    end
end

-- somewhere in the cat.script
function on_message(self, message_id, message, sender)
    if message_id == hash("say_ur_name_say_ur_name") then
        msg.post(sender, "hi_my_name_is", { name = "Slim Shady" })
    end
end

Could someone help me to wrap my head around this behavior? I can provide real-world example that I want to achieve too, if you desire to help with it too.

1 Like

I think you would benefit from this technical writeup to help you think about a way to structure your code using Lua modules.

4 Likes

The post with most likes in this thread

Your link to @Vow journal, that has this

Don’t they contradict each other?
I do understand lua modules and that you need to move some data (is it possible to move all data out of script? It still has some data in itself like sprite size etc), move some logic to modules too. I just don’t understand what exactly I should move. I’m not sure if it is my gaps of knowledge of some game development or programming overall, or defold is really missing some documentation about this. I have searched on a free time this week and I haven’t found any good resource that explains how to correctly split scripts and modules (of course there is no one right way, but at least some explanation in manual or examples in examples section would be good). Maybe it’s not related to game engine itself and more related to some programming paradigms, but when engine did this separation it would be good to provide some documentation on this part.

I have real-world example, so maybe someone could point me what they would do:

  1. Let’s say we a crate created with crate.go, which is some kind of grid based collection of items/resources.
  2. We have an inventory from which we can drag-and-drop items to the crate and from the crate.
  3. We have some assembly line constantly “adding” some items to the crate too.
  4. So we have 1 inventory, we have few crates created with a factory, we have various kind of items and we have one or more (not relevant to example) assembly lines.
  5. Now we dragging item from inventory into one of the crates that is constantly updating it’s state from the attached assembly line and we while holding item under some cell of it should receive information could we put this item into this cell in current situation (like is the crate already full, does it accept that amount of items, does it accept this kind of item etc etc).

How would one do this in defold?
Because in love2d it is easy, and I really not sure what to put where in defold. And your provided article doesn’t answer my question (I totally understand that it could be my lack of knowledge).

6 Likes

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