载入中...
搜索中...
未找到
AnimStateMachine.cpp
浏览该文件的文档.
4
5#include "common/Capability.h"
6#include "common/Exception.h"
8
9#include <algorithm>
10#include <cmath>
11#include <limits>
12#include <stdexcept>
13#include <utility>
14
15namespace eve::animation {
16namespace {
17
18std::vector<AnimStateMachine*>& machines() {
19 static std::vector<AnimStateMachine*> instances;
20 return instances;
21}
22
23double stateNumber(const StateValue& v) { return v.isInt() ? static_cast<double>(v.asInt()) : v.asDouble(); }
24
26class AnimStateProvider : public eve::caps::IStateProvider {
27public:
28 const char* stateKind() const override { return "anim"; }
29
30 bool captureState(StateValue& out) override {
31 StateValue arr = StateValue::array();
32 for (AnimStateMachine* m : machines()) {
33 StateValue sv;
34 if (m->captureState(sv)) arr.pushBack(std::move(sv));
35 }
36 out = std::move(arr);
37 return true;
38 }
39
40 bool restoreState(const StateValue& in, std::string* err) override {
41 if (!in.isArray()) {
42 if (err) *err = "anim: expected array of state machines";
43 return false;
44 }
45 const size_t n = std::min(in.arraySize(), machines().size());
46 for (size_t i = 0; i < n; ++i) {
47 if (!machines()[i]->restoreState(in.at(i), err)) return false;
48 }
49 return true;
50 }
51
52 bool resetToDefaults() override {
53 for (AnimStateMachine* m : machines()) m->resetToDefaults();
54 return true;
55 }
56};
57
58struct Register {
59 Register() {
60 static AnimStateProvider provider;
61 eve::cap::addListener<eve::caps::IStateProvider>(&provider);
62 }
63} g_register;
64
65} // namespace
66
67AnimStateMachine::AnimStateMachine(AnimSkeleton *skeleton) : skeleton_(skeleton) {
68 if (!skeleton_) throw Exception("AnimStateMachine: skeleton is null");
69 pose_.resize(skeleton_->getBoneCount());
70 fromPose_.resize(skeleton_->getBoneCount());
71 toPose_.resize(skeleton_->getBoneCount());
72 skeleton_->applyBindPose(&pose_);
73 machines().push_back(this);
74}
75
77 machines().erase(std::remove(machines().begin(), machines().end(), this), machines().end());
78}
79
80void AnimStateMachine::requireState(const std::string &name) const {
81 if (states_.find(name) == states_.end()) {
82 throw Exception("AnimStateMachine: unknown state '%s'", name.c_str());
83 }
84}
85
86AnimStateMachine::Transition &AnimStateMachine::requireTransition(int id) {
87 if (id < 0 || id >= static_cast<int>(transitions_.size())) {
88 throw Exception("AnimStateMachine: invalid transition id %d", id);
89 }
90 return transitions_[static_cast<size_t>(id)];
91}
92
93AnimStateMachine::FloatOp AnimStateMachine::parseFloatOp(const std::string &op) {
94 if (op == ">") return FloatOp::Gt;
95 if (op == ">=") return FloatOp::Ge;
96 if (op == "<") return FloatOp::Lt;
97 if (op == "<=") return FloatOp::Le;
98 if (op == "==") return FloatOp::Eq;
99 if (op == "!=") return FloatOp::Ne;
100 throw Exception("AnimStateMachine: unknown float op '%s'", op.c_str());
101}
102
103void AnimStateMachine::addState(const std::string &name, AnimClip *clip) {
104 if (name.empty()) throw Exception("AnimStateMachine.addState: name empty");
105 if (!clip) throw Exception("AnimStateMachine.addState: clip is null");
106 if (states_.count(name)) {
107 throw Exception("AnimStateMachine.addState: duplicate state '%s'", name.c_str());
108 }
109 states_[name] = State{name, clip};
110 stateOrder_.push_back(name);
111}
112
113void AnimStateMachine::setEntry(const std::string &name) {
114 requireState(name);
115 currentState_ = name;
116 stateTime_ = 0.f;
117 blending_ = false;
118 started_ = true;
119 states_[name].clip->sample(0.f, &pose_, skeleton_);
120}
121
122bool AnimStateMachine::hasState(const std::string &name) const {
123 return states_.count(name) > 0;
124}
125
126int AnimStateMachine::addTransition(const std::string &from, const std::string &to,
127 float blendSeconds) {
128 requireState(from);
129 requireState(to);
130 if (blendSeconds < 0.f) {
131 throw Exception("AnimStateMachine.addTransition: blendSeconds must be >= 0");
132 }
133 Transition tr;
134 tr.from = from;
135 tr.to = to;
136 tr.blendSeconds = blendSeconds;
137 transitions_.push_back(tr);
138 return static_cast<int>(transitions_.size()) - 1;
139}
140
141void AnimStateMachine::addFloatCondition(int transitionId, const std::string &param,
142 const std::string &op, float threshold) {
143 if (param.empty()) throw Exception("AnimStateMachine.addFloatCondition: param empty");
144 Transition &tr = requireTransition(transitionId);
145 Condition c;
146 c.kind = CondKind::Float;
147 c.param = param;
148 c.op = parseFloatOp(op);
149 c.threshold = threshold;
150 tr.conditions.push_back(c);
151 floats_.emplace(param, 0.f);
152}
153
154void AnimStateMachine::addBoolCondition(int transitionId, const std::string &param, bool value) {
155 if (param.empty()) throw Exception("AnimStateMachine.addBoolCondition: param empty");
156 Transition &tr = requireTransition(transitionId);
157 Condition c;
158 c.kind = CondKind::Bool;
159 c.param = param;
160 c.boolValue = value;
161 tr.conditions.push_back(c);
162 bools_.emplace(param, false);
163}
164
165void AnimStateMachine::addTriggerCondition(int transitionId, const std::string &param) {
166 if (param.empty()) throw Exception("AnimStateMachine.addTriggerCondition: param empty");
167 Transition &tr = requireTransition(transitionId);
168 Condition c;
169 c.kind = CondKind::Trigger;
170 c.param = param;
171 tr.conditions.push_back(c);
172 triggers_.emplace(param, false);
173}
174
175void AnimStateMachine::setExitTime(int transitionId, float normalizedTime) {
176 Transition &tr = requireTransition(transitionId);
177 tr.exitTime = clampf(normalizedTime, 0.f, 1.f);
178 tr.hasExitTime = true;
179}
180
181void AnimStateMachine::setHasExitTime(int transitionId, bool enabled) {
182 requireTransition(transitionId).hasExitTime = enabled;
183}
184
185void AnimStateMachine::setFloat(const std::string &name, float value) { floats_[name] = value; }
186
187float AnimStateMachine::getFloat(const std::string &name) const {
188 auto it = floats_.find(name);
189 return it == floats_.end() ? 0.f : it->second;
190}
191
192void AnimStateMachine::setBool(const std::string &name, bool value) { bools_[name] = value; }
193
194bool AnimStateMachine::getBool(const std::string &name) const {
195 auto it = bools_.find(name);
196 return it == bools_.end() ? false : it->second;
197}
198
199void AnimStateMachine::setTrigger(const std::string &name) { triggers_[name] = true; }
200
201void AnimStateMachine::resetTrigger(const std::string &name) { triggers_[name] = false; }
202
204
205bool AnimStateMachine::conditionsMet(const Transition &tr) const {
206 for (const Condition &c : tr.conditions) {
207 switch (c.kind) {
208 case CondKind::Float: {
209 const float v = getFloat(c.param);
210 bool ok = false;
211 switch (c.op) {
212 case FloatOp::Gt: ok = v > c.threshold; break;
213 case FloatOp::Ge: ok = v >= c.threshold; break;
214 case FloatOp::Lt: ok = v < c.threshold; break;
215 case FloatOp::Le: ok = v <= c.threshold; break;
216 case FloatOp::Eq: ok = std::fabs(v - c.threshold) < 1e-5f; break;
217 case FloatOp::Ne: ok = std::fabs(v - c.threshold) >= 1e-5f; break;
218 }
219 if (!ok) return false;
220 break;
221 }
222 case CondKind::Bool: {
223 if (getBool(c.param) != c.boolValue) return false;
224 break;
225 }
226 case CondKind::Trigger: {
227 auto it = triggers_.find(c.param);
228 if (it == triggers_.end() || !it->second) return false;
229 break;
230 }
231 }
232 }
233 return true;
234}
235
236bool AnimStateMachine::tryTransition() {
237 if (currentState_.empty() || blending_) return false;
238 const State &cur = states_.at(currentState_);
239 const float dur = cur.clip ? cur.clip->getDuration() : 0.f;
240 const float norm = dur > 1e-8f ? clampf(stateTime_ / dur, 0.f, 1.f) : 1.f;
241
242 for (const Transition &tr : transitions_) {
243 if (tr.from != currentState_) continue;
244 if (tr.hasExitTime && norm < tr.exitTime) continue;
245 if (!conditionsMet(tr)) continue;
246
247 // Consume triggers used by this transition.
248 for (const Condition &c : tr.conditions) {
249 if (c.kind == CondKind::Trigger) triggers_[c.param] = false;
250 }
251
252 nextState_ = tr.to;
253 blendDuration_ = tr.blendSeconds;
254 blendElapsed_ = 0.f;
255 stateTime_ = 0.f;
256 if (blendDuration_ <= 1e-6f) {
257 currentState_ = nextState_;
258 blending_ = false;
259 states_.at(currentState_).clip->sample(0.f, &pose_, skeleton_);
260 } else {
261 // Switch logical state immediately; pose cross-fades from previous.
262 fromPose_.copyFrom(&pose_);
263 currentState_ = nextState_;
264 blending_ = true;
265 states_.at(currentState_).clip->sample(0.f, &toPose_, skeleton_);
266 pose_.blendFrom(&fromPose_, &toPose_, 0.f);
267 }
268 return true;
269 }
270 return false;
271}
272
273void AnimStateMachine::updateUnchecked(float dt) {
274 if (dt < 0.f) throw Exception("AnimStateMachine.update: dt must be >= 0");
275 if (!started_) {
276 if (!stateOrder_.empty()) setEntry(stateOrder_.front());
277 else return;
278 }
279 if (currentState_.empty()) return;
280
281 if (blending_) {
282 blendElapsed_ += dt;
283 stateTime_ += dt;
284 const State &dst = states_.at(currentState_);
285 dst.clip->sample(stateTime_, &toPose_, skeleton_);
286 float t = blendDuration_ > 1e-8f ? blendElapsed_ / blendDuration_ : 1.f;
287 if (t >= 1.f) {
288 blending_ = false;
289 pose_.copyFrom(&toPose_);
290 } else {
291 // Keep fromPose frozen at transition start for stable crossfade.
292 pose_.blendFrom(&fromPose_, &toPose_, t);
293 }
294 // Allow chained transitions after blend completes next frame.
295 if (!blending_) tryTransition();
296 return;
297 }
298
299 stateTime_ += dt;
300 const State &st = states_.at(currentState_);
301 st.clip->sample(stateTime_, &pose_, skeleton_);
302 tryTransition();
303}
304
306 if (step.delta.nanoseconds() < 0)
308 "animation state-machine delta must be non-negative"));
309 if (hasLastTick_ && step.tick <= lastTick_)
311 eve::DiagnosticCode::Conflict, "animation state-machine tick must advance monotonically"));
312
313 const float dt = static_cast<float>(step.delta.seconds());
314 if (!std::isfinite(dt))
316 eve::DiagnosticCode::InvalidArgument, "animation state-machine delta is outside float range"));
317 updateUnchecked(dt);
318 lastTick_ = step.tick;
319 hasLastTick_ = true;
321}
322
325 if (!duration) {
326 duration.ignore("legacy animation state-machine update received an invalid duration");
327 return;
328 }
329 auto nextTick = hasLastTick_ ? lastTick_.incremented() : std::optional<eve::SimulationTick>(eve::SimulationTick(1));
330 if (!nextTick) {
332 "animation state-machine simulation tick overflow"))
333 .ignore("legacy animation state-machine update tick overflow");
334 return;
335 }
336 advance({*nextTick, std::move(duration).takeValue()}).ignore("legacy animation state-machine update facade");
337}
338
340 out = StateValue::object();
341 out.set("currentState", StateValue::string(currentState_));
342 out.set("nextState", StateValue::string(nextState_));
343 out.set("stateTime", StateValue::number(stateTime_));
344 out.set("blendDuration", StateValue::number(blendDuration_));
345 out.set("blendElapsed", StateValue::number(blendElapsed_));
346 out.set("blending", StateValue::boolean(blending_));
347 out.set("started", StateValue::boolean(started_));
348 out.set("hasSimulationTick", StateValue::boolean(hasLastTick_));
349 out.set("simulationTick", StateValue::string(std::to_string(lastTick_.value())));
350
352 for (const auto& kv : floats_) floats.set(kv.first, StateValue::number(kv.second));
353 out.set("floats", std::move(floats));
354
356 for (const auto& kv : bools_) bools.set(kv.first, StateValue::boolean(kv.second));
357 out.set("bools", std::move(bools));
358
359 StateValue triggers = StateValue::object();
360 for (const auto& kv : triggers_) triggers.set(kv.first, StateValue::boolean(kv.second));
361 out.set("triggers", std::move(triggers));
362 return true;
363}
364
365bool AnimStateMachine::restoreState(const StateValue& in, std::string* err) {
366 if (!in.isObject()) {
367 if (err) *err = "anim: state is not an object";
368 return false;
369 }
370 const StateValue* current = in.find("currentState");
371 const StateValue* started = in.find("started");
372 if (!current || !current->isString() || !started || !started->isBool()) {
373 if (err) *err = "anim: missing currentState/started";
374 return false;
375 }
376 const std::string stateName = current->asString();
377 const bool wasStarted = started->asBool();
378 if (wasStarted && !stateName.empty() && !hasState(stateName)) {
379 if (err) *err = "anim: unknown state '" + stateName + "'";
380 return false;
381 }
382
384 bool restoredHasTick = false;
385 if (const StateValue* hasTick = in.find("hasSimulationTick"); hasTick && hasTick->isBool())
386 restoredHasTick = hasTick->asBool();
387 if (restoredHasTick) {
388 const StateValue* tick = in.find("simulationTick");
389 if (!tick || !tick->isString()) {
390 if (err) *err = "anim: missing simulationTick";
391 return false;
392 }
393 try {
394 std::size_t parsed = 0;
395 const std::uint64_t value = std::stoull(tick->asString(), &parsed);
396 if (parsed != tick->asString().size()) throw std::invalid_argument("trailing characters");
397 restoredTick = eve::SimulationTick(value);
398 } catch (...) {
399 if (err) *err = "anim: invalid simulationTick";
400 return false;
401 }
402 }
403
404 floats_.clear();
405 bools_.clear();
406 triggers_.clear();
407
408 if (const StateValue* floats = in.find("floats"); floats && floats->isObject()) {
409 for (const auto& key : floats->keys()) {
410 const StateValue* v = floats->find(key);
411 if (v && (v->isInt() || v->isFloat())) floats_[key] = static_cast<float>(stateNumber(*v));
412 }
413 }
414 if (const StateValue* bools = in.find("bools"); bools && bools->isObject()) {
415 for (const auto& key : bools->keys()) {
416 const StateValue* v = bools->find(key);
417 if (v && v->isBool()) bools_[key] = v->asBool();
418 }
419 }
420 if (const StateValue* triggers = in.find("triggers"); triggers && triggers->isObject()) {
421 for (const auto& key : triggers->keys()) {
422 const StateValue* v = triggers->find(key);
423 if (v && v->isBool()) triggers_[key] = v->asBool();
424 }
425 }
426
427 currentState_ = stateName;
428 if (const StateValue* ns = in.find("nextState"); ns && ns->isString()) nextState_ = ns->asString();
429 if (const StateValue* st = in.find("stateTime"); st && (st->isInt() || st->isFloat()))
430 stateTime_ = static_cast<float>(stateNumber(*st));
431 if (const StateValue* bd = in.find("blendDuration"); bd && (bd->isInt() || bd->isFloat()))
432 blendDuration_ = static_cast<float>(stateNumber(*bd));
433 if (const StateValue* be = in.find("blendElapsed"); be && (be->isInt() || be->isFloat()))
434 blendElapsed_ = static_cast<float>(stateNumber(*be));
435 if (const StateValue* bl = in.find("blending"); bl && bl->isBool()) blending_ = bl->asBool();
436 started_ = wasStarted;
437 lastTick_ = restoredTick;
438 hasLastTick_ = restoredHasTick;
439
440 if (started_ && !currentState_.empty()) {
441 states_.at(currentState_).clip->sample(stateTime_, &pose_, skeleton_);
442 }
443 return true;
444}
445
447 floats_.clear();
448 bools_.clear();
449 triggers_.clear();
450 blending_ = false;
451 nextState_.clear();
452 stateTime_ = 0.f;
453 blendDuration_ = 0.f;
454 blendElapsed_ = 0.f;
455 lastTick_ = eve::SimulationTick::zero();
456 hasLastTick_ = false;
457 if (!stateOrder_.empty()) {
458 setEntry(stateOrder_.front());
459 return true;
460 }
461 started_ = false;
462 currentState_.clear();
463 return true;
464}
465
466} // namespace eve::animation
double value
float duration
std::string from
std::vector< BuildingInstanceSnapshot > instances
std::uint32_t key
glm::vec4 clip
glm::vec3 n
Definition Grass.cpp:63
float v
std::int32_t c
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::string name
float tr
std::string id
Definition PlayHost.cpp:108
float begin
float t
double current
SimulationTick tick
float step
Definition TreeMesh.cpp:314
float size
Definition TreeMesh.cpp:156
float m[16]
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:393
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
JSON-compatible state value tree used by state hot reload.
Definition StateValue.h:20
static StateValue object()
Empty object.
Definition StateValue.h:40
static StateValue boolean(bool v)
Boolean value.
static StateValue array()
Empty array.
Definition StateValue.h:38
bool isString() const
Definition StateValue.h:49
bool isBool() const
Definition StateValue.h:48
bool isObject() const
Definition StateValue.h:50
static StateValue number(double v)
Floating-point value.
const StateValue * find(const std::string &key) const
Look up key; nullptr when absent.
static StateValue string(std::string v)
String value.
void set(const std::string &key, StateValue v)
Insert or replace key; only valid on objects.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Definition AnimClip.h:145
Evaluated local (and optional world) pose for an AnimSkeleton. Script type: AnimPose.
Definition AnimPose.h:17
void blendFrom(const AnimPose *a, const AnimPose *b, float t)
Blend from.
Definition AnimPose.cpp:97
void copyFrom(const AnimPose *other)
Copies from.
Definition AnimPose.cpp:91
void resize(int boneCount)
Resize.
Definition AnimPose.cpp:78
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
int getBoneCount() const
Returns the bone count.
void applyBindPose(class AnimPose *pose) const
Fill pose locals with bind pose.
void setHasExitTime(int transitionId, bool enabled)
Sets the has exit time.
float getFloat(const std::string &name) const
Returns the float.
int addTransition(const std::string &from, const std::string &to, float blendSeconds)
Add transition from → to with blend duration.
void addTriggerCondition(int transitionId, const std::string &param)
Adds trigger condition.
bool captureState(StateValue &out) const
Serialize runtime state (params, current/next state, blend).
void update(float dt)
Legacy seconds facade retained for scripts and old callers.
bool hasState(const std::string &name) const
True when state.
AnimStateMachine(AnimSkeleton *skeleton)
Anim state machine.
void resetTrigger(const std::string &name)
Resets trigger.
void addBoolCondition(int transitionId, const std::string &param, bool value)
Adds bool condition.
void setTrigger(const std::string &name)
Sets the trigger.
void addFloatCondition(int transitionId, const std::string &param, const std::string &op, float threshold)
Comparison op: ">", ">=", "<", "<=", "==", "!=".
void setBool(const std::string &name, bool value)
Sets the bool.
void setFloat(const std::string &name, float value)
Sets the float.
bool restoreState(const StateValue &in, std::string *err=nullptr)
Restore runtime state captured by captureState().
void addState(const std::string &name, AnimClip *clip)
Adds state.
bool getBool(const std::string &name) const
Returns the bool.
~AnimStateMachine() override
Anim state machine.
AnimPose * getPose() override
Borrowed pointer accessor. @ownership Borrowed @lifetime Valid while the owning animation object rema...
eve::Result< void > advance(const eve::SimulationStep &step) override
Advance state and pose using one scheduler-owned deterministic step.
void setEntry(const std::string &name)
Sets the entry.
void setExitTime(int transitionId, float normalizedTime)
Optional: require normalized local time in [0,1] >= exitTime before transition.
bool resetToDefaults()
Clear params and re-seat the entry state (restore fallback).
Runtime-state serialization for state hot reload.
constexpr std::optional< StrongUint64 > incremented() const noexcept
Returns the next value, or empty instead of unsigned wraparound.
constexpr std::uint64_t value() const noexcept
Returns the underlying value at an explicit protocol boundary.
static constexpr StrongUint64 zero() noexcept
Returns the zero value for this strong type.
float clampf(float v, float lo, float hi)
Clampf.
Definition AnimMath.h:34
WGPUStringView sv(const char *s)
Build a WGPUStringView from a C string (null-safe, length auto-computed).
Definition wgpu_types.h:20
State
Lifecycle state of a generic transaction plan.
detail::StrongUint64< detail::SimulationTickTag > SimulationTick
Deterministic simulation time step; it is not wall-clock time.
Definition Time.h:31
bool enabled
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
bool started
Definition Graphics.cpp:183