Implementing a Finite State Machine using Lua Modules

Hello All,

I am quite new to Defold but have worked my way through enough tutorials to be comfortable starting small projects. One of these is the implementation of a Finite State Machine (pattern link) to control a game object’s behaviour. I was able to achieve a simple version in a single script by separating out the a states behaviour between the various functions (init, update, on_message, etc.). The next step is to find a way to consolidate a state’s behaviour into one block, which I attempted to do with Lua Modules and ran into trouble.

This is a example of a state written in a lua module (apologies for the use of pseudo code):

local M = {
	state_name = "state_up"
}

local message = "module test message"

function M.message()
	print(message + state_name)
end

function M.state_init(self)
end

function M.state_update(self, dt)
	local p = go.get_position()
	go.set_position(p + vmath.vector3(0, 1, 0) * 100 * dt)
end

function M.state_on_message(self, message_id, message, sender)
end

function M.state_on_input(self, action_id, action)
	if action_id == hash("space") and action.released then
		-- Switch go's current_states to state_idle
	elseif action_id == hash("down") and action.released then
		-- Switch go's current_states to state_down
	end
end

return M

And this is a example of the script that would be attached to a game object:

local state_idle = require("main.state_idle")
local state_up = require("main.state_up")
local state_down =  require("main.state_down")

function init(self)
	self.current_state = state_idle
	msg.post(".", "acquire_input_focus")
end

function update(self, dt)
	self.current_state:state_update(self, dt)
end

function on_message(self, message_id, message, sender)
	self.current_state:state_on_message(self, message_id, message, sender)
end

function on_input(self, action_id, action)
	print(action_id)
	self.current_state:state_on_input(self, action_id, action)
end

However straight away there is a issue passing “dt” as a argument for “self.current_state:state_update(self, dt)”. Where it was expecting dt to be a number valve, what was passed was “userdata”. I have not been able to fix this issue and, looking ahead, can see potential issues with this the whole lua module approach. So my questions are:

  1. Is there a way of passing the value of the userdata as a argument? Or do I need to use a getter to retrieve the valve?
  2. Have I misinterpreted Lua Modules and this approach to a FSM will not work?

I am happy to be told to ‘go back to basics’ with this one. However I was not to able to find much information specifically talking how to use lua modules for a state machine and would be really interested if I was anywhere close.

2 Likes

You’re getting the error with “dt” because you’re calling state methods with the colon (:) syntax. The colon is a shortcut for sending “self”. It automatically sends the object you’re calling the method on as the first argument.

So when you do:

self.current_state:state_update(self, dt)

That function is getting sent three arguments: “self.current_state”, “self”, and “dt”.

You’ve only defined the method “M.state_update” with two arguments, “self” and “dt”, so what it calls “dt” is actually the “self” that is passed in.


Otherwise I don’t see any problems with your approach. It’s the same way I would do it.

3 Likes

Thank you for the detailed answer. I did find it confusing that “dt” would be user data. I will go back to make this correction.

Just a follow up question:

If “self.current” is being passed as a argument to the method in the lua module, could I then assign a state that has been declared in the game objects script?

So, from the lua module example:

function M.state_on_input(current, self, action_id, action)
	if action_id == hash("space") and action.released then
		-- Switch go's current_states to state_idle
		current = state_idle
	elseif action_id == hash("down") and action.released then
		-- Switch go's current_states to state_down
		current = state_down
	end
end

If the self.current = state_up, then the expected behaviour of the space bar would be to assign “self.current” in the game object script to state_idle?

No, the function in the module will not have access to local variables defined in a separate file. You would need to do the same requires in the module file (this is not uncommon or inefficient in any way though).

[Edit] Ah, wait, if you have different state modules requiring each other you’ll get a circular require error. Always annoying. You could either have another module that’s just a list of all the states for everyone to use, or you could do the state switching in the script and have states refer to other states with some kind of ID (probably a string or hashed string).

FYI: your code: “current = state_idle” will not change anything on “self”. “current” in this case is a function argument, so it behaves like a local variable inside that function. Setting it will only change the value of the name “current” inside that function, it won’t affect anything on “self”.

3 Likes