Module mess - how do you structure your projects?

Ever since I understood and started making more use of modules my projects have improved drastically in terms of organisation and readability.

However, I invariably create lots of modules and multiple modules end up wanting to make use of each other. For example, in my latest project I have a “crew” module and a “ship” module. At some point they will each want to call a function in the other. That doesn’t work, of course. Two modules can’t require each other or else the space-time continuum is shattered.

You can try to avoid the modules needing anything from each other, but I’m finding that hard to do.

You can subordinate one to the other, allowing one to require the other but not vice versa. However, I’m now finding myself duplicating main module variables into the submodule (e.g. in the main module having something like: submodule.somevar = M.somevar). That feels messy.

How do you handle this? If you don’t have to, how did you prevent it from being an issue in the first place?

4 Likes

It sounds like you’re using too many modules (gasp that’s not supposed to be possible!) :wink:

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.

Another way of thinking about it is to ask yourself “could I make this chunk of code into an external library that’s usable by others?” Again this comes back to the need for generalization. If you’re placing Ship-specific code (for example) into a module, and that code depends on code in other modules, then it likely shouldn’t be in a module at all.

I see a lot of confusion arising from developers moving too much of their code into .lua files simply to avoid a small amount of boilerplate code. This can cause unintended complexities and confusion. If I had to choose between some boilerplate vs complexity, I would choose boilerplate.

Of course, one exception to the generalization rule is something like raw data tables.

8 Likes

You’re probably right!

I do have a lot of these, but it’s not the sole purpose of the modules.

You’ve given me some stuff to think about. I think I’m overusing modules to the point of it being counterproductive.

Part of the reason, I think, is caused by another bad (?) habit of mine. I like structuring things to work synchronously, which is very easy to achieve with a module (crew.get_crew()) versus, for example, the asynchronous method of sending a message to a .script (msg.post(“crew_controller”, “get_crew”)) and waiting for a response.

Your points about boilerplate are also reasonable, but it has taken me a long time to move away from repeating myself so getting back into the habit will be tricky. However, I’m sure what you’re advocating is a sensible and healthy use of boilerplate whereas my old practices would probably be described as unhealthy.

1 Like

This can usually be solved by using go.property() instead of keeping data under self.<var>. Although I suppose maybe not if crew is a table, since we can’t store tables in go.property().

If crew is a table, maybe try initializing the table in crew.script (or wherever it’s housed). Then, on initialization of ship.script (or wherever you need access to the crew list), call msg.post(“crew_controller”, “get_crew”) and save the result as a local table (self.crew_ref for example). Since Lua doesn’t perform deep copies of tables by default, you should now have access to the memory address of the crew table in both scripts. You won’t have to use get_crew anymore, unless the memory address of the table is changed in crew.script.

5 Likes

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.

3 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

2 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.

1 Like

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.

2 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!).

2 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.

2 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.

2 Likes

Do you think this module is good?

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?

1 Like

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…

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
1 Like

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