gd: added input addon
This commit is contained in:
387
addons/guide/guide.gd
Normal file
387
addons/guide/guide.gd
Normal file
@ -0,0 +1,387 @@
|
||||
extends Node
|
||||
|
||||
const GUIDESet = preload("guide_set.gd")
|
||||
const GUIDEReset = preload("guide_reset.gd")
|
||||
const GUIDEInputTracker = preload("guide_input_tracker.gd")
|
||||
|
||||
## This is emitted whenever input mappings change (either due to mapping
|
||||
## contexts being enabled/disabled or remapping configs being re-applied or
|
||||
## joystick devices being connected/disconnected).
|
||||
## This is useful for updating UI prompts.
|
||||
signal input_mappings_changed()
|
||||
|
||||
## The currently active contexts. Key is the context, value is the priority
|
||||
var _active_contexts:Dictionary = {}
|
||||
## The currently active action mappings.
|
||||
var _active_action_mappings:Array[GUIDEActionMapping] = []
|
||||
|
||||
## The currently active remapping config.
|
||||
var _active_remapping_config:GUIDERemappingConfig
|
||||
|
||||
## All currently active inputs as collected from the active input mappings
|
||||
var _active_inputs:Array[GUIDEInput] = []
|
||||
|
||||
## A dictionary of actions sharing input. Key is the action, value
|
||||
## is an array of lower-priority actions that share input with the
|
||||
## key action.
|
||||
var _actions_sharing_input:Dictionary = {}
|
||||
|
||||
## A reference to the reset node which resets inputs that need a reset per frame
|
||||
## This is an extra node because the reset should run at the end of the frame
|
||||
## before new input is processed at the beginning of the frame.
|
||||
var _reset_node:GUIDEReset
|
||||
|
||||
## The current input state. This is used to track the state of the inputs
|
||||
## and serves as a basis for the GUIDEInputs.
|
||||
var _input_state:GUIDEInputState
|
||||
|
||||
|
||||
func _ready():
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_reset_node = GUIDEReset.new()
|
||||
_input_state = GUIDEInputState.new()
|
||||
add_child(_reset_node)
|
||||
# attach to the current viewport to get input events
|
||||
GUIDEInputTracker._instrument.call_deferred(get_viewport())
|
||||
|
||||
get_tree().node_added.connect(_on_node_added)
|
||||
|
||||
# Emit a change of input mappings whenever a joystick was connected
|
||||
# or disconnected.
|
||||
Input.joy_connection_changed.connect(func(ig, ig2): input_mappings_changed.emit())
|
||||
|
||||
|
||||
## Called when a node is added to the tree. If the node is a window
|
||||
## GUIDE will instrument it to get events when the window is focused.
|
||||
func _on_node_added(node:Node) -> void:
|
||||
if not node is Window:
|
||||
return
|
||||
|
||||
GUIDEInputTracker._instrument(node)
|
||||
|
||||
|
||||
## Injects input into GUIDE. GUIDE will call this automatically but
|
||||
## can also be used to manually inject input for GUIDE to handle
|
||||
func inject_input(event:InputEvent) -> void:
|
||||
if event is InputEventAction:
|
||||
return # we don't react to Godot's built-in events
|
||||
|
||||
# The input state is the sole consumer of input events. It will notify
|
||||
# GUIDEInputs when relevant input events happen. This way we don't need
|
||||
# to process input events multiple times and at the same time always have
|
||||
# the full picture of the input state.
|
||||
_input_state._input(event)
|
||||
|
||||
|
||||
## Applies an input remapping config. This will override all input bindings in the
|
||||
## currently loaded mapping contexts with the bindings from the configuration.
|
||||
## Note that GUIDE will not track changes to the remapping config. If your remapping
|
||||
## config changes, you will need to call this method again.
|
||||
func set_remapping_config(config:GUIDERemappingConfig) -> void:
|
||||
_active_remapping_config = config
|
||||
_update_caches()
|
||||
|
||||
|
||||
## Enables the given context with the given priority. Lower numbers have higher priority. If
|
||||
## disable_others is set to true, all other currently enabled mapping contexts will be disabled.
|
||||
func enable_mapping_context(context:GUIDEMappingContext, disable_others:bool = false, priority:int = 0):
|
||||
if not is_instance_valid(context):
|
||||
push_error("Null context given. Ignoring.")
|
||||
return
|
||||
|
||||
if disable_others:
|
||||
_active_contexts.clear()
|
||||
|
||||
_active_contexts[context] = priority
|
||||
_update_caches()
|
||||
|
||||
|
||||
## Disables the given mapping context.
|
||||
func disable_mapping_context(context:GUIDEMappingContext):
|
||||
if not is_instance_valid(context):
|
||||
push_error("Null context given. Ignoring.")
|
||||
return
|
||||
|
||||
_active_contexts.erase(context)
|
||||
_update_caches()
|
||||
|
||||
|
||||
## Checks whether the given mapping context is currently enabled.
|
||||
func is_mapping_context_enabled(context:GUIDEMappingContext) -> bool:
|
||||
return _active_contexts.has(context)
|
||||
|
||||
|
||||
## Returns the currently enabled mapping contexts
|
||||
func get_enabled_mapping_contexts() -> Array[GUIDEMappingContext]:
|
||||
var result:Array[GUIDEMappingContext] = []
|
||||
for key in _active_contexts.keys():
|
||||
result.append(key)
|
||||
return result
|
||||
|
||||
|
||||
## Processes all currently active actions
|
||||
func _process(delta:float) -> void:
|
||||
var blocked_actions:GUIDESet = GUIDESet.new()
|
||||
|
||||
for action_mapping:GUIDEActionMapping in _active_action_mappings:
|
||||
|
||||
var action:GUIDEAction = action_mapping.action
|
||||
|
||||
# Walk over all input mappings for this action and consolidate state
|
||||
# and result value.
|
||||
var consolidated_value:Vector3 = Vector3.ZERO
|
||||
var consolidated_trigger_state:GUIDETrigger.GUIDETriggerState
|
||||
|
||||
for input_mapping:GUIDEInputMapping in action_mapping.input_mappings:
|
||||
input_mapping._update_state(delta, action.action_value_type)
|
||||
consolidated_value += input_mapping._value
|
||||
consolidated_trigger_state = max(consolidated_trigger_state, input_mapping._state)
|
||||
|
||||
# we do the blocking check only here because triggers may need to run anyways
|
||||
# (e.g. to collect hold times).
|
||||
if blocked_actions.has(action):
|
||||
consolidated_trigger_state = GUIDETrigger.GUIDETriggerState.NONE
|
||||
|
||||
if action.block_lower_priority_actions and \
|
||||
consolidated_trigger_state == GUIDETrigger.GUIDETriggerState.TRIGGERED and \
|
||||
_actions_sharing_input.has(action):
|
||||
for blocked_action in _actions_sharing_input[action]:
|
||||
blocked_actions.add(blocked_action)
|
||||
|
||||
|
||||
# Now state change events.
|
||||
match(action._last_state):
|
||||
GUIDEAction.GUIDEActionState.TRIGGERED:
|
||||
match(consolidated_trigger_state):
|
||||
GUIDETrigger.GUIDETriggerState.NONE:
|
||||
action._completed(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.ONGOING:
|
||||
action._ongoing(consolidated_value, delta)
|
||||
GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
action._triggered(consolidated_value, delta)
|
||||
|
||||
GUIDEAction.GUIDEActionState.ONGOING:
|
||||
match(consolidated_trigger_state):
|
||||
GUIDETrigger.GUIDETriggerState.NONE:
|
||||
action._cancelled(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.ONGOING:
|
||||
action._ongoing(consolidated_value, delta)
|
||||
GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
action._triggered(consolidated_value, delta)
|
||||
|
||||
GUIDEAction.GUIDEActionState.COMPLETED:
|
||||
match(consolidated_trigger_state):
|
||||
GUIDETrigger.GUIDETriggerState.NONE:
|
||||
# make sure the value updated but don't emit any other events
|
||||
action._update_value(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.ONGOING:
|
||||
action._started(consolidated_value)
|
||||
GUIDETrigger.GUIDETriggerState.TRIGGERED:
|
||||
action._triggered(consolidated_value, delta)
|
||||
|
||||
func _update_caches():
|
||||
# Notify existing inputs that they aren no longer required
|
||||
for input:GUIDEInput in _active_inputs:
|
||||
input._reset()
|
||||
input._end_usage()
|
||||
|
||||
# Cancel all actions, so they don't remain in weird states.
|
||||
for mapping:GUIDEActionMapping in _active_action_mappings:
|
||||
match mapping.action._last_state:
|
||||
GUIDEAction.GUIDEActionState.ONGOING:
|
||||
mapping.action._cancelled(Vector3.ZERO)
|
||||
GUIDEAction.GUIDEActionState.TRIGGERED:
|
||||
mapping.action._completed(Vector3.ZERO)
|
||||
# notify all modifiers they are no longer in use
|
||||
for input_mapping in mapping.input_mappings:
|
||||
for modifier in input_mapping.modifiers:
|
||||
modifier._end_usage()
|
||||
|
||||
_active_inputs.clear()
|
||||
_active_action_mappings.clear()
|
||||
_actions_sharing_input.clear()
|
||||
|
||||
var sorted_contexts:Array[Dictionary] = []
|
||||
|
||||
for context:GUIDEMappingContext in _active_contexts.keys():
|
||||
sorted_contexts.append({"context": context, "priority": _active_contexts[context]})
|
||||
|
||||
sorted_contexts.sort_custom( func(a,b): return a.priority < b.priority )
|
||||
|
||||
# The actions we already have processed. Same action may appear in different
|
||||
# contexts, so if we find the same action twice, only the first instance wins.
|
||||
var processed_actions:GUIDESet = GUIDESet.new()
|
||||
var consolidated_inputs:GUIDESet = GUIDESet.new()
|
||||
|
||||
for entry:Dictionary in sorted_contexts:
|
||||
var context:GUIDEMappingContext = entry.context
|
||||
var position:int = 0
|
||||
for action_mapping:GUIDEActionMapping in context.mappings:
|
||||
position += 1
|
||||
var action := action_mapping.action
|
||||
|
||||
# Mapping may be misconfigured, so we need to handle the case
|
||||
# that the action is missing.
|
||||
if action == null:
|
||||
push_warning("Mapping at position %s in context %s has no action set. This mapping will be ignored." % [position, context.resource_path])
|
||||
continue
|
||||
|
||||
# If the action was already configured in a higher priority context,
|
||||
# we'll skip it.
|
||||
if processed_actions.has(action):
|
||||
# skip
|
||||
continue
|
||||
|
||||
processed_actions.add(action)
|
||||
|
||||
# We consolidate the inputs here, so we'll internally build a new
|
||||
# action mapping that uses consolidated inputs rather than the
|
||||
# original ones. This achieves multiple things:
|
||||
# - if two actions check for the same input, we only need to
|
||||
# process the input once instead of twice.
|
||||
# - it allows us to prioritize input, if two actions check for
|
||||
# the same input. This way the first action can consume the
|
||||
# input and not have it affect further actions.
|
||||
# - we make sure nobody shares triggers as they are stateful and
|
||||
# should not be shared.
|
||||
|
||||
var effective_mapping = GUIDEActionMapping.new()
|
||||
effective_mapping.action = action
|
||||
|
||||
# now update the input mappings
|
||||
for index in action_mapping.input_mappings.size():
|
||||
var bound_input:GUIDEInput = action_mapping.input_mappings[index].input
|
||||
|
||||
# if the mapping has an override for the input, apply it.
|
||||
if _active_remapping_config != null and \
|
||||
_active_remapping_config._has(context, action, index):
|
||||
bound_input = _active_remapping_config._get_bound_input_or_null(context, action, index)
|
||||
|
||||
# make a new input mapping
|
||||
var new_input_mapping := GUIDEInputMapping.new()
|
||||
|
||||
# can be null for combo mappings, so check that
|
||||
if bound_input != null:
|
||||
# check if we already have this kind of input
|
||||
var existing = consolidated_inputs.first_match(func(it:GUIDEInput): return it.is_same_as(bound_input))
|
||||
if existing != null:
|
||||
# if we have this already, use the instance we have
|
||||
bound_input = existing
|
||||
else:
|
||||
# otherwise register this input into the consolidated input
|
||||
consolidated_inputs.add(bound_input)
|
||||
|
||||
new_input_mapping.input = bound_input
|
||||
# modifiers cannot be re-bound so we can just use the one
|
||||
# from the original configuration. this is also needed for shared
|
||||
# modifiers to work.
|
||||
new_input_mapping.modifiers = action_mapping.input_mappings[index].modifiers
|
||||
# triggers also cannot be re-bound but we still make a copy
|
||||
# to ensure that no shared triggers exist.
|
||||
new_input_mapping.triggers = []
|
||||
|
||||
for trigger in action_mapping.input_mappings[index].triggers:
|
||||
new_input_mapping.triggers.append(trigger.duplicate())
|
||||
|
||||
new_input_mapping._initialize()
|
||||
|
||||
# and add it to the new mapping
|
||||
effective_mapping.input_mappings.append(new_input_mapping)
|
||||
|
||||
|
||||
# if any binding remains, add the mapping to the list of active
|
||||
# action mappings
|
||||
if not effective_mapping.input_mappings.is_empty():
|
||||
_active_action_mappings.append(effective_mapping)
|
||||
|
||||
# INVARIANT: all _active_action_mappings now have actions.
|
||||
|
||||
# now we have a new set of active inputs
|
||||
for input:GUIDEInput in consolidated_inputs.values():
|
||||
_active_inputs.append(input)
|
||||
|
||||
# prepare the action input share lookup table
|
||||
for i:int in _active_action_mappings.size():
|
||||
|
||||
var mapping = _active_action_mappings[i]
|
||||
|
||||
if mapping.action.block_lower_priority_actions:
|
||||
# first find out if the action uses any chorded actions and
|
||||
# collect all inputs that this action uses
|
||||
var chorded_actions:GUIDESet = GUIDESet.new()
|
||||
var inputs:GUIDESet = GUIDESet.new()
|
||||
var blocked_actions:GUIDESet = GUIDESet.new()
|
||||
for input_mapping:GUIDEInputMapping in mapping.input_mappings:
|
||||
if input_mapping.input != null:
|
||||
inputs.add(input_mapping.input)
|
||||
|
||||
for trigger:GUIDETrigger in input_mapping.triggers:
|
||||
if trigger is GUIDETriggerChordedAction and trigger.action != null:
|
||||
chorded_actions.add(trigger.action)
|
||||
|
||||
# Now the action that has a chorded action (A) needs to make sure that
|
||||
# the chorded action it depends upon (B) is not blocked (otherwise A would
|
||||
# never trigger) and if that chorded action (B) in turn depends on chorded actions. So
|
||||
# if chorded actions build a chain, we need to keep the full
|
||||
# chain unblocked. In addition we need to add the inputs of all
|
||||
# these chorded actions to the list of blocked inputs.
|
||||
for j:int in range(i+1, _active_action_mappings.size()):
|
||||
var inner_mapping = _active_action_mappings[j]
|
||||
# this is a chorded action that is used by one other action
|
||||
# in the chain.
|
||||
if chorded_actions.has(inner_mapping.action):
|
||||
for input_mapping:GUIDEInputMapping in inner_mapping.input_mappings:
|
||||
# put all of its inputs into the list of blocked inputs
|
||||
if input_mapping.input != null:
|
||||
inputs.add(input_mapping.input)
|
||||
|
||||
# also if this mapping in turn again depends on a chorded
|
||||
# action, ad this one to the list of chorded actions
|
||||
for trigger:GUIDETrigger in input_mapping.triggers:
|
||||
if trigger is GUIDETriggerChordedAction and trigger.action != null:
|
||||
chorded_actions.add(trigger.action)
|
||||
|
||||
# now find lower priority actions that share input
|
||||
for j:int in range(i+1, _active_action_mappings.size()):
|
||||
var inner_mapping = _active_action_mappings[j]
|
||||
if chorded_actions.has(inner_mapping.action):
|
||||
continue
|
||||
|
||||
for input_mapping:GUIDEInputMapping in inner_mapping.input_mappings:
|
||||
if input_mapping.input == null:
|
||||
continue
|
||||
|
||||
# because we consolidated input, we can now do an == comparison
|
||||
# to find equal input.
|
||||
if inputs.has(input_mapping.input):
|
||||
blocked_actions.add(inner_mapping.action)
|
||||
# we can continue to the next action
|
||||
break
|
||||
|
||||
if not blocked_actions.is_empty():
|
||||
_actions_sharing_input[mapping.action] = blocked_actions.values()
|
||||
|
||||
_reset_node._inputs_to_reset.clear()
|
||||
for input:GUIDEInput in _active_inputs:
|
||||
# finally collect which inputs we need to reset per frame
|
||||
if input._needs_reset():
|
||||
_reset_node._inputs_to_reset.append(input)
|
||||
|
||||
# Give the state to the input
|
||||
input._state = _input_state
|
||||
# Notify inputs that GUIDE is about to use them
|
||||
input._begin_usage()
|
||||
|
||||
for mapping in _active_action_mappings:
|
||||
for input_mapping in mapping.input_mappings:
|
||||
# notify modifiers they will be used.
|
||||
for modifier in input_mapping.modifiers:
|
||||
modifier._begin_usage()
|
||||
|
||||
# and copy over the hold time threshold from the mapping
|
||||
mapping.action._trigger_hold_threshold = input_mapping._trigger_hold_threshold
|
||||
|
||||
# and notify interested parties that the input mappings have changed
|
||||
input_mappings_changed.emit()
|
||||
|
||||
|
Reference in New Issue
Block a user