From 9a32598eb59296737f4e55fc61b86786b489f58b Mon Sep 17 00:00:00 2001 From: yukkop Date: Fri, 4 Sep 2026 10:14:08 +0000 Subject: [PATCH] fix(controls): combine multiple action bindings --- Cargo.lock | 1 + crate/bevy_controls/src/plugin.rs | 49 ++++++++++------------------- crate/bevy_controls/src/resource.rs | 32 ++++++++++++++++++- 3 files changed, 48 insertions(+), 34 deletions(-) mode change 100644 => 100755 Cargo.lock mode change 100644 => 100755 crate/bevy_controls/src/plugin.rs diff --git a/Cargo.lock b/Cargo.lock old mode 100644 new mode 100755 index 8510659..8d343c2 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,6 +428,7 @@ dependencies = [ "bevy_derive", "bevy_ecs", "bevy_input", + "bevy_platform", "bevy_reflect", "bevy_state", "env_logger", diff --git a/crate/bevy_controls/src/plugin.rs b/crate/bevy_controls/src/plugin.rs old mode 100644 new mode 100755 index 27729ec..4481d4d --- a/crate/bevy_controls/src/plugin.rs +++ b/crate/bevy_controls/src/plugin.rs @@ -159,7 +159,6 @@ impl, Gs: GameState> ControlsPlugin 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, Gs: GameState> ControlsPlugin ) { // 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, &Binding)> = Vec::new(); - - // Iterate over the control bindings and collect all action-binding pairs for (action, config) in controls.iter() { + let mut action_value = InputValue::Empty; + + // Iterate over the control bindings and collect all action-binding pairs for binding in config.bindings.iter() { - action_binding_pairs.push((action, config, binding)); - } - } - // 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())); + // Skip any binding with mismatched condition + if binding + .conditions + .iter() + .any(|condition| matches!(condition, BindingCondition::InGameState(state) if *state != *game_state.get())) + { + continue; + } - // 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 &binding.input { + ButtonCombination::Single(input_type) => { + if let Some(&value) = collected_inputs.get(&*input_type) { + action_value = action_value.merge(value); } } } } - // 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); - } - } - } + 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"); } } diff --git a/crate/bevy_controls/src/resource.rs b/crate/bevy_controls/src/resource.rs index dba184b..4a6c7d8 100644 --- a/crate/bevy_controls/src/resource.rs +++ b/crate/bevy_controls/src/resource.rs @@ -200,7 +200,7 @@ common_traits_conditions! { Mouse(MouseInput), // TODO: Gamepad(GamepadButtonType), // TODO: Touch screen - // TODO: TouchPad inputs + // TODO: TouchPad inputs } } @@ -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! {