Fastest rotation route in rapier2d

I've a Rotation<Real> from rapier2d and want to keep it between 0 and 360:

/// Structure used to rotate a car automatically.
pub struct CarTurn {
    rotating: bool,
    target_degrees: Rotation<Real>,
}

impl CarTurn {
    pub fn turn(&mut self, direction: CarDirection) {
        self.rotating = true;
        self.target_degrees = Rotation::new(direction.rotation_degrees());
        self.target_degrees = Rotation::new(Real::round(angle_util::zero_to_360(self.target_degrees.into_inner())) % 360.0);
    }
}

mod angle_util {
    use rapier2d::prelude::*;
    use nalgebra::Complex;

    pub fn zero_to_360(angle: Complex<Real>) -> Complex<Real> {
        if angle < Complex::from(0.0) { Complex::from(360.0) - (-angle % Complex::from(360.0)) } else { angle % Complex::from(360.0) }
    }

    pub fn minimum_delta(a: Real, b: Real) -> Real {
        let mut ab = a - b;
        let mut ba = b - a;
        ab = if ab < 0.0 { ab + 360.0 } else { ab };
        ab = if ba < 0.0 { ba + 360.0 } else { ba };
        if ab < ba { ab } else { ba }
    }
}

The Complex type doesn't implement traits like Lt, Rem and so on. I've tried converting at all costs (TryInto and unwrap, Into, .into_inner(), .as_...()).

What exactly is the question?

I don't know the Lt trait, but Complex definitely implements Rem.

1 Like

Why are you trying to use a complex number for an angle? That seems likely to be a mistake.

(Complex numbers can be used to represent directions, but when you do that, you do it by interpreting them as unit vectors, not the complex number itself being an angle number.)

1 Like

The goal is to simply rotate a car to a target angle (in degrees) every frame, but I've 8 direction variants (up, up-left etc.) and they always return fixed angles.

I meant PartialOrd

OK, I see the problem here:

This is a unit-magnitude Complex used to express rotation. It is not an angle. You need to work in terms of vectors, not angles. The number 360 will not appear in your code (nor 2π), and you don't need to wrap to 360. To change direction, multiply another unit complex with it (which is equivalent to adding angles).

If you really want to convert to angle, call https://docs.rs/num-complex/0.4.4/num_complex/struct.Complex.html#method.to_polar — but most operations can be expressed in terms of the complex number, often more elegantly (because there is no discontinuity at 360-0).

3 Likes

There's one thing I didn't tell of: the car must rotate to the nearest unit. I've tried two ways in Godot Engine:

  • Using angular velocity
  • Using display rotation

The code would determine the rotation step as either 1 * TURN_RADIUS or -1 * TURN_RADIUS depending on the current rotation and the target rotation.

This is the GDScript I'm trying to rewrite in Rust:

class_name CarTurn

# Turn radius in degrees
var turn_radius: float

var _rigid_body: RigidBody2D = null
var _running: bool = false
var _increment_scale: float = 1
var _final_degrees: float = 0

# (This is a tuple I can't express in GDScript)
var _route_result_go_clockwise: bool = false
var _route_result_delta: float = 0

var _current_rotation: float:
    get:
        return fmod(Angle.zero_to_360(self._rigid_body.rotation_degrees), 360)

func _init(rigid_body: RigidBody2D, turn_radius: float):
    self._rigid_body = rigid_body
    self.turn_radius = turn_radius

func is_running() -> bool:
    return self._running

func turn(final_degrees: float) -> void:
    if self.is_running():
        self.stop()
    self._final_degrees = fmod(roundf(Angle.zero_to_360(final_degrees)), 360.0)
    self._running = true

func stop() -> void:
    self._running = false

func integrate_forces(state: PhysicsDirectBodyState2D) -> void:
    if not self._running:
        return
    var current_rotation = self._current_rotation
    if current_rotation == self._final_degrees:
        self._running = false
        state.angular_velocity = 0
        return
    self._update_route(current_rotation)
    var route_delta = self._route_result_delta
    state.angular_velocity = (1.0 if (route_delta <= 3 and self.turn_radius > 1.0) else self.turn_radius) * self._increment_scale

func _update_route(current_rotation: float) -> void:
    var a := self._final_degrees
    var b := current_rotation
    var ab := a - b
    var ba := b - a
    ab = ab + 360 if ab < 0 else ab
    ba = ba + 360 if ba < 0 else ba
    var go_clockwise := ab < ba
    self._increment_scale = 1 if go_clockwise else -1
    self._route_result_go_clockwise = go_clockwise
    self._route_result_delta = roundf(ab if go_clockwise else ba)

Why?

If you're looking to snap angles for a better user experience, a better implementation than "snap to nearest degree unit" would be one that explicitly snaps to relevant angles.

For example, this should snap angles to the 4 cardinal directions:

if angle.realpart().abs() < SMALL_ANGLE {
  angle = Angle(0, angle.imagpart().signum());  // signum returns -1 or 1 (or NaN)
} else if angle.imagpart().abs() < SMALL_ANGLE {
  angle = Angle(angle.realpart().signum(), 0);
}
1 Like

I'm not sure I wanted to mean "nearest angle", I worded incorrectly at first.

(Streamable: original game from other developer I'm trying to replicate as a massive multiplayer.)

image

Basically this is how the car moves, giving a flexible feel to the car's movement different from other top-down car games:

  • The car moves to one of 8 directions using the arrow keys:
pub enum CarDirection {
    Up,
    UpLeft,
    UpRight,
    Down,
    DownLeft,
    DownRight,
    Left,
    Right,
}
  • A force is applied every frame so that the car's rigid body goes forward. For example, if you're facing right and keep moving right, it'll move straight... but if you're facing right and suddenly move to left a drift or circunference move will occur.
(cos(current_rotation) * MOVE_SPEED, sin(current_rotation) * MOVE_SPEED)
  • The car rotates (tweens) itself automatically to the angle of the last direction you pressed. The step to the tween is either -1 or 1 per turn radius.

I never used complex numbers; does that mean I'll have to use the re and im fields together?

I've wrote it now, but the Rotation<Real> (or Unit<Complex<Real>>) doesn't implement Add. Sorry for the persistence :confused:

How can I increment or decrement the current rotation?

impl CarTurn {
    pub fn new() -> Self {
        Self {
            rotating: false,
            target_degrees: 0.0,
        }
    }

    pub fn turn_start(&mut self, direction: CarDirection) {
        self.rotating = true;
        self.target_degrees = angle_util::zero_to_360(direction.rotation_degrees().to_degrees()).round() % 360.0;
    }

    pub fn turn(&mut self, current_rotation: Rotation<Real>) -> Rotation<Real> {
        if !self.rotating {
            return current_rotation;
        }
        let current_rotation_degrees = current_rotation.into_inner().re.to_degrees();
        if current_rotation_degrees == self.target_degrees {
            self.rotating = false;
            return current_rotation;
        }
        let (go_clockwise, delta) = self.turn_route(current_rotation_degrees);
        let increment = (if delta <= TURN_RADIUS { 1.0 } else { TURN_RADIUS }) * if go_clockwise { 1.0 } else { -1.0 };
        current_rotation + Rotation::new(increment.to_radians())
    }

    fn turn_route(&self, current_rotation_degrees: Real) -> (bool, Real) {
        let a = self.target_degrees;
        let b = current_rotation_degrees;
        angle_util::rotation_goes_clockwise_and_its_delta(a, b)
    }
}

I've changed it to this for now. I suppose it'll work; I'll play with it once I finish the server and the rendering client:

let (go_clockwise, delta) = self.turn_route(current_rotation_degrees);
let increment = (if delta <= TURN_RADIUS { 1.0 } else { TURN_RADIUS }) * if go_clockwise { 1.0 } else { -1.0 };
Rotation::new((current_rotation_degrees + increment).to_radians())

Yes, adding is not the right operation, and it's not available here because it would make the number no longer unit. As I wrote in my previous post, multiplying unit complexes has the same effect as adding angles. (And, relatedly, powf() is analogous to multiples or fractions of an angle: for example, some_rotation.powf(2.) will rotate twice as far.)

I never used complex numbers; does that mean I'll have to use the re and im fields together?

You should generally not need to mention the re and im fields. They do have a useful property: their values are the cosine and sine of the angle, respectively. However, for manipulating the rotation you should generally stick to operations on the complex number as a whole.

And, again, if you need to convert to angle (e.g. to determine how different two rotations are), call .to_polar().

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.