Files
esengine/packages/framework/pathfinding/src/grid/GridMap.ts
YHH 155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00

365 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @zh 网格地图实现
* @en Grid Map Implementation
*/
import type {
IPathfindingMap,
IPathNode,
IPoint,
HeuristicFunction
} from '../core/IPathfinding';
import { createPoint, octileDistance } from '../core/IPathfinding';
// =============================================================================
// 网格节点 | Grid Node
// =============================================================================
/**
* @zh 网格节点
* @en Grid node
*/
export class GridNode implements IPathNode {
readonly id: string;
readonly position: IPoint;
readonly x: number;
readonly y: number;
cost: number;
walkable: boolean;
constructor(x: number, y: number, walkable: boolean = true, cost: number = 1) {
this.x = x;
this.y = y;
this.id = `${x},${y}`;
this.position = createPoint(x, y);
this.walkable = walkable;
this.cost = cost;
}
}
// =============================================================================
// 移动方向 | Movement Directions
// =============================================================================
/**
* @zh 4方向偏移 (上下左右)
* @en 4-directional offsets (up, down, left, right)
*/
export const DIRECTIONS_4 = [
{ dx: 0, dy: -1 }, // Up
{ dx: 1, dy: 0 }, // Right
{ dx: 0, dy: 1 }, // Down
{ dx: -1, dy: 0 } // Left
] as const;
/**
* @zh 8方向偏移 (含对角线)
* @en 8-directional offsets (including diagonals)
*/
export const DIRECTIONS_8 = [
{ dx: 0, dy: -1 }, // Up
{ dx: 1, dy: -1 }, // Up-Right
{ dx: 1, dy: 0 }, // Right
{ dx: 1, dy: 1 }, // Down-Right
{ dx: 0, dy: 1 }, // Down
{ dx: -1, dy: 1 }, // Down-Left
{ dx: -1, dy: 0 }, // Left
{ dx: -1, dy: -1 } // Up-Left
] as const;
// =============================================================================
// 网格地图配置 | Grid Map Options
// =============================================================================
/**
* @zh 网格地图配置
* @en Grid map options
*/
export interface IGridMapOptions {
/** @zh 是否允许对角移动 @en Allow diagonal movement */
allowDiagonal?: boolean;
/** @zh 对角移动代价 @en Diagonal movement cost */
diagonalCost?: number;
/** @zh 是否避免穿角 @en Avoid corner cutting */
avoidCorners?: boolean;
/** @zh 启发式函数 @en Heuristic function */
heuristic?: HeuristicFunction;
}
/**
* @zh 默认网格地图配置
* @en Default grid map options
*/
export const DEFAULT_GRID_OPTIONS: Required<IGridMapOptions> = {
allowDiagonal: true,
diagonalCost: Math.SQRT2,
avoidCorners: true,
heuristic: octileDistance
};
// =============================================================================
// 网格地图 | Grid Map
// =============================================================================
/**
* @zh 网格地图
* @en Grid Map
*
* @zh 基于二维数组的网格地图实现支持4方向和8方向移动
* @en Grid map implementation based on 2D array, supports 4 and 8 directional movement
*
* @example
* ```typescript
* // Create a 10x10 grid
* const grid = new GridMap(10, 10);
*
* // Set some cells as obstacles
* grid.setWalkable(5, 5, false);
* grid.setWalkable(5, 6, false);
*
* // Use with pathfinder
* const pathfinder = new AStarPathfinder(grid);
* const result = pathfinder.findPath(0, 0, 9, 9);
* ```
*/
export class GridMap implements IPathfindingMap {
readonly width: number;
readonly height: number;
private readonly nodes: GridNode[][];
private readonly options: Required<IGridMapOptions>;
constructor(width: number, height: number, options?: IGridMapOptions) {
this.width = width;
this.height = height;
this.options = { ...DEFAULT_GRID_OPTIONS, ...options };
this.nodes = this.createNodes();
}
/**
* @zh 创建网格节点
* @en Create grid nodes
*/
private createNodes(): GridNode[][] {
const nodes: GridNode[][] = [];
for (let y = 0; y < this.height; y++) {
nodes[y] = [];
for (let x = 0; x < this.width; x++) {
nodes[y][x] = new GridNode(x, y, true, 1);
}
}
return nodes;
}
/**
* @zh 获取指定位置的节点
* @en Get node at position
*/
getNodeAt(x: number, y: number): GridNode | null {
if (!this.isInBounds(x, y)) {
return null;
}
return this.nodes[y][x];
}
/**
* @zh 检查坐标是否在边界内
* @en Check if coordinates are within bounds
*/
isInBounds(x: number, y: number): boolean {
return x >= 0 && x < this.width && y >= 0 && y < this.height;
}
/**
* @zh 检查位置是否可通行
* @en Check if position is walkable
*/
isWalkable(x: number, y: number): boolean {
const node = this.getNodeAt(x, y);
return node !== null && node.walkable;
}
/**
* @zh 设置位置是否可通行
* @en Set position walkability
*/
setWalkable(x: number, y: number, walkable: boolean): void {
const node = this.getNodeAt(x, y);
if (node) {
node.walkable = walkable;
}
}
/**
* @zh 设置位置的移动代价
* @en Set movement cost at position
*/
setCost(x: number, y: number, cost: number): void {
const node = this.getNodeAt(x, y);
if (node) {
node.cost = cost;
}
}
/**
* @zh 获取节点的邻居
* @en Get neighbors of a node
*/
getNeighbors(node: IPathNode): GridNode[] {
const neighbors: GridNode[] = [];
const { x, y } = node.position;
const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4;
for (let i = 0; i < directions.length; i++) {
const dir = directions[i];
const nx = x + dir.dx;
const ny = y + dir.dy;
if (!this.isInBounds(nx, ny)) {
continue;
}
const neighbor = this.nodes[ny][nx];
if (!neighbor.walkable) {
continue;
}
// Check corner cutting for diagonal movement
if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) {
const horizontal = this.getNodeAt(x + dir.dx, y);
const vertical = this.getNodeAt(x, y + dir.dy);
if (!horizontal?.walkable || !vertical?.walkable) {
continue;
}
}
neighbors.push(neighbor);
}
return neighbors;
}
/**
* @zh 计算启发式距离
* @en Calculate heuristic distance
*/
heuristic(a: IPoint, b: IPoint): number {
return this.options.heuristic(a, b);
}
/**
* @zh 计算移动代价
* @en Calculate movement cost
*/
getMovementCost(from: IPathNode, to: IPathNode): number {
const dx = Math.abs(from.position.x - to.position.x);
const dy = Math.abs(from.position.y - to.position.y);
// Diagonal movement
if (dx !== 0 && dy !== 0) {
return to.cost * this.options.diagonalCost;
}
// Cardinal movement
return to.cost;
}
/**
* @zh 从二维数组加载地图
* @en Load map from 2D array
*
* @param data - @zh 0=可通行非0=不可通行 @en 0=walkable, non-0=blocked
*/
loadFromArray(data: number[][]): void {
for (let y = 0; y < Math.min(data.length, this.height); y++) {
for (let x = 0; x < Math.min(data[y].length, this.width); x++) {
this.nodes[y][x].walkable = data[y][x] === 0;
}
}
}
/**
* @zh 从字符串加载地图
* @en Load map from string
*
* @param str - @zh 地图字符串,'.'=可通行,'#'=障碍 @en Map string, '.'=walkable, '#'=blocked
*/
loadFromString(str: string): void {
const lines = str.trim().split('\n');
for (let y = 0; y < Math.min(lines.length, this.height); y++) {
const line = lines[y];
for (let x = 0; x < Math.min(line.length, this.width); x++) {
this.nodes[y][x].walkable = line[x] !== '#';
}
}
}
/**
* @zh 导出为字符串
* @en Export to string
*/
toString(): string {
let result = '';
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
result += this.nodes[y][x].walkable ? '.' : '#';
}
result += '\n';
}
return result;
}
/**
* @zh 重置所有节点为可通行
* @en Reset all nodes to walkable
*/
reset(): void {
for (let y = 0; y < this.height; y++) {
for (let x = 0; x < this.width; x++) {
this.nodes[y][x].walkable = true;
this.nodes[y][x].cost = 1;
}
}
}
/**
* @zh 设置矩形区域的通行性
* @en Set walkability for a rectangle region
*/
setRectWalkable(
x: number,
y: number,
width: number,
height: number,
walkable: boolean
): void {
for (let dy = 0; dy < height; dy++) {
for (let dx = 0; dx < width; dx++) {
this.setWalkable(x + dx, y + dy, walkable);
}
}
}
}
// =============================================================================
// 工厂函数 | Factory Function
// =============================================================================
/**
* @zh 创建网格地图
* @en Create grid map
*/
export function createGridMap(
width: number,
height: number,
options?: IGridMapOptions
): GridMap {
return new GridMap(width, height, options);
}