1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
use bevy::{color::palettes::css, prelude::*};
use avian3d::prelude::*;
use bevy_tnua::builtins::{
TnuaBuiltinJump, TnuaBuiltinJumpConfig, TnuaBuiltinWalk, TnuaBuiltinWalkConfig,
};
use bevy_tnua::prelude::*;
use bevy_tnua_avian3d::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
PhysicsPlugins::default(),
// We need both Tnua's main controller plugin, and the plugin to connect to the physics
// backend (in this case Avian 3D)
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))
.run();
}
#[derive(TnuaScheme)]
#[scheme(basis = TnuaBuiltinWalk)]
enum ControlScheme {
Jump(TnuaBuiltinJump),
}
// No Tnua-related setup here - this is just normal Bevy stuff.
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)));
// A directly-down light to tell where the player is going to land.
commands.spawn((
DirectionalLight {
illuminance: 4000.0,
shadow_maps_enabled: true,
..Default::default()
},
Transform::default().looking_at(-Vec3::Y, Vec3::Z),
));
}
// No Tnua-related setup here - this is just normal Bevy (and Avian) stuff.
fn setup_level(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawn the ground.
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),
));
// Spawn a little platform for the player to jump on.
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(4.0, 1.0, 4.0))),
MeshMaterial3d(materials.add(Color::from(css::GRAY))),
Transform::from_xyz(-6.0, 2.0, 0.0),
RigidBody::Static,
Collider::cuboid(4.0, 1.0, 4.0),
));
}
fn setup_player(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut control_scheme_configs: ResMut<Assets<ControlSchemeConfig>>,
) {
commands.spawn((
Mesh3d(meshes.add(Capsule3d {
radius: 0.5,
half_length: 0.5,
})),
MeshMaterial3d(materials.add(Color::from(css::DARK_CYAN))),
Transform::from_xyz(0.0, 2.0, 0.0),
// The player character needs to be configured as a dynamic rigid body of the physics
// engine.
RigidBody::Dynamic,
Collider::capsule(0.5, 1.0),
// This is Tnua's interface component.
TnuaController::<ControlScheme>::default(),
// The configuration asset can be loaded from a file, but here we are creating it by code
// and injecting it to the assets resource.
TnuaConfig::<ControlScheme>(control_scheme_configs.add(ControlSchemeConfig {
basis: TnuaBuiltinWalkConfig {
// 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: 1.5,
// `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()
},
jump: TnuaBuiltinJumpConfig {
// The height is the only mandatory field of the jump action.
height: 4.0,
// `TnuaBuiltinJump` also has customization fields with sensible defaults.
..Default::default()
},
})),
// A sensor shape is not strictly necessary, but without it we'll get weird results.
TnuaAvian3dSensorShape(Collider::cylinder(0.49, 0.0)),
// Tnua can fix the rotation, but the character will still get rotated before it can do so.
// By locking the rotation we can prevent this.
LockedAxes::ROTATION_LOCKED,
));
}
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;
}
// Set the basis every frame. Even if the player doesn't move - just use `desired_velocity:
// Vec3::ZERO` to reset the previous frame's input.
controller.basis = TnuaBuiltinWalk {
// The `desired_motion` determines how the character will move.
desired_motion: direction.normalize_or_zero(),
// The other field is `desired_forward` - but since the character model is a capsule we
// don't care the direction its "forward" is pointing.
..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(ControlScheme::Jump(Default::default()));
}
}