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!
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.
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
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.
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.
To be clear, I meant “there’s no point” not as “because you wouldn’t understand” but as “I’m probably wrong anyway”.
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.
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!).
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.
My modules tend to be self contained and specialized but don’t know anything about data. They take an input and return a result
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.
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?
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…
I have failed many times with the left side method.
However, I have also failed with the right side method.
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.
In this example, the dog or GUI reacts to the cat’s position, items, and button presses.
- 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
Thank you very much.
I need to understand more message passing.
- 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
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.
I think you would benefit from this technical writeup to help you think about a way to structure your code using Lua modules.
I suppose I’ll update this thread with posts that are related to Defold. October 7, 2024 Scripts or Modules? It’s an Architecture Decision, Moreso Than a Data Decision This was a big one. I hope you find it valuable. Have you ever found yourself struggling to decide what data goes where, whether some logic might be better suited to live in a script versus a module, which scripts should be responsible for managing which modules, or some other organizational problem that inevitably results i…
The post with most likes in this thread
In my mind, a module should be created when you notice a significant amount of code that could be generalized. If it can’t be generalized, then it should probably remain in its original .script file instead.
Your link to @Vow journal, that has this
Scripts are drivers. Their sole purpose is to provide channels of communication between the engine and the user through lifecycle functions. Keep them lightweight.
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:
- Let’s say we a crate created with
crate.go
, which is some kind of grid based collection of items/resources. - We have an inventory from which we can drag-and-drop items to the crate and from the crate.
- We have some assembly line constantly “adding” some items to the crate too.
- 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.
- 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).
Don’t they contradict each other?
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.
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.
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.
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.
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)