Skip to content

Instantly share code, notes, and snippets.

@ClickerMonkey
Created January 7, 2022 00:17
Show Gist options
  • Save ClickerMonkey/7243709f6ffb75b7dfaf6c621a996a1d to your computer and use it in GitHub Desktop.
Save ClickerMonkey/7243709f6ffb75b7dfaf6c621a996a1d to your computer and use it in GitHub Desktop.
C++ State Machine Prototype with Animation example
//
// Created by Philip Diffenderfer on 10/24/21.
//
#include <map>
#include <cmath>
#include <functional>
#include <iostream>
template <typename V>
struct AttrimatorPath {
V value;
~AttrimatorPath() {}
};
template <typename V>
struct AttrimatorPathDefinition {
~AttrimatorPathDefinition() {}
};
template <typename T, typename V>
struct AnimatorProperty {
V create() { return V(); }
~AnimatorProperty() {}
};
struct AnimatableTypeBase {
int key;
virtual ~AnimatableTypeBase() {}
};
template <typename V>
struct AnimatableType : AnimatableTypeBase {
using get1 = std::function<std::unique_ptr<AnimatorProperty<void, V>>()>;
using get2 = std::function<std::unique_ptr<AttrimatorPathDefinition<V>>()>;
using get3 = std::function<std::unique_ptr<AttrimatorPath<V>>()>;
int key;
get1 getAnimatorProperty;
get2 getAttrimatorPathDefinition;
get3 getAttrimatorPath;
AnimatableType(int k, get1 g1, get2 g2, get3 g3)
: key(k), getAnimatorProperty(g1), getAttrimatorPathDefinition(g2), getAttrimatorPath(g3) {}
};
class Animate {
public:
static std::map<int, std::unique_ptr<AnimatableTypeBase>> types;
template <typename V>
static void AddType(int key) {
types[key] = std::make_unique<AnimatableType<V>>(
key,
[](){ return std::make_unique<AnimatorProperty<void, V>>(); },
[](){ return std::make_unique<AttrimatorPathDefinition<V>>(); },
[](){ return std::make_unique<AttrimatorPath<V>>(); }
);
}
template <typename V>
static AnimatableType<V>* GetType(int key) {
return dynamic_cast<AnimatableType<V>*>(types[key].get());
}
};
std::map<int, std::unique_ptr<AnimatableTypeBase>> Animate::types;
/**
* Add(Animation, AnimationInfo): AnimationState&
* - create attrimators, provide a function which given a path returns an AnimatorProperty.
*
* Update(float)
* - If no active animation states, exit early
* - AnimatorProperty PreUpdate (clear accumulated value & weight, not dirty)
* - Each active animation state...
* - Each active attrimator
* - Update (could call property accumulate)
* - no more active attrimators? make state inactive
* - if inactive and not keepAlive, remove state
* - AnimatorProperty PostUpdate (apply value on instance for dirty)
*
* Get(Animation): AnimationState*
*
* // Commands on AnimationState (existing state)
* Play(AnimationState&, AnimationOptions) -> make attrimators all active and stop others for the same animation properties
* Queue(AnimationState&, AnimationOptions) -> calculate max remaining finite time from active attrimators, start the attrimators in this state then
* Transition(AnimationState&, AnimationTransition) -> stop others for same properties, but calculate transition paths
* De/Activate(AnimationState&)
* Pause/Resume(AnimationState&) -> if state is active, pause/resume all active attrimators
* Stop(AnimationState&, NOW | FINISH | END) -> stop animating attrimators, if needed SetDefer to end of current attrimator or last
*
* // Commands on Animation (creates new state)
* Play(Animation*, AnimationOptions) -> make attrimators all active and stop others for the same animation properties
* Queue(Animation*, AnimationOptions) -> calculate max remaining finite time from active attrimators, start the attrimators in this state then
* Transition(Animation*, AnimationTransition) -> stop others for same properties, but calculate transition paths
* De/Activate(Animation*)
* Pause/Resume(Animation*) -> if state is active, pause/resume all active attrimators
* Stop(Animation*, NOW | FINISH | END) -> stop animating attrimators, if needed SetDefer to end of current attrimator or last
*
* // Commands on AnimationPropertys
* Play(MatchCondition, AnimationOptions)
* Queue(MatchCondition, AnimationOptions)
* Transition(MatchCondition, AnimationTransition)
* Pause/Resume(MatchCondition)
* Stop(MatchCondition, NOW | FINISH | END)
*
* // Commands on all
* Pause/Resume()
* Stop(NOW | FINISH | END)
*
* // Example
* // State 1 - running around and looking
* Animate Head Looking
* Animate Run blend X
* Animate Walk blend Y
* Animate Strafe blend Z
* // State 2 - stop running
* Outro Run
*
* +--------------------+---------------+------------------+
* | Grounded | Ledge | Air |
* +--------------------+---------------+------------------+
* | Idle, Move F/B/R/L | Idle, U/D/R/L | Jump, Idle, Land |
* +--------------------+---------------+------------------+
*
* StateMachine<Animator, Animation, AnimationOptions> grounded({
* .isDone = []( animator, animation, options } { return animator->GetState(animation).done; },
* .start = []( animator, animation, options ) { animator->Transition(animation, options); },
* .apply = []( animator, animationsAndBlendAmounts ) { return false; }
* });
* grounded.AddState( IDLE, animState, { .blend = abs(MOVE_X * MOVE_Y) } );
* grounded.AddState( FORWARD, animState, { .blend = max(0, MOVE_Y) } );
* grounded.AddState( BACKWARD, animState, { .blend = max(0, -MOVE_Y) } );
* grounded.AddState( RIGHT, animState, { .blend = max(0, MOVE_X) } );
* grounded.AddState( LEFT, animState, { .blend = max(0, -MOVE_X) } );
*
* StateMachine<Animator, Animation, AnimationOptions> ledge(); // same as grounded
* ledge.AddState( IDLE, animState, { .blend = abs(MOVE_X * MOVE_Y) } );
* ledge.AddState( FORWARD, animState, { .blend = max(0, MOVE_Y) } );
* ledge.AddState( BACKWARD, animState, { .blend = max(0, -MOVE_Y) } );
* ledge.AddState( RIGHT, animState, { .blend = max(0, MOVE_X) } );
* ledge.AddState( LEFT, animState, { .blend = max(0, -MOVE_X) } );
*
* // Root machine
* StateMachine<Animator, Animation, AnimationOptions> sm();
* sm.AddState( GROUNDED, grounded );
* sm.AddState( LEDGE, ledge );
* sm.AddState( LEDGE_GRAB, animState );
* sm.AddState( LEDGE_PULL_UP, animState );
* sm.AddState( AIR_JUMP, animState );
* sm.AddState( AIR_UP, animState );
* sm.AddState( AIR_DOWN, animState );
* sm.AddState( AIR_LAND, animState );
*
* // Default inputs
* sm.AddInput( GROUNDED, 1 );
* sm.AddInput( HIT_LEDGE, 0 );
* sm.AddInput( LEDGE_OPEN, 0 );
* sm.AddInput( FALLING, 0 );
* sm.AddInput( JUMP, 0 );
* sm.AddInput( MOVE_X, 0 );
* sm.AddInput( MOVE_Y, 0 );
*
* // Transitions between states (start, condition, end, options)
* sm.AddTransition( GROUNDED, if JUMP, AIR_JUMP, transitionOrQueueOrPlayOptions );
* sm.AddTransition( AIR_JUMP, if true, AIR_UP );
* sm.AddTransition( AIR_UP, if FALLING, AIR_DOWN );
* sm.AddTransition( AIR_DOWN, if GROUNDED, AIR_LAND );
* sm.AddTransition( AIR_LAND, if true, GROUNDED );
* sm.AddTransition( AIR_UP, if HIT_LEDGE, LEDGE_GRAB );
* sm.AddTransition( AIR_DOWN, if HIT_LEDGE, LEDGE_GRAB );
* sm.AddTransition( LEDGE_GRAB, if true, LEDGE );
* sm.AddTransition( LEDGE, if JUMP, AIR_DOWN );
* sm.AddTransition( LEDGE, if LEDGE_OPEN && MOVE_Y > 1, LEDGE_PULL_UP );
* sm.AddTransition( LEDGE_PULL_UP, if true, GROUNDED );
*
* auto inst = sm.NewInstance( animator );
* inst.Update();
* inst.Set( JUMP, 1);
* inst.Get( JUMP );
* inst.GetBlend( state );
* inst.GetStates(); // vector of states and blends
* inst.GetTransitions(); // vector of active transitions (start, end)
*/
struct StateInput { int id; std::string name; float value; };
using StateInputMap = std::map<int, StateInput>;
using StateCondition = std::function<bool(StateInputMap, float dt)>;
template <typename S, typename T, typename O>
struct StateMachine;
using StateBlend = std::function<float(StateInputMap)>
template <typename S, typename T, typename O>
struct StateTransition { int start; int end; StateCondition condition; O options; };
template <typename S, typename T, typename O>
struct State { int id; StateMachine<S, T, O>* sub; T* state; std::string name; StateBlend blend; std::vector<StateTransition<S, T, O>> transitions; };
template <typename S, typename T, typename O>
struct StateInstance {
float blend;
State<S, T, O>* state;
StateMachineInstance<S, T, O>* sub;
O* options;
};
using StateInstances = std::vector<StateInstance>;
template <typename S, typename T, typename O>
struct StateMachineInstance {
StateMachine<S, T, O>* machine;
S* subject;
StateInputMap inputs;
std::vector<StateInstances<S, T, O>> states;
StateMachineInstance(StateMachine<S, T, O>* m, S* s, StateInputMap i): machine(m), subject(s), inputs(i) {}
void Set(int input, float value) { inputs[input] = value; }
float Get(int input) { return inputs[input]; }
void Update(float dt) {
for (auto& state : states) {
for (auto& transition : state.transitions) {
if (state.id == transition.start) {
bool proceed = false;
if (transition.condition) {
proceed = transition.condition(inputs, dt);
} else {
proceed = machine->isDone(subject, state->state->state, &transition.options);
}
if (proceed) {
auto next = machine->states[transition.end];
state.state = next.state;
state.sub = !next.sub ? nullptr : std::make_unique<StateMachineInstance<S, T, O>>(next.sub, subject, &inputs);
state.options = &transition.options;
machine->start(subject, state.state, transition.options);
}
}
}
}
std::vector<StateInstance<S, T, O>> active{};
for (auto& state : states) {
state.blend = next.blend(subject, next.state, transition.options);
if (state.blend > 0) {
active.push_back(state);
}
}
machine->apply(subject, active);
}
StateInstance* GetState(int id) {
for (auto& state : states) {
if (state.state.id === id) {
return &state;
}
}
return nullptr;
}
float GetBlend(int id) {
auto i = GetState(id);
return i == nullptr ? 0 : i->blend;
}
StateInstances& GetStates() { return states; }
StateInputMap& GetInputs() { return inputs; }
};
template <typename S, typename T, typename O>
struct StateMachine {
using StateIsDone = std::function<bool(S*, T*, O*)>;
using StateStart = std::function<void(S*, T*, O*)>;
using StateApply = std::function<void(S*, std::vector<StateInstance<S, T, O>>)>;
std::map<int, State<S, T, O>> states;
// std::map<int, StateTransition<S, T, O>> transitions;
StateInputMap defaultInputs;
StateIsDone isDone;
StateStart start;
StateApply apply;
int AddState(std::string name, StateMachine<S, T, O>* sub, StateBlend blend = {}, int id = -1) {
int stateId = id == -1 ? states.size() : id;
states.push_back({
.name = name,
.id = stateId,
.sub = sub,
.blend = blend,
});
return stateId;
}
int AddState(std::string name, T* state, StateBlend blend = {}, int id = -1) {
int stateId = id == -1 ? states.size() : id;
states.push_back({
.name = name,
.id = stateId,
.state = state,
.blend = blend,
});
return stateId;
}
int AddInput(std::string name, float defaultValue, int id = -1) {
int inputId = id == -1 ? inputs.size() : id;
defaultInputs[inputId] = {
.id = inputId,
.name = name,
.value = defaultValue,
};
return inputId;
}
void AddTransition(int start, int end, StateCondition condition, O options) {
states[start].transitions.push_back({
.start = start,
.end = end,
.condition = condition,
.options = options,
});
}
StateMachineInstance<S, T, O>& NewInstance(S* subject) {
return StateMachineInstance(this, subject, defaultInputs);
}
};
int main()
{
std::cout << "a" << std::endl;
Animate::AddType<float>(1);
std::cout << "b" << std::endl;
auto type = Animate::GetType<float>(1);
std::cout << "c" << std::endl;
auto prop = type->getAnimatorProperty();
auto created = prop->create();
std::cout << created << std::endl;
auto path = type->getAttrimatorPath();
std::cout << path->value << std::endl;
path->value = 23;
std::cout << path->value << std::endl;
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment