载入中...
搜索中...
未找到
AnimationGraph.cpp
浏览该文件的文档.
2
3#include <algorithm>
4#include <map>
5#include <sstream>
6#include <utility>
7
9namespace {
10
11const EditorValue* field(const EditorValue& value, const char* key) {
12 const auto* object = value.getIf<EditorValue::Object>();
13 if (!object) return nullptr;
14 const auto found = object->find(key);
15 return found == object->end() ? nullptr : &found->second;
16}
17
18void diagnostic(AnimationGraphCompileResult& result, const char* rule, std::string message) {
19 result.diagnostics.push_back(eve::editing::ruleDiagnostic(
20 eve::DiagnosticCode::PreconditionViolation, RuleId(rule), DiagnosticSeverity::Error, std::move(message)));
21}
22
23std::string quote(const std::string& value) {
24 std::string result = "\"";
25 for (const char character : value) {
26 if (character == '\\' || character == '\"') result.push_back('\\');
27 result.push_back(character);
28 }
29 result.push_back('\"');
30 return result;
31}
32
33} // namespace
34
36 const GraphPinRecord& to) const {
38 decision.allowed = from.direction == GraphPinDirection::Output &&
39 to.direction == GraphPinDirection::Input && from.type == "animation.flow" &&
40 to.type == "animation.flow" && from.node != to.node;
41 if (!decision.allowed)
43 eve::DiagnosticCode::PreconditionViolation, RuleId("editor.animation.invalid-connection"),
44 DiagnosticSeverity::Error,
45 "Animation graph requires flow output-to-input between distinct nodes"));
46 return decision;
47}
48
50 const std::string& clipAsset) const {
51 if (id.empty() || clipAsset.empty())
52 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected, RuleId("editor.animation.invalid-state"),
53 "Animation state id and clip asset are required");
55 node.id = id;
56 node.type = "animation.state";
58 properties["clip"] = clipAsset;
59 properties["speed"] = 1.0;
60 properties["loop"] = true;
61 node.properties = EditorValue(std::move(properties));
62 node.pins = {{GraphPinId(id.value() + ".in"), id, "animation.flow", GraphPinDirection::Input},
63 {GraphPinId(id.value() + ".out"), id, "animation.flow", GraphPinDirection::Output}};
64 return eve::editing::applied<GraphNodeRecord>(std::move(node));
65}
66
68 if (id.empty())
69 return eve::editing::failed<GraphNodeRecord>(EditorStatus::Rejected,
70 RuleId("editor.animation.invalid-transition"),
71 "Animation transition id is required");
73 node.id = id;
74 node.type = "animation.transition";
76 properties["blendSeconds"] = 0.2;
77 properties["hasExitTime"] = false;
78 properties["exitTime"] = 0.0;
79 properties["conditionKind"] = "none";
80 properties["parameter"] = "";
81 properties["operator"] = ">";
82 properties["threshold"] = 0.0;
83 properties["boolValue"] = true;
84 node.properties = EditorValue(std::move(properties));
85 node.pins = {{GraphPinId(id.value() + ".in"), id, "animation.flow", GraphPinDirection::Input},
86 {GraphPinId(id.value() + ".out"), id, "animation.flow", GraphPinDirection::Output}};
87 return eve::editing::applied<GraphNodeRecord>(std::move(node));
88}
89
92 result.documentRevision = graph.revision;
93 if (graph.domain != domain()) {
94 result.status = EditorStatus::Rejected;
95 diagnostic(result, "editor.animation.wrong-domain", "Graph is not an animation state document");
96 return result;
97 }
98 if (graph.schemaVersion != 1) {
99 result.status = EditorStatus::Unsupported;
100 diagnostic(result, "editor.animation.unsupported-schema", "Animation graph schema version is unsupported");
101 return result;
102 }
103 const auto* parameters = graph.parameters.getIf<EditorValue::Object>();
104 const EditorValue* entryValue = field(graph.parameters, "entry");
105 const auto* entry = entryValue ? entryValue->getIf<std::string>() : nullptr;
106 if (!parameters || !entry || entry->empty()) {
107 result.status = EditorStatus::Failed;
108 diagnostic(result, "editor.animation.entry-required", "Animation graph requires an entry state parameter");
109 return result;
110 }
111
112 std::map<GraphNodeId, const GraphNodeRecord*> nodes;
113 std::map<GraphPinId, const GraphNodeRecord*> pinOwners;
114 for (const GraphNodeRecord& node : graph.nodes) {
115 if (node.type != "animation.state" && node.type != "animation.transition") {
116 diagnostic(result, "editor.animation.unknown-node", "Unknown animation node type: " + node.type);
117 continue;
118 }
119 if (!nodes.emplace(node.id, &node).second)
120 diagnostic(result, "editor.animation.duplicate-node", "Duplicate animation node id: " + node.id.value());
121 for (const GraphPinRecord& pin : node.pins) pinOwners.emplace(pin.id, &node);
122 }
123 const auto entryNode = nodes.find(GraphNodeId(*entry));
124 if (entryNode == nodes.end() || entryNode->second->type != "animation.state")
125 diagnostic(result, "editor.animation.invalid-entry", "Entry does not reference an animation state node");
126
127 std::map<GraphNodeId, GraphNodeId> transitionFrom;
128 std::map<GraphNodeId, GraphNodeId> transitionTo;
129 for (const GraphEdgeRecord& edge : graph.edges) {
130 const auto fromOwner = pinOwners.find(edge.from);
131 const auto toOwner = pinOwners.find(edge.to);
132 if (fromOwner == pinOwners.end() || toOwner == pinOwners.end()) {
133 diagnostic(result, "editor.animation.dangling-edge", "Animation edge references a missing pin");
134 continue;
135 }
136 const GraphNodeRecord* from = fromOwner->second;
137 const GraphNodeRecord* to = toOwner->second;
138 if (from->type == "animation.state" && to->type == "animation.transition") {
139 if (!transitionFrom.emplace(to->id, from->id).second)
140 diagnostic(result, "editor.animation.multiple-transition-sources",
141 "Transition has more than one source state: " + to->id.value());
142 } else if (from->type == "animation.transition" && to->type == "animation.state") {
143 if (!transitionTo.emplace(from->id, to->id).second)
144 diagnostic(result, "editor.animation.multiple-transition-destinations",
145 "Transition has more than one destination state: " + from->id.value());
146 } else
147 diagnostic(result, "editor.animation.invalid-flow",
148 "Flow must alternate state and transition nodes");
149 }
150
151 std::ostringstream definition;
152 definition << "EVANIM_STATE_GRAPH 1\n" << "entry " << quote(*entry) << '\n';
153 for (const auto& [id, node] : nodes) {
154 if (node->type != "animation.state") continue;
155 const auto* clipValue = field(node->properties, "clip");
156 const auto* speedValue = field(node->properties, "speed");
157 const auto* loopValue = field(node->properties, "loop");
158 const auto* clip = clipValue ? clipValue->getIf<std::string>() : nullptr;
159 const auto* speed = speedValue ? speedValue->getIf<double>() : nullptr;
160 const auto* loop = loopValue ? loopValue->getIf<bool>() : nullptr;
161 if (!clip || clip->empty() || !speed || *speed <= 0.0 || !loop) {
162 diagnostic(result, "editor.animation.invalid-state-properties",
163 "State requires clip, positive speed and loop properties: " + id.value());
164 continue;
165 }
166 definition << "state " << quote(id.value()) << ' ' << quote(*clip) << ' ' << *speed << ' '
167 << (*loop ? 1 : 0) << '\n';
168 }
169 for (const auto& [id, node] : nodes) {
170 if (node->type != "animation.transition") continue;
171 if (!transitionFrom.contains(id) || !transitionTo.contains(id)) {
172 diagnostic(result, "editor.animation.incomplete-transition",
173 "Transition requires exactly one source and destination: " + id.value());
174 continue;
175 }
176 const auto* blend = field(node->properties, "blendSeconds");
177 const auto* hasExit = field(node->properties, "hasExitTime");
178 const auto* exitTime = field(node->properties, "exitTime");
179 const auto* kind = field(node->properties, "conditionKind");
180 const auto* parameter = field(node->properties, "parameter");
181 const auto* op = field(node->properties, "operator");
182 const auto* threshold = field(node->properties, "threshold");
183 const auto* boolValue = field(node->properties, "boolValue");
184 const auto* blendNumber = blend ? blend->getIf<double>() : nullptr;
185 const auto* exitEnabled = hasExit ? hasExit->getIf<bool>() : nullptr;
186 const auto* exitNumber = exitTime ? exitTime->getIf<double>() : nullptr;
187 const auto* kindName = kind ? kind->getIf<std::string>() : nullptr;
188 const auto* parameterName = parameter ? parameter->getIf<std::string>() : nullptr;
189 const auto* operatorName = op ? op->getIf<std::string>() : nullptr;
190 const auto* thresholdNumber = threshold ? threshold->getIf<double>() : nullptr;
191 const auto* boolFlag = boolValue ? boolValue->getIf<bool>() : nullptr;
192 const bool kindValid = kindName && (*kindName == "none" || *kindName == "float" ||
193 *kindName == "bool" || *kindName == "trigger");
194 const bool opValid = operatorName && (*operatorName == ">" || *operatorName == ">=" ||
195 *operatorName == "<" || *operatorName == "<=" ||
196 *operatorName == "==" || *operatorName == "!=");
197 if (!blendNumber || *blendNumber < 0.0 || !exitEnabled || !exitNumber || *exitNumber < 0.0 ||
198 *exitNumber > 1.0 || !kindValid || !parameterName || !operatorName || !thresholdNumber ||
199 !boolFlag || (*kindName != "none" && parameterName->empty()) ||
200 (*kindName == "float" && !opValid)) {
201 diagnostic(result, "editor.animation.invalid-transition-properties",
202 "Transition metadata is invalid: " + id.value());
203 continue;
204 }
205 definition << "transition " << quote(id.value()) << ' ' << quote(transitionFrom[id].value()) << ' '
206 << quote(transitionTo[id].value()) << ' ' << *blendNumber << ' '
207 << (*exitEnabled ? 1 : 0) << ' ' << *exitNumber << ' ' << quote(*kindName) << ' '
208 << quote(*parameterName) << ' ' << quote(*operatorName) << ' ' << *thresholdNumber << ' '
209 << (*boolFlag ? 1 : 0) << '\n';
210 }
211 if (!result.diagnostics.empty()) {
212 result.status = EditorStatus::Failed;
213 return result;
214 }
215 result.status = EditorStatus::Applied;
216 result.definition = definition.str();
217 return result;
218}
219
220} // namespace eve::animation_editing
double value
std::string from
std::string message
std::uint32_t key
glm::vec4 clip
float blend
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
TokenKind kind
char quote
std::map< std::string, PropertyRule > properties
std::vector< BvhNode > nodes
std::map< std::string, std::vector< std::string > > graph
Definition Package.cpp:59
std::string id
Definition PlayHost.cpp:108
const RoadNode * node
const RoadEdge * edge
bool found
Move-only operation result carrying either a value or Status.
Definition Result.h:155
Result< GraphNodeRecord > makeStateNode(const GraphNodeId &id, const std::string &clipAsset) const
Construct a state node referencing one animation clip asset.
std::string domain() const override
Domain.
AnimationGraphCompileResult compile(const GraphDocumentData &graph) const
Validate and compile a graph into a deterministic runtime-neutral definition.
Result< GraphNodeRecord > makeTransitionNode(const GraphNodeId &id) const
Construct a transition node with default blend metadata.
GraphConnectionDecision canConnect(const GraphPinRecord &from, const GraphPinRecord &to) const override
Can connect.
Deterministic owning value tree shared by authoring hosts.
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
const EditorValue * field(const EditorValue &value, const char *name)
Compilation result for a stable-reference animation state graph asset.
Result of domain-specific connection validation.
std::vector< Diagnostic > diagnostics
Immutable graph document value sent to a domain compiler.
Directed connection between an output and input pin.
Generic graph node independent of its domain.
Stable typed pin owned by a graph node.