12#include <unordered_set>
18constexpr float kInf = std::numeric_limits<float>::infinity();
19constexpr float kSqrt2 = 1.41421356237f;
21enum class Topology { Ortho4, Ortho8, Hex };
23using NeighborFn = std::function<void(
int nx,
int ny,
float moveCost)>;
25Topology parseTopology(
const std::string &
name, Topology fallback) {
26 if (
name ==
"ortho4" ||
name ==
"orthogonal4" ||
name ==
"4")
return Topology::Ortho4;
27 if (
name ==
"ortho8" ||
name ==
"orthogonal8" ||
name ==
"8")
return Topology::Ortho8;
28 if (
name ==
"hex" ||
name ==
"hexagonal" ||
name ==
"staggered")
return Topology::Hex;
29 if (
name ==
"auto")
return fallback;
33const char *topologyName(Topology t) {
35 case Topology::Ortho4:
37 case Topology::Ortho8:
45Topology topologyFromOrientation(
MapOrientation orientation,
bool preferDiagonal) {
46 switch (orientation) {
53 return preferDiagonal ? Topology::Ortho8 : Topology::Ortho4;
58 const NeighborFn &
fn) {
61 case Topology::Ortho4: {
62 static const int dx[4] = {1, -1, 0, 0};
63 static const int dy[4] = {0, 0, 1, -1};
64 for (
int i = 0; i < 4; ++i)
fn(
x + dx[i],
y + dy[i], 1.f);
67 case Topology::Ortho8: {
68 static const int dx[8] = {1, -1, 0, 0, 1, 1, -1, -1};
69 static const int dy[8] = {0, 0, 1, -1, 1, -1, 1, -1};
70 for (
int i = 0; i < 8; ++i) {
71 const float c = (i < 4) ? 1.f : kSqrt2;
72 fn(
x + dx[i],
y + dy[i],
c);
78 const bool rowOdd = ((
y & 1) != 0);
79 const bool shifted =
staggerOdd ? rowOdd : !rowOdd;
84 fn(
x + 1,
y - 1, 1.f);
86 fn(
x + 1,
y + 1, 1.f);
90 fn(
x - 1,
y - 1, 1.f);
92 fn(
x - 1,
y + 1, 1.f);
96 const bool colOdd = ((
x & 1) != 0);
97 const bool shifted =
staggerOdd ? colOdd : !colOdd;
102 fn(
x - 1,
y + 1, 1.f);
104 fn(
x + 1,
y + 1, 1.f);
108 fn(
x - 1,
y - 1, 1.f);
110 fn(
x + 1,
y - 1, 1.f);
119float heuristic(Topology
topology,
int x0,
int y0,
int x1,
int y1) {
120 const int dx = std::abs(x1 - x0);
121 const int dy = std::abs(y1 - y0);
123 case Topology::Ortho4:
124 return float(dx + dy);
125 case Topology::Ortho8:
126 return float(std::max(dx, dy)) + (kSqrt2 - 1.f) *
float(std::min(dx, dy));
127 case Topology::Hex: {
128 auto toCube = [](
int x,
int y) {
129 const int q =
x - (
y - (
y & 1)) / 2;
131 const int s = -q - r;
132 return std::tuple<int, int, int>{q, r,
s};
134 auto [q0, r0, s0] = toCube(x0, y0);
135 auto [q1, r1, s1] = toCube(x1, y1);
136 return float(std::max({std::abs(q0 - q1), std::abs(r0 - r1), std::abs(s0 - s1)}));
139 return float(dx + dy);
157 int index(
int x,
int y)
const {
return y *
width +
x; }
158 bool inBounds(
int x,
int y)
const {
return x >= 0 &&
y >= 0 &&
x <
width &&
y <
height; }
160 void resize(
int w,
int h) {
163 cost.assign(
size_t(width * height), 1.f);
167 void applyAutoTopologyFromLayer() {
169 topology = topologyFromOrientation(
layer->config()->orientation, diagonal);
175 auto cfg =
layer->config();
176 resize(cfg->mapW, cfg->mapH);
179 if (!topologyManual) applyAutoTopologyFromLayer();
183 void clearLayer() {
layer =
nullptr; }
185 void syncFromLayer() {
187 auto cfg =
layer->config();
189 if (cfg->mapW != width || cfg->mapH != height) resize(cfg->mapW, cfg->mapH);
192 if (!topologyManual) applyAutoTopologyFromLayer();
195 for (
int i = 0; i <
n; ++i) {
196 const uint32_t gid = (i < int(
tiles->gids.size())) ?
tileGid(
tiles->gids[
size_t(i)]) : 0
u;
197 bool blocked =
false;
198 if (blockEmpty && gid == 0
u) blocked =
true;
200 cost[size_t(i)] = blocked ? 0.f : 1.f;
205 void setTopology(
const std::string &
name) {
206 if (
name ==
"auto") {
208 if (layer) applyAutoTopologyFromLayer();
217 void setDiagonal(
bool enable) {
219 if (!topologyManual && layer) applyAutoTopologyFromLayer();
220 else if (!topologyManual)
topology =
diagonal ? Topology::Ortho8 : Topology::Ortho4;
224 void blockGid(
int gid) {
228 if (layer) syncFromLayer();
231 void unblockGid(
int gid) {
235 if (layer) syncFromLayer();
238 void clearBlockedGids() {
241 if (layer) syncFromLayer();
244 void setBlockEmpty(
bool enable) {
247 if (layer) syncFromLayer();
250 bool isWalkable(
int x,
int y)
const {
251 if (!inBounds(
x,
y))
return false;
252 return cost[size_t(index(
x,
y))] > 0.f;
255 void setBlocked(
int x,
int y,
bool blocked) {
256 if (!inBounds(
x,
y))
return;
257 cost[size_t(index(
x,
y))] = blocked ? 0.f : 1.f;
261 void setCellCost(
int x,
int y,
float c) {
262 if (!inBounds(
x,
y))
return;
267 float getCellCost(
int x,
int y)
const {
268 if (!inBounds(
x,
y))
return 0.f;
269 return cost[size_t(index(
x,
y))];
272 void forEachWalkableNeighbor(
int x,
int y,
const NeighborFn &
fn)
const {
273 if (!
fn || !inBounds(
x,
y))
return;
274 forEachNeighbor(topology,
x,
y, staggerAxisY, staggerOdd, [&](
int nx,
int ny,
float moveCost) {
275 if (!isWalkable(nx, ny))
return;
276 if (topology == Topology::Ortho8) {
277 const int dx = nx -
x;
278 const int dy = ny -
y;
279 if (dx != 0 && dy != 0) {
280 if (!isWalkable(
x + dx,
y) || !isWalkable(
x,
y + dy))
return;
283 fn(nx, ny, moveCost * getCellCost(nx, ny));
291 bool operator>(
const HeapNode &o)
const {
return f > o.f; }
320 impl_->grid.clearLayer();
326 impl_->grid.setTopology(
name);
333 impl_->grid.setDiagonal(enable);
340 impl_->grid.blockGid(gid);
345 impl_->grid.unblockGid(gid);
350 impl_->grid.clearBlockedGids();
355 impl_->grid.setBlockEmpty(enable);
362 impl_->grid.setBlocked(
x,
y, blocked);
369 impl_->grid.setCellCost(
x,
y,
cost);
376 impl_->grid.syncFromLayer();
381 impl_->hasCachedField =
false;
382 impl_->cachedField.clear();
387bool ensureSynced(Grid &grid) {
388 if (grid.layer && grid.dirty) grid.syncFromLayer();
389 return grid.width > 0 && grid.height > 0;
392FlowField *buildFlowFieldUncached(Grid &grid,
int gx,
int gy) {
393 auto *field =
new FlowField();
394 if (!ensureSynced(grid) || !grid.isWalkable(gx, gy)) {
395 field->resize(grid.width, grid.height);
396 field->setGoal(gx, gy);
400 const int w = grid.width;
401 const int h = grid.height;
403 field->setGoal(gx, gy);
405 const auto idx = [
w](
int x,
int y) {
return y *
w +
x; };
406 std::vector<float> dist(
size_t(
w *
h), kInf);
407 std::priority_queue<HeapNode, std::vector<HeapNode>, std::greater<HeapNode>> open;
409 dist[size_t(
idx(gx, gy))] = 0.f;
410 field->setCost(gx, gy, 0.f);
411 field->setNext(gx, gy, gx, gy);
412 open.push(HeapNode{0.f,
idx(gx, gy)});
414 while (!open.empty()) {
415 const HeapNode cur = open.top();
417 if (cur.f > dist[
size_t(cur.idx)])
continue;
418 const int cx = cur.idx %
w;
419 const int cy = cur.idx /
w;
420 const float enterCur = grid.getCellCost(
cx,
cy);
421 forEachNeighbor(grid.topology,
cx,
cy, grid.staggerAxisY, grid.staggerOdd,
422 [&](
int nx,
int ny,
float moveCost) {
423 if (!grid.isWalkable(nx, ny)) return;
424 if (grid.topology == Topology::Ortho8) {
425 const int dx = nx - cx;
426 const int dy = ny - cy;
427 if (dx != 0 && dy != 0) {
428 if (!grid.isWalkable(cx + dx, cy) || !grid.isWalkable(cx, cy + dy))
432 const int ni =
idx(nx, ny);
433 const float tentative = dist[size_t(cur.idx)] + moveCost * enterCur;
434 if (tentative + 1e-6f >= dist[
size_t(ni)])
return;
435 dist[size_t(ni)] = tentative;
436 field->setCost(nx, ny, tentative);
437 field->setNext(nx, ny,
cx,
cy);
438 open.push(HeapNode{tentative, ni});
446Path *Pathfinder::findPath(
int sx,
int sy,
int gx,
int gy) {
447 auto *path =
new Path();
448 Grid &grid = impl_->grid;
449 if (!ensureSynced(grid))
return path;
450 if (!grid.isWalkable(sx, sy) || !grid.isWalkable(gx, gy))
return path;
451 if (sx == gx && sy == gy) {
453 path->setTotalCost(0.f);
457 const int w = grid.width;
458 const int n =
w * grid.height;
459 const auto idx = [
w](
int x,
int y) {
return y *
w +
x; };
461 std::vector<float> gScore(
size_t(
n), kInf);
462 std::vector<int>
parent(
size_t(
n), -1);
463 std::vector<uint8_t> closed(
size_t(
n), 0);
465 std::priority_queue<HeapNode, std::vector<HeapNode>, std::greater<HeapNode>> open;
466 const int start =
idx(sx, sy);
467 const int goal =
idx(gx, gy);
468 gScore[size_t(start)] = 0.f;
469 open.push(HeapNode{heuristic(grid.topology, sx, sy, gx, gy), start});
472 while (!open.empty()) {
473 const HeapNode cur = open.top();
475 if (closed[
size_t(cur.idx)])
continue;
476 closed[size_t(cur.idx)] = 1;
477 if (cur.idx == goal) {
481 const int cx = cur.idx %
w;
482 const int cy = cur.idx /
w;
483 grid.forEachWalkableNeighbor(
cx,
cy, [&](
int nx,
int ny,
float edgeCost) {
484 const int ni =
idx(nx, ny);
485 if (closed[
size_t(ni)])
return;
486 const float tentative = gScore[size_t(cur.idx)] + edgeCost;
487 if (tentative >= gScore[
size_t(ni)])
return;
488 gScore[size_t(ni)] = tentative;
489 parent[size_t(ni)] = cur.idx;
490 open.push(HeapNode{tentative + heuristic(grid.topology, nx, ny, gx, gy), ni});
494 if (!found)
return path;
498 path->add(cur %
w, cur /
w);
499 if (cur == start)
break;
500 cur =
parent[size_t(cur)];
503 path->setTotalCost(gScore[
size_t(goal)]);
508 Grid &grid = impl_->grid;
509 if (!ensureSynced(grid)) {
512 empty->setGoal(gx, gy);
516 if (impl_->hasCachedField && !grid.dirty && impl_->cachedGoalX == gx && impl_->cachedGoalY == gy &&
517 impl_->cachedField.getWidth() == grid.width &&
518 impl_->cachedField.getHeight() == grid.height) {
520 *copy = impl_->cachedField;
524 FlowField *field = buildFlowFieldUncached(grid, gx, gy);
525 impl_->cachedField = *field;
526 impl_->cachedGoalX = gx;
527 impl_->cachedGoalY = gy;
528 impl_->hasCachedField =
true;
534 auto *path =
new Path();
535 Grid &grid = impl_->grid;
536 if (!field || !ensureSynced(grid))
return path;
541 const int maxSteps = grid.width * grid.height + 2;
547 if (
x == gx &&
y == gy)
break;
548 const int nx = field->
nextX(
x,
y);
549 const int ny = field->
nextY(
x,
y);
550 if (nx ==
x && ny ==
y)
break;
551 const float c0 = field->
costAt(
x,
y);
552 const float c1 = field->
costAt(nx, ny);
553 if (c0 < kInf && c1 < kInf && c0 >= c1) total += (c0 - c1);
558 path->setTotalCost(total);
559 if (path->getLength() > 0) {
560 const int lx = path->getX(path->getLength() - 1);
561 const int ly = path->getY(path->getLength() - 1);
562 if (lx != gx || ly != gy) path->clear();
567Path *Pathfinder::findGroupPath(
int sx,
int sy,
int gx,
int gy) {
568 Grid &grid = impl_->grid;
569 if (!ensureSynced(grid))
return new Path();
570 if (!impl_->hasCachedField || grid.dirty || impl_->cachedGoalX != gx || impl_->cachedGoalY != gy ||
571 impl_->cachedField.getWidth() != grid.width ||
572 impl_->cachedField.getHeight() != grid.height) {
573 FlowField *tmp = buildFlowFieldUncached(grid, gx, gy);
574 impl_->cachedField = *tmp;
576 impl_->cachedGoalX = gx;
577 impl_->cachedGoalY = gy;
578 impl_->hasCachedField =
true;
581 return followFlow(&impl_->cachedField, sx, sy);
591void Path::add(
int x,
int y) { cells_.push_back(
Cell{
x,
y}); }
593void Path::reverse() {
594 for (
size_t i = 0, j = cells_.size(); i + 1 < j; ++i, --j) std::swap(cells_[i], cells_[j - 1]);
597int Path::getLength()
const {
return int(cells_.size()); }
599int Path::getX(
int index)
const {
600 if (index < 0 || index >=
int(cells_.size()))
return 0;
601 return cells_[size_t(index)].x;
604int Path::getY(
int index)
const {
605 if (index < 0 || index >=
int(cells_.size()))
return 0;
606 return cells_[size_t(index)].y;
609float Path::getTotalCost()
const {
return totalCost_; }
611void Path::setTotalCost(
float cost) { totalCost_ =
cost; }
613void FlowField::clear() {
614 width_ = height_ = 0;
624 const int n = width_ * height_;
625 cost_.assign(
size_t(
n), kUnreachable);
626 nextX_.assign(
size_t(
n), 0);
627 nextY_.assign(
size_t(
n), 0);
628 for (
int y = 0;
y < height_; ++
y) {
629 for (
int x = 0;
x < width_; ++
x) {
630 const int i = index(
x,
y);
631 nextX_[size_t(i)] =
x;
632 nextY_[size_t(i)] =
y;
637void FlowField::setGoal(
int x,
int y) {
642bool FlowField::inBounds(
int x,
int y)
const {
643 return x >= 0 &&
y >= 0 &&
x < width_ &&
y < height_;
646float FlowField::costAt(
int x,
int y)
const {
647 if (!inBounds(
x,
y))
return kUnreachable;
648 return cost_[size_t(index(
x,
y))];
651void FlowField::setCost(
int x,
int y,
float cost) {
652 if (!inBounds(
x,
y))
return;
653 cost_[size_t(index(
x,
y))] =
cost;
656int FlowField::nextX(
int x,
int y)
const {
657 if (!inBounds(
x,
y))
return x;
658 return nextX_[size_t(index(
x,
y))];
661int FlowField::nextY(
int x,
int y)
const {
662 if (!inBounds(
x,
y))
return y;
663 return nextY_[size_t(index(
x,
y))];
666void FlowField::setNext(
int x,
int y,
int nx,
int ny) {
667 if (!inBounds(
x,
y))
return;
668 const int i = index(
x,
y);
669 nextX_[size_t(i)] = nx;
670 nextY_[size_t(i)] = ny;
673bool FlowField::isReachable(
int x,
int y)
const {
674 const float c = costAt(
x,
y);
675 return c < kUnreachable && c >= 0.f;
std::unordered_set< uint32_t > blockedGids
std::vector< float > cost
SettlementPipeline::Stage fn
std::vector< WfcTile > tiles
Integration + direction field for group pathfinding to a single goal. nextX/nextY point to the neighb...
bool isReachable(int x, int y) const
float costAt(int x, int y) const
int nextY(int x, int y) const
int nextX(int x, int y) const
Ordered tile-index waypoints from start to goal (inclusive).
Pathfinding facade. Single-agent: A*. Group (same goal): Flow Field + follow. Grid/topology internals...
std::string getTopology() const
void invalidateCache()
Invalidate cached flow field (also called when grid dirties).
void setBlockEmpty(bool enable)
float getCellCost(int x, int y) const
void bindLayer(TileLayer *layer)
void setCellCost(int x, int y, float cost)
void setTopology(const std::string &name)
bool getBlockEmpty() const
bool isWalkable(int x, int y) const
void setBlocked(int x, int y, bool blocked)
void setSize(int width, int height)
void setDiagonal(bool enable)
ECS tile layer entity. Script mutates tile GIDs / tileset / draw; TileRenderSystem batch-draws atlas ...
void bindLayer(Shader *shader, bool alwaysDark)
放置世界:格子占用(多通道)+ 地形语义 + 已放置建筑实例。 行为由 PlacementSystem 提供;本类暴露便于脚本绑定的薄封装方法。 坐标换算统一走 eve::grid(支持 rectang...
MapOrientation
Tile map layout: orthogonal, isometric, staggered, or hexagonal.
uint32_t tileGid(uint32_t raw)
Strip Tiled flip / rotate flags; keep low 28 bits.