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

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();
}
}
}