SleakEngine 1.0.0
C++23 multi-backend game engine
Loading...
Searching...
No Matches
AnimationStateMachine.cpp
Go to the documentation of this file.
3#include <Core/Logger.hpp>
4#include <cmath>
5#include <algorithm>
6
7namespace Sleak {
8
9int AnimationStateMachine::AddState(const std::string& name, AnimationClip* clip,
10 bool loop, float speed) {
11 int idx = static_cast<int>(m_states.size());
12 m_states.push_back({name, clip, loop, speed});
13 return idx;
14}
15
16int AnimationStateMachine::AddTransition(int from, int to, float blendDuration,
17 bool waitForClipEnd) {
18 int idx = static_cast<int>(m_transitions.size());
20 t.fromState = from;
21 t.toState = to;
22 t.blendDuration = blendDuration;
23 t.waitForClipEnd = waitForClipEnd;
24 m_transitions.push_back(std::move(t));
25 return idx;
26}
27
29 const std::string& paramName,
30 CompareOp op,
31 ParamValue threshold) {
32 if (transIndex >= 0 && transIndex < static_cast<int>(m_transitions.size())) {
33 m_transitions[transIndex].conditions.push_back({paramName, op, threshold});
34 }
35}
36
38 m_currentState = stateIndex;
39 m_currentTime = 0.0f;
40 m_blending = false;
41}
42
43void AnimationStateMachine::SetBool(const std::string& name, bool value) {
44 m_params[name] = value;
45}
46
47void AnimationStateMachine::SetFloat(const std::string& name, float value) {
48 m_params[name] = value;
49}
50
51void AnimationStateMachine::SetInt(const std::string& name, int value) {
52 m_params[name] = value;
53}
54
56 static const std::string empty;
57 if (m_currentState >= 0 && m_currentState < static_cast<int>(m_states.size()))
58 return m_states[m_currentState].name;
59 return empty;
60}
61
63 SampleRequest req;
64
65 if (m_currentState < 0 || m_states.empty())
66 return req;
67
68 const AnimationState& currentState = m_states[m_currentState];
69 AnimationClip* currentClip = currentState.clip;
70 if (!currentClip) return req;
71
72 // Advance current state time
73 m_currentTime += deltaTime * currentState.speed * currentClip->ticksPerSecond;
74
75 // Handle looping/clamping for current state
76 if (m_currentTime > currentClip->duration) {
77 if (currentState.loop) {
78 m_currentTime = std::fmod(m_currentTime, currentClip->duration);
79 } else {
80 m_currentTime = currentClip->duration;
81 }
82 }
83
84 // If blending, advance previous state time too
85 if (m_blending) {
86 const AnimationState& prevState = m_states[m_prevState];
87 AnimationClip* prevClip = prevState.clip;
88 if (prevClip) {
89 m_prevTime += deltaTime * prevState.speed * prevClip->ticksPerSecond;
90 if (m_prevTime > prevClip->duration) {
91 if (prevState.loop)
92 m_prevTime = std::fmod(m_prevTime, prevClip->duration);
93 else
94 m_prevTime = prevClip->duration;
95 }
96 }
97
98 m_blendElapsed += deltaTime;
99 float t = std::min(m_blendElapsed / m_blendDuration, 1.0f);
100
101 if (t >= 1.0f) {
102 // Blend complete
103 m_blending = false;
104 req.clipA = currentClip;
105 req.timeA = m_currentTime;
106 req.clipB = nullptr;
107 req.timeB = 0.0f;
108 req.blendWeight = 0.0f;
109 } else {
110 // Still blending: A = prev, B = current, weight goes 0→1
111 req.clipA = prevClip;
112 req.timeA = m_prevTime;
113 req.clipB = currentClip;
114 req.timeB = m_currentTime;
115 req.blendWeight = t;
116 }
117 return req;
118 }
119
120 // Not blending — check transitions from current state
121 bool clipEnded = !currentState.loop &&
122 m_currentTime >= currentClip->duration - 0.001f;
123
124 for (int i = 0; i < static_cast<int>(m_transitions.size()); ++i) {
125 const AnimationTransition& trans = m_transitions[i];
126 if (trans.fromState != m_currentState)
127 continue;
128
129 // If waitForClipEnd, only transition when clip has finished
130 if (trans.waitForClipEnd && !clipEnded)
131 continue;
132
133 if (EvaluateConditions(trans)) {
134 StartTransition(i);
135
136 // Return first frame of blend
137 const AnimationState& newState = m_states[m_currentState];
138 req.clipA = currentClip;
139 req.timeA = m_prevTime;
140 req.clipB = newState.clip;
141 req.timeB = m_currentTime;
142 req.blendWeight = 0.0f;
143 return req;
144 }
145 }
146
147 // No transition — single clip
148 req.clipA = currentClip;
149 req.timeA = m_currentTime;
150 return req;
151}
152
153bool AnimationStateMachine::EvaluateConditions(const AnimationTransition& trans) const {
154 // If no conditions, transition is always valid (used with waitForClipEnd)
155 if (trans.conditions.empty())
156 return true;
157
158 // AND logic: all conditions must be true
159 for (const auto& cond : trans.conditions) {
160 auto it = m_params.find(cond.paramName);
161 if (it == m_params.end())
162 return false;
163
164 if (!CompareParam(it->second, cond.op, cond.threshold))
165 return false;
166 }
167 return true;
168}
169
170bool AnimationStateMachine::CompareParam(const ParamValue& param, CompareOp op,
171 const ParamValue& threshold) const {
172 // Convert both to float for comparison
173 auto toFloat = [](const ParamValue& v) -> float {
174 if (std::holds_alternative<bool>(v))
175 return std::get<bool>(v) ? 1.0f : 0.0f;
176 if (std::holds_alternative<float>(v))
177 return std::get<float>(v);
178 if (std::holds_alternative<int>(v))
179 return static_cast<float>(std::get<int>(v));
180 return 0.0f;
181 };
182
183 float a = toFloat(param);
184 float b = toFloat(threshold);
185
186 switch (op) {
187 case CompareOp::Equal: return std::fabs(a - b) < 0.001f;
188 case CompareOp::NotEqual: return std::fabs(a - b) >= 0.001f;
189 case CompareOp::Greater: return a > b;
190 case CompareOp::GreaterEqual: return a >= b;
191 case CompareOp::Less: return a < b;
192 case CompareOp::LessEqual: return a <= b;
193 }
194 return false;
195}
196
197void AnimationStateMachine::StartTransition(int transIndex) {
198 const AnimationTransition& trans = m_transitions[transIndex];
199
200 m_prevState = m_currentState;
201 m_prevTime = m_currentTime;
202
203 m_currentState = trans.toState;
204 m_currentTime = 0.0f;
205
206 m_blending = true;
207 m_blendElapsed = 0.0f;
208 m_blendDuration = trans.blendDuration;
209
210 SLEAK_INFO("AnimSM: {} -> {} (blend {:.2f}s)",
211 m_states[m_prevState].name,
212 m_states[m_currentState].name,
213 m_blendDuration);
214}
215
216} // namespace Sleak
#define SLEAK_INFO(...)
Definition Logger.hpp:20
void SetDefaultState(int stateIndex)
Sets the state the machine starts in, with no blend.
int AddTransition(int from, int to, float blendDuration=0.3f, bool waitForClipEnd=false)
Adds a transition edge from -> to, returning its index.
SampleRequest Update(float deltaTime)
Advances playback/blend time, evaluates transitions, and returns what to sample this frame.
void SetFloat(const std::string &name, float value)
void SetBool(const std::string &name, bool value)
void AddTransitionCondition(int transIndex, const std::string &paramName, CompareOp op, ParamValue threshold)
Appends a guard condition to the transition at transIndex.
int AddState(const std::string &name, AnimationClip *clip, bool loop=true, float speed=1.0f)
Adds a state bound to clip, returning its index for use in AddTransition.
void SetInt(const std::string &name, int value)
const std::string & GetCurrentStateName() const
Root namespace for everything the engine exposes.
Definition Camera.hpp:10
std::variant< bool, float, int > ParamValue
std::vector< TransitionCondition > conditions