Skip to content

Commit

Permalink
FSM global transitions (#73)
Browse files Browse the repository at this point in the history
This PR adds:
* Global transitions, optionally enabled per FSM state
* More event modes, allowing you to request a transition to a given
state (without having to know the particular transition id to trigger)

It also cleans up the state machine code, and removes most uses of
"magic strings" for transition and state names when building the
low-level state machine.

---------

Co-authored-by: Sarah <[email protected]>
  • Loading branch information
mbrea-c and SarahIhme authored Oct 4, 2024
1 parent b9435c5 commit 58ec33d
Show file tree
Hide file tree
Showing 21 changed files with 396 additions and 130 deletions.
6 changes: 2 additions & 4 deletions assets/animation_graphs/toplevel.animgraph.ron
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
"user events": EventQueue((
events: [
(
event: (
id: "",
),
event: StringId(""),
weight: 1.0,
percentage: 1.0,
),
Expand All @@ -37,4 +35,4 @@
input_position: (237.51926, 497.61542),
output_position: (727.0, 463.0),
),
)
)
4 changes: 1 addition & 3 deletions assets/animation_graphs/walk_to_run.animgraph.ron
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
nodes: [
(
name: "Done",
node: FireEvent((
id: "end_transition",
)),
node: FireEvent(EndTransition),
),
(
name: "Blend",
Expand Down
13 changes: 9 additions & 4 deletions assets/fsm/locomotion.fsm.ron
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
(
id: "walk",
graph: "animation_graphs/walk.animgraph.ron",
global_transition: Some((
duration: 1.0,
graph: "animation_graphs/walk_to_run.animgraph.ron",
)),
),
(
id: "run",
graph: "animation_graphs/run.animgraph.ron",
global_transition: None,
),
],
transitions: [
(
id: "slow_down",
id: Direct("slow_down"),
source: "run",
target: "walk",
duration: 1.0,
graph: "animation_graphs/walk_to_run.animgraph.ron",
),
(
id: "speed_up",
id: Direct("speed_up"),
source: "walk",
target: "run",
duration: 1.0,
Expand All @@ -31,8 +36,8 @@
},
extra: (
states: {
"walk": (334.10522, 310.10526),
"run": (554.1376, 309.71655),
"walk": (334.10522, 309.52664),
"run": (552.15, 310.6519),
},
),
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
duration_data::DurationData,
pose::Pose,
prelude::AnimationGraph,
state_machine::FSMState,
state_machine::low_level::FSMState,
},
prelude::DataValue,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::GraphContext;
use crate::core::{animation_graph::NodeId, prelude::AnimationGraph, state_machine::StateId};
use crate::core::{
animation_graph::NodeId, prelude::AnimationGraph, state_machine::low_level::LowLevelStateId,
};
use bevy::{asset::AssetId, reflect::Reflect, utils::HashMap};

#[derive(Reflect, Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
Expand All @@ -9,7 +11,7 @@ pub struct GraphContextId(usize);
pub struct SubContextId {
pub ctx_id: GraphContextId,
pub node_id: NodeId,
pub state_id: Option<StateId>,
pub state_id: Option<LowLevelStateId>,
}

#[derive(Reflect, Debug)]
Expand Down
8 changes: 4 additions & 4 deletions crates/bevy_animation_graph/src/core/context/pass_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
duration_data::DurationData,
errors::GraphError,
pose::BoneId,
state_machine::{LowLevelStateMachine, StateId},
state_machine::low_level::{LowLevelStateId, LowLevelStateMachine},
},
prelude::{AnimationGraph, DataValue},
};
Expand Down Expand Up @@ -40,11 +40,11 @@ pub struct FsmContext<'a> {

#[derive(Clone)]
pub struct StateStack {
pub stack: Vec<(StateId, StateRole)>,
pub stack: Vec<(LowLevelStateId, StateRole)>,
}

impl StateStack {
pub fn last_state(&self) -> StateId {
pub fn last_state(&self) -> LowLevelStateId {
self.stack.last().unwrap().0.clone()
}
pub fn last_role(&self) -> StateRole {
Expand Down Expand Up @@ -278,7 +278,7 @@ impl<'a> PassContext<'a> {
pub fn str_ctx_stack(&self) -> String {
let mut s = if let Some(fsm_ctx) = &self.fsm_context {
format!(
"- [FSM] {}: {}",
"- [FSM] {}: {:?}",
"state_id",
fsm_ctx.state_stack.last_state()
)
Expand Down
22 changes: 19 additions & 3 deletions crates/bevy_animation_graph/src/core/edge_data/events.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
use bevy::reflect::{std_traits::ReflectDefault, Reflect};
use serde::{Deserialize, Serialize};

use crate::core::state_machine::high_level::{StateId, TransitionId};

/// Event data
#[derive(Clone, Debug, Reflect, Serialize, Deserialize, Default)]
#[derive(Clone, Debug, Reflect, Serialize, Deserialize)]
#[reflect(Default)]
pub struct AnimationEvent {
pub id: String,
pub enum AnimationEvent {
/// Trigger the most specific transition from transitioning into the provided state. That
/// will be:
/// * A direct transition, if present, or
/// * A global transition, if present
TransitionToState(StateId),
/// Trigger a specific transition (if possible)
Transition(TransitionId),
EndTransition,
StringId(String),
}

impl Default for AnimationEvent {
fn default() -> Self {
Self::StringId("".to_string())
}
}

/// Structure containing a sampled event and relevant metadata
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_animation_graph/src/core/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::pose::Pose;
use super::prelude::GraphClip;
use super::skeleton::loader::SkeletonLoader;
use super::skeleton::Skeleton;
use super::state_machine::high_level::GlobalTransition;
use super::systems::apply_animation_to_targets;
use super::{
animated_scene::{
Expand Down Expand Up @@ -89,6 +90,7 @@ impl AnimationGraphPlugin {
.register_type::<PatternMapperSerial>()
.register_type::<BlendMode>()
.register_type::<BlendSyncMode>()
.register_type::<GlobalTransition>()
.register_type::<()>()
.register_type_data::<(), ReflectDefault>()
// --- Node registrations
Expand Down
2 changes: 0 additions & 2 deletions crates/bevy_animation_graph/src/core/state_machine/common.rs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{serial::StateMachineSerial, State, StateMachine, Transition};
use super::{serial::StateMachineSerial, GlobalTransition, State, StateMachine, Transition};
use crate::core::errors::AssetLoaderError;
use bevy::asset::{io::Reader, AssetLoader, AsyncReadExt, LoadContext};

Expand Down Expand Up @@ -26,9 +26,15 @@ impl AssetLoader for StateMachineLoader {
};

for state_serial in serial.states {
let global_transition_data =
state_serial.global_transition.map(|gt| GlobalTransition {
duration: gt.duration,
graph: load_context.load(gt.graph),
});
fsm.add_state(State {
id: state_serial.id,
graph: load_context.load(state_serial.graph),
global_transition: global_transition_data,
});
}

Expand Down
120 changes: 101 additions & 19 deletions crates/bevy_animation_graph/src/core/state_machine/high_level/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
pub mod loader;
pub mod serial;

use super::{LowLevelStateMachine, StateId, TransitionId};
use super::low_level::{
LowLevelState, LowLevelStateId, LowLevelStateMachine, LowLevelTransition, LowLevelTransitionId,
LowLevelTransitionType,
};
use crate::core::{
animation_graph::{AnimationGraph, PinMap},
edge_data::DataValue,
Expand All @@ -10,16 +13,43 @@ use crate::core::{
use bevy::{
asset::{Asset, Handle},
math::Vec2,
prelude::ReflectDefault,
reflect::Reflect,
utils::HashMap,
};
use serde::{Deserialize, Serialize};

/// Unique within a high-level FSM
pub type StateId = String;

/// Unique within a high-level FSM
#[derive(Reflect, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TransitionId {
/// Direct transitions are still indexed by String Id
Direct(String),
/// There can only be a single global transition between any state pair
Global(StateId, StateId),
}

impl Default for TransitionId {
fn default() -> Self {
Self::Direct("".to_string())
}
}

/// Specification of a state node in the low-level FSM
#[derive(Reflect, Debug, Clone, Default)]
pub struct State {
pub id: StateId,
pub graph: Handle<AnimationGraph>,
pub global_transition: Option<GlobalTransition>,
}

#[derive(Reflect, Debug, Clone, Default)]
#[reflect(Default)]
pub struct GlobalTransition {
pub duration: f32,
pub graph: Handle<AnimationGraph>,
}

/// Stores the positions of nodes in the canvas for the editor
Expand Down Expand Up @@ -229,40 +259,92 @@ impl StateMachine {
pub fn update_low_level_fsm(&mut self) {
let mut llfsm = LowLevelStateMachine::new();

llfsm.start_state = Some(self.start_state.clone());
llfsm.start_state = Some(LowLevelStateId::HlState(self.start_state.clone()));
llfsm.input_data = self.input_data.clone();

for state in self.states.values() {
llfsm.add_state(super::core::LowLevelState {
id: state.id.clone(),
llfsm.add_state(super::low_level::LowLevelState {
id: LowLevelStateId::HlState(state.id.clone()),
graph: state.graph.clone(),
transition: None,
hl_transition: None,
});

if state.global_transition.is_some() {
// TODO: the source state is inaccurate since it will come from several places
for source_state in self.states.values() {
if source_state.id != state.id {
let transition_id =
TransitionId::Global(source_state.id.clone(), state.id.clone());

llfsm.add_state(LowLevelState {
id: LowLevelStateId::GlobalTransition(
source_state.id.clone(),
state.id.clone(),
),
graph: state.global_transition.as_ref().unwrap().graph.clone(),
hl_transition: Some(super::low_level::TransitionData {
source: source_state.id.clone(),
target: state.id.clone(),
hl_transition_id: transition_id.clone(),
duration: state.global_transition.as_ref().unwrap().duration,
}),
});
llfsm.add_transition(LowLevelTransition {
id: LowLevelTransitionId::Start(transition_id.clone()),
source: LowLevelStateId::HlState(source_state.id.clone()),
target: LowLevelStateId::GlobalTransition(
source_state.id.clone(),
state.id.clone(),
),
transition_type: LowLevelTransitionType::Global,
hl_source: source_state.id.clone(),
hl_target: state.id.clone(),
});
llfsm.add_transition(LowLevelTransition {
id: LowLevelTransitionId::End(transition_id.clone()),
source: LowLevelStateId::GlobalTransition(
source_state.id.clone(),
state.id.clone(),
),
target: LowLevelStateId::HlState(state.id.clone()),
transition_type: LowLevelTransitionType::Global,
hl_source: source_state.id.clone(),
hl_target: state.id.clone(),
});
}
}
}
}

for transition in self.transitions.values() {
llfsm.add_state(super::core::LowLevelState {
id: format!("{}_state", transition.id),
llfsm.add_state(LowLevelState {
id: LowLevelStateId::DirectTransition(transition.id.clone()),
graph: transition.graph.clone(),
transition: Some(super::core::TransitionData {
hl_transition: Some(super::low_level::TransitionData {
source: transition.source.clone(),
target: transition.target.clone(),
hl_transition_id: transition.id.clone(),
duration: transition.duration,
}),
});

llfsm.add_transition(
transition.source.clone(),
transition.id.clone(),
format!("{}_state", transition.id),
);

llfsm.add_transition(
format!("{}_state", transition.id),
"end_transition".into(),
transition.target.clone(),
);
llfsm.add_transition(LowLevelTransition {
id: LowLevelTransitionId::Start(transition.id.clone()),
source: LowLevelStateId::HlState(transition.source.clone()),
target: LowLevelStateId::DirectTransition(transition.id.clone()),
transition_type: LowLevelTransitionType::Direct,
hl_source: transition.source.clone(),
hl_target: transition.target.clone(),
});

llfsm.add_transition(LowLevelTransition {
id: LowLevelTransitionId::End(transition.id.clone()),
source: LowLevelStateId::DirectTransition(transition.id.clone()),
target: LowLevelStateId::HlState(transition.target.clone()),
transition_type: LowLevelTransitionType::Direct,
hl_source: transition.source.clone(),
hl_target: transition.target.clone(),
});
}

self.low_level_fsm = llfsm;
Expand Down
Loading

0 comments on commit 58ec33d

Please sign in to comment.