Tnua and dolly (#2)
* character controller + camera rig * make tnua work * cash collect
This commit is contained in:
38
src/camera.rs
Normal file
38
src/camera.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
47
src/main.rs
47
src/main.rs
@@ -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
189
src/player.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user