use bevy::prelude::*;
use avian3d::prelude::*;
use bevy_tnua::builtins::{
TnuaBuiltinJump, TnuaBuiltinJumpConfig, TnuaBuiltinWalk, TnuaBuiltinWalkConfig,
};
use bevy_tnua::{
TnuaAnimatingState, TnuaAnimatingStateDirective, builtins::TnuaBuiltinJumpMemory, prelude::*,
};
use bevy_tnua_avian3d::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
PhysicsPlugins::default(),
TnuaControllerPlugin::<ControlScheme>::new(FixedUpdate),
TnuaAvian3dPlugin::new(FixedUpdate),
))
.add_systems(
Startup,
(setup_camera_and_lights, setup_level, setup_player),
)
.add_systems(Update, apply_controls.in_set(TnuaUserControlsSystems))
.add_systems(FixedUpdate, (prepare_animations, handle_animating))
.run();
}
#[derive(TnuaScheme)]
#[scheme(basis = TnuaBuiltinWalk)]
enum ControlScheme {
Jump(TnuaBuiltinJump),
}
pub enum AnimationState {
Standing,
Running(f32),
Jumping,
Falling,
}
#[derive(Resource)]
struct AnimationNodes {
standing: AnimationNodeIndex,
running: AnimationNodeIndex,
jumping: AnimationNodeIndex,
falling: AnimationNodeIndex,
}
fn setup_camera_and_lights(mut commands: Commands) {
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 16.0, 40.0).looking_at(Vec3::new(0.0, 10.0, 0.0), Vec3::Y),
));
commands.spawn((PointLight::default(), Transform::from_xyz(5.0, 5.0, 5.0)));
commands.spawn((
DirectionalLight {
illuminance: 4000.0,
shadows_enabled: true,
..Default::default()
},
Transform::default().looking_at(-Vec3::Y, Vec3::Z),
));
}
fn setup_level(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(128.0, 128.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
RigidBody::Static,
Collider::half_space(Vec3::Y),
));
}
#[derive(Resource)]
struct PlayerGltfHandle(Handle<Gltf>);
fn setup_player(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut control_scheme_configs: ResMut<Assets<ControlSchemeConfig>>,
) {
commands.insert_resource(PlayerGltfHandle(asset_server.load("player.glb")));
commands.spawn((
SceneRoot(asset_server.load("player.glb#Scene0")),
Transform::from_xyz(0.0, 2.0, 0.0),
TnuaAnimatingState::<AnimationState>::default(),
RigidBody::Dynamic,
Collider::capsule(0.5, 1.0),
TnuaController::<ControlScheme>::default(),
TnuaConfig::<ControlScheme>(control_scheme_configs.add(ControlSchemeConfig {
basis: TnuaBuiltinWalkConfig {
speed: 10.0,
float_height: 2.0,
..Default::default()
},
jump: TnuaBuiltinJumpConfig {
height: 4.0,
..Default::default()
},
})),
TnuaAvian3dSensorShape(Collider::cylinder(0.49, 0.0)),
LockedAxes::ROTATION_LOCKED.unlock_rotation_y(),
));
}
fn prepare_animations(
handle: Option<Res<PlayerGltfHandle>>,
gltf_assets: Res<Assets<Gltf>>,
mut commands: Commands,
animation_player_query: Query<Entity, With<AnimationPlayer>>,
mut animation_graphs_assets: ResMut<Assets<AnimationGraph>>,
) {
let Some(handle) = handle else { return };
let Some(gltf) = gltf_assets.get(&handle.0) else {
return;
};
let Ok(animation_player_entity) = animation_player_query.single() else {
return;
};
let mut graph = AnimationGraph::new();
let root_node = graph.root;
commands.insert_resource(AnimationNodes {
standing: graph.add_clip(gltf.named_animations["Standing"].clone(), 1.0, root_node),
running: graph.add_clip(gltf.named_animations["Running"].clone(), 1.0, root_node),
jumping: graph.add_clip(gltf.named_animations["Jumping"].clone(), 1.0, root_node),
falling: graph.add_clip(gltf.named_animations["Falling"].clone(), 1.0, root_node),
});
commands
.entity(animation_player_entity)
.insert(AnimationGraphHandle(animation_graphs_assets.add(graph)));
commands.remove_resource::<PlayerGltfHandle>();
}
fn apply_controls(
keyboard: Res<ButtonInput<KeyCode>>,
mut query: Query<&mut TnuaController<ControlScheme>>,
) {
let Ok(mut controller) = query.single_mut() else {
return;
};
controller.initiate_action_feeding();
let mut direction = Vec3::ZERO;
if keyboard.pressed(KeyCode::ArrowUp) {
direction -= Vec3::Z;
}
if keyboard.pressed(KeyCode::ArrowDown) {
direction += Vec3::Z;
}
if keyboard.pressed(KeyCode::ArrowLeft) {
direction -= Vec3::X;
}
if keyboard.pressed(KeyCode::ArrowRight) {
direction += Vec3::X;
}
controller.basis = TnuaBuiltinWalk {
desired_motion: direction.normalize_or_zero(),
desired_forward: Dir3::new(direction).ok(),
};
if keyboard.pressed(KeyCode::Space) {
controller.action(ControlScheme::Jump(Default::default()));
}
}
fn handle_animating(
mut player_query: Query<(
&TnuaController<ControlScheme>,
&mut TnuaAnimatingState<AnimationState>,
)>,
mut animation_player_query: Query<&mut AnimationPlayer>,
animation_nodes: Option<Res<AnimationNodes>>,
) {
let Ok((controller, mut animating_state)) = player_query.single_mut() else {
return;
};
let Ok(mut animation_player) = animation_player_query.single_mut() else {
return;
};
let Some(animation_nodes) = animation_nodes else {
return;
};
let current_status_for_animating = match controller.current_action.as_ref() {
Some(ControlSchemeActionState::Jump(state)) => {
match state.memory {
TnuaBuiltinJumpMemory::NoJump => return,
TnuaBuiltinJumpMemory::StartingJump { .. } => AnimationState::Jumping,
TnuaBuiltinJumpMemory::SlowDownTooFastSlopeJump { .. } => AnimationState::Jumping,
TnuaBuiltinJumpMemory::MaintainingJump { .. } => AnimationState::Jumping,
TnuaBuiltinJumpMemory::StoppedMaintainingJump => AnimationState::Jumping,
TnuaBuiltinJumpMemory::FallSection => AnimationState::Falling,
}
}
None => {
if controller.basis_memory.standing_on_entity().is_none() {
AnimationState::Falling
} else {
let speed = controller.basis_memory.running_velocity.length();
if 0.01 < speed {
AnimationState::Running(0.1 * speed)
} else {
AnimationState::Standing
}
}
}
};
let animating_directive = animating_state.update_by_discriminant(current_status_for_animating);
match animating_directive {
TnuaAnimatingStateDirective::Maintain { state } => {
if let AnimationState::Running(speed) = state
&& let Some(animation) = animation_player.animation_mut(animation_nodes.running)
{
animation.set_speed(*speed);
}
}
TnuaAnimatingStateDirective::Alter {
old_state: _,
state,
} => {
animation_player.stop_all();
match state {
AnimationState::Standing => {
animation_player
.start(animation_nodes.standing)
.set_speed(1.0)
.repeat();
}
AnimationState::Running(speed) => {
animation_player
.start(animation_nodes.running)
.set_speed(*speed)
.repeat();
}
AnimationState::Jumping => {
animation_player
.start(animation_nodes.jumping)
.set_speed(2.0);
}
AnimationState::Falling => {
animation_player
.start(animation_nodes.falling)
.set_speed(1.0);
}
}
}
}
}