custom camera rig

* should not go into level geometry anymore
This commit is contained in:
2025-03-19 20:23:48 +01:00
parent b6efa7e85d
commit 7951d613c4
7 changed files with 117 additions and 106 deletions

12
Cargo.lock generated
View File

@@ -697,17 +697,6 @@ dependencies = [
"sysinfo", "sysinfo",
] ]
[[package]]
name = "bevy_dolly"
version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68783929254625d3ffa54a6fed7ff7216abf90296eea5e3885cde419d5daead0"
dependencies = [
"bevy",
"bevy_math",
"bevy_transform",
]
[[package]] [[package]]
name = "bevy_ecs" name = "bevy_ecs"
version = "0.15.3" version = "0.15.3"
@@ -2816,7 +2805,6 @@ dependencies = [
"bevy-tnua", "bevy-tnua",
"bevy-tnua-avian3d", "bevy-tnua-avian3d",
"bevy_asset_loader", "bevy_asset_loader",
"bevy_dolly",
"bevy_sprite3d", "bevy_sprite3d",
"bevy_trenchbroom", "bevy_trenchbroom",
"nil", "nil",

View File

@@ -14,7 +14,6 @@ nil = "0.14.0"
bevy-inspector-egui = "0.29.1" bevy-inspector-egui = "0.29.1"
bevy-tnua = "0.21.0" bevy-tnua = "0.21.0"
bevy-tnua-avian3d = "0.2.0" bevy-tnua-avian3d = "0.2.0"
bevy_dolly = { version = "0.0.5", default-features = false }
bevy_asset_loader = "0.22.0" bevy_asset_loader = "0.22.0"
bevy_sprite3d = "4.0.0" bevy_sprite3d = "4.0.0"
rand = "0.8.5" rand = "0.8.5"

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
billboards::Billboard, billboards::Billboard,
player::{Player, PlayerHead}, player::{Player, PlayerRig},
tb_entities::EnemySpawn, tb_entities::EnemySpawn,
}; };
use bevy::prelude::*; use bevy::prelude::*;
@@ -84,7 +84,7 @@ fn update(
mut state: ResMut<AimState>, mut state: ResMut<AimState>,
query: Query<(Entity, &Transform), With<EnemySpawn>>, query: Query<(Entity, &Transform), With<EnemySpawn>>,
player_pos: Query<&Transform, With<Player>>, player_pos: Query<&Transform, With<Player>>,
player_rot: Query<&Transform, With<PlayerHead>>, player_rot: Query<&Transform, With<PlayerRig>>,
) { ) {
let Some(player_pos) = player_pos.iter().next().map(|t| t.translation) else { let Some(player_pos) = player_pos.iter().next().map(|t| t.translation) else {
return; return;

View File

@@ -1,38 +1,77 @@
use avian3d::prelude::*;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_dolly::prelude::*;
impl GameCameraRig { use crate::physics_layers::GameLayer;
pub fn new_with_arm(arm: Vec3) -> Self {
Self(
CameraRig::builder()
.with(Position::new(Vec3::ZERO))
.with(Rotation::new(Quat::IDENTITY))
.with(Smooth::new_position(1.25).predictive(true))
.with(Smooth::new_rotation(0.5))
.with(Arm::new(arm))
.with(
LookAt::new(Vec3::ZERO + Vec3::Y)
.tracking_smoothness(0.2)
.tracking_predictive(true),
)
.build(),
)
}
pub fn set_position_target(&mut self, target_position: Vec3, target_rotation: Quat) { #[derive(Component, Reflect, Debug)]
self.driver_mut::<Position>().position = target_position; pub struct CameraTarget;
self.driver_mut::<Rotation>().rotation = target_rotation;
self.driver_mut::<LookAt>().target = target_position + Vec3::Y; #[derive(Component, Reflect, Debug)]
pub struct CameraArmRotation;
#[derive(Component, Reflect, Debug)]
pub struct MainCamera {
arm: Vec3,
}
impl MainCamera {
fn new(arm: Vec3) -> Self {
Self { arm }
} }
} }
/// A custom camera rig which combines smoothed movement with a look-at driver. pub fn plugin(app: &mut App) {
#[derive(Component, Debug, Deref, DerefMut)] app.add_systems(Startup, startup);
pub struct GameCameraRig(CameraRig); app.add_systems(Update, update);
}
// Turn the nested rig into a driver, so it can be used in another rig.
impl RigDriver for GameCameraRig { fn startup(mut commands: Commands) {
fn update(&mut self, params: RigUpdateParams) -> Transform { commands.spawn((
self.0.update(params.delta_time_seconds) Camera3d::default(),
} MainCamera::new(Vec3::new(0., 1., -12.)),
));
}
fn update(
mut cam: Query<
(&MainCamera, &mut Transform),
(Without<CameraTarget>, Without<CameraArmRotation>),
>,
target: Query<&GlobalTransform, (With<CameraTarget>, Without<CameraArmRotation>)>,
arm_rotation: Query<&Transform, With<CameraArmRotation>>,
spatial_query: SpatialQuery,
) {
let Ok(target) = target.get_single().map(|t| t.translation()) else {
return;
};
let Ok(rotation) = arm_rotation.get_single() else {
return;
};
let Ok((camera, mut cam_transform)) = cam.get_single_mut() else {
return;
};
let target_transform = Transform::from_translation(target)
* Transform::from_rotation(rotation.rotation)
* Transform::from_translation(camera.arm);
let ideal_cam_pos = target_transform.translation;
let max_distance = camera.arm.length();
let Ok(direction) = Dir3::new(ideal_cam_pos - target) else {
return;
};
let filter = SpatialQueryFilter::from_mask(LayerMask(GameLayer::Level.to_bits()));
let cam_pos = if let Some(first_hit) =
spatial_query.cast_ray(target, direction, max_distance, false, &filter)
{
target + (direction * first_hit.distance)
} else {
ideal_cam_pos
};
*cam_transform = Transform::from_translation(cam_pos).looking_at(target, Vec3::Y);
} }

View File

@@ -10,6 +10,7 @@ mod heads_ui;
mod keys; mod keys;
mod movables; mod movables;
mod npc; mod npc;
mod physics_layers;
mod platforms; mod platforms;
mod player; mod player;
mod shooting; mod shooting;
@@ -25,11 +26,10 @@ use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::prelude::*; use bevy::prelude::*;
use bevy::render::view::ColorGrading; use bevy::render::view::ColorGrading;
use bevy::scene::SceneInstanceReady; use bevy::scene::SceneInstanceReady;
use bevy_dolly::prelude::*;
use bevy_tnua::prelude::TnuaControllerPlugin; use bevy_tnua::prelude::TnuaControllerPlugin;
use bevy_tnua_avian3d::TnuaAvian3dPlugin; use bevy_tnua_avian3d::TnuaAvian3dPlugin;
use bevy_trenchbroom::prelude::*; use bevy_trenchbroom::prelude::*;
use camera::GameCameraRig; use physics_layers::GameLayer;
#[derive(Resource, Reflect, Debug)] #[derive(Resource, Reflect, Debug)]
#[reflect(Resource)] #[reflect(Resource)]
@@ -41,9 +41,6 @@ struct DebugVisuals {
pub cam_follow: bool, pub cam_follow: bool,
} }
#[derive(Component, Debug)]
pub struct MainCamera;
fn main() { fn main() {
let mut app = App::new(); let mut app = App::new();
@@ -73,8 +70,6 @@ fn main() {
// app.add_plugins(PhysicsDebugPlugin::default()); // app.add_plugins(PhysicsDebugPlugin::default());
// app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::default()); // app.add_plugins(bevy_inspector_egui::quick::WorldInspectorPlugin::default());
app.add_systems(Update, Dolly::<MainCamera>::update_active);
app.add_plugins(alien::plugin); app.add_plugins(alien::plugin);
app.add_plugins(cash::plugin); app.add_plugins(cash::plugin);
app.add_plugins(player::plugin); app.add_plugins(player::plugin);
@@ -91,6 +86,7 @@ fn main() {
app.add_plugins(cutscene::plugin); app.add_plugins(cutscene::plugin);
app.add_plugins(controls::plugin); app.add_plugins(controls::plugin);
app.add_plugins(sounds::plugin); app.add_plugins(sounds::plugin);
app.add_plugins(camera::plugin);
app.insert_resource(AmbientLight { app.insert_resource(AmbientLight {
color: Color::WHITE, color: Color::WHITE,
@@ -99,7 +95,7 @@ fn main() {
app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz"))); app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz")));
app.add_systems(Startup, (setup_cam, write_trenchbroom_config, music)); app.add_systems(Startup, (write_trenchbroom_config, music));
app.add_systems(PostStartup, setup_scene); app.add_systems(PostStartup, setup_scene);
app.add_systems( app.add_systems(
Update, Update,
@@ -109,19 +105,12 @@ fn main() {
app.run(); app.run();
} }
fn setup_cam(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
MainCamera,
Rig::builder()
.with(GameCameraRig::new_with_arm(Vec3::new(0., 5., 14.)))
.build(),
));
}
fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) { fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands commands
.spawn(SceneRoot(asset_server.load("maps/map1.map#Scene"))) .spawn((
CollisionLayers::new(LayerMask(GameLayer::Level.to_bits()), LayerMask::ALL),
SceneRoot(asset_server.load("maps/map1.map#Scene")),
))
.observe(|_t: Trigger<SceneInstanceReady>| { .observe(|_t: Trigger<SceneInstanceReady>| {
//TODO: use for state driven map loading //TODO: use for state driven map loading
info!("map loaded"); info!("map loaded");

8
src/physics_layers.rs Normal file
View File

@@ -0,0 +1,8 @@
use avian3d::prelude::PhysicsLayer;
#[derive(PhysicsLayer, Clone, Copy, Debug, Default)]
pub enum GameLayer {
#[default]
Level,
Player,
}

View File

@@ -1,10 +1,10 @@
use crate::{ use crate::{
DebugVisuals,
alien::{ALIEN_ASSET_PATH, Animations}, alien::{ALIEN_ASSET_PATH, Animations},
camera::GameCameraRig, camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent}, cash::{Cash, CashCollectEvent},
controls::Controls, controls::Controls,
heads_ui::HeadChanged, heads_ui::HeadChanged,
physics_layers::GameLayer,
tb_entities::SpawnPoint, tb_entities::SpawnPoint,
}; };
use avian3d::prelude::*; use avian3d::prelude::*;
@@ -12,10 +12,9 @@ use bevy::{
prelude::*, prelude::*,
window::{CursorGrabMode, PrimaryWindow}, window::{CursorGrabMode, PrimaryWindow},
}; };
use bevy_dolly::prelude::Rig;
use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*}; use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*};
use bevy_tnua_avian3d::TnuaAvian3dSensorShape; use bevy_tnua_avian3d::TnuaAvian3dSensorShape;
use std::{f32::consts::PI, time::Duration}; use std::time::Duration;
#[derive(Component, Default)] #[derive(Component, Default)]
pub struct Player; pub struct Player;
@@ -24,7 +23,7 @@ pub struct Player;
struct PlayerAnimations; struct PlayerAnimations;
#[derive(Component, Default)] #[derive(Component, Default)]
pub struct PlayerHead; struct PlayerHead;
#[derive(Component, Default)] #[derive(Component, Default)]
pub struct PlayerRig; pub struct PlayerRig;
@@ -47,7 +46,6 @@ pub fn plugin(app: &mut App) {
Update, Update,
( (
spawn, spawn,
update_camera,
collect_cash, collect_cash,
toggle_animation, toggle_animation,
setup_animations_marker_for_player, setup_animations_marker_for_player,
@@ -86,30 +84,39 @@ fn spawn(
.spawn(( .spawn((
Name::from("player"), Name::from("player"),
Player, Player,
CameraTarget,
transform, transform,
TransformInterpolation, TransformInterpolation,
TransformExtrapolation,
Visibility::default(), Visibility::default(),
RigidBody::Dynamic, RigidBody::Dynamic,
Collider::capsule(1.2, 1.5), Collider::capsule(1.2, 1.5),
CollisionLayers::new(LayerMask(GameLayer::Player.to_bits()), LayerMask::ALL),
LockedAxes::ROTATION_LOCKED, LockedAxes::ROTATION_LOCKED,
TnuaController::default(), TnuaController::default(),
TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)), TnuaAvian3dSensorShape(Collider::cylinder(0.8, 0.0)),
)) ))
.with_child(( .with_children(|parent| {
Name::from("head"), parent
PlayerHead, .spawn((
Transform::from_translation(Vec3::new(0., -0.5, 0.)) Name::from("body rig"),
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI)), PlayerRig,
SceneRoot(mesh), CameraArmRotation,
)) Transform::from_translation(Vec3::new(0., -3., 0.))
.with_child(( .with_rotation(Quat::from_rotation_y(std::f32::consts::PI))
Name::from("body rig"), .with_scale(Vec3::splat(1.4)),
PlayerRig, SceneRoot(
Transform::from_translation(Vec3::new(0., -3., 0.)) asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH)),
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI)) ),
.with_scale(Vec3::splat(1.5)), ))
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(ALIEN_ASSET_PATH))), .with_child((
)); Name::from("head"),
PlayerHead,
Transform::from_translation(Vec3::new(0., 1.6, 0.))
.with_scale(Vec3::splat(0.7)),
SceneRoot(mesh),
));
});
commands.spawn(( commands.spawn((
AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")), AudioPlayer::new(asset_server.load("sfx/heads/angry demonstrator.ogg")),
@@ -122,7 +129,7 @@ fn spawn(
fn rotate_view( fn rotate_view(
mut controls: ResMut<Controls>, mut controls: ResMut<Controls>,
// todo: Put the player head as a child of the rig to avoid this mess: // todo: Put the player head as a child of the rig to avoid this mess:
mut player: Query<&mut Transform, Or<(With<PlayerRig>, With<PlayerHead>)>>, mut player: Query<&mut Transform, With<PlayerRig>>,
) { ) {
for mut tr in &mut player { for mut tr in &mut player {
tr.rotate_y(controls.keyboard_state.look_dir.x * -0.001); tr.rotate_y(controls.keyboard_state.look_dir.x * -0.001);
@@ -207,25 +214,6 @@ fn apply_controls(
} }
} }
fn update_camera(
player: Query<&GlobalTransform, With<PlayerRig>>,
mut rig: Single<&mut Rig>,
res: Res<DebugVisuals>,
) {
let Some(player) = player.iter().next() else {
return;
};
if !res.cam_follow {
return;
}
rig.driver_mut::<GameCameraRig>().set_position_target(
player.translation(),
player.rotation() * Quat::from_rotation_y(PI),
);
}
fn collect_cash( fn collect_cash(
mut commands: Commands, mut commands: Commands,
mut collision_event_reader: EventReader<CollisionStarted>, mut collision_event_reader: EventReader<CollisionStarted>,