Best approach for inheritence/composition for game objects?

I’ve been struggling heavily with thinking about how to best structure a game instead of actually making a game lately, and one thing I’ve been hung up on is the best way to handle shared base functionality between objects in Defold.

For example, say I’m working on a platformer with defeatable enemies. I’d want health/damage handling, respawning, and basic movement/collision to be encapsulated and shared between all enemy objects in the game, among other functionality.

I can think of two approaches to handle this, but they both have caveats:

  1. Putting the functionality in a module that’s required and called by the enemy’s script. Main issue is that the Lua in Defold manual directly advises against modfying the internals of a game object with a module, and in this example I’d most likely be manipulating the position of a game object at the very least.
  2. Use two script components per enemy object (one with base functionality, the other with enemy-specific code) and communicate between the two with messages and properties. Problem with this is that the manuals state there’s no consistent order to update() being called, and I assume this is also true for multiple scripts in a single object. In this example, I’d want to guarantee the base script component executes first.

Any suggestions on how I’d work around these issues, or any approaches that would be better than either of these? Defold is very much a “there’s more than one way to do it” engine from what I understand, so I’m not looking for a one-size-fits-all solution.

3 Likes

We’ve all been there! :smiley:

And we recently started a nice discussion about game architectures (again :sweat_smile: ) on this forum (you can find many such topics in here, including the few recent ones) and the answer is always - “it depends” :smiley:

I’m at the stage, when I’m making a platformer/metroidvania with wildly separated modules, I try to separate data from functions and I try to structure a good architecture for it, that is not slowing down the performance.

Just keep in mind you are sharing one instance of a module, so that if you modify the data in it - you are modifying it for all scripts that require such module

BUT

You can put your data inside a table - each game object can have a separate sub-table in it (keyed with game object’s id) with all needed for it data (OOP-like) and you can operate on separated game objects by its ids.

Or you can use a function to create a table as an “instance” for each script you use this module in (even more OOP). Example: https://github.com/britzl/platypus - introduces function .create(config) that gives you an instance, so that you can use it for player and e.g. for all enemies in a platformer. Notice, almost whole platypus module is inside create function, because Lua can store even functions in a table.

This is composition approach, example: https://github.com/adamwestman/defkit
It’s also a good approach, I’m just not a big fan of it. :smiley:

Ben James released a ton of games made with Defold and you can see how he structurized the games and keeps things simple:

4 Likes

Thank you for the practical examples! I never considered passing the module’s functions to a table in the script and using it as an instance, the approach Platypus uses certainly seems simpler than trying to juggle a state table back-and-forth between the script and module. (part of me wonders what the memory impact of storing the functions in each script that needs it is, but the last thing I need to get hung up on is pre-optimisation as well haha)

The Defkit example is interesting (I didn’t know you could have that many scripts on one object just fine), although I’m still a bit worried about execution order in a composition approach. I suppose what I could do for any script components that need to execute in a certain order is move the update()code to on_message() and make the script I want to update first post a message to the second script in it’s update. (assuming that wouldn’t introduce coupling, I think I can avoid that by making the script to message at the end of the update a property)

2 Likes

Your thinking is correct here of course :wink:

BUT

If functions are stateless, you can exclude them out of tables and operate on data passed from instances. This is a way it would work also, if you would for example write your functions in Defold script and only use self (as a table for given instance) - this is also a good practice :wink:

ALSO

You can use metatables approach and that way you will not create copies of the same functions for each instance.

So platypus is an example of “instancing” through returning table:

M.create = function()
    platypus = {
        -- ... all your methods and fields like it would be OOP
    }
    return platypus
end

In metatables “OOP” approach you would do:

M.create = function()
    local instance = {
        -- put your "fields/members" here
    }
    return setmetatable(instance, { __index = M })
end)

-- and then define methods:
function M.function_a() 
   -- ...
end

function M.function_b() 
   -- ...
end

I only put “OOP” in brackets, because you don’t have inheritance, but inheritance is also possible (but I just don’t recommend it :sweat_smile: or better said - use only when really needed )

Read more:

https://www.lua.org/pil/16.1.html

Just my opinion here, so please consult it with anyone else + I don’t know everything about that need, but if you need certain order, the design of the program looks procedural, hence separating it into smaller independent parts is more difficult. If you try to design everything in a reactive way, you most probably get rid of that need :wink:

P.S.

function M.function_a() end

is a sugar syntax for:

M.function_a = function() end
2 Likes

If I’m getting Lua correctly, so in the first example, you would create and call:

local perry = platypus.create()
perry.my_method()

…whereas in the second form, it would be created the same but called differently?

local my_table = my_module.create()
my_table:function_a()
-- the first argument of function_a is the self of the instance my_table
1 Like

Not necessarily

Firstly,
: is another syntax sugar for passing instance as first parameter, so:

my_table:function_a()

is same as:

my_table.function_a(my_table)

So only if method needs access to any member, then yes, you can call it like the two options above. If not, then don’t pass the whole table, just arguments you need :wink:

Look at a snippet from my animator module:

function ANIM.create(config) --id, initial_anim, initial_flip, initial_color, callbacks)
	if not config then config = {} end
	local instance = {
		id = config.id or go.get_id(),
		anim = config.initial_anim or hash("idle"),
		color = config.initial_color or vmath.vector4(1), --c.f_white
		callbacks = config.callbacks or {}
	}
	if config.initial_flip ~= nil then
		instance.my_flip = config.initial_flip
	else
		instance.my_flip = false
	end
	instance.dir_flip = instance.my_flip and -1 or 1
	log.d("instantiated with anim: "..instance.anim.." flip: "..(instance.my_flip and "right" or "left"), "anim")
	return setmetatable(instance, { __index = ANIM })
end

function ANIM:play(anim, callback, force)
	assert(anim, "You must provide initial anim for animator")
	if (self.anim ~= anim ) or force then
		log.t("Plays "..anim.." ID "..self.id.." FLIP "..(self.my_flip and "true" or "false"), "hero_anim")
		sprite.play_flipbook("#sprite", anim, callback)
		self.anim = anim
	end
end

function ANIM.play_now(anim)
	sprite.play_flipbook("#sprite", anim)
end

Last one function .play_now is a method that don’t need access to any member (so also could be removed from such module, but I keep it here, because it is purely related to animation).

In retrospective, when I look at my code, I see that even this .play_now should have access to a member I should add - sprite address xD I’m just assuming here that all my objects have one sprite component and I always name it “sprite”

Yes, you’re correct about it :wink: Everything you need is accessible to your created instance in such case.

2 Likes