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_derive",
"bevy_ecs", "bevy_ecs",
"bevy_input", "bevy_input",
"bevy_platform",
"bevy_reflect", "bevy_reflect",
"bevy_state", "bevy_state",
"env_logger", "env_logger",
+16 -33
View File
@@ -159,7 +159,6 @@ impl<A: Action, Ic: InputsContainer<A>, Gs: GameState> ControlsPlugin<A, Ic, Gs>
collected_inputs 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. /// 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 /// 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 // Check if the input container is accessible
if let Some(inputs) = inputs_container.me_mut() { 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();
// Iterate over the control bindings and collect all action-binding pairs
for (action, config) in controls.iter() { 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() { 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 // Skip any binding with mismatched condition
action_binding_pairs.sort_by(|a, b| b.2.input.len().cmp(&a.2.input.len())); 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 match &binding.input {
'bindings_loop: for (action, _config, binding) in action_binding_pairs.iter() { ButtonCombination::Single(input_type) => {
// Check the binding conditions before applying if let Some(&value) = collected_inputs.get(&*input_type) {
for condition in &binding.conditions { action_value = action_value.merge(value);
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 inputs.forced_set(*action, action_value);
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);
}
}
}
} }
} else { } 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"); log::warn!("cannot find me in inputs container");
} }
} }
+31 -1
View File
@@ -200,7 +200,7 @@ common_traits_conditions! {
Mouse(MouseInput), Mouse(MouseInput),
// TODO: Gamepad(GamepadButtonType), // TODO: Gamepad(GamepadButtonType),
// TODO: Touch screen // TODO: Touch screen
// TODO: TouchPad inputs // TODO: TouchPad inputs
} }
} }
@@ -431,6 +431,36 @@ impl InputValue {
InputValue::Empty => 0., 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! { common_traits_conditions! {