Created
January 7, 2022 00:17
-
-
Save ClickerMonkey/7243709f6ffb75b7dfaf6c621a996a1d to your computer and use it in GitHub Desktop.
C++ State Machine Prototype with Animation example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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