mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-02 13:38:08 +00:00
Optimize mover rendering and segment plan pipeline
This commit is contained in:
@@ -8,7 +8,6 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { densePathToLosKeypointSegments } from "../game/MotionPlans";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { distSortUnit } from "../Util";
|
||||
@@ -115,22 +114,7 @@ export class TradeShipExecution implements Execution {
|
||||
if (dst !== this.motionPlanDst) {
|
||||
this.motionPlanId++;
|
||||
const from = result.node;
|
||||
const segPlan = this.pathFinder.planSegments?.(from, dst) ??
|
||||
(() => {
|
||||
const densePath = this.pathFinder.findPath(from, dst);
|
||||
return densePath
|
||||
? densePathToLosKeypointSegments(
|
||||
densePath,
|
||||
this.mg.map(),
|
||||
(t) =>
|
||||
this.mg.isWater(t) ||
|
||||
(this.mg.isLand(t) && this.mg.isShoreline(t)),
|
||||
)
|
||||
: null;
|
||||
})() ?? {
|
||||
points: Uint32Array.from([from]),
|
||||
segmentSteps: new Uint32Array(0),
|
||||
};
|
||||
const segPlan = this.safeSegmentPlan(from, dst);
|
||||
|
||||
this.mg.recordMotionPlan({
|
||||
kind: "grid_segments",
|
||||
@@ -226,4 +210,23 @@ export class TradeShipExecution implements Execution {
|
||||
dstPort(): TileRef {
|
||||
return this._dstPort.tile();
|
||||
}
|
||||
|
||||
private safeSegmentPlan(from: TileRef, to: TileRef): {
|
||||
points: Uint32Array;
|
||||
segmentSteps: Uint32Array;
|
||||
} {
|
||||
const segPlan = this.pathFinder.planSegments?.(from, to);
|
||||
if (segPlan) {
|
||||
return segPlan;
|
||||
}
|
||||
|
||||
const map = this.mg.map();
|
||||
console.warn(
|
||||
`TradeShipExecution: missing segment plan from (${map.x(from)},${map.y(from)}) to (${map.x(to)},${map.y(to)}); using defensive single-point fallback`,
|
||||
);
|
||||
return {
|
||||
points: Uint32Array.from([from]),
|
||||
segmentSteps: new Uint32Array(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,7 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import {
|
||||
densePathToLosKeypointSegments,
|
||||
MotionPlanRecord,
|
||||
} from "../game/MotionPlans";
|
||||
import { MotionPlanRecord } from "../game/MotionPlans";
|
||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
@@ -116,22 +113,7 @@ export class TransportShipExecution implements Execution {
|
||||
targetTile: this.dst,
|
||||
});
|
||||
|
||||
const segPlan = this.pathFinder.planSegments?.(this.src, this.dst) ??
|
||||
(() => {
|
||||
const densePath = this.pathFinder.findPath(this.src, this.dst);
|
||||
return densePath
|
||||
? densePathToLosKeypointSegments(
|
||||
densePath,
|
||||
this.mg.map(),
|
||||
(t) =>
|
||||
this.mg.isWater(t) ||
|
||||
(this.mg.isLand(t) && this.mg.isShoreline(t)),
|
||||
)
|
||||
: null;
|
||||
})() ?? {
|
||||
points: Uint32Array.from([this.src]),
|
||||
segmentSteps: new Uint32Array(0),
|
||||
};
|
||||
const segPlan = this.safeSegmentPlan(this.src, this.dst);
|
||||
|
||||
const motionPlan: MotionPlanRecord = {
|
||||
kind: "grid_segments",
|
||||
@@ -281,22 +263,7 @@ export class TransportShipExecution implements Execution {
|
||||
if (this.dst !== null && this.dst !== this.motionPlanDst) {
|
||||
this.motionPlanId++;
|
||||
const from = this.boat.tile();
|
||||
const segPlan = this.pathFinder.planSegments?.(from, this.dst) ??
|
||||
(() => {
|
||||
const densePath = this.pathFinder.findPath(from, this.dst);
|
||||
return densePath
|
||||
? densePathToLosKeypointSegments(
|
||||
densePath,
|
||||
this.mg.map(),
|
||||
(t) =>
|
||||
this.mg.isWater(t) ||
|
||||
(this.mg.isLand(t) && this.mg.isShoreline(t)),
|
||||
)
|
||||
: null;
|
||||
})() ?? {
|
||||
points: Uint32Array.from([from]),
|
||||
segmentSteps: new Uint32Array(0),
|
||||
};
|
||||
const segPlan = this.safeSegmentPlan(from, this.dst);
|
||||
|
||||
this.mg.recordMotionPlan({
|
||||
kind: "grid_segments",
|
||||
@@ -318,4 +285,23 @@ export class TransportShipExecution implements Execution {
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
private safeSegmentPlan(from: TileRef, to: TileRef): {
|
||||
points: Uint32Array;
|
||||
segmentSteps: Uint32Array;
|
||||
} {
|
||||
const segPlan = this.pathFinder.planSegments?.(from, to);
|
||||
if (segPlan) {
|
||||
return segPlan;
|
||||
}
|
||||
|
||||
const map = this.mg.map();
|
||||
console.warn(
|
||||
`TransportShipExecution: missing segment plan from (${map.x(from)},${map.y(from)}) to (${map.x(to)},${map.y(to)}); using defensive single-point fallback`,
|
||||
);
|
||||
return {
|
||||
points: Uint32Array.from([from]),
|
||||
segmentSteps: new Uint32Array(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { GameMap } from "./GameMap";
|
||||
import { TileRef } from "./GameMap";
|
||||
|
||||
export enum PackedMotionPlanKind {
|
||||
@@ -292,91 +291,3 @@ export function densePathToKeypointSegments(path: ArrayLike<number>): {
|
||||
segmentSteps: Uint32Array.from(segmentSteps),
|
||||
};
|
||||
}
|
||||
|
||||
function canTraverseDda(
|
||||
map: GameMap,
|
||||
from: TileRef,
|
||||
to: TileRef,
|
||||
isTraversable: (t: TileRef) => boolean,
|
||||
): boolean {
|
||||
const x0 = map.x(from);
|
||||
const y0 = map.y(from);
|
||||
const x1 = map.x(to);
|
||||
const y1 = map.y(to);
|
||||
|
||||
const dx = x1 - x0;
|
||||
const dy = y1 - y0;
|
||||
const steps = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
if (steps === 0) {
|
||||
return isTraversable(from);
|
||||
}
|
||||
|
||||
for (let t = 0; t <= steps; t++) {
|
||||
const x = Math.round(x0 + (dx * t) / steps);
|
||||
const y = Math.round(y0 + (dy * t) / steps);
|
||||
if (!map.isValidCoord(x, y)) {
|
||||
return false;
|
||||
}
|
||||
const ref = map.ref(x, y);
|
||||
if (!isTraversable(ref)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function densePathToLosKeypointSegments(
|
||||
path: readonly TileRef[] | Uint32Array,
|
||||
map: GameMap,
|
||||
isTraversable: (t: TileRef) => boolean,
|
||||
): { points: Uint32Array; segmentSteps: Uint32Array } | null {
|
||||
const len = path.length >>> 0;
|
||||
if (len === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const first = (path[0] ?? 0) as TileRef;
|
||||
if (len === 1) {
|
||||
return {
|
||||
points: Uint32Array.from([first >>> 0]),
|
||||
segmentSteps: new Uint32Array(0),
|
||||
};
|
||||
}
|
||||
|
||||
const points: number[] = [first >>> 0];
|
||||
const segmentSteps: number[] = [];
|
||||
|
||||
let i = 0;
|
||||
while (i < len - 1) {
|
||||
let best = i + 1;
|
||||
let lo = i + 1;
|
||||
let hi = len - 1;
|
||||
|
||||
// Binary search for farthest "visible" point along the existing path.
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
const ok = canTraverseDda(
|
||||
map,
|
||||
path[i] as TileRef,
|
||||
path[mid] as TileRef,
|
||||
isTraversable,
|
||||
);
|
||||
if (ok) {
|
||||
best = mid;
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
points.push((path[best] as TileRef) >>> 0);
|
||||
segmentSteps.push(best - i);
|
||||
i = best;
|
||||
}
|
||||
|
||||
return {
|
||||
points: Uint32Array.from(points),
|
||||
segmentSteps: Uint32Array.from(segmentSteps),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ export class PathFinding {
|
||||
const pf = new AStarWater(miniMap);
|
||||
|
||||
return PathFinderBuilder.create(pf)
|
||||
.wrap((pf) => new SmoothingWaterTransformer(pf, miniMap))
|
||||
.wrap((pf) => new ShoreCoercingTransformer(pf, miniMap))
|
||||
.wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap))
|
||||
.buildWithStepper(tileStepperConfig(game));
|
||||
|
||||
@@ -111,11 +111,42 @@ export class PathFinderStepper<T> implements SteppingPathFinder<T> {
|
||||
});
|
||||
|
||||
if (allFailed) {
|
||||
if (!Array.isArray(from)) {
|
||||
this.path = null;
|
||||
this.pathIndex = 0;
|
||||
this.lastTo = to;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.finder.findPath(from, to);
|
||||
const isSingleSource = !Array.isArray(from);
|
||||
if (isSingleSource) {
|
||||
if (this.lastTo === null || !this.config.equals(this.lastTo, to)) {
|
||||
this.path = null;
|
||||
this.pathIndex = 0;
|
||||
this.lastTo = to;
|
||||
}
|
||||
}
|
||||
|
||||
const path = this.finder.findPath(from, to);
|
||||
|
||||
if (isSingleSource) {
|
||||
if (path === null) {
|
||||
this.path = null;
|
||||
this.pathIndex = 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.path = path;
|
||||
this.pathIndex = 0;
|
||||
if (path.length > 0 && this.config.equals(path[0], from)) {
|
||||
this.pathIndex = 1;
|
||||
}
|
||||
this.lastTo = to;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
planSegments(from: T | T[], to: T): SegmentPlan | null {
|
||||
@@ -126,7 +157,7 @@ export class PathFinderStepper<T> implements SteppingPathFinder<T> {
|
||||
// If called with multi-source, don't try to prime the step cache (next() uses single-source).
|
||||
if (Array.isArray(from)) {
|
||||
// Still compute a path first so inner transformers can cache their segment plan off findPath().
|
||||
this.finder.findPath(from, to);
|
||||
this.findPath(from, to);
|
||||
return this.finder.planSegments(from, to);
|
||||
}
|
||||
|
||||
@@ -148,28 +179,9 @@ export class PathFinderStepper<T> implements SteppingPathFinder<T> {
|
||||
};
|
||||
}
|
||||
|
||||
if (this.lastTo === null || !this.config.equals(this.lastTo, to)) {
|
||||
this.path = null;
|
||||
this.pathIndex = 0;
|
||||
this.lastTo = to;
|
||||
}
|
||||
|
||||
if (this.path === null) {
|
||||
try {
|
||||
this.path = this.finder.findPath(from, to);
|
||||
} catch (err) {
|
||||
console.error("PathFinder threw an error during findPath", err);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.path === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.pathIndex = 0;
|
||||
if (this.path.length > 0 && this.config.equals(this.path[0], from)) {
|
||||
this.pathIndex = 1;
|
||||
}
|
||||
const path = this.findPath(from, to);
|
||||
if (path === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.finder.planSegments(from, to);
|
||||
|
||||
@@ -130,9 +130,63 @@ export class MiniMapTransformer implements PathFinder<number> {
|
||||
steps.push(segSteps >>> 0);
|
||||
}
|
||||
|
||||
const compressed = this.compressCollinearSegments(points, steps);
|
||||
|
||||
return {
|
||||
points: Uint32Array.from(points),
|
||||
segmentSteps: Uint32Array.from(steps),
|
||||
points: Uint32Array.from(compressed.points),
|
||||
segmentSteps: Uint32Array.from(compressed.segmentSteps),
|
||||
};
|
||||
}
|
||||
|
||||
private compressCollinearSegments(
|
||||
points: number[],
|
||||
segmentSteps: number[],
|
||||
): { points: number[]; segmentSteps: number[] } {
|
||||
if (points.length <= 2 || segmentSteps.length <= 1) {
|
||||
return { points, segmentSteps };
|
||||
}
|
||||
|
||||
const outPoints: number[] = [points[0] >>> 0];
|
||||
const outSteps: number[] = [];
|
||||
|
||||
let runSteps = segmentSteps[0] >>> 0;
|
||||
let runDir = this.segmentDirection(points[0] as TileRef, points[1] as TileRef);
|
||||
|
||||
for (let i = 1; i < segmentSteps.length; i++) {
|
||||
const segDir = this.segmentDirection(
|
||||
points[i] as TileRef,
|
||||
points[i + 1] as TileRef,
|
||||
);
|
||||
|
||||
if (segDir.dx === runDir.dx && segDir.dy === runDir.dy) {
|
||||
runSteps = (runSteps + (segmentSteps[i] >>> 0)) >>> 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
outPoints.push(points[i] >>> 0);
|
||||
outSteps.push(runSteps >>> 0);
|
||||
runDir = segDir;
|
||||
runSteps = segmentSteps[i] >>> 0;
|
||||
}
|
||||
|
||||
outPoints.push(points[points.length - 1] >>> 0);
|
||||
outSteps.push(runSteps >>> 0);
|
||||
|
||||
return {
|
||||
points: outPoints,
|
||||
segmentSteps: outSteps,
|
||||
};
|
||||
}
|
||||
|
||||
private segmentDirection(
|
||||
from: TileRef,
|
||||
to: TileRef,
|
||||
): { dx: number; dy: number } {
|
||||
const dx = this.map.x(to) - this.map.x(from);
|
||||
const dy = this.map.y(to) - this.map.y(from);
|
||||
return {
|
||||
dx: Math.sign(dx),
|
||||
dy: Math.sign(dy),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user