saddle-camera-fps-camera

Reusable first-person camera and locomotion-aware view-motion toolkit for Bevy


Reusable first-person camera toolkit for Bevy with a minimal orientation core and opt-in motion/effects layers.

FpsCameraPlugin is now a pure camera module by default: it handles look intent, optional free-look, external motion ingestion, additive external camera effects, projection sync, and final transform sync. Internal walk/jump/crouch simulation, gait-driven bob, recoil, landing, footsteps, comfort scaling, and viewmodel lag all live behind optional plugins.

Quick Start

[dependencies]
saddle-camera-fps-camera = { git = "https://github.com/julien-blanchon/saddle-camera-fps-camera" }
bevy = "0.18"
[dependencies]
saddle-camera-fps-camera = { git = "https://github.com/julien-blanchon/saddle-camera-fps-camera" }
bevy = "0.18"
use bevy::prelude::*;
use saddle_camera_fps_camera::{FpsCamera, FpsCameraConfig, FpsCameraPlugin};

#[derive(States, Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum DemoState {
    #[default]
    Gameplay,
}

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            FpsCameraPlugin::new(OnEnter(DemoState::Gameplay), OnExit(DemoState::Gameplay), Update),
        ))
        .init_state::<DemoState>()
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn((
        Name::new("Player Camera"),
        Camera3d::default(),
        Transform::from_xyz(0.0, 1.62, 6.0),
        FpsCamera,
        FpsCameraConfig::default(),
    ));
}
use bevy::prelude::*;
use saddle_camera_fps_camera::{FpsCamera, FpsCameraConfig, FpsCameraPlugin};

#[derive(States, Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum DemoState {
    #[default]
    Gameplay,
}

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins,
            FpsCameraPlugin::new(OnEnter(DemoState::Gameplay), OnExit(DemoState::Gameplay), Update),
        ))
        .init_state::<DemoState>()
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn((
        Name::new("Player Camera"),
        Camera3d::default(),
        Transform::from_xyz(0.0, 1.62, 6.0),
        FpsCamera,
        FpsCameraConfig::default(),
    ));
}

For examples and always-on tools, FpsCameraPlugin::always_on(Update) is the simple constructor.

If another controller owns authoritative movement, write FpsCameraExternalMotion every frame. If you want the old built-in locomotion + effects stack, use FpsCameraLegacyPlugin or opt into the granular plugins directly.

Plugin Layers

PluginPurpose
FpsCameraPluginMinimal core: look, free-look recentering, external motion sync, external additive effects, projection sync, transform sync
FpsCameraLocomotionPluginOptional internal flat-ground locomotion with sprint/crouch/jump state
FpsCameraEffectsPluginOptional bob, landing, recoil, trauma shake, dynamic FOV, lean, viewmodel lag, and footstep/landing messages
FpsCameraLegacyPluginConvenience wrapper that restores the pre-split full-feature stack

Public API

TypePurpose
FpsCameraSystemsPublic ordering hooks: ReadIntent, UpdateLocomotion, UpdateCameraState, ComposeEffects, SyncProjection, SyncTransform
FpsCameraMarker component for camera entities managed by this crate
FpsCameraConfigUnified tuning surface. Core uses look/aim/free-look/base FOV/collision, optional plugins consume locomotion and presentation fields
FpsCameraIntentExternal intent inbox. Core consumes look + aim/free-look input; optional plugins may also consume move/jump/sprint/crouch/lean
FpsCameraRuntimeReadable runtime state: logical position, velocity, yaw/pitch, stance alphas, trauma, bob phase, FOV, and composed render output
FpsCameraExternalMotionAuthoritative movement seam for position, velocity, grounded state, landing impulse, and optional eye-height/stance overrides
FpsCameraExternalEffectsOptional additive extension seam for custom translation, rotation, or FOV effects
FpsCameraCollisionFeedbackOptional collision feedback component for external physics to push the camera away from walls
CameraEffectLayer / CameraEffectStackPure additive layer model for composing cosmetic view motion
MessagesFootstepEvent, LandedEvent, CameraShakeRequest, CameraRecoilRequest are registered by FpsCameraEffectsPlugin / FpsCameraLegacyPlugin

Configuration Overview

FpsCameraConfig is intentionally split by concern:

  • Core: look, aim, free_look, fov.base_fov, collision
  • Optional locomotion: movement, crouch, jump
  • Optional presentation: head_bob, tilt, landing, recoil, shake, viewmodel, comfort
  • Shared by optional layers: fov.speed_boost, fov.sprint_boost, lean

This keeps the public config flat for legacy/full-feature users while making the default runtime a pure camera-orientation stack.

Integration Seams

  • FpsCameraPlugin alone gives you a stable first-person orientation core that can stay fixed at its spawn transform or follow FpsCameraExternalMotion.
  • For real character controllers or physics, feed FpsCameraExternalMotion every frame and let optional plugins derive presentation from that authoritative state.
  • If another system needs custom view effects, write FpsCameraExternalEffects instead of mutating Transform directly.
  • For camera collision avoidance, feed FpsCameraCollisionFeedback from your physics pipeline. The crate applies push-back in SyncTransform without depending on any specific physics engine.
  • FpsCameraRuntime::viewmodel_translation and viewmodel_rotation are only populated when FpsCameraEffectsPlugin (or FpsCameraLegacyPlugin) is active.

Examples

ExamplePurposeRun
basicLegacy convenience wrapper with built-in walk/jump/crouch defaultscargo run -p saddle-camera-fps-camera-example-basic
external_motionMinimal core + optional effects layered on top of an authoritative controllercargo run -p saddle-camera-fps-camera-example-external-motion
groundedLegacy wrapper with heavier sprint, crouch, jump, and landing feedbackcargo run -p saddle-camera-fps-camera-example-grounded
effectsLegacy wrapper with timed recoil and trauma pulsescargo run -p saddle-camera-fps-camera-example-effects
tacticalLegacy wrapper tuned for ADS, lean, and free-lookcargo run -p saddle-camera-fps-camera-example-tactical
comfortLegacy wrapper with low-motion accessibility-focused tuningcargo run -p saddle-camera-fps-camera-example-comfort

Every example includes a live saddle-pane control surface so the main parameters can be tuned while the scene is running. Press Tab to toggle mouse capture and interact with the pane, or Esc to release the cursor.

The P0 external-motion integration demo also ships with example-level smoke coverage:

cargo run -p saddle-camera-fps-camera-example-external-motion --features e2e -- fps_external_motion_smoke
cargo run -p saddle-camera-fps-camera-example-external-motion --features e2e -- fps_external_motion_smoke

Workspace Lab

The richer lab app lives inside the crate at shared/camera/saddle-camera-fps-camera/examples/lab:

cargo run -p saddle-camera-fps-camera-lab
cargo run -p saddle-camera-fps-camera-lab

With E2E enabled:

cargo run -p saddle-camera-fps-camera-lab --features e2e -- fps_camera_smoke
cargo run -p saddle-camera-fps-camera-lab --features e2e -- fps_camera_smoke

More Docs