bevy-ichun 0.6.0

A simple kinematic character controller for avian3d
Documentation
//! # Rotate Action
//!
//! This module provides rotation functionality for kinematic character controllers.
//! It handles camera-style rotation with pitch clamping to prevent gimbal lock issues.

use bevy::{
    math::{Dir3, EulerRot, Quat, Vec3},
    transform::components::Transform,
};

/// Action for applying rotation to a character's transform.
///
/// This action handles first- and third-person camera-style, looking at and looking to rotation.
///
/// The `rotation` field stores the type of rotation with its needed data.
///
/// ## Example
///
/// ```rust
/// // Mouse input (full movement since last frame)
/// let mouse_rotation = RotateAction::new(RotationType::RotationVec(Vec3::new(mouse_delta.x, mouse_delta.y, 0.0)));
/// ```
///
/// ```
/// let loking_to_action = RotateAction::new(RotationType::LookingTo(LookingFields::new(Dir3::X, Dir3::Y)));
/// ```
/// ```
/// let loking_at_action = RotateAction::new(RotationType::LookingAt(LookingFields::new(Vec3::X, Dir3::Y)));
/// ```
#[derive(Default, Debug)]
pub struct RotateAction {
    /// The rotation type to apply.
    pub rotation: RotationType,
}

impl RotateAction {
    /// Creates a new rotation action with the specified rotation type.
    ///
    /// # Arguments
    ///
    /// * `rotation` - The rotation to apply.
    pub fn new(rotation: RotationType) -> Self {
        Self { rotation }
    }

    /// Applies the desired rotation to the KCC
    pub(super) fn apply(&mut self, transform: &mut Transform) {
        match self.rotation {
            RotationType::LookingTo(looking_fields) => {
                transform.rotation = transform
                    .looking_to(looking_fields.look, looking_fields.up)
                    .rotation;
            }
            RotationType::LookingAt(looking_fields) => {
                transform.rotation = transform
                    .looking_at(looking_fields.look, looking_fields.up)
                    .rotation;
            }
            RotationType::RotationVec(rotation) => {
                // Note that we are not multiplying by delta_time here.
                // The reason is that for mouse movement, we already get the full movement that happened since the last frame.
                // This means that if we multiply by delta_time, we will get a smaller rotation than intended by the user.
                // This situation is reversed when reading e.g. analog input from a gamepad however, where the same rules
                // as for keyboard input apply. Such an input should be multiplied by delta_time to get the intended rotation
                // independent of the framerate.

                // Extract rotation components and negate for intuitive mouse control
                let delta_yaw = -rotation.x; // Negative for right-positive mouse movement
                let delta_pitch = -rotation.y; // Negative for up-positive mouse movement

                // Convert current rotation to Euler angles for easier manipulation
                let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);

                // Apply yaw rotation (horizontal turning)
                let yaw = yaw + delta_yaw;

                // Apply pitch rotation with clamping to prevent gimbal lock
                // If the pitch was ±¹⁄₂ π, the camera would look straight up or down.
                // When the user wants to move the camera back to the horizon, which way should the camera face?
                // The camera has no way of knowing what direction was "forward" before landing in that extreme position,
                // so the direction picked will for all intents and purposes be arbitrary.
                // Another issue is that for mathematical reasons, the yaw will effectively be flipped when the pitch is at the extremes.
                // To not run into these issues, we clamp the pitch to a safe range.
                const PITCH_LIMIT: f32 = std::f32::consts::FRAC_PI_2 - 0.01; // ~89.4 degrees
                let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);

                // Convert back to quaternion and apply to transform
                // YXZ order: Yaw (around Y), then Pitch (around X), then Roll (around Z)
                transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
            }
        }
    }
}

#[derive(Debug)]
pub enum RotationType {
    /// The rotation to apply as a 3D vector.
    ///
    /// * **X component**: Yaw rotation (horizontal, left/right)
    /// * **Y component**: Pitch rotation (vertical, up/down)
    /// * **Z component**: Roll rotation (typically unused for character controllers, also not supported yet)
    ///
    /// For mouse input, these values typically represent the mouse movement delta
    /// since the last frame. Positive X rotates right, positive Y rotates up.
    /// Note: This is frame rate independent.
    RotationVec(Vec3),
    /// Uses `looking_to` internally and takes the needed fields for this function.
    LookingTo(LookingFields),
    /// Uses `looking_at` internally and takes the needed fields for this function.
    LookingAt(LookingFields),
}

impl Default for RotationType {
    /// The default `RotationType` is the one used for mouse input.
    fn default() -> Self {
        RotationType::RotationVec(Vec3::ZERO)
    }
}

/// Struct which defines fields used for `looking_to` or `looking_at` functions
#[derive(Debug, Copy, Clone)]
pub struct LookingFields {
    /// It's the first argument for the respective function.
    /// If `RotationType::LookingTo` this is the direction as Dir3 (or normalized Vec3) to look to.
    /// If `RotationType::LookingAt` this is the point as Vec3 to look at.
    look: Vec3,
    /// The up direction.
    up: Dir3,
}

impl LookingFields {
    pub fn new(look: Vec3, up: Dir3) -> Self {
        Self { look, up }
    }
}