Skip to content

Instantly share code, notes, and snippets.

@Lightnet
Created February 26, 2025 22:54
Show Gist options
  • Save Lightnet/426b64d32635416fa0f359264322eea5 to your computer and use it in GitHub Desktop.
Save Lightnet/426b64d32635416fa0f359264322eea5 to your computer and use it in GitHub Desktop.
example switch cameras
// example_cameras.rs
// A 3D first-person multi-camera example using Bevy 0.15.3 and Bevy Rapier3D 0.28.
// Features multiple players with switchable first-person cameras, mouse capture, WASD movement, Space key jumping, and a debug UI.
// Player with ID 0 is active by default; debug UI shows the current player ID via a dedicated 2D camera.
// --- Notes ---
// This application creates a 3D world with:
// - A static ground box and directional boxes (North, South, East, West) colored by world axes (Z, -Z, X, -X).
// - Players identified by `PlayerID`, each with a `Player` component and a `CameraPlayer` child (Camera3d).
// - Player 0 starts controllable with its camera active; Tab cycles through players.
// - Mouse capture (any input) and release (Escape) for rotation.
// - WASD moves the active player in the camera’s forward direction; Space triggers a jump when grounded (first-person perspective).
// - Debug UI (toggleable with F1) displays FPS and active player ID (e.g., "Player 0" or "Player 1") via a dedicated Camera2d.
// - A directional light ensures visibility with shadows.
// --- How It Works ---
// - **Players**: Each entity has a `Player` component with a unique `PlayerID` (usize). Queried with `Entity` to access relationships.
// - **Cameras**: Each player has a `CameraPlayer` child (Camera3d) at head level (0.0, 0.5, 0.0). A separate Camera2d renders the UI.
// - **Control**: `GameConfig.active_player_id` determines the controlled player. Systems use `Parent` to match players and 3D cameras.
// - **Movement**: WASD uses the active camera’s forward/right vectors (XZ plane); Space adds vertical velocity for jumping when grounded.
// - **Rotation**: Mouse rotates the active player’s Camera3d, with the body inheriting rotation via parenting.
// - **Switching**: Tab increments `active_player_id`, cycling through players and updating `is_active` for their Camera3d.
// - **Debug UI**: Rendered by Camera2d with high order (1), updates every frame with the current `active_player_id` when `debug_visible` is true.
// --- Imports ---
use bevy::prelude::*;
use bevy::window::CursorGrabMode;
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, DiagnosticsStore};
use bevy_rapier3d::prelude::*;
// --- Components ---
#[derive(Component)]
struct Player {
id: usize, // Unique identifier for each player
}
#[derive(Component)]
struct CameraPlayer; // Marks the 3D camera component for each player
#[derive(Component)]
struct DebugText; // Marks the debug UI text entity
// --- Resources ---
#[derive(Resource)]
struct CaptureState {
captured: bool, // True when mouse is locked and hidden
}
#[derive(Resource)]
struct GameConfig {
movement_speed: f32,
mouse_sensitivity: f32,
active_player_id: usize, // ID of the currently controlled player (0-based)
debug_visible: bool,
jump_velocity: f32, // Initial upward velocity for jumping
gravity: f32, // Downward acceleration for falling
ground_timer: f32, // Time window (seconds) to allow jumping after grounding
}
impl Default for GameConfig {
fn default() -> Self {
GameConfig {
movement_speed: 8.0,
mouse_sensitivity: 0.2,
active_player_id: 0, // Start with Player ID 0 active
debug_visible: true,
jump_velocity: 5.0, // Matches grok_base01.rs
gravity: -9.81, // Standard gravity
ground_timer: 0.5, // 0.5s window for jumping after landing
}
}
}
// --- Main Application Setup ---
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(bevy::log::LogPlugin {
..default()
}))
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default())
.add_plugins(RapierDebugRenderPlugin::default())
.add_plugins(FrameTimeDiagnosticsPlugin)
.insert_resource(CaptureState { captured: false })
.insert_resource(GameConfig::default())
.add_systems(Startup, setup)
.add_systems(Update, (player_movement, camera_rotation, update_debug_ui))
.run();
}
// --- Systems ---
// Sets up the initial game world with entities
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
// Ground box: large, static base surface
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(10.0, 0.2, 10.0))),
MeshMaterial3d(materials.add(Color::srgb(0.5, 0.5, 0.5))),
Transform::default(),
RigidBody::Fixed,
Collider::cuboid(5.0, 0.1, 5.0),
));
// North box (Z-axis, blue)
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb(0.0, 0.0, 1.0))),
Transform::from_xyz(0.0, 0.5, 5.0),
RigidBody::Fixed,
Collider::cuboid(0.5, 0.5, 0.5),
));
// South box (-Z-axis, cyan)
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb(0.0, 1.0, 1.0))),
Transform::from_xyz(0.0, 0.5, -5.0),
RigidBody::Fixed,
Collider::cuboid(0.5, 0.5, 0.5),
));
// East box (X-axis, red)
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb(1.0, 0.0, 0.0))),
Transform::from_xyz(5.0, 0.5, 0.0),
RigidBody::Fixed,
Collider::cuboid(0.5, 0.5, 0.5),
));
// West box (-X-axis, pink)
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::srgb(1.0, 0.0, 1.0))),
Transform::from_xyz(-5.0, 0.5, 0.0),
RigidBody::Fixed,
Collider::cuboid(0.5, 0.5, 0.5),
));
// Player 1 (ID 0) with CameraPlayer
let player1_entity = commands
.spawn((
Mesh3d(meshes.add(Cuboid::new(0.5, 1.0, 0.5))),
MeshMaterial3d(materials.add(Color::srgb(0.0, 1.0, 0.0))), // Green
Transform::from_xyz(-2.0, 0.5, 0.0),
RigidBody::KinematicPositionBased,
Collider::cuboid(0.25, 0.5, 0.25),
KinematicCharacterController {
offset: CharacterLength::Absolute(0.01),
..default()
},
Player { id: 0 },
))
.id();
commands
.spawn((
Camera {
is_active: true,
order: 0, // Lower order for 3D cameras
..default()
},
Camera3d::default(),
Transform::from_xyz(0.0, 0.5, 0.0).looking_at(Vec3::new(0.0, 0.5, -1.0), Vec3::Y),
CameraPlayer,
))
.set_parent(player1_entity);
// Player 2 (ID 1) with CameraPlayer
let player2_entity = commands
.spawn((
Mesh3d(meshes.add(Cuboid::new(0.5, 1.0, 0.5))),
MeshMaterial3d(materials.add(Color::srgb(1.0, 1.0, 0.0))), // Yellow
Transform::from_xyz(2.0, 0.5, 0.0),
RigidBody::KinematicPositionBased,
Collider::cuboid(0.25, 0.5, 0.25),
KinematicCharacterController {
offset: CharacterLength::Absolute(0.01),
..default()
},
Player { id: 1 },
))
.id();
commands
.spawn((
Camera {
is_active: false,
order: 0, // Same order as Player 1’s camera
..default()
},
Camera3d::default(),
Transform::from_xyz(0.0, 0.5, 0.0).looking_at(Vec3::new(0.0, 0.5, -1.0), Vec3::Y),
CameraPlayer,
))
.set_parent(player2_entity);
// Lights
commands.spawn((
DirectionalLight {
illuminance: 10000.0,
shadows_enabled: true,
..default()
},
Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
));
// Debug UI with dedicated Camera2d
let font = asset_server.load("fonts/FiraSans-Bold.ttf");
commands.spawn((
Node {
position_type: PositionType::Absolute,
top: Val::Px(5.0),
left: Val::Px(5.0),
..default()
},
)).with_children(|parent| {
parent.spawn((
Text::new(""),
TextFont {
font,
font_size: 20.0,
..default()
},
TextColor(Color::WHITE),
DebugText,
));
});
// Add Camera2d for UI rendering, last in order to overlay 3D
commands.spawn((
Camera2d {
..default()
},
Camera {
order: 1, // Higher order ensures UI renders on top
..default()
},
));
}
// Handles player movement, including WASD and jumping with Space
fn player_movement(
time: Res<Time>,
keys: Res<ButtonInput<KeyCode>>,
config: Res<GameConfig>,
mut player_query: Query<(
Entity,
&mut KinematicCharacterController,
Option<&KinematicCharacterControllerOutput>,
&Player,
)>,
camera_query: Query<(&Transform, &Parent), With<CameraPlayer>>,
capture: Res<CaptureState>,
mut grounded_timer: Local<f32>, // Tracks time since last grounded for jumping
) {
let delta_time = time.delta_secs();
if !capture.captured {
return;
}
for (player_entity, mut controller, output, player) in player_query.iter_mut() {
if player.id == config.active_player_id {
if let Some((camera_transform, _)) = camera_query.iter().find(|(_, parent)| parent.get() == player_entity) {
let grounded = output.map(|o| o.grounded).unwrap_or(false);
let mut movement = Vec3::ZERO;
let mut vertical_movement = controller.translation.unwrap_or(Vec3::ZERO).y;
// Reset grounded timer when on ground
if grounded {
*grounded_timer = config.ground_timer; // 0.5s window
if vertical_movement < 0.0 { // Reset falling velocity
vertical_movement = 0.0;
}
}
// Horizontal movement based on camera direction
let forward = Vec3::new(camera_transform.forward().x, 0.0, camera_transform.forward().z).normalize_or_zero();
let right = Vec3::new(camera_transform.right().x, 0.0, camera_transform.right().z).normalize_or_zero();
if keys.pressed(KeyCode::KeyW) { movement += forward; }
if keys.pressed(KeyCode::KeyS) { movement -= forward; }
if keys.pressed(KeyCode::KeyA) { movement -= right; }
if keys.pressed(KeyCode::KeyD) { movement += right; }
if movement != Vec3::ZERO {
movement = movement.normalize() * config.movement_speed * delta_time;
}
// Jumping with Space key within grounded timer window
if *grounded_timer > 0.0 {
*grounded_timer -= delta_time;
if keys.just_pressed(KeyCode::Space) {
vertical_movement = config.jump_velocity; // Apply jump velocity
*grounded_timer = 0.0; // Reset timer to prevent double jump
println!("Jump initiated for Player {}: {}", player.id, vertical_movement);
}
}
// Apply gravity when not grounded or during jump
if !grounded || vertical_movement > 0.0 {
vertical_movement += config.gravity * delta_time;
}
// Combine horizontal and vertical movement
movement.y = vertical_movement * delta_time;
controller.translation = Some(movement);
}
break;
}
}
}
// Manages camera rotation, mouse capture, and player switching
fn camera_rotation(
mut windows: Query<&mut Window>,
mouse: Res<ButtonInput<MouseButton>>,
keys: Res<ButtonInput<KeyCode>>,
mut motion: EventReader<bevy::input::mouse::MouseMotion>,
mut capture: ResMut<CaptureState>,
mut config: ResMut<GameConfig>,
player_query: Query<(Entity, &Player)>,
mut camera_query: Query<(&mut Transform, &mut Camera, &Parent, &CameraPlayer)>,
) {
let mut window = windows.single_mut();
if !capture.captured {
if keys.get_just_pressed().next().is_some() || mouse.get_just_pressed().next().is_some() {
capture.captured = true;
window.cursor_options.visible = false;
window.cursor_options.grab_mode = CursorGrabMode::Locked;
println!("Mouse captured");
}
}
if capture.captured && keys.just_pressed(KeyCode::Escape) {
capture.captured = false;
window.cursor_options.visible = true;
window.cursor_options.grab_mode = CursorGrabMode::None;
println!("Mouse released");
}
let player_count = player_query.iter().count();
if keys.just_pressed(KeyCode::Tab) {
config.active_player_id = (config.active_player_id + 1) % player_count;
for (_, mut camera, parent, _) in camera_query.iter_mut() {
let player_id = player_query.iter().find(|(entity, _)| entity == &parent.get()).unwrap().1.id;
camera.is_active = player_id == config.active_player_id;
}
}
if capture.captured {
for (mut camera_transform, _, parent, _) in camera_query.iter_mut() {
let player_id = player_query.iter().find(|(entity, _)| entity == &parent.get()).unwrap().1.id;
if player_id == config.active_player_id {
for ev in motion.read() {
let delta = ev.delta * config.mouse_sensitivity * 0.01;
camera_transform.rotate_y(-delta.x);
camera_transform.rotate_local_x(-delta.y);
let (yaw, pitch, _) = camera_transform.rotation.to_euler(EulerRot::YXZ);
camera_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch.clamp(-std::f32::consts::PI / 2.0 + 0.01, std::f32::consts::PI / 2.0 - 0.01), 0.0);
}
break;
}
}
}
}
// Updates the debug UI with game state information
fn update_debug_ui(
diagnostics: Res<DiagnosticsStore>,
keys: Res<ButtonInput<KeyCode>>,
mut text_query: Query<&mut Text, With<DebugText>>,
config: Res<GameConfig>,
) {
let mut text = text_query.single_mut();
if config.debug_visible {
let fps = diagnostics
.get(&FrameTimeDiagnosticsPlugin::FPS)
.and_then(|d| d.smoothed())
.unwrap_or(0.0);
let camera_name = format!("Player {}", config.active_player_id);
let active_camera = config.active_player_id;
text.0 = format!(
"FPS: {:.1}\nCamera Name: {}\nActive Camera: {}",
fps,
camera_name,
active_camera
);
} else {
text.0 = String::new(); // Clear text when hidden
}
// Toggle debug visibility with F1
if keys.just_pressed(KeyCode::F1) {
//let mut config = config.into_inner();
//config.debug_visible = !config.debug_visible;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment