Consuming all inputs before dispatch message and fixed_update

Hi,

I want to code a input system for a multiplayer fighting game, and I need to handle complicated stuffs about combos, timed actions, buffered inputs, short/long press etc..
So my idea is to have a global input_controller.script which will handle all keys and timings and players, store the result in state table tracking gameplay actions. This global state would then be accessed to other scripts in the game. Or the controller can use defold-event to trigger an event for each action. I also like to have dynamic bindings and “input profiles” to define different sets of actions (like “gameplay_fight”, “gameplay_car”, “ui_menus”, “ui_debug” etc.) that can be enable/disable.
I already used the greate defold-input extension, which provide a really good basis but I want to go farther.

So I have questions about the application lifecycle of Defold, regarding Defold application lifecycle manual and Defold application lifecycle manual.

  1. When there is multiple inputs in the same frame, does ALL on_input() function be called before the message dispatch phase to happen, or is each message dispatched after each on_input() ?
  2. When we are in the message-dispatch phase, and we repost a message, does it work “rounds after rounds” like "Every messages of round 1 are called (on_message functions) for ALL objects, THEN every reposts messages are called on ALL objects (round2), THEN every re-reposts etc..) or does it work like “immediat dispatch” as like "a message of round 1 is executed, if it repost and it is added to queue but the repost could be executed before another round1 message in another gameobject) ?

I want to do a thing like that, but I want to know if it is reliable before coding many things…
I did write some fake code to get the big picture:


--- ----------- GENERIC GLOBAL input_controller.script
local inputs = require('libs.inputs') -- module to handle global state of inputs and some usefull functions
 -- actions of this particular game - define mappings between keys, time of bufferings, combinations of keys, profiles etc.
 -- this module use the libs.inputs module to build the mappings.
 -- ultimately, actions are also defold-event events, so we can register to them in other scripts
local myactions = require('game.actions.lua')

---@package
---@param self inputs_controller.script
function init(self)
  self.compute_actions = false 
  self.action_mapper = inputs.configureActions(myactions) -- object to track input events and compute actions states
  self.actions_state = action_mapper.init() -- will store resulting actions states
end

---@package
---@param self inputs_controller.script
---@param action_id hash
---@param action on_input.action
---@return boolean|nil
function on_input(self, action_id, action)
  local time = socket.gettime()
  if action_id then 
    self.action_mapper:track_trigger(action_id, action, time) -- track input events
  else
    self.action_mapper:track_mouse(action, time} -- track mouse and touch events
  end
  -- post message to compute actions only one time per frame, after ALL on_input but BEFORE fixed_update
   if not self.compute_actions then
      self.compute_actions = true
      msg.post('#', 'compute_actions')
   end
end

---@package
---@param self inputs_controller.script
---@param message_id hash
---@param message table
---@param sender url
function on_message(self, message_id, message, sender)
  if message_id == hash('compute_actions') then -- executes after ALL on_input but BEFORE fixed_update
    -- here we have all input triggers and mouses states --> compute actions 
    self.actions_state = self.action_mapper:computeNewActionsState(self.actions_state) -- some complicated calculations about combos, timing, actions states etc.
    -- now we have the new actions state --> we can post "action" messages to objects or trigger events via defold-events. They will be executed BEFORE fixed_update
    for action_id, action_state in pairs(self.actions_state) do
      action_state.event:trigger() -- use defold-event to trigger system wide
      -- or suppose we have a property to set target object, juste post to it, this object will handle routing
      msg.post(target_object,'action',{action_id=action_id, action_state = action_state})
    end
  elseif message_id == hash('reset_states') then  -- a message to reset triggers after each frame
     self.action_mapper:reset()
  end
end

function on_late_update(self,dt) 
  msg.post('#','reset_states') -- post msg to clear all triggers states at the end of all update phase
end

--- -------------------------------------------------------------------------------------
---- in a script with on_message handling:
local myactions = require('game.actions.lua')
---@package
---@param self inputs_controller.script
---@param message_id hash
---@param message table
---@param sender url
function on_message(self, message_id, message, sender)
  if message_id == hash('action') then 
    if message.action_id == myactions.JUMP then
     -- do things for jump
   end
end

--- -----------------------------------------------------------------------------------
---- OR in any script with Event registering -> our actions are events!
local myactions = require('game.actions.lua')
function init(self)
 myactions.JUMP:subscribe(function(self, data)
    -- do jump things here 
     self.direction.y = 1
 end)
 myactions.MOVE_LEFT:subscribe(function(self, data)
   -- do move left
    self.direction.x = -1
 end)
end

function fixed_update(self, dt) 
  -- here actions event have already been executed
  self.direction = vmath.normalize(self.direction)
  self.position = self.position + self.direction * self.speed
  --- etc..
end
----------------------------------

So if somebody understands what I want to do, is it a good idea? will it work and stand the passing of time and Defold engine updates?

All inputs are handled first, then messages are dispatched.

You mean if you do a msg.post() in on_message()? If I recall correctly the new message will be handled after all existing messages have been handled. Message should always be delivered in order.

You should be able to whip up a quick example to verify this.

Thanks, yes when we msg.post() inside on_message(). I want to repost to the same script to handle some sequential stages of input handling, I need one stages finish before the next. But I want everything to be done before fixed_update.

I did an example and it SEEMS to work like that, but I need to know it REALLY ALWAYS works like that at the engine level…
I dont want to build something that will fall apart as soon as I have many objects because of bad assomption.

Unless it’s a hard restriction, perhaps collect all the data in on_input() and store it in an array.
And then, first thing in the fixed_update, do the work of going through the inputs?

it is a hard restriction, I want to make a generic configurable input controller that produces a global input/actions state, which state should be up to date and usable for every objects in their fixed_update function…
Also in GUI scripts too.

so I cannot handle that in the fixed_update of my controller.

My scenario is the following, assuming everything happens in this order:

  • INPUT CONTROLLER: init() :

    • require custom input module.
    • Msg.post(‘.’, ‘activate_mappers’) – will execute between init and first on_input
  • in a particular game controller or gui script: init() :

    • require input module.
    • Build particular config of ActionMapper → self.gameplay_actionMapper (this should be done in a module)
    • Then input_module.register(‘gameplay’, self.gamplay_actionMapper) – registre a profile. So I can have another for gui, or another for gamplay_v2 etc.
    • input_module.activate(‘gameplay’) – effectively activate the profile ‘gameplay’
  • INPUT_CONTROLLER: on_message() message_id = ‘activate_mappers’

    • handle profile activation
    • kind of a ‘acquire_input_focus’ of this profile. there will be also ‘desactivate_mappers’ and ‘only_activate_mappers’ / ‘reactivate_mappers’ to temporarily activate only a list of profiles (for GUI modals for example)
    • when it comes from init() functions of game objects, this will execute before first on_input. So everything is settled since frame 1.
  • INPUT_CONTROLLER: on_input()

    • track all defold input events, store timings and stats → inside input_module internal state
    • the game.input_binding has ALL keys registered
    • msg.post(‘.’, ‘handle_mappers’) – where the magic happens
  • INPUT_CONTROLLER: on_message() ‘on_input’

    • here I want to track input events that comes from third party libraries, like defold-in for gesture…, and treat them like raw input events (like SWIPE, PINCH etc.). So I need to reroute these into my INPUT_CONTOLLER with some msg.
    • msg.post(‘.’, ‘handle_mappers’) – where the magic happens
  • INPUT_CONTROLLER: on_message() ‘handle_mappers’

    • this message has a “repostcounter” and will repost to itself for example 5 times to ensure we have handled all other_on_input messages in the first stages. It will be a constraints of the my library to not have more then 5 reposts from others objects that sends input to it.
    • then when it is the last repost, we can handle all states at once:
      • for each activated actionMapper do
        • self.actions[profile] = actionMappers[profile]( self.gathered_input_state)
        • here each action is a Defold-event Event + has state so: for each actions we can trigger the events to propagate: action:trigger(action.state)
        • note that since we are still between on_input() and fixed_upate() phase, these triggred events will be called BEFORE fixed_update() also, like if it is an on_input() of the subscribing object…
  • finaly, in any gameobject script or gui script we can:

    • in fixed_update(), update(), late_update() → test the global state:
      • if input_module.actions[profile].JUMP.active then … end
    • or subscribe to Defold events in init():
      • gameplay_actionMapper.actions.JUMP:subscribe(do_jump, self)

This seems complicated… but I want to code it once for all for all my next games and make it reusable.
For the ActionMapper there will be an API to configure mappings for your game Actions from complicated key combinations or mouse or touch events, time buffering etc. and handling also dynamic profiles (think fixed A/B/C commands choices or “rebind hotkeys menu”), multiplayers and gamepads.
Moreover, I will treat mouse/touche specifically. For example, every key- actions will receive also the mouse/touche/ gesture state with it, so you can implement thing like CTRL+mouse move combinations to trigger “COPY_DRAG_DROP” action.

I want it to be extensible for third party input detection so I want the “on_input” message to be a mean to hookup and send new “raw” input types.
This is where I need to ensure multiple stages of message handling.

*everything is subject to totally change :sweat_smile: for now it is just in my head, I am designing concepts and the API