载入中...
搜索中...
未找到
RTSFormation.cpp
浏览该文件的文档.
1#include <algorithm>
2#include <cmath>
3#include <numbers>
4#include <vector>
6
7namespace eve::rts {
11
13 if (!std::isfinite(spacing) || spacing <= 0.0f)
15 "formation spacing must be finite and positive", "spacing"));
16 if (!std::isfinite(rotationRadians))
18 Diagnostic::error(DiagnosticCode::InvalidArgument, "formation rotation must be finite", "rotationRadians"));
19 if (columns < 0)
21 Diagnostic::error(DiagnosticCode::InvalidArgument, "formation columns must be non-negative", "columns"));
22 switch (kind) {
28 }
30 Diagnostic::error(DiagnosticCode::InvalidArgument, "formation kind is invalid", "kind"));
31}
32
34 const FormationSpec& spec) {
35 auto valid = spec.validate();
36 if (!valid) return Result<std::vector<WorldPosition>>::failure(valid.status());
37 if (!isFinitePosition(anchor))
38 return Result<std::vector<WorldPosition>>::failure(
39 Diagnostic::error(DiagnosticCode::InvalidArgument, "formation anchor must be finite", "anchor"));
40
41 std::vector<WorldPosition> result;
42 result.reserve(count);
43 if (count == 0)
44 return Result<std::vector<WorldPosition>>::success(std::move(result), Status::success(StatusCode::NoOp));
45
46 const float spacing = spec.spacing;
47 switch (spec.kind) {
49 const float center = static_cast<float>(count - 1) * 0.5f;
50 for (std::size_t index = 0; index < count; ++index) {
51 result.push_back({anchor.x + (static_cast<float>(index) - center) * spacing, anchor.y});
52 }
53 break;
54 }
56 const float center = static_cast<float>(count - 1) * 0.5f;
57 for (std::size_t index = 0; index < count; ++index)
58 result.push_back({anchor.x, anchor.y + (static_cast<float>(index) - center) * spacing});
59 break;
60 }
62 result.push_back(anchor);
63 for (std::size_t ring = 1; result.size() < count; ++ring) {
64 const std::size_t slots = 6 * ring;
65 const double radius = static_cast<double>(ring) * spacing * 1.5;
66 for (std::size_t slot = 0; slot < slots && result.size() < count; ++slot) {
67 const double angle =
68 2.0 * std::numbers::pi * static_cast<double>(slot) / static_cast<double>(slots);
69 result.push_back({static_cast<float>(anchor.x + radius * std::cos(angle)),
70 static_cast<float>(anchor.y + radius * std::sin(angle))});
71 }
72 }
73 break;
74 }
76 const std::size_t columns =
77 spec.columns > 0 ? static_cast<std::size_t>(spec.columns)
78 : static_cast<std::size_t>(std::ceil(std::sqrt(static_cast<double>(count))));
79 if (columns == 0)
81 DiagnosticCode::InvariantViolation, "grid formation computed zero columns", "columns"));
82 const std::size_t rows = (count + columns - 1) / columns;
83 const float columnCenter = static_cast<float>(columns - 1) * 0.5f;
84 const float rowCenter = static_cast<float>(rows - 1) * 0.5f;
85 for (std::size_t index = 0; index < count; ++index) {
86 const std::size_t row = index / columns;
87 const std::size_t column = index % columns;
88 result.push_back({anchor.x + (static_cast<float>(column) - columnCenter) * spacing,
89 anchor.y + (static_cast<float>(row) - rowCenter) * spacing});
90 }
91 break;
92 }
94 std::size_t row = 0;
95 std::size_t rowStart = 0;
96 for (std::size_t index = 0; index < count; ++index) {
97 while (index >= rowStart + row + 1) {
98 rowStart += row + 1;
99 ++row;
100 }
101 const std::size_t slot = index - rowStart;
102 const float rowCenter = static_cast<float>(row) * 0.5f;
103 result.push_back({anchor.x + (static_cast<float>(slot) - rowCenter) * spacing,
104 anchor.y + static_cast<float>(row) * spacing});
105 }
106 break;
107 }
108 }
109 if (spec.rotationRadians != 0.f) {
110 const double c = std::cos(static_cast<double>(spec.rotationRadians));
111 const double s = std::sin(static_cast<double>(spec.rotationRadians));
112 for (auto& point : result) {
113 const double x = static_cast<double>(point.x) - anchor.x;
114 const double y = static_cast<double>(point.y) - anchor.y;
115 point = {static_cast<float>(anchor.x + c * x - s * y), static_cast<float>(anchor.y + s * x + c * y)};
116 }
117 }
118 if (std::any_of(result.begin(), result.end(), [](WorldPosition point) { return !isFinitePosition(point); }))
119 return Result<std::vector<WorldPosition>>::failure(Diagnostic::error(
120 DiagnosticCode::InvalidArgument, "Formation exceeds finite world coordinates", "formation"));
121 return Result<std::vector<WorldPosition>>::success(std::move(result), Status::success(StatusCode::Applied));
122}
123
124Result<FanOutReceipt> CommandFanOutSystem::fanOut(std::span<const ecs::EntityHandle> unitHandles,
125 const CommandSpec& command, const FormationSpec& formation) {
126 if (unitHandles.empty())
128 DiagnosticCode::InvalidArgument, "RTS command fan-out requires at least one Unit", "selection.units"));
129 auto commandValid = command.validate();
130 if (!commandValid) return Result<FanOutReceipt>::failure(commandValid.status());
131
132 auto formationValid = formation.validate();
133 if (!formationValid) return Result<FanOutReceipt>::failure(formationValid.status());
134
135 // The explicit View is the closure proof for this command boundary: a
136 // Unit subclass is accepted by the Unit registry, while a Building root
137 // cannot enter the selected set merely because it has an Identity field.
138 std::vector<ecs::EntityHandle> visibleUnits;
139 {
140 auto view = ecs::View<Unit, Unit::Identity, Unit::Orders>();
141 for (auto it = view.begin(); it != view.end(); ++it) {
142 auto [identity, orders] = *it;
143 (void)orders;
144 Unit* unit = identity == nullptr ? nullptr : dynamic_cast<Unit*>(ecs::try_get(identity->self));
145 if (unit != nullptr) visibleUnits.push_back(ecs::handle_of(unit));
146 }
147 }
148
149 std::vector<Unit*> selected;
150 selected.reserve(unitHandles.size());
151 float largestRadius = 0.f, secondRadius = 0.f;
152 for (const auto& handle : unitHandles) {
153 auto* entity = ecs::try_get(handle);
154 auto* unit = entity == nullptr ? nullptr : dynamic_cast<Unit*>(entity);
155 if (unit == nullptr)
158 "RTS fan-out selection contains a stale or non-Unit handle", "selection.units"));
159 const auto liveHandle = ecs::handle_of(unit);
160 const bool inView = std::any_of(visibleUnits.begin(), visibleUnits.end(), [&liveHandle](const auto& candidate) {
161 return isSameHandle(candidate, liveHandle);
162 });
163 if (!inView)
166 "RTS Unit selection is outside the declared View closure", "selection.units"));
167 if (std::find(selected.begin(), selected.end(), unit) != selected.end())
169 DiagnosticCode::InvalidArgument, "Formation selection contains duplicate units", "selection.units"));
170 const float radius = unit->crowd()->radius;
171 if (!std::isfinite(radius) || radius <= 0.f || !isFinitePosition({unit->motion()->x, unit->motion()->y}))
174 "Formation unit geometry must be finite with positive radius", "selection.units"));
175 if (radius >= largestRadius) {
176 secondRadius = largestRadius;
177 largestRadius = radius;
178 } else
179 secondRadius = std::max(secondRadius, radius);
180 selected.push_back(unit);
181 }
182
183 FormationSpec resolved = formation;
184 if (selected.size() > 1) resolved.spacing = std::max(resolved.spacing, largestRadius + secondRadius);
185 auto targets = FormationPlanner::plan(selected.size(), command.target, resolved);
186 if (!targets) return Result<FanOutReceipt>::failure(targets.status());
187 auto plannedTargets = std::move(targets).takeValue();
188 if (std::any_of(plannedTargets.begin(), plannedTargets.end(), [](WorldPosition p) { return !isFinitePosition(p); }))
190 DiagnosticCode::InvalidArgument, "Formation exceeds finite world coordinates", "formation"));
191
192 struct AvailableSlot {
194 int index = -1;
195 };
196 std::vector<AvailableSlot> available;
197 available.reserve(plannedTargets.size());
198 for (std::size_t index = 0; index < plannedTargets.size(); ++index)
199 available.push_back({plannedTargets[index], static_cast<int>(index)});
200 std::vector<std::size_t> assignmentOrder(selected.size());
201 for (std::size_t index = 0; index < selected.size(); ++index) assignmentOrder[index] = index;
202 std::sort(assignmentOrder.begin(), assignmentOrder.end(), [&](std::size_t left, std::size_t right) {
203 const auto leftMotion = selected[left]->motion();
204 const auto rightMotion = selected[right]->motion();
205 const float leftDistance = distanceSquared(leftMotion->x, leftMotion->y, command.target.x, command.target.y);
206 const float rightDistance = distanceSquared(rightMotion->x, rightMotion->y, command.target.x, command.target.y);
207 if (leftDistance != rightDistance) return leftDistance > rightDistance;
208 return selected[left]->identity()->self.id < selected[right]->identity()->self.id;
209 });
210 std::vector<AvailableSlot> assignedSlots(selected.size());
211 for (const std::size_t selectedIndex : assignmentOrder) {
212 const auto motion = selected[selectedIndex]->motion();
213 auto best = std::min_element(available.begin(), available.end(), [&](const auto& left, const auto& right) {
214 const float leftDistance = distanceSquared(motion->x, motion->y, left.position.x, left.position.y);
215 const float rightDistance = distanceSquared(motion->x, motion->y, right.position.x, right.position.y);
216 return leftDistance != rightDistance ? leftDistance < rightDistance : left.index < right.index;
217 });
218 assignedSlots[selectedIndex] = *best;
219 available.erase(best);
220 }
221
222 // Greedy placement can reserve a nearby unit's slot for a distant unit,
223 // creating unnecessary crossings. Relax pair assignments before issuing
224 // orders; keep both the original slot identity and position together.
225 // A bounded number of sweeps preserves quadratic command-admission cost.
226 const auto travelDistance = [&](std::size_t unitIndex, WorldPosition target) {
227 const auto motion = selected[unitIndex]->motion();
228 return std::hypot(static_cast<double>(motion->x) - target.x, static_cast<double>(motion->y) - target.y);
229 };
230 for (int sweep = 0; sweep < 4; ++sweep) {
231 bool changed = false;
232 for (std::size_t first = 0; first < assignmentOrder.size(); ++first) {
233 const auto a = assignmentOrder[first];
234 for (std::size_t second = first + 1; second < assignmentOrder.size(); ++second) {
235 const auto b = assignmentOrder[second];
236 const double before =
237 travelDistance(a, assignedSlots[a].position) + travelDistance(b, assignedSlots[b].position);
238 const double after =
239 travelDistance(a, assignedSlots[b].position) + travelDistance(b, assignedSlots[a].position);
240 if (after + 1e-7 * std::max(1.0, before) < before) {
241 std::swap(assignedSlots[a], assignedSlots[b]);
242 changed = true;
243 }
244 }
245 }
246 if (!changed) break;
247 }
248
249 FanOutReceipt receipt;
250 receipt.requested = selected.size();
251 receipt.orderIds.reserve(selected.size());
252 std::vector<OrderComponent::Snapshot> previous;
253 std::vector<std::uint64_t> previousEpochs;
254 previous.reserve(selected.size());
255 for (Unit* unit : selected) {
256 auto snapshot = unit->orders()->values.snapshotState();
257 if (!snapshot) return Result<FanOutReceipt>::failure(snapshot.status());
258 previous.push_back(std::move(snapshot).takeValue());
259 previousEpochs.push_back(unit->orders()->values.membershipEpoch_);
260 }
261 for (std::size_t index = 0; index < selected.size(); ++index) {
262 CommandSpec assigned = command;
263 assigned.target = assignedSlots[index].position;
264 auto order = assigned.append ? selected[index]->orders()->values.enqueue(assigned, assignedSlots[index].index)
265 : selected[index]->orders()->values.replace(assigned, assignedSlots[index].index);
266 if (!order) {
267 const Status status = order.status();
268 for (std::size_t rollback = 0; rollback < selected.size(); ++rollback) {
269 auto& component = selected[rollback]->orders()->values;
270 auto restored = component.restoreState(previous[rollback]);
271 if (!restored) return Result<FanOutReceipt>::failure(restored.status());
272 component.membershipEpoch_ = previousEpochs[rollback];
273 }
275 }
276 receipt.orderIds.push_back(std::move(order).takeValue());
277 }
278 receipt.accepted = receipt.orderIds.size();
280}
281
282} // namespace eve::rts
LogicalId target
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
const std::string & s
Vec3 anchor
Definition CaveMesh.cpp:90
glm::vec4 p[6]
int column
int rows
wgpu::PopErrorScopeStatus status
HexVec3 left
HexVec3 right
std::int32_t second
std::int32_t c
std::int32_t first
std::array< float, 3 > position
bool valid
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< std::int32_t > order
graphics::Canvas * previous
float radius
PrimitiveHandle handle
bool inView
glm::mat4 view
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
std::uint32_t count
std::map< Cell, int > best
TacticalUnit * unit
int spacing
int columns
uint32_t index
glm::vec3 point
float angle
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
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
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static Result< std::vector< WorldPosition > > plan(std::size_t count, WorldPosition anchor, const FormationSpec &spec)
Plan positions around an anchor for a fixed unit count.
RTS unit domain root.
Definition RTSTypes.h:495
float distanceSquared(float ax, float ay, float bx, float by)
bool isSameHandle(const ecs::EntityHandle &left, const ecs::EntityHandle &right)
bool isFinitePosition(WorldPosition position)
One domain-neutral command submitted to a unit's generic order queue.
Definition RTSTypes.h:101
Result< void > validate() const
Validate coordinates and command metadata without mutating state.
Definition RTSTypes.cpp:201
WorldPosition target
Definition RTSTypes.h:103
Receipt containing all order ids accepted by command fan-out.
Definition RTSSystems.h:202
std::vector< std::string > orderIds
Definition RTSSystems.h:205
Input to the pure formation planner.
Definition RTSSystems.h:34
EVENGINE_API_DOMAINS Result< void > validate() const
Validate layout kind, spacing, grid columns and finite rotation.
float rotationRadians
Counterclockwise rotation around the anchor; zero preserves legacy layouts.
Definition RTSSystems.h:38
Two-dimensional deterministic RTS world position.
Definition RTSTypes.h:48