Tnua and dolly (#2)

* character controller + camera rig
* make tnua work
* cash collect
This commit is contained in:
extrawurst
2025-03-05 00:04:15 +01:00
committed by GitHub
parent 6cbab44fb3
commit f44f51aa8f
6 changed files with 309 additions and 35 deletions

38
src/camera.rs Normal file
View File

@@ -0,0 +1,38 @@
use bevy::prelude::*;
use bevy_dolly::prelude::*;
impl GameCameraRig {
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(10.))
.with(Arm::new(arm))
.with(
LookAt::new(Vec3::ZERO + Vec3::Y)
.tracking_smoothness(1.25)
.tracking_predictive(true),
)
.build(),
)
}
pub fn set_position_target(&mut self, target_position: Vec3, target_rotation: Quat) {
self.driver_mut::<Position>().position = target_position;
self.driver_mut::<Rotation>().rotation = target_rotation;
self.driver_mut::<LookAt>().target = target_position + Vec3::Y;
}
}
/// A custom camera rig which combines smoothed movement with a look-at driver.
#[derive(Component, Debug, Deref, DerefMut)]
pub struct GameCameraRig(CameraRig);
// Turn the nested rig into a driver, so it can be used in another rig.
impl RigDriver for GameCameraRig {
fn update(&mut self, params: RigUpdateParams) -> Transform {
self.0.update(params.delta_time_seconds)
}
}

View File

@@ -1,5 +1,7 @@
mod alien;
mod camera;
mod cash;
mod player;
mod tb_entities;
use avian3d::PhysicsPlugins;
@@ -7,8 +9,11 @@ use avian3d::prelude::*;
use bevy::core_pipeline::tonemapping::Tonemapping;
use bevy::prelude::*;
use bevy::render::view::ColorGrading;
use bevy_flycam::prelude::*;
use bevy_dolly::prelude::*;
use bevy_tnua::prelude::TnuaControllerPlugin;
use bevy_tnua_avian3d::TnuaAvian3dPlugin;
use bevy_trenchbroom::prelude::*;
use camera::GameCameraRig;
#[derive(Resource, Reflect, Debug)]
#[reflect(Resource)]
@@ -19,6 +24,9 @@ struct DebugVisuals {
pub shadows: bool,
}
#[derive(Component, Debug)]
pub struct MainCamera;
fn main() {
let mut app = App::new();
@@ -35,18 +43,18 @@ fn main() {
}));
app.add_plugins(PhysicsPlugins::default());
// app.add_plugins(PhysicsDebugPlugin::default());
app.add_plugins(PhysicsDebugPlugin::default());
app.add_plugins((
TnuaControllerPlugin::new(FixedUpdate),
TnuaAvian3dPlugin::new(FixedUpdate),
));
// bevy_flycam setup so we can get a closer look at the scene, mainly for debugging
app.add_plugins(PlayerPlugin);
app.add_systems(Update, Dolly::<MainCamera>::update_active);
app.add_plugins(alien::plugin);
app.add_plugins(cash::plugin);
app.add_plugins(player::plugin);
app.insert_resource(MovementSettings {
sensitivity: 0.00005,
speed: 12.,
});
app.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 400.,
@@ -55,7 +63,7 @@ fn main() {
app.add_plugins(TrenchBroomPlugin(TrenchBroomConfig::new("hedz")));
app.add_systems(Startup, (write_trenchbroom_config, music));
app.add_systems(Startup, (setup_cam, write_trenchbroom_config, music));
app.add_systems(PostStartup, setup_scene);
app.add_systems(
Update,
@@ -65,6 +73,16 @@ fn main() {
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>) {
commands.spawn(SceneRoot(asset_server.load("maps/map1.map#Scene")));
@@ -80,14 +98,6 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
..default()
},
));
commands.spawn((
SceneRoot(
asset_server
.load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry_demonstrator.glb")),
),
Transform::from_xyz(0.0, 5.0, 0.0),
));
}
fn spawn_box(
@@ -110,9 +120,6 @@ fn spawn_box(
fn music(asset_server: Res<AssetServer>, mut commands: Commands) {
commands.spawn(AudioPlayer::new(asset_server.load("sfx/music/02.ogg")));
commands.spawn(AudioPlayer::new(
asset_server.load("sfx/heads/angry_demonstrator.ogg"),
));
}
fn write_trenchbroom_config(server: Res<TrenchBroomServer>) {

189
src/player.rs Normal file
View File

@@ -0,0 +1,189 @@
use crate::{camera::GameCameraRig, cash::Cash, tb_entities::SpawnPoint};
use avian3d::prelude::*;
use bevy::{
input::mouse::MouseMotion,
prelude::*,
window::{CursorGrabMode, PrimaryWindow},
};
use bevy_dolly::prelude::Rig;
use bevy_tnua::{TnuaUserControlsSystemSet, prelude::*};
use bevy_tnua_avian3d::TnuaAvian3dSensorShape;
#[derive(Component, Default)]
pub struct Player;
#[derive(Resource, Default)]
struct PlayerSpawned {
spawned: bool,
}
pub fn plugin(app: &mut App) {
app.init_resource::<PlayerSpawned>();
app.add_systems(Startup, initial_grab_cursor);
app.add_systems(Update, (spawn, update_camera, cursor_events, collect_cash));
app.add_systems(
FixedUpdate,
apply_controls.in_set(TnuaUserControlsSystemSet),
);
}
fn spawn(
mut commands: Commands,
asset_server: Res<AssetServer>,
query: Query<&Transform, With<SpawnPoint>>,
mut player_spawned: ResMut<PlayerSpawned>,
) {
if player_spawned.spawned {
return;
}
let Some(spawn) = query.iter().next() else {
return;
};
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
let mesh = asset_server
.load(GltfAssetLabel::Scene(0).from_asset("models/heads/angry demonstrator.glb"));
commands
.spawn((
Name::from("player"),
Player,
transform,
Visibility::default(),
RigidBody::Dynamic,
Collider::capsule(1.2, 1.5),
LockedAxes::ROTATION_LOCKED,
TnuaController::default(),
TnuaAvian3dSensorShape(Collider::cylinder(1.0, 0.0)),
))
.with_child((
Transform::from_translation(Vec3::new(0., 1.0, 0.))
.with_rotation(Quat::from_rotation_y(std::f32::consts::PI)),
SceneRoot(mesh),
));
commands.spawn(AudioPlayer::new(
asset_server.load("sfx/heads/angry demonstrator.ogg"),
));
player_spawned.spawned = true;
}
fn cursor_events(
mut mouse: EventReader<MouseMotion>,
mut player: Query<&mut Transform, With<Player>>,
) {
let Ok(mut player) = player.get_single_mut() else {
return;
};
for ev in mouse.read() {
player.rotate_y(ev.delta.x * -0.001);
}
}
fn toggle_grab_cursor(window: &mut Window) {
match window.cursor_options.grab_mode {
CursorGrabMode::None => {
window.cursor_options.grab_mode = CursorGrabMode::Confined;
window.cursor_options.visible = false;
}
_ => {
window.cursor_options.grab_mode = CursorGrabMode::None;
window.cursor_options.visible = true;
}
}
}
fn initial_grab_cursor(mut primary_window: Query<&mut Window, With<PrimaryWindow>>) {
if let Ok(mut window) = primary_window.get_single_mut() {
toggle_grab_cursor(&mut window);
} else {
warn!("Primary window not found for `initial_grab_cursor`!");
}
}
fn apply_controls(
keyboard: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut TnuaController>,
player: Query<&Transform, With<Player>>,
) {
let Ok(mut controller) = query.get_single_mut() else {
return;
};
let Ok(player) = player.get_single() else {
return;
};
let mut direction = Vec3::ZERO;
if keyboard.pressed(KeyCode::KeyW) {
direction = player.forward().as_vec3();
}
if keyboard.pressed(KeyCode::KeyS) {
direction = player.forward().as_vec3() * -1.;
}
if keyboard.pressed(KeyCode::KeyA) {
direction += player.left().as_vec3();
}
if keyboard.pressed(KeyCode::KeyD) {
direction += player.right().as_vec3();
}
controller.basis(TnuaBuiltinWalk {
// The `desired_velocity` determines how the character will move.
desired_velocity: direction.normalize_or_zero() * 10.0,
// The `float_height` must be greater (even if by little) from the distance between the
// character's center and the lowest point of its collider.
float_height: 3.0,
// `TnuaBuiltinWalk` has many other fields for customizing the movement - but they have
// sensible defaults. Refer to the `TnuaBuiltinWalk`'s documentation to learn what they do.
..Default::default()
});
// Feed the jump action every frame as long as the player holds the jump button. If the player
// stops holding the jump button, simply stop feeding the action.
if keyboard.pressed(KeyCode::Space) {
controller.action(TnuaBuiltinJump {
// The height is the only mandatory field of the jump button.
height: 4.0,
// `TnuaBuiltinJump` also has customization fields with sensible defaults.
..Default::default()
});
}
}
fn update_camera(player: Query<&Transform, With<Player>>, mut rig: Single<&mut Rig>) {
let Some(player) = player.iter().next() else {
return;
};
rig.driver_mut::<GameCameraRig>()
.set_position_target(player.translation, player.rotation);
}
fn collect_cash(
mut commands: Commands,
mut collision_event_reader: EventReader<CollisionStarted>,
query_player: Query<&Player>,
query_cash: Query<&Cash>,
asset_server: Res<AssetServer>,
) {
for CollisionStarted(e1, e2) in collision_event_reader.read() {
let collect = if query_player.contains(*e1) && query_cash.contains(*e2) {
Some(*e2)
} else if query_player.contains(*e2) && query_cash.contains(*e1) {
Some(*e1)
} else {
None
};
if let Some(cash) = collect {
commands.spawn(AudioPlayer::new(asset_server.load("sfx/effects/cash.ogg")));
commands.entity(cash).despawn();
}
}
}

View File

@@ -89,9 +89,12 @@ impl CashSpawn {
let mesh = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/cash.glb"));
world
.commands()
.entity(entity)
.insert((Name::new("cash"), SceneRoot(mesh), Cash));
world.commands().entity(entity).insert((
Name::new("cash"),
SceneRoot(mesh),
Cash,
Collider::cuboid(2., 3.0, 2.),
Sensor,
));
}
}