fix(controls): combine multiple action bindings

This commit is contained in:
2026-09-04 10:14:08 +00:00
parent 11c5ff2aff
commit 9a32598eb5
3 changed files with 48 additions and 34 deletions
Generated Regular → Executable
+1
View File
@@ -428,6 +428,7 @@ dependencies = [
"bevy_derive",
"bevy_ecs",
"bevy_input",
"bevy_platform",
"bevy_reflect",
"bevy_state",
"env_logger",
+15 -32
View File
@@ -159,7 +159,6 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
collected_inputs
}
// TODO: ! Zero out values that did not arrive
/// Fills the player's inputs based on the collected inputs and the current control bindings.
///
/// This function takes in the current inputs, updates them according to the specified control
@@ -174,49 +173,33 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
) {
// Check if the input container is accessible
if let Some(inputs) = inputs_container.me_mut() {
// Initialize a vector to hold tuples of actions and their corresponding bindings
// TODO: only on controls change
let mut action_binding_pairs: Vec<(&A, &BindingConfig<Gs>, &Binding<Gs>)> = Vec::new();
for (action, config) in controls.iter() {
let mut action_value = InputValue::Empty;
// Iterate over the control bindings and collect all action-binding pairs
for (action, config) in controls.iter() {
for binding in config.bindings.iter() {
action_binding_pairs.push((action, config, binding));
}
// Skip any binding with mismatched condition
if binding
.conditions
.iter()
.any(|condition| matches!(condition, BindingCondition::InGameState(state) if *state != *game_state.get()))
{
continue;
}
// Sort the action-binding pairs by the length of the input sequence, in descending order
action_binding_pairs.sort_by(|a, b| b.2.input.len().cmp(&a.2.input.len()));
// Loop over each binding to apply the corresponding input actions
'bindings_loop: for (action, _config, binding) in action_binding_pairs.iter() {
// Check the binding conditions before applying
for condition in &binding.conditions {
match condition {
// Skip this binding if the game state does not match the condition
BindingCondition::InGameState(state) => {
if *state != *game_state.get() {
continue 'bindings_loop;
}
}
}
}
// Match the binding input type to update the player's inputs
match &binding.input {
ButtonCombination::Single(input_type) => {
// TODO: Exclude this input if it has already been used
if let Some(value) = collected_inputs.get(&*input_type) {
inputs.forced_set(**action, *value);
} else {
inputs.forced_set(**action, InputValue::Empty);
if let Some(&value) = collected_inputs.get(&*input_type) {
action_value = action_value.merge(value);
}
}
}
}
inputs.forced_set(*action, action_value);
}
} else {
// Log a warning if the inputs container cannot be accessed
// TODO: Ensure this warning is only printed once
log::warn!("cannot find me in inputs container");
}
}
+30
View File
@@ -431,6 +431,36 @@ impl InputValue {
InputValue::Empty => 0.,
}
}
/// Merges another input value into this one.
///
/// `Empty` acts as an identity value. Boolean inputs are combined using
/// logical OR. For other non-empty values, the existing value is preserved.
///
/// This is used when multiple control bindings contribute to the same action.
pub(crate) fn merge(self, other: Self) -> Self {
match (self, other) {
// Empty doesn't affect an existing value
(Self::Empty, rhs) => rhs,
(lhs, Self::Empty) => lhs,
// Multiple boolean bindings combine with OR
(Self::Boolean(lhs), Self::Boolean(rhs)) => Self::Boolean(lhs || rhs),
// For floats choose max
// TODO: maybe user want to chose multiply same inputs of chose max
(Self::Float(lhs), Self::Float(rhs)) => {
if rhs.abs() > lhs.abs() {
Self::Float(rhs)
} else {
Self::Float(lhs)
}
}
(lhs, _) if !lhs.is_empty() => lhs,
(_, rhs) => rhs,
}
}
}
common_traits_conditions! {