Animation example

main
Elijah Voigt 1 year ago
parent 28890a870b
commit be7627e9c0

@ -4,6 +4,10 @@ target/x86_64-unknown-linux-gnu/release/set: src/*
target/x86_64-unknown-linux-gnu/quick/set: src/* target/x86_64-unknown-linux-gnu/quick/set: src/*
cargo build --target x86_64-unknown-linux-gnu --features bevy/dynamic_linking --profile quick cargo build --target x86_64-unknown-linux-gnu --features bevy/dynamic_linking --profile quick
.PHONY: target/x86_64-unknown-linux-gnu/quick/examples/animation
target/x86_64-unknown-linux-gnu/quick/examples/animation: examples/animation.rs src/*
cargo run --example animation --target x86_64-unknown-linux-gnu --features bevy/dynamic_linking --profile quick
### ###
### ###
@ -46,6 +50,10 @@ native/debug/build: target/x86_64-unknown-linux-gnu/quick/set
native/release/build: target/x86_64-unknown-linux-gnu/release/set native/release/build: target/x86_64-unknown-linux-gnu/release/set
### ###
###
examples/animation: target/x86_64-unknown-linux-gnu/quick/examples/animation
###
### ###
native/debug/run: native/debug/build native/debug/run: native/debug/build
cargo run --target x86_64-unknown-linux-gnu --features bevy/dynamic_linking --profile quick cargo run --target x86_64-unknown-linux-gnu --features bevy/dynamic_linking --profile quick

Binary file not shown.

Binary file not shown.

@ -0,0 +1,163 @@
use bevy::{
animation::{animated_field, AnimationTarget, AnimationTargetId},
input::{keyboard::KeyboardInput, ButtonState},
prelude::*,
utils::HashMap,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, init)
.add_systems(Update, switch)
.run();
}
#[derive(Resource)]
struct AnimationStore(HashMap<String, (AnimationGraphHandle, AnimationNodeIndex)>);
fn init(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut animations: ResMut<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
commands.spawn(Camera2d);
commands.spawn((
Text2d("Press 1/2/3 to trigger animation".into()),
Node {
top: Val::Px(-50.0),
height: Val::Percent(100.0),
..default()
},
));
let mut hash_map = HashMap::new();
{
let mut a = AnimationClip::default();
let animatable_curve = AnimatableCurve::new(
animated_field!(Transform::translation),
UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
Vec3::new(200.0, 200.0, 0.0),
Vec3::new(-200.0, 200.0, 0.0),
Vec3::new(-200.0, -200.0, 0.0),
Vec3::new(200.0, -200.0, 0.0),
// in case seamless looping is wanted, the last keyframe should
// be the same as the first one
Vec3::new(200.0, 200.0, 0.0),
]))
.expect("should be able to build translation curve because we pass in valid samples"),
);
a.add_curve_to_target(
AnimationTargetId::from_name(&Name::new("Circle")),
animatable_curve,
);
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(a));
let graph_handle = AnimationGraphHandle(graphs.add(graph));
hash_map.insert("Box".into(), (graph_handle, animation_index));
};
{
let mut a = AnimationClip::default();
let animatable_curve = AnimatableCurve::new(
animated_field!(Transform::translation),
UnevenSampleAutoCurve::new([0.0, 1.0, 2.0].into_iter().zip([
Vec3::new(200.0, 200.0, 0.0),
Vec3::new(-200.0, -200.0, 0.0),
Vec3::new(200.0, 200.0, 0.0),
]))
.expect("should be able to build translation curve because we pass in valid samples"),
);
a.add_curve_to_target(
AnimationTargetId::from_name(&Name::new("Circle")),
animatable_curve,
);
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(a));
let graph_handle = AnimationGraphHandle(graphs.add(graph));
hash_map.insert("Diag1".into(), (graph_handle, animation_index));
};
{
let mut a = AnimationClip::default();
let animatable_curve = AnimatableCurve::new(
animated_field!(Transform::translation),
UnevenSampleAutoCurve::new([0.0, 1.0, 2.0].into_iter().zip([
Vec3::new(-200.0, 200.0, 0.0),
Vec3::new(200.0, -200.0, 0.0),
// in case seamless looping is wanted, the last keyframe should
// be the same as the first one
Vec3::new(-200.0, 200.0, 0.0),
]))
.expect("should be able to build translation curve because we pass in valid samples"),
);
a.add_curve_to_target(
AnimationTargetId::from_name(&Name::new("Circle")),
animatable_curve,
);
let (graph, animation_index) = AnimationGraph::from_clip(animations.add(a));
let graph_handle = AnimationGraphHandle(graphs.add(graph));
hash_map.insert("Diag2".into(), (graph_handle, animation_index));
};
let store = AnimationStore(hash_map);
commands.insert_resource(store);
let shape = meshes.add(Circle::new(50.0));
let material = materials.add(Color::WHITE);
commands.spawn((
Mesh2d(shape),
MeshMaterial2d(material),
Name::new("Circle"),
AnimationPlayer::default(),
));
}
fn switch(
mut commands: Commands,
mut evr_kbd: EventReader<KeyboardInput>,
mut query: Query<(Entity, &Name, &mut AnimationPlayer)>,
store: Res<AnimationStore>,
) {
evr_kbd
.read()
.filter(|ev| ev.state == ButtonState::Pressed)
.for_each(|ev| match ev.key_code {
KeyCode::Digit1 => {
let (g, ai) = store.0.get("Box".into()).unwrap();
let (e, n, mut ap) = query.single_mut();
ap.stop_all().play(ai.clone());
commands.entity(e).insert((
g.clone(),
AnimationTarget {
id: AnimationTargetId::from_name(n),
player: e,
},
));
}
KeyCode::Digit2 => {
let (g, ai) = store.0.get("Diag1".into()).unwrap();
let (e, n, mut ap) = query.single_mut();
ap.stop_all().play(ai.clone());
commands.entity(e).insert((
g.clone(),
AnimationTarget {
id: AnimationTargetId::from_name(n),
player: e,
},
));
}
KeyCode::Digit3 => {
let (g, ai) = store.0.get("Diag2".into()).unwrap();
let (e, n, mut ap) = query.single_mut();
ap.stop_all().play(ai.clone());
commands.entity(e).insert((
g.clone(),
AnimationTarget {
id: AnimationTargetId::from_name(n),
player: e,
},
));
}
_ => (),
});
}

@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{deck::Card, menu::UiMessage, GameState}; use crate::{deck::Card, menu::UiMessage, setup::CardAnimations, GameState};
pub struct PlayPlugin; pub struct PlayPlugin;
@ -9,8 +9,7 @@ impl Plugin for PlayPlugin {
app.add_event::<ServeCards>() app.add_event::<ServeCards>()
.add_observer(serve_cards) .add_observer(serve_cards)
.add_systems(OnEnter(GameState::Main), deal_cards) .add_systems(OnEnter(GameState::Main), deal_cards)
.add_systems(Update, deal_cards.run_if(set_added)) .add_systems(Update, deal_cards.run_if(set_added));
.add_systems(Update, spin_cards);
} }
} }
@ -33,26 +32,25 @@ pub(crate) struct PlayLocation {
#[derive(Component)] #[derive(Component)]
pub(crate) struct SetNumber(pub u8); pub(crate) struct SetNumber(pub u8);
/// Debug system rotating selected cards for visual flare
fn spin_cards(mut query: Query<&mut Transform, (With<Card>, With<Selected>)>, time: Res<Time>) {
let dt = time.delta().as_secs_f32();
query.iter_mut().for_each(|mut t| {
t.rotate_z(dt * 0.5);
});
}
/// Observer system for adding/removing selected components when a card is clicked /// Observer system for adding/removing selected components when a card is clicked
pub(crate) fn toggle_selected( pub(crate) fn toggle_selected(
trigger: Trigger<Pointer<Click>>, trigger: Trigger<Pointer<Click>>,
mut commands: Commands, mut commands: Commands,
query: Query<&Selected>, query: Query<&Selected>,
mut players: Query<(&mut AnimationPlayer, &CardAnimations)>,
) { ) {
commands.trigger(UiMessage("".into())); commands.trigger(UiMessage("".into()));
let e = trigger.entity(); let e = trigger.entity();
if query.contains(e) { if query.contains(e) {
commands.entity(e).remove::<Selected>(); commands.entity(e).remove::<Selected>();
info!("stopping animation");
let (mut player, anims) = players.get_mut(e).unwrap();
player.stop(anims.rotate);
} else { } else {
commands.entity(e).insert(Selected); commands.entity(e).insert(Selected);
info!("starting animation");
let (mut player, anims) = players.get_mut(e).unwrap();
player.play(anims.rotate);
} }
} }

@ -1,5 +1,10 @@
use std::f32::consts::PI;
use crate::{deck::*, *}; use crate::{deck::*, *};
use bevy::prelude::*; use bevy::{
animation::{animated_field, AnimationTarget, AnimationTargetId},
prelude::*,
};
use view::ViewState; use view::ViewState;
pub struct SetupPlugin; pub struct SetupPlugin;
@ -13,10 +18,26 @@ impl Plugin for SetupPlugin {
} }
} }
#[derive(Component)]
pub(crate) struct CardAnimations {
pub rotate: AnimationNodeIndex,
}
/// Setup drawing our cards on the screen /// Setup drawing our cards on the screen
pub(crate) fn setup_cards(mut commands: Commands, deck: Res<Deck>) { pub(crate) fn setup_cards(
mut commands: Commands,
deck: Res<Deck>,
mut clips: ResMut<Assets<AnimationClip>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
) {
let animation_player = AnimationPlayer::default();
commands commands
.spawn((Transform::default(), Visibility::default(), ViewState::Play)) .spawn((
Transform::default(),
Visibility::default(),
ViewState::Play,
animation_player,
))
.with_children(|parent| { .with_children(|parent| {
Deck::iter_shuffled() Deck::iter_shuffled()
.enumerate() .enumerate()
@ -31,8 +52,50 @@ pub(crate) fn setup_cards(mut commands: Commands, deck: Res<Deck>) {
..this ..this
}; };
let order = play::DeckOrder(i as u8); let order = play::DeckOrder(i as u8);
let name = Name::new(format!("{}", this_card));
let animation_components = {
let mut animation = AnimationClip::default();
let target = AnimationTargetId::from_name(&name);
let curve = AnimatableCurve::new(
animated_field!(Transform::rotation),
AnimatableKeyframeCurve::new(
[0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([
Quat::IDENTITY,
Quat::from_axis_angle(Vec3::Y, PI / 2.),
Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
Quat::IDENTITY,
]),
)
.expect("Rotation animation"),
);
animation.add_curve_to_target(target, curve);
let animation_handle = clips.add(animation);
let (graph, rotate) = AnimationGraph::from_clip(animation_handle);
let animation_graph_handle = AnimationGraphHandle(graphs.add(graph));
let indexes = CardAnimations { rotate };
let target = AnimationTarget {
id: target,
player: parent.parent_entity(),
};
(animation_graph_handle, indexes, target)
};
// Card Animations:
// active_card_animation = AnimationClip::default() // simple rotation
// for each (X, Y) in board:
// deck_to_X_Y = AnimationClip::default() // Move from deck to X Y position
// X_Y_to_sets = AnimationClip::default() // Move from X Y position to sets pile
// All need AnimationTarget, AnimationGraphHandle, and AnimationPlayer
parent parent
.spawn((this_sprite, this_card, order, Visibility::Hidden)) .spawn((
this_sprite,
this_card,
order,
Visibility::Hidden,
name,
animation_components,
))
.observe(play::place_card) .observe(play::place_card)
.observe(debug::set_debug_card) .observe(debug::set_debug_card)
.observe(debug::hide_debug_card) .observe(debug::hide_debug_card)
@ -77,3 +140,9 @@ pub(crate) fn start_play(
game_state.set(GameState::Main); game_state.set(GameState::Main);
view_state.set(ViewState::Menu); view_state.set(ViewState::Menu);
} }
fn setup_animations(mut commands: Commands) {
// TODO: (example: https://bevyengine.org/examples/animation/animated-transform/)
// Button Animations:
// active_button_animation = AnimationClip::default() // color and size
}

@ -1,10 +1,11 @@
TODO: TODO:
* "New Game" button
* Print "no set"/"too many cards" alert
* Guarentee there is always a set
* Better shuffling * Better shuffling
* Make "set" button visually interesting when 3 cards selected
* Use Animation for rotating cards
* Animate cards deck -> board
* Animate cards deck -> set-pile
Later: Later:
* View all cards with some indication of in-set * View all cards with some indication of in-set
* Music! * Music! (w/ sam)
* Make button(s) look pretty * Make button(s) look pretty

Loading…
Cancel
Save