Skip to content

Commit efb8e1d

Browse files
authored
Split AnimationTarget into two components (#20774)
## Objective Add flexibility by refactoring `AnimationTarget` into two separate components. This will smooth the path for future animation features. ## Background `bevy_animation` animates entities by assigning them `AnimationTarget` components: ```rust struct AnimationTarget { player: Entity, id: AnimationTargetId, } ``` - `player: Entity` links to an entity that contains an `AnimationPlayer` component. An `AnimationPlayer` plays `AnimationClip` assets. - `id: AnimationTargetId` identifies which tracks in an `AnimationClip` apply to the target entity. When loading a glTF these components are automatically created. They can also be created manually. ## Problem The two parts of `AnimationTarget` often go together but sometimes would be better separated: 1. I might want to calculate the `AnimationTargetId` first, but not link it up to an `AnimationPlayer` until later (see #18262 for an example). 2. I might want to use `AnimationTargetId` but not use `AnimationPlayer` - maybe I've got a different component that plays `AnimationClip`s. In theory `player` could be left as `Entity::PLACEHOLDER`, but that's messy and will trigger a warning in `animate_targets`. ## Solution This PR splits `AnimationTarget` into two components: 1. `AnimationTargetId` is just the original struct with a component derive. 2. `AnimationPlayerTarget` is a new unit struct `(Entity)`. I'm not convinced `AnimationPlayerTarget` is a good name, but it does fit the usual source/target naming for entity relationships. `AnimationPlayerRef` was another candidate. `AnimationPlayerTarget` could be a relationship target, but there would be a performance cost from making `AnimationPlayer` a relationship source. Maybe it's still a good idea, but that's probably best left to another PR. ### Performance Profiled on `many_foxes` - difference was negligible. ### Testing Examples `animated_mesh`, `animated_transform`, `animated_ui`, `animation_masks`, `eased_motion`, `scene_viewer`. ## Future If this PR lands then I'll probably file a follow up that adds more flexibility to the the glTF loader creation of `AnimationTargetId` and `AnimationPlayer`. This will help #18262 and enable some other features.
1 parent 955b222 commit efb8e1d

File tree

8 files changed

+98
-94
lines changed

8 files changed

+98
-94
lines changed

crates/bevy_animation/src/lib.rs

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ impl VariableCurve {
9797
/// apply.
9898
///
9999
/// Because animation clips refer to targets by UUID, they can target any
100-
/// [`AnimationTarget`] with that ID.
100+
/// entity with that ID.
101101
#[derive(Asset, Reflect, Clone, Debug, Default)]
102102
#[reflect(Clone, Default)]
103103
pub struct AnimationClip {
@@ -158,29 +158,30 @@ type AnimationEvents = HashMap<AnimationEventTarget, Vec<TimedAnimationEvent>>;
158158
/// animation curves.
159159
pub type AnimationCurves = HashMap<AnimationTargetId, Vec<VariableCurve>, NoOpHash>;
160160

161-
/// A unique [UUID] for an animation target (e.g. bone in a skinned mesh).
161+
/// A component that identifies which parts of an [`AnimationClip`] asset can
162+
/// be applied to an entity. Typically used alongside the
163+
/// [`AnimatedBy`] component.
162164
///
163-
/// The [`AnimationClip`] asset and the [`AnimationTarget`] component both use
164-
/// this to refer to targets (e.g. bones in a skinned mesh) to be animated.
165-
///
166-
/// When importing an armature or an animation clip, asset loaders typically use
167-
/// the full path name from the armature to the bone to generate these UUIDs.
168-
/// The ID is unique to the full path name and based only on the names. So, for
169-
/// example, any imported armature with a bone at the root named `Hips` will
170-
/// assign the same [`AnimationTargetId`] to its root bone. Likewise, any
171-
/// imported animation clip that animates a root bone named `Hips` will
172-
/// reference the same [`AnimationTargetId`]. Any animation is playable on any
173-
/// armature as long as the bone names match, which allows for easy animation
174-
/// retargeting.
165+
/// `AnimationTargetId` is implemented as a [UUID]. When importing an armature
166+
/// or an animation clip, asset loaders typically use the full path name from
167+
/// the armature to the bone to generate these UUIDs. The ID is unique to the
168+
/// full path name and based only on the names. So, for example, any imported
169+
/// armature with a bone at the root named `Hips` will assign the same
170+
/// [`AnimationTargetId`] to its root bone. Likewise, any imported animation
171+
/// clip that animates a root bone named `Hips` will reference the same
172+
/// [`AnimationTargetId`]. Any animation is playable on any armature as long as
173+
/// the bone names match, which allows for easy animation retargeting.
175174
///
176175
/// Note that asset loaders generally use the *full* path name to generate the
177176
/// [`AnimationTargetId`]. Thus a bone named `Chest` directly connected to a
178177
/// bone named `Hips` will have a different ID from a bone named `Chest` that's
179178
/// connected to a bone named `Stomach`.
180179
///
181180
/// [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier
182-
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug, Serialize, Deserialize)]
183-
#[reflect(Clone)]
181+
#[derive(
182+
Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Reflect, Debug, Serialize, Deserialize, Component,
183+
)]
184+
#[reflect(Component, Clone)]
184185
pub struct AnimationTargetId(pub Uuid);
185186

186187
impl Hash for AnimationTargetId {
@@ -190,39 +191,26 @@ impl Hash for AnimationTargetId {
190191
}
191192
}
192193

193-
/// An entity that can be animated by an [`AnimationPlayer`].
194-
///
195-
/// These are frequently referred to as *bones* or *joints*, because they often
196-
/// refer to individually-animatable parts of an armature.
194+
/// A component that links an animated entity to an entity containing an
195+
/// [`AnimationPlayer`]. Typically used alongside the [`AnimationTargetId`]
196+
/// component - the linked `AnimationPlayer` plays [`AnimationClip`] assets, and
197+
/// the `AnimationTargetId` identifies which curves in the `AnimationClip` will
198+
/// affect the target entity.
197199
///
198-
/// Asset loaders for armatures are responsible for adding these as necessary.
199-
/// Typically, they're generated from hashed versions of the entire name path
200-
/// from the root of the armature to the bone. See the [`AnimationTargetId`]
201-
/// documentation for more details.
202-
///
203-
/// By convention, asset loaders add [`AnimationTarget`] components to the
200+
/// By convention, asset loaders add [`AnimationTargetId`] components to the
204201
/// descendants of an [`AnimationPlayer`], as well as to the [`AnimationPlayer`]
205202
/// entity itself, but Bevy doesn't require this in any way. So, for example,
206203
/// it's entirely possible for an [`AnimationPlayer`] to animate a target that
207204
/// it isn't an ancestor of. If you add a new bone to or delete a bone from an
208-
/// armature at runtime, you may want to update the [`AnimationTarget`]
205+
/// armature at runtime, you may want to update the [`AnimationTargetId`]
209206
/// component as appropriate, as Bevy won't do this automatically.
210207
///
211208
/// Note that each entity can only be animated by one animation player at a
212-
/// time. However, you can change [`AnimationTarget`]'s `player` property at
213-
/// runtime to change which player is responsible for animating the entity.
214-
#[derive(Clone, Copy, Component, Reflect)]
209+
/// time. However, you can change [`AnimatedBy`] components at runtime and
210+
/// link them to a different player.
211+
#[derive(Clone, Copy, Component, Reflect, Debug)]
215212
#[reflect(Component, Clone)]
216-
pub struct AnimationTarget {
217-
/// The ID of this animation target.
218-
///
219-
/// Typically, this is derived from the path.
220-
pub id: AnimationTargetId,
221-
222-
/// The entity containing the [`AnimationPlayer`].
223-
#[entities]
224-
pub player: Entity,
225-
}
213+
pub struct AnimatedBy(#[entities] pub Entity);
226214

227215
impl AnimationClip {
228216
#[inline]
@@ -271,8 +259,8 @@ impl AnimationClip {
271259
self.duration = duration_sec;
272260
}
273261

274-
/// Adds an [`AnimationCurve`] to an [`AnimationTarget`] named by an
275-
/// [`AnimationTargetId`].
262+
/// Adds an [`AnimationCurve`] that can target an entity with the given
263+
/// [`AnimationTargetId`] component.
276264
///
277265
/// If the curve extends beyond the current duration of this clip, this
278266
/// method lengthens this clip to include the entire time span that the
@@ -327,7 +315,7 @@ impl AnimationClip {
327315
.push(variable_curve);
328316
}
329317

330-
/// Add an [`EntityEvent`] with no [`AnimationTarget`] to this [`AnimationClip`].
318+
/// Add an [`EntityEvent`] with no [`AnimationTargetId`].
331319
///
332320
/// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds)
333321
/// is reached in the animation.
@@ -347,7 +335,7 @@ impl AnimationClip {
347335
);
348336
}
349337

350-
/// Add an [`EntityEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`].
338+
/// Add an [`EntityEvent`] with an [`AnimationTargetId`].
351339
///
352340
/// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds)
353341
/// is reached in the animation.
@@ -373,7 +361,7 @@ impl AnimationClip {
373361
);
374362
}
375363

376-
/// Add an event function with no [`AnimationTarget`] to this [`AnimationClip`].
364+
/// Add an event function with no [`AnimationTargetId`] to this [`AnimationClip`].
377365
///
378366
/// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds)
379367
/// is reached in the animation.
@@ -396,7 +384,7 @@ impl AnimationClip {
396384
self.add_event_internal(AnimationEventTarget::Root, time, func);
397385
}
398386

399-
/// Add an event function to an [`AnimationTarget`] named by an [`AnimationTargetId`].
387+
/// Add an event function with an [`AnimationTargetId`].
400388
///
401389
/// The `func` will trigger on the entity matching the target once the `time` (in seconds)
402390
/// is reached in the animation.
@@ -1033,8 +1021,16 @@ pub fn advance_animations(
10331021
}
10341022

10351023
/// A type alias for [`EntityMutExcept`] as used in animation.
1036-
pub type AnimationEntityMut<'w, 's> =
1037-
EntityMutExcept<'w, 's, (AnimationTarget, AnimationPlayer, AnimationGraphHandle)>;
1024+
pub type AnimationEntityMut<'w, 's> = EntityMutExcept<
1025+
'w,
1026+
's,
1027+
(
1028+
AnimationTargetId,
1029+
AnimatedBy,
1030+
AnimationPlayer,
1031+
AnimationGraphHandle,
1032+
),
1033+
>;
10381034

10391035
/// A system that modifies animation targets (e.g. bones in a skinned mesh)
10401036
/// according to the currently-playing animations.
@@ -1044,18 +1040,13 @@ pub fn animate_targets(
10441040
graphs: Res<Assets<AnimationGraph>>,
10451041
threaded_animation_graphs: Res<ThreadedAnimationGraphs>,
10461042
players: Query<(&AnimationPlayer, &AnimationGraphHandle)>,
1047-
mut targets: Query<(Entity, &AnimationTarget, AnimationEntityMut)>,
1043+
mut targets: Query<(Entity, &AnimationTargetId, &AnimatedBy, AnimationEntityMut)>,
10481044
animation_evaluation_state: Local<ThreadLocal<RefCell<AnimationEvaluationState>>>,
10491045
) {
10501046
// Evaluate all animation targets in parallel.
10511047
targets
10521048
.par_iter_mut()
1053-
.for_each(|(entity, target, entity_mut)| {
1054-
let &AnimationTarget {
1055-
id: target_id,
1056-
player: player_id,
1057-
} = target;
1058-
1049+
.for_each(|(entity, &target_id, &AnimatedBy(player_id), entity_mut)| {
10591050
let (animation_player, animation_graph_id) =
10601051
if let Ok((player, graph_handle)) = players.get(player_id) {
10611052
(player, graph_handle.id())

crates/bevy_gltf/src/loader/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
};
1010

1111
#[cfg(feature = "bevy_animation")]
12-
use bevy_animation::{prelude::*, AnimationTarget, AnimationTargetId};
12+
use bevy_animation::{prelude::*, AnimatedBy, AnimationTargetId};
1313
use bevy_asset::{
1414
io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError,
1515
RenderAssetUsages,
@@ -1439,10 +1439,10 @@ fn load_node(
14391439
if let Some(ref mut animation_context) = animation_context {
14401440
animation_context.path.push(name);
14411441

1442-
node.insert(AnimationTarget {
1443-
id: AnimationTargetId::from_names(animation_context.path.iter()),
1444-
player: animation_context.root,
1445-
});
1442+
node.insert((
1443+
AnimationTargetId::from_names(animation_context.path.iter()),
1444+
AnimatedBy(animation_context.root),
1445+
));
14461446
}
14471447

14481448
if let Some(extras) = gltf_node.extras() {

examples/animation/animated_transform.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::f32::consts::PI;
44

55
use bevy::{
6-
animation::{animated_field, AnimationTarget, AnimationTargetId},
6+
animation::{animated_field, AnimatedBy, AnimationTargetId},
77
prelude::*,
88
};
99

@@ -152,26 +152,20 @@ fn setup(
152152
))
153153
.id();
154154
commands.entity(planet_entity).insert((
155-
AnimationTarget {
156-
id: planet_animation_target_id,
157-
player: planet_entity,
158-
},
155+
planet_animation_target_id,
156+
AnimatedBy(planet_entity),
159157
children![(
160158
Transform::default(),
161159
Visibility::default(),
162160
orbit_controller,
163-
AnimationTarget {
164-
id: orbit_controller_animation_target_id,
165-
player: planet_entity,
166-
},
161+
orbit_controller_animation_target_id,
162+
AnimatedBy(planet_entity),
167163
children![(
168164
Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))),
169165
MeshMaterial3d(materials.add(Color::srgb(0.3, 0.9, 0.3))),
170166
Transform::from_xyz(1.5, 0.0, 0.0),
171-
AnimationTarget {
172-
id: satellite_animation_target_id,
173-
player: planet_entity,
174-
},
167+
satellite_animation_target_id,
168+
AnimatedBy(planet_entity),
175169
satellite,
176170
)],
177171
)],

examples/animation/animated_ui.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
33
use bevy::{
44
animation::{
5-
animated_field, AnimationEntityMut, AnimationEvaluationError, AnimationTarget,
6-
AnimationTargetId,
5+
animated_field, AnimatedBy, AnimationEntityMut, AnimationEvaluationError, AnimationTargetId,
76
},
87
prelude::*,
98
};
@@ -149,10 +148,8 @@ fn setup(
149148
},
150149
TextColor(Color::Srgba(Srgba::RED)),
151150
TextLayout::new_with_justify(Justify::Center),
152-
AnimationTarget {
153-
id: animation_target_id,
154-
player,
155-
},
151+
animation_target_id,
152+
AnimatedBy(player),
156153
animation_target_name,
157154
)]);
158155
}

examples/animation/animation_masks.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Demonstrates how to use masks to limit the scope of animations.
22
33
use bevy::{
4-
animation::{AnimationTarget, AnimationTargetId},
4+
animation::{AnimatedBy, AnimationTargetId},
55
color::palettes::css::{LIGHT_GRAY, WHITE},
66
prelude::*,
77
};
@@ -342,7 +342,7 @@ fn setup_animation_graph_once_loaded(
342342
asset_server: Res<AssetServer>,
343343
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
344344
mut players: Query<(Entity, &mut AnimationPlayer), Added<AnimationPlayer>>,
345-
targets: Query<(Entity, &AnimationTarget)>,
345+
targets: Query<(Entity, &AnimationTargetId)>,
346346
) {
347347
for (entity, mut player) in &mut players {
348348
// Load the animation clip from the glTF file.
@@ -389,8 +389,11 @@ fn setup_animation_graph_once_loaded(
389389
// don't do that, those bones will play all animations at once, which is
390390
// ugly.
391391
for (target_entity, target) in &targets {
392-
if !all_animation_target_ids.contains(&target.id) {
393-
commands.entity(target_entity).remove::<AnimationTarget>();
392+
if !all_animation_target_ids.contains(target) {
393+
commands
394+
.entity(target_entity)
395+
.remove::<AnimationTargetId>()
396+
.remove::<AnimatedBy>();
394397
}
395398
}
396399

examples/animation/eased_motion.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::f32::consts::FRAC_PI_2;
44

55
use bevy::{
6-
animation::{animated_field, AnimationTarget, AnimationTargetId},
6+
animation::{animated_field, AnimatedBy, AnimationTargetId},
77
color::palettes::css::{ORANGE, SILVER},
88
math::vec3,
99
prelude::*,
@@ -47,10 +47,9 @@ fn setup(
4747
))
4848
.id();
4949

50-
commands.entity(cube_entity).insert(AnimationTarget {
51-
id: animation_target_id,
52-
player: cube_entity,
53-
});
50+
commands
51+
.entity(cube_entity)
52+
.insert((animation_target_id, AnimatedBy(cube_entity)));
5453

5554
// Some light to see something
5655
commands.spawn((

examples/tools/scene_viewer/animation_plugin.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! Control animations of entities in the loaded scene.
22
use std::collections::HashMap;
33

4-
use bevy::{animation::AnimationTarget, ecs::entity::EntityHashMap, gltf::Gltf, prelude::*};
4+
use bevy::{animation::AnimationTargetId, ecs::entity::EntityHashMap, gltf::Gltf, prelude::*};
55

66
use crate::scene_viewer_plugin::SceneHandle;
77

@@ -35,7 +35,7 @@ impl Clips {
3535
/// the common case).
3636
fn assign_clips(
3737
mut players: Query<&mut AnimationPlayer>,
38-
targets: Query<(Entity, &AnimationTarget)>,
38+
targets: Query<(&AnimationTargetId, Entity)>,
3939
children: Query<&ChildOf>,
4040
scene_handle: Res<SceneHandle>,
4141
clips: Res<Assets<AnimationClip>>,
@@ -64,10 +64,7 @@ fn assign_clips(
6464
info!("Animation names: {names:?}");
6565

6666
// Map animation target IDs to entities.
67-
let animation_target_id_to_entity: HashMap<_, _> = targets
68-
.iter()
69-
.map(|(entity, target)| (target.id, entity))
70-
.collect();
67+
let animation_target_id_to_entity: HashMap<_, _> = targets.iter().collect();
7168

7269
// Build up a list of all animation clips that belong to each player. A clip
7370
// is considered to belong to an animation player if all targets of the clip
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: "`AnimationTarget` replaced by separate components"
3+
pull_requests: [20774]
4+
---
5+
6+
The `AnimationTarget` component has been split into two separate components.
7+
`AnimationTarget::id` is now an `AnimationTargetId` component, and
8+
`AnimationTarget::player` is now an `AnimatedBy` component.
9+
10+
This change was made to add flexibility. It's now possible to calculate the
11+
`AnimationTargetId` first, but defer the choice of player until later.
12+
13+
Before:
14+
15+
```rust
16+
entity.insert(AnimationTarget { id: AnimationTargetId(id), player: player_entity });
17+
```
18+
19+
After:
20+
21+
```rust
22+
entity.insert((AnimationTargetId(id), AnimatedBy(player_entity)));
23+
```

0 commit comments

Comments
 (0)