Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
//! [`AxisBinding`] and [`Axis2DBinding`] — directional inputs composed from
|
||||
//! [`Binding`]s into floats and [`Vec2`]s.
|
||||
//!
|
||||
//! A 1D axis pairs a "positive" binding set with a "negative" binding set;
|
||||
//! each direction held contributes ±1. If both directions are held the
|
||||
//! contributions cancel and the axis reads 0 — a "soft brake" any third-
|
||||
//! person camera or twin-stick character controller needs out of the box.
|
||||
//! Each direction supports several bindings (a WASD axis can also accept
|
||||
//! arrow keys), and the same physical key can appear in many axes' direction
|
||||
//! sets.
|
||||
//!
|
||||
//! A 2D axis is just a pair of 1D axes (X then Y). Diagonals are
|
||||
//! intentionally **not** normalized at this layer — some games want
|
||||
//! Quake-style diagonal speedup, others want unit-length input. Whichever
|
||||
//! convention a game wants, applying it once at the call site is clearer
|
||||
//! than having to undo a default at every site that disagrees.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::math::Vec2;
|
||||
|
||||
use super::{Binding, InputState};
|
||||
|
||||
/// One direction of an axis — typically positive (right / forward / up) or
|
||||
/// negative (left / back / down) — bound to one or more physical inputs.
|
||||
/// Any binding held contributes a full unit; multiple held bindings on the
|
||||
/// same direction do not stack.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AxisBinding {
|
||||
/// Bindings that pull the axis toward +1.
|
||||
pub positive: Vec<Binding>,
|
||||
/// Bindings that pull the axis toward -1.
|
||||
pub negative: Vec<Binding>,
|
||||
}
|
||||
|
||||
impl AxisBinding {
|
||||
/// A new axis with the given direction binding lists.
|
||||
pub fn new(
|
||||
positive: impl IntoIterator<Item = Binding>,
|
||||
negative: impl IntoIterator<Item = Binding>,
|
||||
) -> Self {
|
||||
Self {
|
||||
positive: positive.into_iter().collect(),
|
||||
negative: negative.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the axis against `input`. Returns -1, 0, or +1 (the
|
||||
/// directions OR'd together — multiple held bindings on the same side
|
||||
/// don't stack).
|
||||
pub fn value(&self, input: &InputState) -> f32 {
|
||||
let pos = self.positive.iter().any(|b| b.held(input));
|
||||
let neg = self.negative.iter().any(|b| b.held(input));
|
||||
match (pos, neg) {
|
||||
(true, false) => 1.0,
|
||||
(false, true) => -1.0,
|
||||
// Both held → mutual cancel; neither → idle. Same result.
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A 2D axis composed of two [`AxisBinding`]s (X and Y).
|
||||
///
|
||||
/// Output is the unmodified vector `(x.value, y.value)` — diagonals are
|
||||
/// `(±1, ±1)`, magnitude √2. Normalize at the call site if your game wants
|
||||
/// unit-length movement.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Axis2DBinding {
|
||||
/// The X (right − left) axis.
|
||||
pub x: AxisBinding,
|
||||
/// The Y (up − down) axis.
|
||||
pub y: AxisBinding,
|
||||
}
|
||||
|
||||
impl Axis2DBinding {
|
||||
/// A 2D axis from four direction binding lists in the usual order
|
||||
/// (`right`, `left`, `up`, `down`).
|
||||
pub fn new(
|
||||
right: impl IntoIterator<Item = Binding>,
|
||||
left: impl IntoIterator<Item = Binding>,
|
||||
up: impl IntoIterator<Item = Binding>,
|
||||
down: impl IntoIterator<Item = Binding>,
|
||||
) -> Self {
|
||||
Self {
|
||||
x: AxisBinding::new(right, left),
|
||||
y: AxisBinding::new(up, down),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the axis against `input`, returning the raw `(x, y)` value
|
||||
/// without normalization.
|
||||
pub fn value(&self, input: &InputState) -> Vec2 {
|
||||
Vec2::new(self.x.value(input), self.y.value(input))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
fn ad_axis() -> AxisBinding {
|
||||
AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_axis_is_zero() {
|
||||
let input = InputState::new();
|
||||
assert_eq!(ad_axis().value(&input), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positive_direction_returns_plus_one() {
|
||||
let mut input = InputState::new();
|
||||
input.press_key(KeyCode::KeyD);
|
||||
assert_eq!(ad_axis().value(&input), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_direction_returns_minus_one() {
|
||||
let mut input = InputState::new();
|
||||
input.press_key(KeyCode::KeyA);
|
||||
assert_eq!(ad_axis().value(&input), -1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_directions_held_cancel_to_zero() {
|
||||
let mut input = InputState::new();
|
||||
input.press_key(KeyCode::KeyA);
|
||||
input.press_key(KeyCode::KeyD);
|
||||
assert_eq!(
|
||||
ad_axis().value(&input),
|
||||
0.0,
|
||||
"left+right held simultaneously must read as idle"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_bindings_on_same_direction_do_not_stack() {
|
||||
// WASD + arrow keys both contribute, but holding two positives is
|
||||
// still +1 (not +2). The axis is a directional indicator, not an
|
||||
// accumulator.
|
||||
let axis = AxisBinding::new(
|
||||
[
|
||||
Binding::Key(KeyCode::KeyD),
|
||||
Binding::Key(KeyCode::ArrowRight),
|
||||
],
|
||||
[
|
||||
Binding::Key(KeyCode::KeyA),
|
||||
Binding::Key(KeyCode::ArrowLeft),
|
||||
],
|
||||
);
|
||||
let mut input = InputState::new();
|
||||
input.press_key(KeyCode::KeyD);
|
||||
input.press_key(KeyCode::ArrowRight);
|
||||
assert_eq!(axis.value(&input), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_2d_returns_vector_components_independently() {
|
||||
let axis = Axis2DBinding::new(
|
||||
[Binding::Key(KeyCode::KeyD)],
|
||||
[Binding::Key(KeyCode::KeyA)],
|
||||
[Binding::Key(KeyCode::KeyW)],
|
||||
[Binding::Key(KeyCode::KeyS)],
|
||||
);
|
||||
let mut input = InputState::new();
|
||||
|
||||
input.press_key(KeyCode::KeyD);
|
||||
input.press_key(KeyCode::KeyW);
|
||||
assert_eq!(axis.value(&input), Vec2::new(1.0, 1.0));
|
||||
|
||||
input.release_key(KeyCode::KeyD);
|
||||
input.press_key(KeyCode::KeyA);
|
||||
// Now A + W held.
|
||||
assert_eq!(axis.value(&input), Vec2::new(-1.0, 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_2d_diagonal_is_unnormalized() {
|
||||
// Diagonals are (±1, ±1) — caller normalizes if it cares.
|
||||
let axis = Axis2DBinding::new(
|
||||
[Binding::Key(KeyCode::KeyD)],
|
||||
[Binding::Key(KeyCode::KeyA)],
|
||||
[Binding::Key(KeyCode::KeyW)],
|
||||
[Binding::Key(KeyCode::KeyS)],
|
||||
);
|
||||
let mut input = InputState::new();
|
||||
input.press_key(KeyCode::KeyD);
|
||||
input.press_key(KeyCode::KeyW);
|
||||
let v = axis.value(&input);
|
||||
assert!(
|
||||
(v.length() - 2_f32.sqrt()).abs() < 1e-6,
|
||||
"diagonal must be sqrt(2), got {}",
|
||||
v.length()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_ron_round_trip() {
|
||||
let axis = Axis2DBinding::new(
|
||||
[Binding::Key(KeyCode::KeyD)],
|
||||
[Binding::Key(KeyCode::KeyA)],
|
||||
[Binding::Key(KeyCode::KeyW)],
|
||||
[Binding::Key(KeyCode::KeyS)],
|
||||
);
|
||||
let s = ron::to_string(&axis).unwrap();
|
||||
let parsed: Axis2DBinding = ron::from_str(&s).unwrap();
|
||||
assert_eq!(parsed, axis);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user