载入中...
搜索中...
未找到
PixelWorldPhysics.cpp
浏览该文件的文档.
2
3#include "common/Exception.h"
4#include "physics/Body.h"
5#include "physics/World.h"
6
7#include <algorithm>
8#include <cmath>
9#include <limits>
10#include <map>
11#include <set>
12#include <string>
13
15namespace {
16
17std::vector<FragmentCollisionRect> decompose(const eve::pixelworld::PixelFragment& fragment) {
18 const int width = fragment.width, height = fragment.height;
19 std::vector<std::uint8_t> consumed(std::size_t(width) * std::size_t(height));
20 std::vector<FragmentCollisionRect> result;
21 const auto occupied = [&](int x, int y) {
22 return fragment.cells[std::size_t(y) * std::size_t(width) + std::size_t(x)].material !=
24 };
25 for (int y = 0; y < height; ++y)
26 for (int x = 0; x < width; ++x) {
27 const std::size_t start = std::size_t(y) * std::size_t(width) + std::size_t(x);
28 if (consumed[start] || !occupied(x, y)) continue;
29 int rectWidth = 1;
30 while (x + rectWidth < width && !consumed[start + std::size_t(rectWidth)] &&
31 occupied(x + rectWidth, y))
32 ++rectWidth;
33 int rectHeight = 1;
34 for (; y + rectHeight < height; ++rectHeight) {
35 bool complete = true;
36 for (int ox = 0; ox < rectWidth; ++ox) {
37 const std::size_t index = std::size_t(y + rectHeight) * std::size_t(width) +
38 std::size_t(x + ox);
39 if (consumed[index] || !occupied(x + ox, y + rectHeight)) {
40 complete = false;
41 break;
42 }
43 }
44 if (!complete) break;
45 }
46 for (int oy = 0; oy < rectHeight; ++oy)
47 for (int ox = 0; ox < rectWidth; ++ox)
48 consumed[std::size_t(y + oy) * std::size_t(width) + std::size_t(x + ox)] = 1;
49 result.push_back({x, y, rectWidth, rectHeight});
50 }
51 return result;
52}
53
55 turns = ((turns % 4) + 4) % 4;
57 if (turns == 0) return result;
58 result.width = (turns % 2 == 0) ? source.width : source.height;
59 result.height = (turns % 2 == 0) ? source.height : source.width;
60 result.cells.assign(std::size_t(result.width) * std::size_t(result.height), {});
61 for (int y = 0; y < source.height; ++y)
62 for (int x = 0; x < source.width; ++x) {
63 int rx = 0, ry = 0;
64 if (turns == 1) {
65 rx = source.height - 1 - y;
66 ry = x;
67 } else if (turns == 2) {
68 rx = source.width - 1 - x;
69 ry = source.height - 1 - y;
70 } else {
71 rx = y;
72 ry = source.width - 1 - x;
73 }
74 result.cells[std::size_t(ry) * std::size_t(result.width) + std::size_t(rx)] =
75 source.cells[std::size_t(y) * std::size_t(source.width) + std::size_t(x)];
76 }
77 return result;
78}
79
80struct GridPoint {
81 int x = 0;
82 int y = 0;
83 auto operator<=>(const GridPoint&) const = default;
84};
85
86struct GridEdge {
87 GridPoint from;
88 GridPoint to;
89};
90
91int direction(const GridEdge& edge) {
92 if (edge.to.x > edge.from.x) return 0;
93 if (edge.to.y > edge.from.y) return 1;
94 if (edge.to.x < edge.from.x) return 2;
95 return 3;
96}
97
98int turnRank(int previous, int next) {
99 const int turn = (next - previous + 4) % 4;
100 if (turn == 1) return 0;
101 if (turn == 0) return 1;
102 if (turn == 3) return 2;
103 return 3;
104}
105
106bool collinear(GridPoint a, GridPoint b, GridPoint c) {
107 return (b.x - a.x) * (c.y - b.y) == (b.y - a.y) * (c.x - b.x);
108}
109
110bool validCellBounds(float minimum, float maximum) {
111 return std::isfinite(minimum) && std::isfinite(maximum) &&
112 double(minimum) >= double(std::numeric_limits<int>::min()) + 2.0 &&
113 double(maximum) <= double(std::numeric_limits<int>::max()) - 2.0;
114}
115
116} // namespace
117
119 const eve::pixelworld::PixelWorld& pixelWorld, int chunkX, int chunkY,
120 std::uint32_t maximumVertices) {
121 if (maximumVertices < 2)
123 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "maximumVertices must be at least two",
124 "maximumVertices", {}, "pixelworld_physics"));
126 const std::int64_t wideOriginX = std::int64_t(chunkX) * size;
127 const std::int64_t wideOriginY = std::int64_t(chunkY) * size;
128 if (wideOriginX < std::numeric_limits<int>::min() ||
129 wideOriginX + size > std::numeric_limits<int>::max() ||
130 wideOriginY < std::numeric_limits<int>::min() ||
131 wideOriginY + size > std::numeric_limits<int>::max())
133 eve::DiagnosticCode::InvalidArgument, "Chunk coordinates exceed world coordinate range", "chunk", {},
134 "pixelworld_physics"));
135 const int originX = int(wideOriginX), originY = int(wideOriginY);
136 const auto solid = [&](int localX, int localY) {
137 return pixelWorld.isSolidMaterial(
138 pixelWorld.getCell(originX + localX, originY + localY).material);
139 };
140 std::vector<GridEdge> edges;
141 edges.reserve(size * 4);
142 for (int y = 0; y < size; ++y)
143 for (int x = 0; x < size; ++x) {
144 if (!solid(x, y)) continue;
145 if (!solid(x, y - 1)) edges.push_back({{x, y}, {x + 1, y}});
146 if (!solid(x + 1, y)) edges.push_back({{x + 1, y}, {x + 1, y + 1}});
147 if (!solid(x, y + 1)) edges.push_back({{x + 1, y + 1}, {x, y + 1}});
148 if (!solid(x - 1, y)) edges.push_back({{x, y + 1}, {x, y}});
149 }
150 std::map<GridPoint, std::vector<std::size_t>> outgoing;
151 for (std::size_t index = 0; index < edges.size(); ++index)
152 outgoing[edges[index].from].push_back(index);
153 std::vector<std::uint8_t> used(edges.size());
154 std::vector<TerrainCollisionContour> contours;
155 std::uint64_t vertexCount = 0;
156 for (std::size_t first = 0; first < edges.size(); ++first) {
157 if (used[first]) continue;
158 std::vector<GridPoint> points{edges[first].from};
159 std::size_t current = first;
160 bool loop = false;
161 while (true) {
162 used[current] = 1;
163 const GridPoint end = edges[current].to;
164 if (end == points.front()) {
165 loop = true;
166 break;
167 }
168 points.push_back(end);
169 const auto found = outgoing.find(end);
170 if (found == outgoing.end()) break;
171 std::size_t next = edges.size();
172 int bestRank = 5;
173 for (const std::size_t candidate : found->second) {
174 if (used[candidate]) continue;
175 const int rank = turnRank(direction(edges[current]), direction(edges[candidate]));
176 if (rank < bestRank || (rank == bestRank && candidate < next)) {
177 bestRank = rank;
178 next = candidate;
179 }
180 }
181 if (next == edges.size()) break;
182 current = next;
183 }
184 bool changed = true;
185 while (changed && points.size() > (loop ? 3U : 2U)) {
186 changed = false;
187 for (std::size_t index = 0; index < points.size(); ++index) {
188 if (!loop && (index == 0 || index + 1 == points.size())) continue;
189 const std::size_t before = (index + points.size() - 1) % points.size();
190 const std::size_t after = (index + 1) % points.size();
191 if (collinear(points[before], points[index], points[after])) {
192 points.erase(points.begin() + std::ptrdiff_t(index));
193 changed = true;
194 break;
195 }
196 }
197 }
198 if (points.size() < (loop ? 3U : 2U)) continue;
199 vertexCount += points.size();
200 if (vertexCount > maximumVertices)
202 eve::DiagnosticCode::PreconditionViolation, "terrain contour exceeds maximumVertices",
203 "maximumVertices", {}, "pixelworld_physics"));
204 TerrainCollisionContour contour;
205 contour.loop = loop;
206 contour.vertices.reserve(points.size() * 2);
207 for (const GridPoint point : points) {
208 contour.vertices.push_back(float(point.x));
209 contour.vertices.push_back(float(point.y));
210 }
211 contours.push_back(std::move(contour));
212 }
213 return eve::Result<std::vector<TerrainCollisionContour>>::success(std::move(contours));
214}
215
217 const eve::pixelworld::PixelWorld& pixelWorld, float centerX, float centerY, float radius,
218 std::uint32_t maximumCells) {
219 if (!std::isfinite(centerX) || !std::isfinite(centerY) || !std::isfinite(radius) ||
220 radius <= 0.f || maximumCells == 0)
222 "circle probe parameters are invalid",
223 "probe", {}, "pixelworld_physics"));
224 if (!validCellBounds(centerX - radius, centerX + radius) ||
225 !validCellBounds(centerY - radius, centerY + radius))
227 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "circle probe exceeds world coordinate range",
228 "probe", {}, "pixelworld_physics"));
229 const int minX = int(std::floor(centerX - radius));
230 const int maxX = int(std::floor(centerX + radius));
231 const int minY = int(std::floor(centerY - radius));
232 const int maxY = int(std::floor(centerY + radius));
233 const std::uint64_t width = std::uint64_t(std::int64_t(maxX) - minX + 1);
234 const std::uint64_t height = std::uint64_t(std::int64_t(maxY) - minY + 1);
235 if (width > maximumCells || height > maximumCells || width * height > maximumCells)
237 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "circle probe exceeds maximumCells",
238 "maximumCells", {}, "pixelworld_physics"));
240 for (int y = minY; y <= maxY; ++y)
241 for (int x = minX; x <= maxX; ++x) {
242 const auto material = pixelWorld.getCell(x, y).material;
243 if (!pixelWorld.isSolidMaterial(material)) continue;
244 const float closestX = std::clamp(centerX, float(x), float(x + 1));
245 const float closestY = std::clamp(centerY, float(y), float(y + 1));
246 const float dx = centerX - closestX, dy = centerY - closestY;
247 const float distanceSquared = dx * dx + dy * dy;
248 if (distanceSquared >= radius * radius) continue;
249 const float distance = std::sqrt(distanceSquared);
250 const float depth = radius - distance;
251 if (best.hit && depth < best.depth) continue;
252 if (best.hit && depth == best.depth && std::pair(y, x) >= std::pair(best.cellY, best.cellX))
253 continue;
254 best.hit = true;
255 best.material = material;
256 best.cellX = x;
257 best.cellY = y;
258 best.pointX = closestX;
259 best.pointY = closestY;
260 best.depth = depth;
261 if (distance > 0.f) {
262 best.normalX = dx / distance;
263 best.normalY = dy / distance;
264 } else {
265 const float left = centerX - float(x), right = float(x + 1) - centerX;
266 const float top = centerY - float(y), bottom = float(y + 1) - centerY;
267 const float nearest = std::min({left, right, top, bottom});
268 if (nearest == left) best.normalX = -1.f;
269 else if (nearest == right) best.normalX = 1.f;
270 else if (nearest == top) best.normalY = -1.f;
271 else best.normalY = 1.f;
272 }
273 }
275}
276
278 const eve::pixelworld::PixelWorld& pixelWorld, float startX, float startY, float endX,
279 float endY, float radius, std::uint32_t maximumCells) {
280 if (!std::isfinite(startX) || !std::isfinite(startY) || !std::isfinite(endX) ||
281 !std::isfinite(endY) || !std::isfinite(radius) || radius <= 0.f || maximumCells == 0)
283 "circle sweep parameters are invalid",
284 "sweep", {}, "pixelworld_physics"));
285 if (!validCellBounds(std::min(startX, endX) - radius,
286 std::max(startX, endX) + radius) ||
287 !validCellBounds(std::min(startY, endY) - radius,
288 std::max(startY, endY) + radius))
290 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "circle sweep exceeds world coordinate range",
291 "sweep", {}, "pixelworld_physics"));
292 auto initial = probeTerrainCircle(pixelWorld, startX, startY, radius, maximumCells);
293 if (!initial.ok()) return initial;
294 if (initial.value().hit) return initial;
295 const int minX = int(std::floor(std::min(startX, endX) - radius));
296 const int maxX = int(std::floor(std::max(startX, endX) + radius));
297 const int minY = int(std::floor(std::min(startY, endY) - radius));
298 const int maxY = int(std::floor(std::max(startY, endY) + radius));
299 const std::uint64_t width = std::uint64_t(std::int64_t(maxX) - minX + 1);
300 const std::uint64_t height = std::uint64_t(std::int64_t(maxY) - minY + 1);
301 if (width > maximumCells || height > maximumCells || width * height > maximumCells)
303 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "circle sweep exceeds maximumCells",
304 "maximumCells", {}, "pixelworld_physics"));
305 const float deltaX = endX - startX, deltaY = endY - startY;
307 best.fraction = 1.f;
308 for (int y = minY; y <= maxY; ++y)
309 for (int x = minX; x <= maxX; ++x) {
310 const auto material = pixelWorld.getCell(x, y).material;
311 if (!pixelWorld.isSolidMaterial(material)) continue;
312 float cellFraction = 2.f, cellNormalX = 0.f, cellNormalY = 0.f;
313 const auto consider = [&](float fraction, float nx, float ny) {
314 if (fraction >= 0.f && fraction <= 1.f && fraction < cellFraction) {
315 cellFraction = fraction;
316 cellNormalX = nx;
317 cellNormalY = ny;
318 }
319 };
320 if (deltaX > 0.f) {
321 const float fraction = (float(x) - radius - startX) / deltaX;
322 const float centerY = startY + deltaY * fraction;
323 if (centerY >= float(y) && centerY <= float(y + 1))
324 consider(fraction, -1.f, 0.f);
325 } else if (deltaX < 0.f) {
326 const float fraction = (float(x + 1) + radius - startX) / deltaX;
327 const float centerY = startY + deltaY * fraction;
328 if (centerY >= float(y) && centerY <= float(y + 1))
329 consider(fraction, 1.f, 0.f);
330 }
331 if (deltaY > 0.f) {
332 const float fraction = (float(y) - radius - startY) / deltaY;
333 const float centerX = startX + deltaX * fraction;
334 if (centerX >= float(x) && centerX <= float(x + 1))
335 consider(fraction, 0.f, -1.f);
336 } else if (deltaY < 0.f) {
337 const float fraction = (float(y + 1) + radius - startY) / deltaY;
338 const float centerX = startX + deltaX * fraction;
339 if (centerX >= float(x) && centerX <= float(x + 1))
340 consider(fraction, 0.f, 1.f);
341 }
342 const float quadraticA = deltaX * deltaX + deltaY * deltaY;
343 if (quadraticA > 0.f) {
344 struct Corner {
345 float x;
346 float y;
347 int quadrantX;
348 int quadrantY;
349 };
350 const Corner corners[] = {
351 {float(x), float(y), -1, -1}, {float(x + 1), float(y), 1, -1},
352 {float(x + 1), float(y + 1), 1, 1}, {float(x), float(y + 1), -1, 1}};
353 for (const Corner corner : corners) {
354 const float offsetX = startX - corner.x, offsetY = startY - corner.y;
355 const float quadraticB = 2.f * (offsetX * deltaX + offsetY * deltaY);
356 const float quadraticC = offsetX * offsetX + offsetY * offsetY - radius * radius;
357 const float discriminant = quadraticB * quadraticB - 4.f * quadraticA * quadraticC;
358 if (discriminant < 0.f) continue;
359 const float fraction =
360 (-quadraticB - std::sqrt(discriminant)) / (2.f * quadraticA);
361 if (fraction < 0.f || fraction > 1.f) continue;
362 const float hitX = startX + deltaX * fraction;
363 const float hitY = startY + deltaY * fraction;
364 if ((corner.quadrantX < 0 && hitX > corner.x) ||
365 (corner.quadrantX > 0 && hitX < corner.x) ||
366 (corner.quadrantY < 0 && hitY > corner.y) ||
367 (corner.quadrantY > 0 && hitY < corner.y))
368 continue;
369 consider(fraction, (hitX - corner.x) / radius,
370 (hitY - corner.y) / radius);
371 }
372 }
373 if (cellFraction > 1.f) continue;
374 if (best.hit && cellFraction > best.fraction) continue;
375 if (best.hit && cellFraction == best.fraction &&
376 std::pair(y, x) >= std::pair(best.cellY, best.cellX))
377 continue;
378 best.hit = true;
379 best.material = material;
380 best.cellX = x;
381 best.cellY = y;
382 best.fraction = cellFraction;
383 best.pointX = startX + deltaX * cellFraction;
384 best.pointY = startY + deltaY * cellFraction;
385 best.normalX = cellNormalX;
386 best.normalY = cellNormalY;
387 }
388 if (!best.hit) best.fraction = 1.f;
390}
391
392PixelFragmentBody::PixelFragmentBody(eve::pixelworld::PixelFragment fragment,
394 std::vector<FragmentCollisionRect> rectangles)
395 : fragment_(std::move(fragment)), link_(link), rectangles_(std::move(rectangles)) {}
396
399 if (fragment.width <= 0 || fragment.height <= 0 || fragment.id == 0 ||
400 std::uint64_t(fragment.width) * std::uint64_t(fragment.height) != fragment.cells.size())
402 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "fragment bitmap metadata is invalid",
403 "fragment", {}, "pixelworld_physics"));
404 if (!std::isfinite(config.density) || config.density <= 0.f ||
405 !std::isfinite(config.friction) || config.friction < 0.f ||
406 !std::isfinite(config.restitution) || config.restitution < 0.f || config.maximumFixtures == 0)
408 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "fragment body config is invalid", "config",
409 {}, "pixelworld_physics"));
410 auto rectangles = decompose(fragment);
411 if (rectangles.empty() || rectangles.size() > config.maximumFixtures)
414 "fragment collision decomposition is empty or exceeds maximumFixtures",
415 "maximumFixtures", {}, "pixelworld_physics"));
416
417 eve::physics::Body* body = nullptr;
418 try {
419 const float centerX = float(fragment.originX) + float(fragment.width) * 0.5f;
420 const float centerY = float(fragment.originY) + float(fragment.height) * 0.5f;
421 body = world.newBody("dynamic", centerX, centerY);
422 for (const FragmentCollisionRect& rect : rectangles) {
423 const float offsetX = float(rect.x) + float(rect.width) * 0.5f - float(fragment.width) * 0.5f;
424 const float offsetY = float(rect.y) + float(rect.height) * 0.5f - float(fragment.height) * 0.5f;
425 body->newRectangleFixtureAt(float(rect.width), float(rect.height), offsetX, offsetY,
426 config.density, config.friction, config.restitution);
427 }
429 if (!link.ok()) {
430 body->destroy();
431 return eve::Result<std::unique_ptr<PixelFragmentBody>>::failure(link.status());
432 }
434 std::unique_ptr<PixelFragmentBody>(new PixelFragmentBody(
435 std::move(fragment), std::move(link).takeValue(), std::move(rectangles))));
436 } catch (const std::exception& error) {
437 if (body && body->isValid()) body->destroy();
439 eve::DiagnosticCode::Failed, std::string("failed to create fragment fixtures: ") + error.what(), "physics",
440 {}, "pixelworld_physics"));
441 }
442}
443
444const std::vector<FragmentCollisionRect>& PixelFragmentBody::collisionRects() const noexcept {
445 return rectangles_;
446}
447
448std::uint64_t PixelFragmentBody::fragmentId() const noexcept { return fragment_.id; }
449eve::physics::PhysicsLink PixelFragmentBody::physicsLink() const noexcept { return link_; }
450bool PixelFragmentBody::isRasterized() const noexcept { return rasterized_; }
451
452eve::Result<FragmentSettleReceipt> PixelFragmentBody::settleIfSleeping(
453 eve::physics::World& physicsWorld, eve::pixelworld::PixelWorld& pixelWorld) {
454 if (rasterized_ || physicsReleased_)
456 eve::Diagnostic::error(eve::DiagnosticCode::PreconditionViolation, "fragment body is no longer active",
457 "state", {}, "pixelworld_physics"));
458 auto resolved = link_.resolve(physicsWorld);
459 if (!resolved.ok()) return eve::Result<FragmentSettleReceipt>::failure(resolved.status());
460 eve::physics::Body* body = resolved.value();
461 if (body->isAwake())
463 {FragmentSettleDisposition::StillAwake, fragment_.id, 0, 0});
464
465 constexpr double halfPi = 1.57079632679489661923;
466 const int turns = int(std::llround(double(body->getAngle()) / halfPi));
467 auto candidate = rotated(fragment_, turns);
468 const int originX = int(std::lround(double(body->getX()) - double(candidate.width) * 0.5));
469 const int originY = int(std::lround(double(body->getY()) - double(candidate.height) * 0.5));
470 auto rasterized = pixelWorld.rasterizeFragment(candidate, originX, originY);
471 if (!rasterized.ok()) return eve::Result<FragmentSettleReceipt>::failure(rasterized.status());
472 body->destroy();
473 physicsReleased_ = true;
474 rasterized_ = true;
476 {FragmentSettleDisposition::Rasterized, fragment_.id,
477 rasterized.value().cellsPlaced, ((turns % 4) + 4) % 4});
478}
479
480eve::Result<void> PixelFragmentBody::releasePhysics(eve::physics::World& physicsWorld) {
481 if (physicsReleased_) return eve::Result<void>::success();
482 auto resolved = link_.resolve(physicsWorld);
483 if (!resolved.ok()) return eve::Result<void>::failure(resolved.status());
484 resolved.value()->destroy();
485 physicsReleased_ = true;
487}
488
490 using Coord = std::pair<int, int>;
491 std::map<Coord, eve::physics::PhysicsLink> bodies;
494 std::weak_ptr<const void> physicsLifetime;
495 std::uint64_t revision = 0;
496};
497
498PixelTerrainCollisionCache::PixelTerrainCollisionCache() : impl_(std::make_unique<Impl>()) {}
502
504 eve::physics::World& physicsWorld, const eve::pixelworld::PixelWorld& pixelWorld,
505 std::uint32_t maximumFixturesPerChunk) {
506 if (maximumFixturesPerChunk == 0)
508 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "maximumFixturesPerChunk must be positive",
509 "maximumFixturesPerChunk", {}, "pixelworld_physics"));
510 const bool samePixelWorld = impl_->pixelWorld == pixelWorld.worldLink();
511 const bool samePhysicsWorld = impl_->physicsWorld == physicsWorld.runtimeHandle();
512 if (!samePhysicsWorld && !impl_->bodies.empty() && !impl_->physicsLifetime.expired())
515 "terrain collision cache must be cleared from its live physics world before rebinding", "physicsWorld", {},
516 "pixelworld_physics"));
517 const std::uint64_t since = (samePixelWorld && samePhysicsWorld) ? impl_->revision : 0;
518 const auto changed = pixelWorld.snapshotChangedChunks(since);
519 std::set<Impl::Coord> rebuildCoords;
520 for (const auto& chunk : changed) {
521 const Impl::Coord coord{chunk.x, chunk.y};
522 rebuildCoords.insert(coord);
523 constexpr Impl::Coord neighbors[] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
524 for (const auto& [dx, dy] : neighbors) {
525 const Impl::Coord neighbor{chunk.x + dx, chunk.y + dy};
526 if (impl_->bodies.contains(neighbor)) rebuildCoords.insert(neighbor);
527 }
528 }
529
530 struct Candidate {
531 Impl::Coord coord;
533 std::uint32_t fixtures = 0;
534 };
535 std::vector<Candidate> staged;
536 staged.reserve(rebuildCoords.size());
537 const auto destroyStaged = [&] {
538 for (const Candidate& candidate : staged) {
539 auto resolved = candidate.link.resolve(physicsWorld);
540 if (resolved.ok()) resolved.value()->destroy();
541 }
542 };
543
544 for (const auto& coord : rebuildCoords) {
545 auto extracted = extractTerrainContours(pixelWorld, coord.first, coord.second);
546 if (!extracted.ok()) {
547 destroyStaged();
548 return eve::Result<TerrainCollisionSyncReceipt>::failure(extracted.status());
549 }
550 auto contours = std::move(extracted).takeValue();
551 if (contours.size() > maximumFixturesPerChunk) {
552 destroyStaged();
554 eve::DiagnosticCode::PreconditionViolation, "terrain Chunk contours exceed maximumFixturesPerChunk",
555 "chunk", {}, "pixelworld_physics"));
556 }
557 if (contours.empty()) {
558 staged.push_back({coord, {}, 0});
559 continue;
560 }
561 eve::physics::Body* body = nullptr;
562 try {
563 body = physicsWorld.newBody(
564 "static", float(coord.first) * float(eve::pixelworld::kPixelChunkSize),
565 float(coord.second) * float(eve::pixelworld::kPixelChunkSize));
566 for (const TerrainCollisionContour& contour : contours)
567 body->newChainFixture(contour.vertices, contour.loop, 0.5f, 0.f);
569 if (!link.ok()) {
570 body->destroy();
571 destroyStaged();
573 }
574 staged.push_back({coord, std::move(link).takeValue(),
575 std::uint32_t(contours.size())});
576 } catch (const std::exception& error) {
577 if (body && body->isValid()) body->destroy();
578 destroyStaged();
580 eve::DiagnosticCode::Failed, std::string("failed to stage terrain collision body: ") + error.what(),
581 "physics", {}, "pixelworld_physics"));
582 }
583 }
584
586 receipt.sourceRevision = pixelWorld.revision();
587 receipt.chunksRebuilt = std::uint32_t(staged.size());
588 if (!samePhysicsWorld) {
589 impl_->bodies.clear();
590 } else if (!samePixelWorld) {
591 for (const auto& [coord, link] : impl_->bodies) {
592 (void)coord;
593 auto resolved = link.resolve(physicsWorld);
594 if (resolved.ok()) resolved.value()->destroy();
595 ++receipt.bodiesRemoved;
596 }
597 impl_->bodies.clear();
598 }
599 for (const Candidate& candidate : staged) {
600 const auto old = impl_->bodies.find(candidate.coord);
601 if (old != impl_->bodies.end()) {
602 auto resolved = old->second.resolve(physicsWorld);
603 if (resolved.ok()) resolved.value()->destroy();
604 impl_->bodies.erase(old);
605 ++receipt.bodiesRemoved;
606 }
607 if (candidate.fixtures != 0) {
608 impl_->bodies.emplace(candidate.coord, candidate.link);
609 receipt.fixturesCreated += candidate.fixtures;
610 }
611 }
612 impl_->pixelWorld = pixelWorld.worldLink();
613 impl_->physicsWorld = physicsWorld.runtimeHandle();
614 impl_->physicsLifetime = physicsWorld.lifetimeToken();
615 impl_->revision = pixelWorld.revision();
617}
618
620 if (impl_->physicsWorld.isValid() && impl_->physicsWorld != physicsWorld.runtimeHandle())
622 eve::DiagnosticCode::StaleHandle, "terrain collision cache belongs to another physics world",
623 "physicsWorld", {}, "pixelworld_physics"));
624 for (const auto& [coord, link] : impl_->bodies) {
625 (void)coord;
626 auto resolved = link.resolve(physicsWorld);
627 if (resolved.ok()) resolved.value()->destroy();
628 }
629 impl_->bodies.clear();
630 impl_->pixelWorld = {};
631 impl_->physicsWorld = eve::physics::PhysicsWorldHandle::invalid();
632 impl_->physicsLifetime.reset();
633 impl_->revision = 0;
635}
636
637std::uint64_t PixelTerrainCollisionCache::sourceRevision() const noexcept { return impl_->revision; }
638std::size_t PixelTerrainCollisionCache::bodyCount() const noexcept { return impl_->bodies.size(); }
639
640} // namespace eve::pixelworld_physics
Duration start
SQInteger top
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
std::string from
float nx
float ny
std::uint32_t vertexCount
float maximum[3]
float minimum[3]
HexVec3 left
HexVec3 right
std::int32_t second
std::int32_t c
std::int32_t first
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
std::uint32_t height
std::uint32_t width
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
float distance
graphics::Canvas * previous
std::string error
Definition Package.cpp:60
World3D * world
GridPoint from
std::uint64_t revision
float radius
std::shared_ptr< const std::vector< glm::vec2 > > points
Material * material
RoadLaneDirection direction
const RoadEdge * edge
bool found
double current
float dy
float dx
bool occupied
std::map< Cell, int > best
TacticalUnit::TurnResources turn
float offsetX
float offsetY
float size
Definition TreeMesh.cpp:156
std::string body
uint32_t index
const UnitySourceAsset & source
std::uint32_t depth
double oy
double ox
std::vector< int > edges
int turns
glm::vec3 point
float bottom
eve::Value fixtures
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
static constexpr RuntimeHandle invalid() noexcept
Returns the canonical invalid handle.
2D rigid body (Box2D) in pixel-space coordinates. Owned by a World; create shapes with newRectangleFi...
Definition Body.h:21
Fixture * newChainFixture(const std::vector< float > &vertices, bool loop=false, float friction=0.2f, float restitution=0.f)
Create an open chain or closed loop from packed local pixel-space XY vertices.
Definition Body.cpp:305
Fixture * newRectangleFixtureAt(float width, float height, float offsetX, float offsetY, float density=1.f, float friction=0.2f, float restitution=0.f)
Creates an offset rectangle fixture in pixel-space units.
Definition Body.cpp:233
Box2D world wrapper (2D physics) with pixel-space coordinates. Handles stepping, gravity,...
Definition World.h:41
PhysicsWorldHandle runtimeHandle() const noexcept
Process-local identity used by PhysicsLink; invalid after destruction.
Definition World.h:111
Sparse, chunked, deterministic 2D falling-material world.
Definition PixelWorld.h:224
eve::Result< PixelFragmentRasterReceipt > rasterizeFragment(const PixelFragment &fragment, int originX, int originY)
Transactionally rasterize a detached fragment at a new world-space origin.
bool isSolidMaterial(MaterialId material) const noexcept
Query whether a validated material id participates in structural solid collision.
PixelCell getCell(int x, int y) const noexcept
Return the cell at world coordinates; absent chunks read as air.
Owning detached bitmap plus a non-owning, stale-safe dynamic body link.
Incremental static collision projection of authoritative PixelWorld chunks.
~PixelTerrainCollisionCache()
Pixel terrain collision cache.
std::size_t bodyCount() const noexcept
Number of chunks currently owning a non-empty static collision body.
eve::Result< void > clearPhysics(eve::physics::World &physicsWorld)
Explicitly destroy every resolvable projected body and reset revision tracking.
std::uint64_t sourceRevision() const noexcept
Last authoritative revision fully represented by this cache.
constexpr HexDirection next(HexDirection d) noexcept
The next direction clockwise (NW wraps to NE).
Definition HexMetrics.h:76
eve::Result< PixelTerrainContact > sweepTerrainCircle(const eve::pixelworld::PixelWorld &pixelWorld, float startX, float startY, float endX, float endY, float radius, std::uint32_t maximumCells)
Sweep a circular character shape continuously through authoritative solid material cells.
eve::Result< std::vector< TerrainCollisionContour > > extractTerrainContours(const eve::pixelworld::PixelWorld &pixelWorld, int chunkX, int chunkY, std::uint32_t maximumVertices)
Extract simplified binary Marching-Squares-equivalent boundary chains for one Chunk.
eve::Result< PixelTerrainContact > probeTerrainCircle(const eve::pixelworld::PixelWorld &pixelWorld, float centerX, float centerY, float radius, std::uint32_t maximumCells)
Probe a circular character contact directly against authoritative solid material cells.
constexpr int kPixelChunkSize
Definition PixelWorld.h:20
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Owning material bitmap detached atomically from a PixelWorld.
Definition PixelWorld.h:198
std::vector< PixelCell > cells
Definition PixelWorld.h:206
Policy for dynamic fragment fixture creation and terrain settlement.
One deterministic axis-aligned fixture produced from a fragment bitmap.
std::map< Coord, eve::physics::PhysicsLink > bodies
Deterministic contact returned by an authoritative terrain probe or sweep.
float fraction
Sweep time in [0,1], or zero for a probe.
One simplified local-space boundary contour generated for a terrain Chunk.
Counters for one transactional dirty-Chunk terrain collision sync.
bool consumed
Definition Graphics.cpp:184