-
Notifications
You must be signed in to change notification settings - Fork 16
/
example_animating.rs
350 lines (319 loc) · 14.3 KB
/
example_animating.rs
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
// ###############################################################################################
// # The important system for this example is `handle_animating`. Everything else is just setup. #
// ###############################################################################################
use bevy::prelude::*;
use avian3d::prelude::*;
use bevy_tnua::{
builtins::TnuaBuiltinJumpState, prelude::*, TnuaAnimatingState, TnuaAnimatingStateDirective,
};
use bevy_tnua_avian3d::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
PhysicsPlugins::default(),
TnuaControllerPlugin::default(),
TnuaAvian3dPlugin::default(),
))
.add_systems(
Startup,
(setup_camera_and_lights, setup_level, setup_player),
)
.add_systems(
Update,
(
apply_controls.in_set(TnuaUserControlsSystemSet),
prepare_animations,
handle_animating,
),
)
.run();
}
// This enum projects the player's state into something we can use to decide which animation to
// play. Each variant of this enum corresponds to an animation, and the variant data can affect the
// animation's parameters.
//
// By itself this does not do much, but we can attach a `TnuaAnimatingState<AnimationState>`
// component to the player entity and use it to track the animating state.
pub enum AnimationState {
Standing,
Running(f32),
Jumping,
Falling,
}
// Bevy's animation handling is a bit manual. We'll use this struct to register the animation clips
// as nodes in the animation graph.
#[derive(Resource)]
struct AnimationNodes {
standing: AnimationNodeIndex,
running: AnimationNodeIndex,
jumping: AnimationNodeIndex,
falling: AnimationNodeIndex,
}
// No Tnua-related setup here - this is just normal Bevy stuff.
fn setup_camera_and_lights(mut commands: Commands) {
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 16.0, 40.0)
.looking_at(Vec3::new(0.0, 10.0, 0.0), Vec3::Y),
..Default::default()
});
commands.spawn(PointLightBundle {
transform: Transform::from_xyz(5.0, 5.0, 5.0),
..default()
});
// A directly-down light to tell where the player is going to land.
commands.spawn(DirectionalLightBundle {
directional_light: DirectionalLight {
illuminance: 4000.0,
shadows_enabled: true,
..Default::default()
},
transform: Transform::default().looking_at(-Vec3::Y, Vec3::Z),
..Default::default()
});
}
// 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((
PbrBundle {
mesh: meshes.add(Plane3d::default().mesh().size(128.0, 128.0)),
material: materials.add(Color::WHITE),
..Default::default()
},
RigidBody::Static,
Collider::half_space(Vec3::Y),
));
}
// Bevy assets are a bit weird. We'll use this resource to hold onto the model file so that we can
// extract the animation clips from it and build the animation graph.
#[derive(Resource)]
struct PlayerGltfHandle(Handle<Gltf>);
fn setup_player(mut commands: Commands, asset_server: Res<AssetServer>) {
// We'll need this in `prepare_animations` to build the animation graph.
commands.insert_resource(PlayerGltfHandle(asset_server.load("player.glb")));
commands.spawn((
SceneBundle {
scene: asset_server.load("player.glb#Scene0"),
transform: Transform::from_xyz(0.0, 2.0, 0.0),
..Default::default()
},
// We'll need this in the `handle_animating` system to keep track of the players animating
// state.
TnuaAnimatingState::<AnimationState>::default(),
// 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 bundle holds the main components.
TnuaControllerBundle::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.unlock_rotation_y(),
));
// NOTE: if this was Rapier, we'd also need `TnuaRapier3dIOBundle`. Avian does not need it.
}
// No Tnua-related setup here - this is just for dealing with Bevy's animation graph.
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.get_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(animation_graphs_assets.add(graph));
// So that we won't run this again
commands.remove_resource::<PlayerGltfHandle>();
}
fn apply_controls(keyboard: Res<ButtonInput<KeyCode>>, mut query: Query<&mut TnuaController>) {
let Ok(mut controller) = query.get_single_mut() else {
return;
};
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;
}
// Feed the basis every frame. Even if the player doesn't move - just use `desired_velocity:
// Vec3::ZERO`. `TnuaController` starts without a basis, which will make the character collider
// just fall.
controller.basis(TnuaBuiltinWalk {
// The `desired_velocity` determines how the character will move.
desired_velocity: direction.normalize_or_zero() * 10.0,
desired_forward: Dir3::new(direction).ok(),
// 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: 2.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()
});
}
}
// This is the important system for this example
fn handle_animating(
mut player_query: Query<(&TnuaController, &mut TnuaAnimatingState<AnimationState>)>,
mut animation_player_query: Query<&mut AnimationPlayer>,
animation_nodes: Option<Res<AnimationNodes>>,
) {
// An actual game should match the animation player and the controller. Here we cheat for
// simplicity and use the only controller and only player.
let Ok((controller, mut animating_state)) = player_query.get_single_mut() else {
return;
};
let Ok(mut animation_player) = animation_player_query.get_single_mut() else {
return;
};
let Some(animation_nodes) = animation_nodes else {
return;
};
// Here we use the data from TnuaController to determine what the character is currently doing,
// so that we can later use that information to decide which animation to play.
// First we look at the `action_name` to determine which action (if at all) the character is
// currently performing:
let current_status_for_animating = match controller.action_name() {
// Unless you provide the action names yourself, prefer matching against the `NAME` const
// of the `TnuaAction` trait. Once `type_name` is stabilized as `const` Tnua will use it to
// generate these names automatically, which may result in a change to the name.
Some(TnuaBuiltinJump::NAME) => {
// In case of jump, we want to cast it so that we can get the concrete jump state.
let (_, jump_state) = controller
.concrete_action::<TnuaBuiltinJump>()
.expect("action name mismatch");
// Depending on the state of the jump, we need to decide if we want to play the jump
// animation or the fall animation.
match jump_state {
TnuaBuiltinJumpState::NoJump => return,
TnuaBuiltinJumpState::StartingJump { .. } => AnimationState::Jumping,
TnuaBuiltinJumpState::SlowDownTooFastSlopeJump { .. } => AnimationState::Jumping,
TnuaBuiltinJumpState::MaintainingJump => AnimationState::Jumping,
TnuaBuiltinJumpState::StoppedMaintainingJump => AnimationState::Jumping,
TnuaBuiltinJumpState::FallSection => AnimationState::Falling,
}
}
// Tnua should only have the `action_name` of the actions you feed to it. If it has
// anything else - consider it a bug.
Some(other) => panic!("Unknown action {other}"),
// No action name means that no action is currently being performed - which means the
// animation should be decided by the basis.
None => {
// If there is no action going on, we'll base the animation on the state of the
// basis.
let Some((_, basis_state)) = controller.concrete_basis::<TnuaBuiltinWalk>() else {
// Since we only use the walk basis in this example, if we can't get get this
// basis' state it probably means the system ran before any basis was set, so we
// just stkip this frame.
return;
};
if basis_state.standing_on_entity().is_none() {
// The walk basis keeps track of what the character is standing on. If it doesn't
// stand on anything, `standing_on_entity` will be empty - which means the
// character has walked off a cliff and needs to fall.
AnimationState::Falling
} else {
let speed = basis_state.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 } => {
// `Maintain` means that we did not switch to a different variant, so there is no need
// to change animations.
// Specifically for the running animation, even when the state remains the speed can
// still change. When it does, we simply need to update the speed in the animation
// player.
if let AnimationState::Running(speed) = state {
if let Some(animation) = animation_player.animation_mut(animation_nodes.running) {
animation.set_speed(*speed);
}
}
}
TnuaAnimatingStateDirective::Alter {
old_state: _,
state,
} => {
// `Alter` means that we have switched to a different variant and need to play a
// different animation.
// First - stop the currently running animation. We don't check which one is running
// here because we just assume it belongs to the old state, but more sophisticated code
// can try to phase from the old animation to the new one.
animation_player.stop_all();
// Depending on the new state, we choose the animation to run and its parameters (here
// they are the speed and whether or not to repeat)
match state {
AnimationState::Standing => {
animation_player
.start(animation_nodes.standing)
.set_speed(1.0)
.repeat();
}
AnimationState::Running(speed) => {
animation_player
.start(animation_nodes.running)
// The running animation, in particular, has a speed that depends on how
// fast the character is running. Note that if the speed changes while the
// character is still running we won't get `Alter` again - so it's
// important to also update the speed in `Maintain { State: 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);
}
}
}
}
}