f56a1eea3b
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>
160 lines
5.5 KiB
Rust
160 lines
5.5 KiB
Rust
//! The [`RigidBody`] component and its [`RigidBodyKind`].
|
|
//!
|
|
//! A rigid body is the dynamics half of a physics object: it says *how* an
|
|
//! entity moves under the simulation (or that it does not). The *shape* it
|
|
//! collides with is a separate [`Collider`](crate::Collider) component, so the
|
|
//! two concerns compose — a body with no collider is a point mass, a collider
|
|
//! with no body is static world geometry.
|
|
//!
|
|
//! Like every Oxide component these are plain serializable data with
|
|
//! `#[derive(Reflect)]`, so they are editable from the inspector and from
|
|
//! scripts with no per-type editor code. The simulation reads them when it
|
|
//! builds the rapier world; the
|
|
//! authored fields here are inputs, never live simulation state (velocities and
|
|
//! poses live on the [`Transform`](oxide_engine::math::Transform) and inside
|
|
//! rapier).
|
|
|
|
use oxide_engine::reflect::{Reflect, ReflectEnum};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// How a [`RigidBody`] participates in the simulation.
|
|
///
|
|
/// Mirrors rapier's body types, named for engine users:
|
|
/// - [`Dynamic`](Self::Dynamic) — fully simulated; moved by gravity, forces,
|
|
/// and contacts.
|
|
/// - [`Kinematic`](Self::Kinematic) — moved only by the game (its
|
|
/// [`Transform`](oxide_engine::math::Transform)); pushes dynamic bodies but is
|
|
/// never pushed back. The basis for the character controller (Stage 9 later
|
|
/// piece) and for moving platforms.
|
|
/// - [`Static`](Self::Static) — never moves; immovable world geometry (floors,
|
|
/// walls). The cheapest body — no integration cost.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, ReflectEnum)]
|
|
pub enum RigidBodyKind {
|
|
/// Fully simulated — gravity, forces, and contacts move it.
|
|
#[default]
|
|
Dynamic,
|
|
/// Moved only by the game; affects dynamics but is not affected by them.
|
|
Kinematic,
|
|
/// Immovable world geometry.
|
|
Static,
|
|
}
|
|
|
|
impl RigidBodyKind {
|
|
/// Every kind, for editor menus.
|
|
pub const ALL: [RigidBodyKind; 3] = [
|
|
RigidBodyKind::Dynamic,
|
|
RigidBodyKind::Kinematic,
|
|
RigidBodyKind::Static,
|
|
];
|
|
|
|
/// A human-readable label.
|
|
pub fn label(self) -> &'static str {
|
|
match self {
|
|
RigidBodyKind::Dynamic => "Dynamic",
|
|
RigidBodyKind::Kinematic => "Kinematic",
|
|
RigidBodyKind::Static => "Static",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Component: the dynamics of a physics object.
|
|
///
|
|
/// Attach alongside a [`Collider`](crate::Collider) to make an entity take part
|
|
/// in the simulation. The fields are authoring inputs consumed when the rapier
|
|
/// body is created; runtime velocity/pose are not stored here (the
|
|
/// [`Transform`](oxide_engine::math::Transform) is the authoritative pose, which
|
|
/// the simulation writes back each step).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
|
|
pub struct RigidBody {
|
|
/// How the body participates in the simulation.
|
|
pub kind: RigidBodyKind,
|
|
/// Mass in kilograms. Ignored for non-dynamic bodies. `0` means "derive the
|
|
/// mass from the collider's [`density`](crate::Collider::density)".
|
|
pub mass: f32,
|
|
/// Linear velocity damping (drag), `0` = none. Slows translation over time.
|
|
pub linear_damping: f32,
|
|
/// Angular velocity damping, `0` = none. Slows rotation over time.
|
|
pub angular_damping: f32,
|
|
/// Per-body multiplier on global gravity (`1` = normal, `0` = floats,
|
|
/// negative = anti-gravity). Lets some bodies ignore gravity individually.
|
|
pub gravity_scale: f32,
|
|
/// Enable continuous collision detection — prevents fast bodies tunnelling
|
|
/// through thin geometry, at extra cost. Off by default.
|
|
pub ccd: bool,
|
|
}
|
|
|
|
impl Default for RigidBody {
|
|
fn default() -> Self {
|
|
Self {
|
|
kind: RigidBodyKind::Dynamic,
|
|
// 0 = derive mass from collider density (rapier's default behaviour).
|
|
mass: 0.0,
|
|
linear_damping: 0.0,
|
|
angular_damping: 0.0,
|
|
gravity_scale: 1.0,
|
|
ccd: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RigidBody {
|
|
/// A static (immovable) body — convenience for world geometry.
|
|
pub fn static_body() -> Self {
|
|
Self {
|
|
kind: RigidBodyKind::Static,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
/// A kinematic body — moved by the game, not by the simulation.
|
|
pub fn kinematic() -> Self {
|
|
Self {
|
|
kind: RigidBodyKind::Kinematic,
|
|
..Self::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_is_a_dynamic_body() {
|
|
let b = RigidBody::default();
|
|
assert_eq!(b.kind, RigidBodyKind::Dynamic);
|
|
assert_eq!(b.gravity_scale, 1.0);
|
|
assert!(!b.ccd);
|
|
}
|
|
|
|
#[test]
|
|
fn constructors_set_the_kind() {
|
|
assert_eq!(RigidBody::static_body().kind, RigidBodyKind::Static);
|
|
assert_eq!(RigidBody::kinematic().kind, RigidBodyKind::Kinematic);
|
|
}
|
|
|
|
#[test]
|
|
fn round_trips_through_ron() {
|
|
let b = RigidBody {
|
|
kind: RigidBodyKind::Kinematic,
|
|
mass: 2.5,
|
|
linear_damping: 0.1,
|
|
angular_damping: 0.2,
|
|
gravity_scale: 0.0,
|
|
ccd: true,
|
|
};
|
|
let ron = ron::to_string(&b).unwrap();
|
|
let back: RigidBody = ron::from_str(&ron).unwrap();
|
|
assert_eq!(b, back);
|
|
}
|
|
|
|
#[test]
|
|
fn kind_variants_are_reflected_for_a_combo() {
|
|
// The editor lists these in a dropdown via ReflectEnum.
|
|
assert_eq!(
|
|
RigidBodyKind::variants(),
|
|
&["Dynamic", "Kinematic", "Static"]
|
|
);
|
|
}
|
|
}
|