#![warn(missing_docs)]
use std::{
fmt::{Debug, Display},
sync::Arc,
time::Duration,
};
use bevy::{
ecs::schedule::{InternedScheduleLabel, ScheduleLabel},
prelude::*,
utils::HashMap,
};
pub struct SimpleStateMachinePlugin {
schedule: InternedScheduleLabel,
}
impl Plugin for SimpleStateMachinePlugin {
fn build(&self, app: &mut App) {
app.add_event::<TransitionEndedEvent>()
.register_type::<AnimationStateMachine>()
.register_type::<AnimationStateRef>()
.register_type::<AnimationState>()
.register_type::<StateMachineVariableType>()
.register_type::<StateMachineTransition>()
.add_systems(
self.schedule.to_owned(),
(
Self::init_state_machines.in_set(StateMachineSet::StateMachineSet),
Self::check_transitions.in_set(StateMachineSet::StateMachineSet),
),
);
}
}
impl Default for SimpleStateMachinePlugin {
fn default() -> Self {
Self::new()
}
}
impl SimpleStateMachinePlugin {
pub fn new() -> Self {
Self::new_in_schedule(Update)
}
pub fn new_in_schedule(schedule: impl ScheduleLabel) -> Self {
Self {
schedule: schedule.intern(),
}
}
fn check_transitions(
mut state_machines_query: Query<(Entity, &mut AnimationStateMachine, &mut AnimationPlayer)>,
mut event_writer: EventWriter<TransitionEndedEvent>,
) {
for (entity, mut state_machine, mut player) in &mut state_machines_query {
if let Some(current_state) = state_machine.current_state() {
if current_state.interruptible || player.is_finished() {
for transition in state_machine.transitions_from_current_state() {
if transition.trigger.evaluate(&state_machine.variables) {
if let Some(next_state) =
state_machine.get_state(transition.end_state.unwrap())
{
debug!("triggering {}", transition);
state_machine.current_state = next_state.name;
if let Some(transition_duration) = transition.transition_duration {
player
.play_with_transition(next_state.clip, transition_duration);
} else {
player.play(next_state.clip);
}
event_writer.send(TransitionEndedEvent {
entity,
origin: current_state.state_ref(),
end: transition.end_state,
});
}
}
}
}
}
}
}
fn init_state_machines(
mut state_machines_query: Query<
(&AnimationStateMachine, &mut AnimationPlayer),
Added<AnimationStateMachine>,
>,
) {
for (state_machine, mut player) in &mut state_machines_query {
if let Some(current_state) = state_machine.current_state() {
player.play(current_state.clip);
}
}
}
}
#[derive(SystemSet, Clone, Hash, Debug, PartialEq, Eq)]
pub enum StateMachineSet {
StateMachineSet,
}
pub type StateMachineVariables = HashMap<String, StateMachineVariableType>;
#[derive(Clone, Reflect, PartialEq)]
pub enum StateMachineVariableType {
Bool(bool),
F32(f32),
I32(i32),
U32(u32),
String(String),
}
impl StateMachineVariableType {
pub fn is_bool(&self, value: bool) -> bool {
*self == Self::Bool(value)
}
pub fn is_i32(&self, value: i32) -> bool {
*self == Self::I32(value)
}
pub fn is_u32(&self, value: u32) -> bool {
*self == Self::U32(value)
}
pub fn is_f32(&self, value: f32) -> bool {
*self == Self::F32(value)
}
}
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct AnimationStateMachine {
current_state: String,
states: HashMap<String, AnimationState>,
transitions: Vec<StateMachineTransition>,
variables: StateMachineVariables,
}
impl AnimationStateMachine {
pub fn new<T: ToString>(
current_state: T,
states: HashMap<T, AnimationState>,
transitions: Vec<StateMachineTransition>,
variables: HashMap<T, StateMachineVariableType>,
) -> Self {
Self {
current_state: current_state.to_string(),
states: states
.iter()
.map(|(name, state)| (name.to_string(), state.to_owned()))
.collect(),
transitions,
variables: variables
.iter()
.map(|(name, var)| (name.to_string(), var.to_owned()))
.collect(),
}
}
#[inline]
fn current_state(&self) -> Option<AnimationState> {
self.get_state(&self.current_state)
}
fn get_state(&self, state_name: &String) -> Option<AnimationState> {
match self.states.contains_key(state_name) {
true => Some(self.states[state_name].to_owned()),
false => None,
}
}
fn transitions_from_state(&self, state_name: &String) -> Vec<StateMachineTransition> {
self.transitions
.iter()
.filter(|t| {
t.start_state == AnimationStateRef::StateName(state_name.to_owned())
|| t.start_state.is_any()
})
.map(|t| t.to_owned())
.collect()
}
fn transitions_from_current_state(&self) -> Vec<StateMachineTransition> {
self.transitions_from_state(&self.current_state)
}
pub fn update_variable<T: ToString>(&mut self, name: T, value: StateMachineVariableType) {
self.variables.insert(name.to_string(), value);
}
}
#[derive(Default, Debug, Clone, Reflect)]
pub struct AnimationState {
pub clip: Handle<AnimationClip>,
pub name: String,
pub interruptible: bool,
}
impl AnimationState {
fn state_ref(&self) -> AnimationStateRef {
AnimationStateRef::StateName(self.name.to_owned())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Reflect)]
pub enum AnimationStateRef {
AnyState,
StateName(String),
}
impl AnimationStateRef {
pub fn from_string<T: ToString>(name: T) -> Self {
Self::StateName(name.to_string())
}
#[inline]
fn unwrap(&self) -> &String {
match self {
Self::AnyState => panic!("Unexpected AnimationStateRef::AnyState"),
Self::StateName(state) => state,
}
}
pub fn is_any(&self) -> bool {
matches!(self, Self::AnyState)
}
}
impl Display for AnimationStateRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AnyState => write!(f, "AnyState"),
Self::StateName(state_name) => write!(f, "{state_name}"),
}
}
}
#[derive(Clone, Reflect)]
pub struct StateMachineTransition {
pub start_state: AnimationStateRef,
pub end_state: AnimationStateRef,
#[reflect(ignore)]
pub trigger: StateMachineTrigger,
pub transition_duration: Option<Duration>,
}
impl StateMachineTransition {
pub fn immediate(
start_state: AnimationStateRef,
end_state: AnimationStateRef,
trigger: StateMachineTrigger,
) -> Self {
Self {
start_state,
end_state,
trigger,
transition_duration: None,
}
}
pub fn blend(
start_state: AnimationStateRef,
end_state: AnimationStateRef,
trigger: StateMachineTrigger,
transition_duration: Duration,
) -> Self {
Self {
start_state,
end_state,
trigger,
transition_duration: Some(transition_duration),
}
}
}
impl Display for StateMachineTransition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"StateMachineTransition({} -> {})",
self.start_state, self.end_state,
)
}
}
#[derive(Default, Clone)]
pub enum StateMachineTrigger {
#[default]
Never,
Always,
Condition(Arc<dyn Fn(&StateMachineVariables) -> bool + Send + Sync>),
}
impl StateMachineTrigger {
pub fn from(f: impl Fn(&StateMachineVariables) -> bool + Send + Sync + 'static) -> Self {
Self::Condition(Arc::new(f))
}
fn evaluate(&self, variables: &StateMachineVariables) -> bool {
match self {
Self::Never => false,
Self::Always => true,
Self::Condition(f) => (f)(variables),
}
}
}
#[derive(Debug, Clone, Event)]
pub struct TransitionEndedEvent {
pub entity: Entity,
pub origin: AnimationStateRef,
pub end: AnimationStateRef,
}