State tables, metatables or closures? Which flavor do you prefer for your modules?

Once I’ve started working on more complex projects, logic has started to flow from scripts to lua modules.

First, I started using state tables, as I came from Pico8 and that’s how I did things there, more or less. A function returns a table and other functions use it as a parameter and operate with data.

Then I tried metatables, closer to OOP… but they feel messy. Needless? Dunno.

Finally there’s closures, which I have not used in Defold, which feel like some javascript I’ve done in the past.

Last project I published used the midclass lib, which seems to do a lot of other things, buy I still like the simplicity of state tables. Or even closures! Requiring a lib to create OOP like objects, while being easy to use, feels… weird, given there are other options.

It’s all explained here Lua modules in Defold

What do you use and recommend? Why? As long as there are no optimization requirements, is it just a matter of personal taste?

I’ve been going a bit back and forth (can probably be seen if you look at my repositories over time) and I’m now at a place where I prefer the simplicity of state tables. I prefer it for two reasons: 1) Lua modules do not contain any internal state. It’s all logic and 2) Having the state in a table makes it easy to inspect, easy to test and easy to serialize.

3 Likes

I also like state tables, but autocompletion just doesn’t seem to work, and for larger projects that’s pretty uncomfortable (which should probably be a thread on its own…). Do you use lua annotations?

I use state tables, with each “type” of state table having a module like this without any annotations:

function M.create(args...) return { --[[init]] } end
function M.some_procedure(t, args...) --[[do stuff]] end
...

I use vscode with Defold Buddy and Defold Toolkit. It gives me proper auto-complete for module functions but not for state table members. However, since vscode keeps track of symbols in the project, I get poor man’s auto-complete for members. This has worked fine for me so far since I have relatively well-defined types that I can remember the members of most of the time — if not, it’s relatively quick to navigate to the associated create function to look up the member names.

Edit: I went with state tables to keep things simple, avoid any custom/unexpected behavior caused by metatables, make it easy to copy state and read/write it to file, allow tables (and arrays) to operated upon by any function in a straight-forward manner, and make it easy to transform a table of one “type” to another.

I use closures when I want to customize the behavior of a function, e.g. passing a comparison predicate to a sorting function or a callback to a for_each_whatever function. But I rarely use closures to add new “members” to instances or things like that, to avoid mixing behavior and state, and allow auto-complete index these functions.

One thing to consider, I believe, is do you want to be able to clone and restore the gamestate by copying/replacing one or a few tables. If it’s just state tables, this is easy. If you mix data and closures (with captured values), it’s less easy.

3 Likes

Thing is, when using state tables, that you need to store the table and keep track of the module that operates on that table, somehow, right? In the project I’m working on I see no way of doing that without changing it’s whole structure.

In this project I’m using middleclass. actors have behaviours, which invoke commands. So when I set an actor’s behaviour I do something like:

function actor:set_behaviour(new_behaviour)
  self.last_behaviour = self.behaviour
  self.behaviour = new_behaviour
end

Those behaviours sometimes need parameters to operate, like the choose_tile_behaviour:

function Behaviour:initialize(next_command, max_range)
  self.next_command = next_command
  self.max_range = max_range
end

So I assign it like this:

actor:set_behaviour(behaviour_choose_tile:new(self, self.leap_length))

The behaviour_choose_tile module needs those parameter to work. If I used state tables, I would have to store those parameter somewhere. But also which module to use! That puzzles me.

Sorry, I’m still trying to figure how to steer away from OOP, for… no specific reason? And in this case it’s really handy to have data and functions that manipulate it all together?

1 Like

It might not be possible to switch to state tables without a big rework — and if your current structure works fine in your project, it’s probably not worth the effort. In your next project you could try another approach to learn the pros and cons.

Maybe you can have where actors are stored convey their type or behavior(s).

I have a structure reminiscent of an ECS. Conceptually, I define a number of archetypes. Two tables (entities) are of the same archetype if they have the same members and should be updated with the same procedure (have the same behavior), e.g. idle_player_unit or forest_tile. I store all entities of the same archetype in a single array, and by them being in this array determines how they will be processed. On update I iterate over the array and run the same code (system) on each element.

Consequently, in my gamestate table I have a few dozen archetype arrays, storing all the entities currently in the game world.

When an entity is to change behavior, I move it to another archetype array and add/remove/update any relevant members of the table. I have a module function defined for each valid transition, e.g. player_unit_from_idle_to_moving(entity, destination...). On update, when iterating over all the idle_player_units, I may call this function for one or more of the entities.

This way I have no need to store behaviors/functions directly on entities.

I do have a few places in my input handling where I store which module to call and with what arguments. It’s something I will try to refactor in the future. But essentially, I create an “interaction” by passing it a reference to the module that will handle the logic response, and the instance/state table to pass to it. E.g. instantiate interaction with act_on_tile.create(presenter_module, presenter_instance), and then it calls back with t.presenter_module.act_on_node_sequence(t.presenter_instance...).

1 Like

Oh, that sounds indeed similar to ECS! Certianly not applicable to my current project, but interesting indeed. And at that point, why not fully embracing ECS? I’ve used ECS in Unity for years - my own implementation - and for action-based games it’s a nice architecture. I believe there’s a nice ECS solution already available for Defold, if you don’t want to create your own.

There’s a few reasons why I didn’t go with an ECS:

  • Without a type system or compile time code execution in Lua, identifying archetypes and components, making queries etc. is somewhat complex and probably hard to get performant.
  • Since Lua doesn’t allow you to lay out and manage memory as is generally done in ECS, you don’t get the performance benefits that would bring.
  • By explicitly defining all archetypes and transitions, I get simpler code (in some ways) that is safer and easier to use and navigate (I’m hesitant to have everything “dynamic” since vscode then wouldn’t be able to provide as much auto-complete, go to definition, error hints etc.). This works fine since I’m not making a big game.

In practice, I only really have the “entity” part of ECS. It gives me flexibility to structure my code in the best way for whatever problem I want to solve, while still reducing the complexity of how the gamestate is represented (most things are entities, so less time spent thinking about where and how to store state).

1 Like

Ok, I see your reasons. I never used ECS because of optimizations, however, but because of the separation between logic and data.

I was trying to figure out how to do that in Defold. Not with ECS, just using modules to define data structures used by .scripts and other modules. But at some point I need tables that connect gameobjebt urls with logic entities (or the other way round), and… aaaagh. I’m not entirely happy about the architecture that is arising and therefore this thread…

In the Discord channel someone mentioned that maybe one should resort to modules just when there’s no other option, and try to stick to .script files as much as possible.

Maybe I’ve missed a piece of the documentation where best practices are suggested?

I don’t think that is a very good recommend. There is absolutely nothing wrong with using Lua modules. Especially for reusable game logic or centralised game state.

3 Likes

I understand there’s no magic bullet, that every project has different requirements. I’ll keep looking for examples of good practices in both Defold and Löve communities!

The way I thought of this issue when I designed my solution (and it sounds like you’ve had similar thoughts) was what a script and the associated game object represents. Looking at it like an MVC split, does it own and manage the model, the view, and/or controller?

I went with a model-view-presenter architecture, where the script is a thin view (mostly exposing the Defold objects to my presenter code) and the presenter is a module+instance created by the view/script as it gets initialized.

The script and associated presenter then only manage the view state. I.e. the position of game objects are not their logical position but instead their visual position. Each presenter keeps their view up to date with the model by subscribing to callbacks from the model. Presenters also listen to input, and push commands onto a queue that is processed when the model updates.

So in my case, the game object and component urls are cached by the script and read by the presenter when it updates the view. The model doesn’t know about these urls; instead it only calls the callbacks that presenters have registered with it.

I see this discussion and I feel that sometimes too much focus is spent on trying to achieve an “optimal” structure ahead of time.
The main problem is that each project is different, so is each team/person, so what’s “good” isn’t really known at the start.

You can of course rely on previous experience, and that will get you a long way.
However, as many of the participants in this discussion acknowledges, this is a new tool for them, and previous experience might not always apply in a way that’s expected, hence this discussion. So, regardless of experience, I think testing it out for yourself, is a must.
The project structure also reflects your type of game, and how you intend to work with it, and that’s very different between games and studios.
E.g. a small proect with a single level setup, is likely to be very different from a game with 1000’s of levels, with support for DLC’s etc.

My advice is to simply continue with your project, using the knowledge you have at this moment.
After a while, you’ll form new ideas on how to best organize the project to best suit you and your game. And at that point, you may or may not choose to restructure parts of the project, to solve a particular problem.

6 Likes

+1 to the above. I absolutely don’t want to discourage anyone from doing this kind of thinking, I just want to note that you can definitely ship games without delving too deep into this. I don’t even really understand the different options under discussion here, however I wouldn’t be surprised if I have naturally drifted towards some of the practices here by accident just through experience (even if I don’t have a name for what I’m doing).

6 Likes

Ok, no no, I’m not looking for an optimal structure ahead of time. I know that doesn’t make sense. The game is already working and a prototype is published, I’m now refactoring some parts that I don’t feel are quite right, which is what begun this conversation.

What is true, however, is that the discussion derived from state tables, metatables and closures to project architecture O:)

2 Likes