Fixes #17412 ## Objective `Parent` uses the "has a X" naming convention. There is increasing sentiment that we should use the "is a X" naming convention for relationships (following #17398). This leaves `Children` as-is because there is prevailing sentiment that `Children` is clearer than `ParentOf` in many cases (especially when treating it like a collection). This renames `Parent` to `ChildOf`. This is just the implementation PR. To discuss the path forward, do so in #17412. ## Migration Guide - The `Parent` component has been renamed to `ChildOf`.
		
			
				
	
	
		
			198 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
//! Control animations of entities in the loaded scene.
 | 
						|
use std::collections::HashMap;
 | 
						|
 | 
						|
use bevy::{
 | 
						|
    animation::AnimationTarget, ecs::entity::hash_map::EntityHashMap, gltf::Gltf, prelude::*,
 | 
						|
};
 | 
						|
 | 
						|
use crate::scene_viewer_plugin::SceneHandle;
 | 
						|
 | 
						|
/// Controls animation clips for a unique entity.
 | 
						|
#[derive(Component)]
 | 
						|
struct Clips {
 | 
						|
    nodes: Vec<AnimationNodeIndex>,
 | 
						|
    current: usize,
 | 
						|
}
 | 
						|
impl Clips {
 | 
						|
    fn new(clips: Vec<AnimationNodeIndex>) -> Self {
 | 
						|
        Clips {
 | 
						|
            nodes: clips,
 | 
						|
            current: 0,
 | 
						|
        }
 | 
						|
    }
 | 
						|
    /// # Panics
 | 
						|
    ///
 | 
						|
    /// When no clips are present.
 | 
						|
    fn current(&self) -> AnimationNodeIndex {
 | 
						|
        self.nodes[self.current]
 | 
						|
    }
 | 
						|
    fn advance_to_next(&mut self) {
 | 
						|
        self.current = (self.current + 1) % self.nodes.len();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
/// Automatically assign [`AnimationClip`]s to [`AnimationPlayer`] and play
 | 
						|
/// them, if the clips refer to descendants of the animation player (which is
 | 
						|
/// the common case).
 | 
						|
fn assign_clips(
 | 
						|
    mut players: Query<&mut AnimationPlayer>,
 | 
						|
    targets: Query<(Entity, &AnimationTarget)>,
 | 
						|
    children: Query<&ChildOf>,
 | 
						|
    scene_handle: Res<SceneHandle>,
 | 
						|
    clips: Res<Assets<AnimationClip>>,
 | 
						|
    gltf_assets: Res<Assets<Gltf>>,
 | 
						|
    assets: Res<AssetServer>,
 | 
						|
    mut graphs: ResMut<Assets<AnimationGraph>>,
 | 
						|
    mut commands: Commands,
 | 
						|
    mut setup: Local<bool>,
 | 
						|
) {
 | 
						|
    if scene_handle.is_loaded && !*setup {
 | 
						|
        *setup = true;
 | 
						|
    } else {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    let gltf = gltf_assets.get(&scene_handle.gltf_handle).unwrap();
 | 
						|
    let animations = &gltf.animations;
 | 
						|
    if animations.is_empty() {
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    let count = animations.len();
 | 
						|
    let plural = if count == 1 { "" } else { "s" };
 | 
						|
    info!("Found {} animation{plural}", animations.len());
 | 
						|
    let names: Vec<_> = gltf.named_animations.keys().collect();
 | 
						|
    info!("Animation names: {names:?}");
 | 
						|
 | 
						|
    // Map animation target IDs to entities.
 | 
						|
    let animation_target_id_to_entity: HashMap<_, _> = targets
 | 
						|
        .iter()
 | 
						|
        .map(|(entity, target)| (target.id, entity))
 | 
						|
        .collect();
 | 
						|
 | 
						|
    // Build up a list of all animation clips that belong to each player. A clip
 | 
						|
    // is considered to belong to an animation player if all targets of the clip
 | 
						|
    // refer to entities whose nearest ancestor player is that animation player.
 | 
						|
 | 
						|
    let mut player_to_graph: EntityHashMap<(AnimationGraph, Vec<AnimationNodeIndex>)> =
 | 
						|
        EntityHashMap::default();
 | 
						|
 | 
						|
    for (clip_id, clip) in clips.iter() {
 | 
						|
        let mut ancestor_player = None;
 | 
						|
        for target_id in clip.curves().keys() {
 | 
						|
            // If the animation clip refers to entities that aren't present in
 | 
						|
            // the scene, bail.
 | 
						|
            let Some(&target) = animation_target_id_to_entity.get(target_id) else {
 | 
						|
                continue;
 | 
						|
            };
 | 
						|
 | 
						|
            // Find the nearest ancestor animation player.
 | 
						|
            let mut current = Some(target);
 | 
						|
            while let Some(entity) = current {
 | 
						|
                if players.contains(entity) {
 | 
						|
                    match ancestor_player {
 | 
						|
                        None => {
 | 
						|
                            // If we haven't found a player yet, record the one
 | 
						|
                            // we found.
 | 
						|
                            ancestor_player = Some(entity);
 | 
						|
                        }
 | 
						|
                        Some(ancestor) => {
 | 
						|
                            // If we have found a player, then make sure it's
 | 
						|
                            // the same player we located before.
 | 
						|
                            if ancestor != entity {
 | 
						|
                                // It's a different player. Bail.
 | 
						|
                                ancestor_player = None;
 | 
						|
                                break;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
 | 
						|
                // Go to the next parent.
 | 
						|
                current = children.get(entity).ok().map(ChildOf::get);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        let Some(ancestor_player) = ancestor_player else {
 | 
						|
            warn!(
 | 
						|
                "Unexpected animation hierarchy for animation clip {}; ignoring.",
 | 
						|
                clip_id
 | 
						|
            );
 | 
						|
            continue;
 | 
						|
        };
 | 
						|
 | 
						|
        let Some(clip_handle) = assets.get_id_handle(clip_id) else {
 | 
						|
            warn!("Clip {} wasn't loaded.", clip_id);
 | 
						|
            continue;
 | 
						|
        };
 | 
						|
 | 
						|
        let &mut (ref mut graph, ref mut clip_indices) =
 | 
						|
            player_to_graph.entry(ancestor_player).or_default();
 | 
						|
        let node_index = graph.add_clip(clip_handle, 1.0, graph.root);
 | 
						|
        clip_indices.push(node_index);
 | 
						|
    }
 | 
						|
 | 
						|
    // Now that we've built up a list of all clips that belong to each player,
 | 
						|
    // package them up into a `Clips` component, play the first such animation,
 | 
						|
    // and add that component to the player.
 | 
						|
    for (player_entity, (graph, clips)) in player_to_graph {
 | 
						|
        let Ok(mut player) = players.get_mut(player_entity) else {
 | 
						|
            warn!("Animation targets referenced a nonexistent player. This shouldn't happen.");
 | 
						|
            continue;
 | 
						|
        };
 | 
						|
        let graph = graphs.add(graph);
 | 
						|
        let animations = Clips::new(clips);
 | 
						|
        player.play(animations.current()).repeat();
 | 
						|
        commands
 | 
						|
            .entity(player_entity)
 | 
						|
            .insert(animations)
 | 
						|
            .insert(AnimationGraphHandle(graph));
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
fn handle_inputs(
 | 
						|
    keyboard_input: Res<ButtonInput<KeyCode>>,
 | 
						|
    mut animation_player: Query<(&mut AnimationPlayer, &mut Clips, Entity, Option<&Name>)>,
 | 
						|
) {
 | 
						|
    for (mut player, mut clips, entity, name) in &mut animation_player {
 | 
						|
        let display_entity_name = match name {
 | 
						|
            Some(name) => name.to_string(),
 | 
						|
            None => format!("entity {entity}"),
 | 
						|
        };
 | 
						|
        if keyboard_input.just_pressed(KeyCode::Space) {
 | 
						|
            if player.all_paused() {
 | 
						|
                info!("resuming animations for {display_entity_name}");
 | 
						|
                player.resume_all();
 | 
						|
            } else {
 | 
						|
                info!("pausing animation for {display_entity_name}");
 | 
						|
                player.pause_all();
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if clips.nodes.len() <= 1 {
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        if keyboard_input.just_pressed(KeyCode::Enter) {
 | 
						|
            info!("switching to new animation for {display_entity_name}");
 | 
						|
 | 
						|
            let resume = !player.all_paused();
 | 
						|
            // set the current animation to its start and pause it to reset to its starting state
 | 
						|
            player.rewind_all().pause_all();
 | 
						|
 | 
						|
            clips.advance_to_next();
 | 
						|
            let current_clip = clips.current();
 | 
						|
            player.play(current_clip).repeat();
 | 
						|
            if resume {
 | 
						|
                player.resume_all();
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
pub struct AnimationManipulationPlugin;
 | 
						|
impl Plugin for AnimationManipulationPlugin {
 | 
						|
    fn build(&self, app: &mut App) {
 | 
						|
        app.add_systems(Update, (handle_inputs, assign_clips));
 | 
						|
    }
 | 
						|
}
 |