载入中...
搜索中...
未找到
ParticleEffect.cpp
浏览该文件的文档.
2
3#include "common/Json.h"
4#include "common/Module.h"
9
10#include <algorithm>
11#include <cmath>
12#include <filesystem>
13#include <memory>
14#include <unordered_set>
15
16namespace eve::particles {
17namespace {
18
19using Json = eve::json::Value;
20
21std::vector<ParticleEffect*>& timelineRegistry() {
22 static std::vector<ParticleEffect*> registry;
23 return registry;
24}
25
26int objectBuffer(const Json& object, int fallback) {
27 if (!object.isObject() || !object.has("buffer")) return fallback;
28 return std::max(1, object.getInt("buffer", fallback));
29}
30
31void readOffset(const Json& object, float& x, float& y) {
32 if (!object.isObject()) return;
33 if (object.has("offset")) {
34 const Json offset = object.get("offset");
35 if (offset.isArray() && offset.size() >= 2) {
36 x = offset.at(0).asFloat(x);
37 y = offset.at(1).asFloat(y);
38 }
39 }
40 if (object.has("x")) x = object.getFloat("x", x);
41 if (object.has("y")) y = object.getFloat("y", y);
42}
43
44std::string resolveAssetPath(const std::string& sourcePath, const std::string& referencedPath) {
45 if (sourcePath.empty() || referencedPath.empty()) return referencedPath;
46 const std::filesystem::path reference(referencedPath);
47 if (reference.is_absolute()) return reference.generic_string();
48 return (std::filesystem::path(sourcePath).parent_path() / reference).lexically_normal().generic_string();
49}
50
51bool applyParameters(ParticleEmitter* emitter, const Json& parameters) {
52 if (!emitter || !parameters.isObject()) return false;
53 for (const auto& key : parameters.keys()) emitter->setFloatParameter(key, parameters.getFloat(key.c_str(), 0.f));
54 return true;
55}
56
57bool validTrigger(const std::string& trigger) {
58 return trigger == "birth" || trigger == "death" || trigger == "collision";
59}
60
61bool validCueAction(const std::string& action) {
62 return action == "start" || action == "stop" || action == "pause" || action == "reset" || action == "emit" ||
63 action == "setParameter";
64}
65
66Result<void> parseEventRoutes(const Json& root, std::vector<ParticleEffectEventRoute>& out) {
67 out.clear();
68 if (!root.isObject() || !root.has("eventRoutes")) return Result<void>::success();
69 const Json routes = root.get("eventRoutes");
70 if (!routes.isArray())
72 Diagnostic::error(DiagnosticCode::InvalidArgument, "effect eventRoutes must be an array", "eventRoutes"));
73 for (std::size_t i = 0; i < routes.size(); ++i) {
74 const Json routeObject = routes.at(i);
75 if (!routeObject.isObject())
77 Diagnostic::error(DiagnosticCode::InvalidArgument, "event route must be an object", "eventRoutes"));
78 ParticleEffectEventRoute route;
79 route.from = routeObject.has("from") ? routeObject.getString("from") : std::string();
80 route.to = routeObject.has("to") ? routeObject.getString("to") : std::string();
81 route.on = routeObject.has("on") ? routeObject.getString("on") : std::string("death");
82 route.inheritVelocity = routeObject.has("inheritVelocity") ? routeObject.getFloat("inheritVelocity", 0.f) : 0.f;
83 if (route.from.empty() || route.to.empty())
85 DiagnosticCode::InvalidArgument, "event route requires from and to emitter names", "eventRoutes"));
86 if (!validTrigger(route.on))
88 "event route on must be birth, death, or collision",
89 "eventRoutes.on"));
90 if (route.from == route.to)
92 "event route cannot target its own emitter", "eventRoutes"));
93 out.push_back(std::move(route));
94 }
95 return Result<void>::success();
96}
97
98Result<void> parseTimeline(const Json& root, ParticleEffectTimeline& out) {
99 out = {};
100 if (!root.isObject() || !root.has("timeline")) return Result<void>::success();
101 const Json timelineObject = root.get("timeline");
102 if (!timelineObject.isObject())
104 Diagnostic::error(DiagnosticCode::InvalidArgument, "effect timeline must be an object", "timeline"));
105 out.duration = timelineObject.has("duration") ? timelineObject.getFloat("duration", 0.f) : 0.f;
106 out.looping = timelineObject.has("looping") ? timelineObject.getBool("looping", false) : false;
107 if (!std::isfinite(out.duration) || out.duration < 0.f)
109 "timeline.duration must be a finite non-negative number",
110 "timeline.duration"));
111 if (!timelineObject.has("cues")) return Result<void>::success();
112 const Json cues = timelineObject.get("cues");
113 if (!cues.isArray())
115 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline.cues must be an array", "timeline.cues"));
116 for (std::size_t i = 0; i < cues.size(); ++i) {
117 const Json cueObject = cues.at(i);
118 if (!cueObject.isObject())
120 Diagnostic::error(DiagnosticCode::InvalidArgument, "timeline cue must be an object", "timeline.cues"));
121 ParticleEffectTimelineCue cue;
122 cue.time = cueObject.has("time") ? cueObject.getFloat("time", 0.f) : 0.f;
123 cue.action = cueObject.has("action") ? cueObject.getString("action") : std::string();
124 cue.emitter = cueObject.has("emitter") ? cueObject.getString("emitter") : std::string();
125 cue.parameter = cueObject.has("parameter") ? cueObject.getString("parameter") : std::string();
126 cue.value = cueObject.has("value") ? cueObject.getFloat("value", 0.f) : 0.f;
127 cue.count = cueObject.has("count") ? cueObject.getInt("count", 0) : 0;
128 if (!std::isfinite(cue.time) || cue.time < 0.f)
130 "timeline cue time must be finite and non-negative",
131 "timeline.cues.time"));
132 if (!validCueAction(cue.action))
135 "timeline cue action must be start, stop, pause, reset, emit, or setParameter",
136 "timeline.cues.action"));
137 if (cue.action == "emit" && cue.count <= 0)
139 DiagnosticCode::InvalidArgument, "timeline emit cue requires a positive count", "timeline.cues.count"));
140 if (cue.action == "setParameter" && cue.parameter.empty())
142 "timeline setParameter cue requires a parameter name",
143 "timeline.cues.parameter"));
144 if ((cue.action == "start" || cue.action == "stop" || cue.action == "pause" || cue.action == "reset" ||
145 cue.action == "emit") &&
146 cue.emitter.empty())
148 "timeline emitter cue requires an emitter name",
149 "timeline.cues.emitter"));
150 out.cues.push_back(std::move(cue));
151 }
152 std::stable_sort(
153 out.cues.begin(), out.cues.end(),
154 [](const ParticleEffectTimelineCue& a, const ParticleEffectTimelineCue& b) { return a.time < b.time; });
155 return Result<void>::success();
156}
157
158} // namespace
159
160Result<ParticleEffect*> ParticleEffect::parse(const std::string& json, const std::string& sourcePath) {
161 std::string parseError;
162 auto document = eve::json::Document::parse(json, &parseError);
163 const Json root = document.root();
164 if (!document.valid() || !root.isObject())
167 parseError.empty() ? "effect root must be an object" : parseError, "json"));
168
169 if (!root.has("type") || root.getString("type") != "eve.particle-effect")
171 Diagnostic::error(DiagnosticCode::InvalidArgument, "effect type must be eve.particle-effect", "type"));
172 const int version = root.has("version") ? root.getInt("version", 0) : 0;
173 if (version < ParticleEffect::kMinSupportedVersion || version > ParticleEffect::kMaxSupportedVersion)
175 DiagnosticCode::Unsupported, "unsupported particle effect version: " + std::to_string(version), "version"));
176
177 const Json emitterArray = root.get("emitters");
178 if (!emitterArray.isArray() || emitterArray.size() == 0)
180 DiagnosticCode::InvalidArgument, "effect emitters must be a non-empty array", "emitters"));
181
182 auto effect = std::unique_ptr<ParticleEffect>(new ParticleEffect());
183 effect->version_ = version;
184 effect->sourcePath_ = sourcePath;
185
186 Json globalParameters;
187 if (root.has("parameters")) {
188 globalParameters = root.get("parameters");
189 if (!globalParameters.isObject())
191 DiagnosticCode::InvalidArgument, "effect parameters must be an object", "parameters"));
192 for (const auto& key : globalParameters.keys())
193 effect->parameters_[key] = globalParameters.getFloat(key.c_str(), 0.f);
194 }
195
196 auto routes = parseEventRoutes(root, effect->eventRoutes_);
197 if (!routes) return Result<ParticleEffect*>::failure(routes.status());
198 auto timeline = parseTimeline(root, effect->timeline_);
200 if (version >= 2 && effect->eventRoutes_.empty() && effect->timeline_.cues.empty() &&
201 effect->timeline_.duration <= 0.f) {
202 // v2 may omit both, but still records schema identity for migration tests.
203 }
204
205 std::unordered_set<std::string> names;
206 for (std::size_t i = 0; i < emitterArray.size(); ++i) {
207 const Json layerObject = emitterArray.at(i);
208 if (!layerObject.isObject())
211 "emitter layer " + std::to_string(i) + " must be an object", "emitters"));
212
213 const std::string name = layerObject.has("name") ? layerObject.getString("name") : std::string();
214 if (name.empty())
217 "emitter layer " + std::to_string(i) + " requires a name", "emitters.name"));
218 if (!names.insert(name).second)
220 Diagnostic::error(DiagnosticCode::Conflict, "duplicate emitter layer name: " + name, "emitters.name"));
221
222 Json embeddedConfig;
223 if (layerObject.has("emitter")) {
224 embeddedConfig = layerObject.get("emitter");
225 if (!embeddedConfig.isObject())
228 "emitter layer " + name + " has an invalid emitter object", "emitters.emitter"));
229 }
230 const std::string configPath = layerObject.has("config") ? layerObject.getString("config") : std::string();
231 if (embeddedConfig.isObject() && !configPath.empty())
234 "emitter layer " + name + " cannot define both emitter and config", "emitters"));
235 if (!embeddedConfig.isObject() && configPath.empty())
237 DiagnosticCode::InvalidArgument, "emitter layer " + name + " requires emitter or config", "emitters"));
238
239 int buffer = layerObject.has("buffer") ? layerObject.getInt("buffer", 1000) : 1000;
240 buffer = objectBuffer(embeddedConfig, buffer);
241 auto* emitter = ParticleEmitter::createEmitter(std::max(1, buffer));
242 std::string configError;
243 bool configured = false;
244 if (embeddedConfig.isObject()) {
245 auto applied = applyConfigDocument(emitter, embeddedConfig);
246 configured = static_cast<bool>(applied);
247 if (!configured) {
248 const auto* diag = applied.error();
249 configError = (diag && !diag->message().empty()) ? diag->message() : "config root must be object";
250 }
251 } else {
252 configured = loadConfigFile(emitter, resolveAssetPath(sourcePath, configPath), &configError);
253 }
254 if (!configured) {
255 emitter->release();
257 Diagnostic::error(DiagnosticCode::Failed, "emitter layer " + name + ": " + configError, "emitters"));
258 }
259
260 applyParameters(emitter, globalParameters);
261 if (layerObject.has("parameters")) {
262 const Json layerParameters = layerObject.get("parameters");
263 if (!layerParameters.isObject()) {
264 emitter->release();
266 DiagnosticCode::InvalidArgument, "emitter layer " + name + " parameters must be an object",
267 "emitters.parameters"));
268 }
269 applyParameters(emitter, layerParameters);
270 }
271
272 Layer layer;
273 layer.name = name;
274 layer.emitter = emitter;
275 readOffset(layerObject, layer.offsetX, layer.offsetY);
276 const float localRotation = layerObject.has("rotation") ? layerObject.getFloat("rotation", 0.f) : 0.f;
277 layer.baseDirection = emitter->getDirection() + localRotation;
278 layer.baseLayer = emitter->getLayer();
279 layer.enabled = layerObject.has("enabled") ? layerObject.getBool("enabled", true) : true;
280 emitter->setVisible(layer.enabled);
281 if (!layer.enabled) emitter->stop();
282 effect->layers_.push_back(std::move(layer));
283 }
284
285 for (const auto& route : effect->eventRoutes_) {
286 if (!effect->getEmitterByName(route.from) || !effect->getEmitterByName(route.to))
288 DiagnosticCode::NotFound, "event route references unknown emitter: " + route.from + " -> " + route.to,
289 "eventRoutes"));
290 }
291 for (const auto& cue : effect->timeline_.cues) {
292 if (!cue.emitter.empty() && !effect->getEmitterByName(cue.emitter))
294 Diagnostic::error(DiagnosticCode::NotFound, "timeline cue references unknown emitter: " + cue.emitter,
295 "timeline.cues.emitter"));
296 }
297
298 effect->wireEventRoutes();
299 effect->rewindTimelineClock();
300 effect->syncTransform();
301 effect->syncLayer();
303}
304
306 unregisterFromTimelineTicks();
307 for (auto& layer : layers_) {
308 if (layer.emitter) layer.emitter->release();
309 layer.emitter = nullptr;
310 }
311}
312
313ParticleEffect* ParticleEffect::fromText(const std::string& json, const std::string& sourcePath, std::string* error) {
314 auto parsed = tryFromText(json, sourcePath);
315 if (!parsed) {
316 if (error) {
317 if (parsed.error())
318 *error = parsed.error()->message();
319 else
320 *error = "effect parse failed";
321 }
322 parsed.ignore();
323 return nullptr;
324 }
325 if (error) error->clear();
326 return parsed.value();
327}
328
329ParticleEffect* ParticleEffect::fromFile(const std::string& path, std::string* error) {
330 auto parsed = tryFromFile(path);
331 if (!parsed) {
332 if (error) {
333 if (parsed.error())
334 *error = parsed.error()->message();
335 else
336 *error = "effect load failed";
337 }
338 parsed.ignore();
339 return nullptr;
340 }
341 if (error) error->clear();
342 return parsed.value();
343}
344
345Result<ParticleEffect*> ParticleEffect::tryFromText(const std::string& json, const std::string& sourcePath) {
346 return parse(json, sourcePath);
347}
348
350 if (path.empty())
352 Diagnostic::error(DiagnosticCode::InvalidArgument, "empty effect path", "path"));
353 auto* filesystem = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
354 if (!filesystem) filesystem = eve::filesystem::Filesystem::create();
355 std::unique_ptr<eve::filesystem::FileData> data;
356 try {
357 data.reset(filesystem->read(path));
358 } catch (...) {
360 Diagnostic::error(DiagnosticCode::NotFound, "read failed: " + path, "path"));
361 }
362 if (!data || data->getSize() == 0)
364 Diagnostic::error(DiagnosticCode::Failed, "empty file: " + path, "path"));
365 return tryFromText(std::string(static_cast<const char*>(data->getData()), data->getSize()), path);
366}
367
368int ParticleEffect::getEmitterCount() const { return static_cast<int>(layers_.size()); }
369
370std::string ParticleEffect::getEmitterName(int index) const {
371 if (index < 0 || index >= getEmitterCount()) return {};
372 return layers_[static_cast<std::size_t>(index)].name;
373}
374
376 if (index < 0 || index >= getEmitterCount()) return nullptr;
377 return layers_[static_cast<std::size_t>(index)].emitter;
378}
379
381 auto layer = std::find_if(layers_.begin(), layers_.end(), [&](const Layer& value) { return value.name == name; });
382 return layer == layers_.end() ? nullptr : layer->emitter;
383}
384
385std::span<const ParticleEffectEventRoute> ParticleEffect::eventRoutes() const {
386 return std::span<const ParticleEffectEventRoute>(eventRoutes_.data(), eventRoutes_.size());
387}
388
390 double maximumNonLooping = 0.0;
391 double maximumLoopPeriod = 0.0;
392 bool hasEnabledLayer = false;
393 for (const auto& layer : layers_) {
394 if (!layer.enabled || !layer.emitter) continue;
395 hasEnabledLayer = true;
396 const double emitterLife = layer.emitter->getEmitterLifetime();
397 const double particleLife = layer.emitter->getParticleLifetimeMax();
398 if (!std::isfinite(emitterLife) || !std::isfinite(particleLife) || particleLife < 0.0)
400 DiagnosticCode::InvalidArgument, "Particle effect contains invalid lifetime data", "emitters"));
401 if (layer.emitter->getLooping()) {
402 if (emitterLife <= 0.0)
404 DiagnosticCode::Unsupported, "Particle effect contains an unbounded looping emitter", "emitters"));
405 maximumLoopPeriod = std::max(maximumLoopPeriod, emitterLife);
406 continue;
407 }
408 if (emitterLife < 0.0)
410 DiagnosticCode::Unsupported, "Particle effect contains an unbounded emitter", "emitters"));
411 maximumNonLooping = std::max(maximumNonLooping, emitterLife + particleLife);
412 }
413 if (!hasEnabledLayer)
415 Diagnostic::error(DiagnosticCode::NotFound, "Particle effect has no enabled emitters", "emitters"));
416 double seconds = maximumNonLooping > 0.001 ? maximumNonLooping : maximumLoopPeriod;
417 if (timeline_.duration > 0.f) seconds = std::max(seconds, double(timeline_.duration));
418 auto duration = Duration::fromSeconds(std::max(0.01, seconds));
419 if (!duration) return Result<Duration>::failure(duration.status());
420 return Result<Duration>::success(duration.value());
421}
422
424 auto parsed = tryFromText(json, sourcePath_);
425 if (!parsed) return Result<void>::failure(parsed.status());
426 std::unique_ptr<ParticleEffect> candidate(parsed.value());
427 adoptParsed(std::move(*candidate), true);
429}
430
432 const std::string loadPath = path.empty() ? sourcePath_ : path;
433 auto parsed = tryFromFile(loadPath);
434 if (!parsed) return Result<void>::failure(parsed.status());
435 std::unique_ptr<ParticleEffect> candidate(parsed.value());
436 adoptParsed(std::move(*candidate), true);
438}
439
440void ParticleEffect::adoptParsed(ParticleEffect&& other, bool preserveWorldState) {
441 unregisterFromTimelineTicks();
442 for (auto& layer : layers_) {
443 if (layer.emitter) layer.emitter->release();
444 layer.emitter = nullptr;
445 }
446 layers_ = std::move(other.layers_);
447 other.layers_.clear();
448 version_ = other.version_;
449 sourcePath_ = std::move(other.sourcePath_);
450 parameters_ = std::move(other.parameters_);
451 eventRoutes_ = std::move(other.eventRoutes_);
452 timeline_ = std::move(other.timeline_);
453 runtimeCues_.clear();
454 for (const auto& cue : timeline_.cues) runtimeCues_.push_back(RuntimeCue{cue, false});
455 timelineSeconds_ = 0.0;
456 timelinePlaying_ = false;
457 timelinePaused_ = false;
458 if (!preserveWorldState) {
459 x_ = other.x_;
460 y_ = other.y_;
461 rotation_ = other.rotation_;
462 scale_ = other.scale_;
463 layer_ = other.layer_;
464 visible_ = other.visible_;
465 }
466 syncTransform();
467 syncLayer();
468 setVisible(visible_);
469}
470
472 if (!timelinePlaying_ || timelinePaused_) return Result<void>::success(Status::success(StatusCode::NoOp));
473 if (step.delta.nanoseconds() < 0 || !std::isfinite(step.delta.seconds()))
475 DiagnosticCode::InvalidArgument, "timeline step duration must be finite and non-negative", "step.delta"));
476 if (runtimeCues_.empty() && timeline_.duration <= 0.f)
478
479 const double previous = timelineSeconds_;
480 double next = previous + step.delta.seconds();
481 auto fired = fireDueCues(previous, next);
482 if (!fired) return fired;
483
484 if (timeline_.duration > 0.f && next + 1e-9 >= double(timeline_.duration)) {
485 if (timeline_.looping) {
486 const double overflow = next - double(timeline_.duration);
487 rewindTimelineClock();
488 timelinePlaying_ = true;
489 timelinePaused_ = false;
490 registerForTimelineTicks();
491 auto looped = fireDueCues(-1e-9, std::max(0.0, overflow));
492 if (!looped) return looped;
493 timelineSeconds_ = std::max(0.0, overflow);
495 }
496 timelineSeconds_ = double(timeline_.duration);
497 timelinePlaying_ = false;
498 unregisterFromTimelineTicks();
500 }
501
502 timelineSeconds_ = next;
504}
505
507 if (!std::isfinite(dt) || dt < 0.f) return;
508 auto duration = Duration::fromSeconds(double(dt));
509 if (!duration) {
510 duration.ignore();
511 return;
512 }
514 step.delta = duration.value();
516}
517
519 auto& registry = timelineRegistry();
520 for (std::size_t i = 0; i < registry.size(); ++i) {
521 if (ParticleEffect* effect = registry[i]) effect->updateTimeline(dt);
522 }
523}
524
526 bool changed = false;
527 auto& registry = timelineRegistry();
528 for (std::size_t i = 0; i < registry.size(); ++i) {
529 ParticleEffect* effect = registry[i];
530 if (!effect) continue;
531 auto advanced = effect->advanceTimeline(step);
532 if (!advanced) return advanced;
533 if (advanced.status().code() == StatusCode::Applied) changed = true;
534 advanced.ignore();
535 }
537}
538
539void ParticleEffect::syncTransform() {
540 const float c = std::cos(rotation_);
541 const float s = std::sin(rotation_);
542 for (auto& layer : layers_) {
543 const float ox = layer.offsetX * scale_;
544 const float oy = layer.offsetY * scale_;
545 layer.emitter->setPosition(x_ + ox * c - oy * s, y_ + ox * s + oy * c);
546 layer.emitter->setDirection(layer.baseDirection + rotation_);
547 }
548}
549
550void ParticleEffect::syncLayer() {
551 for (auto& value : layers_) value.emitter->setLayer(value.baseLayer + layer_);
552}
553
554void ParticleEffect::wireEventRoutes() {
555 for (auto& layer : layers_)
556 if (layer.emitter) layer.emitter->clearSubEmitters();
557 for (const auto& route : eventRoutes_) {
558 auto* from = getEmitterByName(route.from);
559 auto* to = getEmitterByName(route.to);
560 if (from && to) from->addSubEmitter(to, route.on, route.inheritVelocity);
561 }
562}
563
564void ParticleEffect::rewindTimelineClock() {
565 timelineSeconds_ = 0.0;
566 runtimeCues_.clear();
567 for (const auto& cue : timeline_.cues) runtimeCues_.push_back(RuntimeCue{cue, false});
568}
569
570void ParticleEffect::registerForTimelineTicks() {
571 if (timelineRegistered_) return;
572 if (runtimeCues_.empty() && timeline_.duration <= 0.f) return;
573 timelineRegistry().push_back(this);
574 timelineRegistered_ = true;
575}
576
577void ParticleEffect::unregisterFromTimelineTicks() {
578 if (!timelineRegistered_) return;
579 auto& registry = timelineRegistry();
580 registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end());
581 timelineRegistered_ = false;
582}
583
584Result<void> ParticleEffect::fireDueCues(double previousSeconds, double currentSeconds) {
585 bool changed = false;
586 for (auto& runtime : runtimeCues_) {
587 if (runtime.fired) continue;
588 const double cueTime = double(runtime.cue.time);
589 // Inclusive of currentSeconds so t=0 cues fire on start(previous=-eps, current=0).
590 if (!(cueTime > previousSeconds && cueTime <= currentSeconds + 1e-12)) continue;
591 auto applied = applyCue(runtime.cue);
592 if (!applied) return applied;
593 runtime.fired = true;
594 changed = true;
595 }
597}
598
599Result<void> ParticleEffect::applyCue(const ParticleEffectTimelineCue& cue) {
600 if (cue.action == "setParameter") {
601 setFloatParameter(cue.parameter, cue.value);
603 }
604 auto* emitter = getEmitterByName(cue.emitter);
605 if (!emitter)
607 Diagnostic::error(DiagnosticCode::NotFound, "timeline cue emitter is missing: " + cue.emitter, "emitter"));
608 if (cue.action == "start") {
609 emitter->start();
610 } else if (cue.action == "stop") {
611 emitter->stop();
612 } else if (cue.action == "pause") {
613 emitter->pause();
614 } else if (cue.action == "reset") {
615 emitter->reset();
616 } else if (cue.action == "emit") {
617 emitter->emit(cue.count);
618 } else {
620 "unsupported timeline cue action: " + cue.action, "action"));
621 }
623}
624
625void ParticleEffect::setPosition(float x, float y) {
626 x_ = x;
627 y_ = y;
628 syncTransform();
629}
630
631void ParticleEffect::setRotation(float radians) {
632 rotation_ = radians;
633 syncTransform();
634}
635
637 scale_ = std::max(0.f, scale);
638 syncTransform();
639}
640
642 layer_ = layer;
643 syncLayer();
644}
645
647 visible_ = visible;
648 for (auto& layer : layers_) layer.emitter->setVisible(visible && layer.enabled);
649}
650
652 for (auto& layer : layers_)
653 if (layer.enabled) layer.emitter->start();
654 rewindTimelineClock();
655 timelinePlaying_ = true;
656 timelinePaused_ = false;
657 registerForTimelineTicks();
658 fireDueCues(-1e-9, 0.0).ignore();
659}
660
662 for (auto& layer : layers_) layer.emitter->stop();
663 timelinePlaying_ = false;
664 timelinePaused_ = false;
665 unregisterFromTimelineTicks();
666}
667
669 for (auto& layer : layers_)
670 if (layer.enabled) layer.emitter->pause();
671 timelinePaused_ = true;
672}
673
675 for (auto& layer : layers_) layer.emitter->reset();
676 rewindTimelineClock();
677 timelinePlaying_ = false;
678 timelinePaused_ = false;
679 unregisterFromTimelineTicks();
680}
681
682bool ParticleEffect::emit(const std::string& emitterName, int count) {
683 auto* emitter = getEmitterByName(emitterName);
684 if (!emitter || count <= 0) return false;
685 emitter->emit(count);
686 return true;
687}
688
689void ParticleEffect::setFloatParameter(const std::string& name, float value) {
690 if (name.empty()) return;
691 parameters_[name] = value;
692 for (auto& layer : layers_) layer.emitter->setFloatParameter(name, value);
693}
694
695float ParticleEffect::getFloatParameter(const std::string& name) const {
696 auto parameter = parameters_.find(name);
697 return parameter == parameters_.end() ? 0.f : parameter->second;
698}
699
700bool ParticleEffect::hasFloatParameter(const std::string& name) const {
701 return parameters_.find(name) != parameters_.end();
702}
703
704} // namespace eve::particles
double value
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
int root
Definition AnimSmr.cpp:119
std::string from
AuthorityStoreHandleRef reference
Definition Authority.cpp:24
const std::string & s
std::uint32_t key
std::int32_t c
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
size_t offset
std::array< float, 3 > scale
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
graphics::Canvas * previous
std::unique_ptr< gpgpu::GpuBuffer > buffer
Definition OnnxGpgpu.cpp:26
std::string error
Definition Package.cpp:60
std::unique_ptr< ParticleEffect > effect
TileLayer * layer
std::string action
Definition PlayHost.cpp:117
std::string path
Definition PlayHost.cpp:110
std::uint32_t count
std::unique_ptr< TrailEmitter > emitter
std::string sourcePath
bool visible
float step
Definition TreeMesh.cpp:314
uint32_t index
double oy
double ox
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
void ignore(std::string_view reason={}) const noexcept
Explicitly discard this result after documenting the reason.
Definition Result.h:537
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
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static Document parse(const std::string &text, std::string *error=nullptr)
Parse.
Definition Json.cpp:581
EVENGINE_API_FOUNDATION public API.
Definition Json.h:38
Runtime instance of a versioned, multi-emitter particle effect asset.
static Result< ParticleEffect * > tryFromFile(const std::string &path)
Load an effect asset file and return a structured Result.
static Result< ParticleEffect * > tryFromText(const std::string &json, const std::string &sourcePath={})
Parse an effect asset and return a structured Result.
bool hasFloatParameter(const std::string &name) const
Return true when the asset declares the parameter.
ParticleEmitter * getEmitter(int index) const
Return an emitter layer by index, or null.
Result< void > reloadFromText(const std::string &json)
Atomically replace this instance from JSON text.
Result< Duration > naturalDuration() const
Calculate when every enabled finite emitter and its particles have ended.
void pause()
Pause every emitter timeline and the effect clock.
void setPosition(float x, float y)
Set the group transform origin in world space.
void setScale(float scale)
Scale local emitter offsets.
void reset()
Reset every emitter timeline, clear particles, and rewind cues.
static ParticleEffect * fromFile(const std::string &path, std::string *error=nullptr)
Read, parse, and instantiate an effect asset.
const ParticleEffectTimeline & timeline() const
Return authored timeline controls.
ParticleEffect(const ParticleEffect &)=delete
Result< void > reloadFromFile(const std::string &path={})
Atomically reload from sourcePath, or an explicit path override.
void setVisible(bool visible)
Set visibility for every emitter layer.
static Result< void > advanceRegisteredTimelines(const SimulationStep &step)
Advance registered timelines from an injected scheduler step.
Result< void > advanceTimeline(const SimulationStep &step)
Advance the effect-local timeline and fire due cues.
ParticleEmitter * getEmitterByName(const std::string &name) const
Return an emitter layer by name, or null.
void setLayer(int layer)
Offset all emitter render layers while preserving asset-local ordering.
static ParticleEffect * fromText(const std::string &json, const std::string &sourcePath, std::string *error=nullptr)
Parse and instantiate an effect asset from JSON text.
void start()
Start every enabled emitter and the effect timeline clock.
std::span< const ParticleEffectEventRoute > eventRoutes() const
Return authored event routes (borrowed snapshot of asset data).
void stop()
Stop every emitter timeline and the effect clock.
void setFloatParameter(const std::string &name, float value)
Set one gameplay parameter on every layer that declares or binds it.
int getEmitterCount() const
Return the number of named emitter layers.
float getFloatParameter(const std::string &name) const
Return an effect parameter value, or zero when unknown.
~ParticleEffect()
Destroy the group and every emitter it owns.
std::string getEmitterName(int index) const
Return a stable layer name by index, or empty text.
static constexpr int kMaxSupportedVersion
Highest schema version accepted by this build.
static void tickRegisteredTimelines(float dt)
Advance every effect that auto-registered a playing timeline.
void setRotation(float radians)
Rotate local offsets and emitter directions in radians.
bool emit(const std::string &emitterName, int count)
Emit a manual burst from one named layer.
void updateTimeline(float dt)
Legacy seconds facade for timeline cues; consumes Result internally.
ECS emitter entity. Script configures components; ParticleSimSystem / ParticleRenderSystem drive per-...
static ParticleEmitter * createEmitter(int bufferSize=1000)
Result< T > applied(T value, std::vector< Diagnostic > diagnostics={})
Construct an Applied result with an owning payload and optional diagnostics.
bool loadConfigFile(ParticleEmitter *emitter, const std::string &path, std::string *error)
Read path via Filesystem, apply, and bind Resource.path + modtime for hot reload. Returns false if fi...
eve::Result< void > applyConfigDocument(ParticleEmitter *emitter, eve::json::Value root)
Apply particle JSON config onto an emitter, including material texture resources. Does not change buf...
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
One deterministic fixed-step emitted by SimulationClock.
Definition Time.h:158
Duration delta
Fixed simulation duration for this step.
Definition Time.h:162