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;
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);
161 bool inBounds(
int x,
int y)
const {
return x >= 0 &&
y >= 0 &&
x <
width &&
y <
height; }
163 void resize(
int w,
int h) {
166 cost.assign(
size_t(width * height), 1.f);
167 enterMask.assign(
size_t(width * height), 0xff);
168 exitMask.assign(
size_t(width * height), 0xff);
172 void applyAutoTopologyFromLayer() {
174 topology = topologyFromOrientation(
layer->config()->orientation, diagonal);
180 auto cfg =
layer->config();
181 resize(cfg->mapW, cfg->mapH);
184 if (!topologyManual) applyAutoTopologyFromLayer();
188 void clearLayer() {
layer =
nullptr; }
190 void syncFromLayer() {
192 auto cfg =
layer->config();
194 if (cfg->mapW != width || cfg->mapH != height) resize(cfg->mapW, cfg->mapH);
197 if (!topologyManual) applyAutoTopologyFromLayer();
199 const auto tileset =
layer->tileset();
201 for (
int i = 0; i <
n; ++i) {
202 const uint32_t gid = (i < int(
tiles->gids.size())) ?
tileGid(
tiles->gids[
size_t(i)]) : 0
u;
203 bool blocked =
false;
204 float movementCost = 1.f;
205 uint8_t cellEnterMask = 0xff;
206 uint8_t cellExitMask = 0xff;
207 if (blockEmpty && gid == 0
u) blocked =
true;
210 const auto visual = std::find_if(
211 tileset->visuals.begin(), tileset->visuals.end(),
212 [gid](
const TileLayer::Tileset::Visual &candidate) { return candidate.gid == int(gid); });
213 if (visual != tileset->visuals.end()) {
214 blocked = blocked || !visual->walkable;
215 movementCost = std::max(0.001f, visual->cost);
216 cellEnterMask = visual->enterMask;
217 cellExitMask = visual->exitMask;
220 cost[size_t(i)] = blocked ? 0.f : movementCost;
228 void setTopology(
const std::string &
name) {
229 if (
name ==
"auto") {
231 if (layer) applyAutoTopologyFromLayer();
240 void setDiagonal(
bool enable) {
242 if (!topologyManual && layer) applyAutoTopologyFromLayer();
243 else if (!topologyManual)
topology =
diagonal ? Topology::Ortho8 : Topology::Ortho4;
247 void blockGid(
int gid) {
251 if (layer) syncFromLayer();
254 void unblockGid(
int gid) {
258 if (layer) syncFromLayer();
261 void clearBlockedGids() {
264 if (layer) syncFromLayer();
267 void setBlockEmpty(
bool enable) {
270 if (layer) syncFromLayer();
273 bool isWalkable(
int x,
int y)
const {
274 if (!inBounds(
x,
y))
return false;
278 void setBlocked(
int x,
int y,
bool blocked) {
279 if (!inBounds(
x,
y))
return;
284 void setCellCost(
int x,
int y,
float c) {
285 if (!inBounds(
x,
y))
return;
290 float getCellCost(
int x,
int y)
const {
291 if (!inBounds(
x,
y))
return 0.f;
295 bool canTraverseCardinal(
int x,
int y,
int nx,
int ny)
const {
296 if (!isWalkable(
x,
y) || !isWalkable(
nx,
ny))
return false;
299 if (
nx ==
x &&
ny ==
y - 1) {
302 }
else if (
nx ==
x + 1 &&
ny ==
y) {
305 }
else if (
nx ==
x &&
ny ==
y + 1) {
308 }
else if (
nx ==
x - 1 &&
ny ==
y) {
317 void forEachWalkableNeighbor(
int x,
int y,
const NeighborFn &
fn)
const {
318 if (!
fn || !inBounds(
x,
y))
return;
319 forEachNeighbor(topology,
x,
y, staggerAxisY, staggerOdd, [&](
int nx,
int ny,
float moveCost) {
320 if (!isWalkable(
nx,
ny))
return;
321 if (!canTraverseCardinal(
x,
y,
nx,
ny))
return;
322 if (topology == Topology::Ortho8) {
323 const int dx =
nx -
x;
324 const int dy =
ny -
y;
325 if (
dx != 0 &&
dy != 0) {
326 if (!canTraverseCardinal(
x,
y,
x +
dx,
y) || !canTraverseCardinal(
x,
y,
x,
y +
dy))
return;
337 bool operator>(
const HeapNode &o)
const {
return f > o.f; }
366 impl_->grid.clearLayer();
372 impl_->grid.setTopology(
name);
379 impl_->grid.setDiagonal(enable);
386 impl_->grid.blockGid(gid);
391 impl_->grid.unblockGid(gid);
396 impl_->grid.clearBlockedGids();
401 impl_->grid.setBlockEmpty(enable);
408 impl_->grid.setBlocked(
x,
y, blocked);
415 impl_->grid.setCellCost(
x,
y,
cost);
422 impl_->grid.syncFromLayer();
427 impl_->hasCachedField =
false;
428 impl_->cachedField.clear();
433bool ensureSynced(Grid &grid) {
434 if (grid.layer && (grid.dirty || grid.layerRevision != grid.layer->tiles()->revision))
435 grid.syncFromLayer();
436 return grid.width > 0 && grid.height > 0;
439FlowField *buildFlowFieldUncached(Grid &grid,
int gx,
int gy) {
440 auto *field =
new FlowField();
441 if (!ensureSynced(grid) || !grid.isWalkable(gx, gy)) {
442 field->resize(grid.width, grid.height);
443 field->setGoal(gx, gy);
447 const int w = grid.width;
448 const int h = grid.height;
450 field->setGoal(gx, gy);
452 const auto idx = [
w](
int x,
int y) {
return y *
w +
x; };
453 std::vector<float> dist(
size_t(
w *
h), kInf);
454 std::priority_queue<HeapNode, std::vector<HeapNode>, std::greater<HeapNode>> open;
456 dist[size_t(
idx(gx, gy))] = 0.f;
457 field->setCost(gx, gy, 0.f);
458 field->setNext(gx, gy, gx, gy);
459 open.push(HeapNode{0.f,
idx(gx, gy)});
461 while (!open.empty()) {
462 const HeapNode cur = open.top();
464 if (cur.f > dist[
size_t(cur.idx)])
continue;
465 const int cx = cur.idx %
w;
466 const int cy = cur.idx /
w;
467 const float enterCur =
grid.getCellCost(
cx,
cy);
469 [&](
int nx,
int ny,
float moveCost) {
470 if (!grid.isWalkable(nx, ny)) return;
471 if (grid.topology == Topology::Ortho8) {
472 const int dx = nx - cx;
473 const int dy = ny - cy;
474 if (dx != 0 && dy != 0) {
475 if (!grid.isWalkable(cx + dx, cy) || !grid.isWalkable(cx, cy + dy))
480 const float tentative = dist[size_t(cur.idx)] +
moveCost * enterCur;
481 if (tentative + 1e-6f >= dist[
size_t(ni)])
return;
482 dist[size_t(ni)] = tentative;
485 open.push(HeapNode{tentative, ni});
493Path *Pathfinder::findPath(
int sx,
int sy,
int gx,
int gy) {
494 return findPath(
sx,
sy, gx, gy, {});
497Path *Pathfinder::findPath(
int sx,
int sy,
int gx,
int gy,
498 const std::function<
float(
int,
int)> &entryPenalty) {
500 Grid &grid = impl_->grid;
501 if (!ensureSynced(grid))
return path;
502 if (!grid.isWalkable(
sx,
sy) || !grid.isWalkable(gx, gy))
return path;
503 if (
sx == gx &&
sy == gy) {
505 path->setTotalCost(0.f);
509 const int w = grid.width;
510 const int n =
w * grid.height;
511 const auto idx = [
w](
int x,
int y) {
return y *
w +
x; };
513 std::vector<float> gScore(
size_t(
n), kInf);
514 std::vector<int>
parent(
size_t(
n), -1);
515 std::vector<uint8_t> closed(
size_t(
n), 0);
517 std::priority_queue<HeapNode, std::vector<HeapNode>, std::greater<HeapNode>> open;
519 const int goal =
idx(gx, gy);
520 gScore[size_t(
start)] = 0.f;
521 open.push(HeapNode{heuristic(grid.topology,
sx,
sy, gx, gy),
start});
524 while (!open.empty()) {
525 const HeapNode cur = open.top();
527 if (closed[
size_t(cur.idx)])
continue;
528 closed[size_t(cur.idx)] = 1;
529 if (cur.idx == goal) {
533 const int cx = cur.idx %
w;
534 const int cy = cur.idx /
w;
535 grid.forEachWalkableNeighbor(
cx,
cy, [&](
int nx,
int ny,
float edgeCost) {
537 if (closed[
size_t(ni)])
return;
538 float penalty = entryPenalty ? entryPenalty(
nx,
ny) : 0.0f;
539 if (!std::isfinite(penalty))
return;
540 penalty = std::max(0.0f, penalty);
541 const float tentative = gScore[size_t(cur.idx)] + edgeCost + penalty;
542 if (tentative >= gScore[
size_t(ni)])
return;
543 gScore[size_t(ni)] = tentative;
544 parent[size_t(ni)] = cur.idx;
545 open.push(HeapNode{tentative + heuristic(grid.topology,
nx,
ny, gx, gy), ni});
553 path->add(cur %
w, cur /
w);
554 if (cur ==
start)
break;
555 cur =
parent[size_t(cur)];
558 path->setTotalCost(gScore[
size_t(goal)]);
563 Grid &grid = impl_->grid;
564 if (!ensureSynced(grid)) {
567 empty->setGoal(gx, gy);
571 if (impl_->hasCachedField && !grid.dirty && impl_->cachedGoalX == gx && impl_->cachedGoalY == gy &&
572 impl_->cachedField.getWidth() == grid.width &&
573 impl_->cachedField.getHeight() == grid.height) {
575 *copy = impl_->cachedField;
579 FlowField *field = buildFlowFieldUncached(grid, gx, gy);
580 impl_->cachedField = *field;
581 impl_->cachedGoalX = gx;
582 impl_->cachedGoalY = gy;
583 impl_->hasCachedField =
true;
590 Grid &grid = impl_->grid;
591 if (!field || !ensureSynced(grid))
return path;
592 if (!field->isReachable(
sx,
sy))
return path;
594 const int gx = field->getGoalX();
595 const int gy = field->getGoalY();
596 const int maxSteps = grid.width * grid.height + 2;
602 if (
x == gx &&
y == gy)
break;
603 const int nx = field->nextX(
x,
y);
604 const int ny = field->nextY(
x,
y);
605 if (
nx ==
x &&
ny ==
y)
break;
606 const float c0 = field->costAt(
x,
y);
607 const float c1 = field->costAt(
nx,
ny);
608 if (c0 < kInf && c1 < kInf && c0 >= c1) total += (c0 - c1);
613 path->setTotalCost(total);
614 if (
path->getLength() > 0) {
615 const int lx =
path->getX(
path->getLength() - 1);
616 const int ly =
path->getY(
path->getLength() - 1);
617 if (lx != gx || ly != gy)
path->clear();
622Path *Pathfinder::findGroupPath(
int sx,
int sy,
int gx,
int gy) {
623 Grid &grid = impl_->grid;
624 if (!ensureSynced(grid))
return new Path();
625 if (!impl_->hasCachedField || grid.dirty || impl_->cachedGoalX != gx || impl_->cachedGoalY != gy ||
626 impl_->cachedField.getWidth() != grid.width ||
627 impl_->cachedField.getHeight() != grid.height) {
628 FlowField *tmp = buildFlowFieldUncached(grid, gx, gy);
629 impl_->cachedField = *tmp;
631 impl_->cachedGoalX = gx;
632 impl_->cachedGoalY = gy;
633 impl_->hasCachedField =
true;
636 return followFlow(&impl_->cachedField,
sx,
sy);
646void Path::add(
int x,
int y) { cells_.push_back(
Cell{
x,
y}); }
648void Path::reverse() {
649 for (
size_t i = 0, j = cells_.size(); i + 1 < j; ++i, --j) std::swap(cells_[i], cells_[j - 1]);
652int Path::getLength()
const {
return int(cells_.size()); }
655 if (index < 0 || index >=
int(cells_.size()))
return 0;
656 return cells_[size_t(
index)].x;
660 if (index < 0 || index >=
int(cells_.size()))
return 0;
661 return cells_[size_t(
index)].y;
664float Path::getTotalCost()
const {
return totalCost_; }
666void Path::setTotalCost(
float cost) { totalCost_ =
cost; }
668void FlowField::clear() {
669 width_ = height_ = 0;
679 const int n = width_ * height_;
680 cost_.assign(
size_t(
n), kUnreachable);
681 nextX_.assign(
size_t(
n), 0);
682 nextY_.assign(
size_t(
n), 0);
683 for (
int y = 0;
y < height_; ++
y) {
684 for (
int x = 0;
x < width_; ++
x) {
686 nextX_[size_t(i)] =
x;
687 nextY_[size_t(i)] =
y;
692void FlowField::setGoal(
int x,
int y) {
697bool FlowField::inBounds(
int x,
int y)
const {
698 return x >= 0 &&
y >= 0 &&
x < width_ &&
y < height_;
701float FlowField::costAt(
int x,
int y)
const {
702 if (!inBounds(
x,
y))
return kUnreachable;
703 return cost_[size_t(
index(
x,
y))];
706void FlowField::setCost(
int x,
int y,
float cost) {
707 if (!inBounds(
x,
y))
return;
711int FlowField::nextX(
int x,
int y)
const {
712 if (!inBounds(
x,
y))
return x;
713 return nextX_[size_t(
index(
x,
y))];
716int FlowField::nextY(
int x,
int y)
const {
717 if (!inBounds(
x,
y))
return y;
718 return nextY_[size_t(
index(
x,
y))];
721void FlowField::setNext(
int x,
int y,
int nx,
int ny) {
722 if (!inBounds(
x,
y))
return;
724 nextX_[size_t(i)] =
nx;
725 nextY_[size_t(i)] =
ny;
728bool FlowField::isReachable(
int x,
int y)
const {
729 const float c = costAt(
x,
y);
730 return c < kUnreachable && c >= 0.f;
eve::resource::CostSpec cost
std::array< double, 10 > q
std::unordered_set< uint32_t > blockedGids
std::vector< uint8_t > exitMask
std::vector< uint8_t > enterMask
RoadLaneDirection direction
std::vector< WfcTile > tiles
Integration + direction field for group pathfinding to a single goal. nextX/nextY point to the neighb...
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
Returns the topology.
void invalidateCache()
Invalidate cached flow field (also called when grid dirties).
void setBlockEmpty(bool enable)
Sets the block empty.
void syncFromLayer()
Synchronizes from layer.
float getCellCost(int x, int y) const
Returns the cell cost.
void bindLayer(TileLayer *layer)
Binds layer.
void setCellCost(int x, int y, float cost)
Sets the cell cost.
void setTopology(const std::string &name)
Sets the topology.
bool getBlockEmpty() const
Returns the block empty.
bool isWalkable(int x, int y) const
True when walkable.
bool getDiagonal() const
Returns the diagonal.
void blockGid(int gid)
Block gid.
void setBlocked(int x, int y, bool blocked)
Sets the blocked.
void setSize(int width, int height)
Sets the size.
void clearBlockedGids()
Clears blocked gids.
void unblockGid(int gid)
Unblock gid.
void setDiagonal(bool enable)
Sets the diagonal.
ECS tile layer entity. Script mutates tile GIDs / tileset / draw; TileRenderSystem batch-draws atlas ...
void bindLayer(Shader *shader, bool alwaysDark)
Binds layer.
std::int32_t moveCost(const HexMap &map, HexCoordinates from, HexCoordinates to, HexDirection direction, const HexOccupancyQuery &occupied)
Cost of moving between two adjacent cells.
constexpr HexDirection opposite(HexDirection d) noexcept
The direction opposite to d.
放置世界:格子占用(多通道)+ 地形语义 + 已放置建筑实例。 行为由 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.
const EditorValue * field(const EditorValue &value, const char *name)
WidgetDesc grid(int columns, std::vector< WidgetDesc > children, std::string id)
Fixed-column grid; children are placed in source order.
SettlementPipeline::Stage fn