chore: update pathfinding, add rpc/world-streaming docs, refactor world-streaming location (#376)

This commit is contained in:
YHH
2025-12-28 19:18:28 +08:00
committed by GitHub
parent 838cda91aa
commit 0662b07445
44 changed files with 3850 additions and 165 deletions

View File

@@ -0,0 +1,425 @@
import type { Entity, IScene, IService } from '@esengine/ecs-framework';
import type { IChunkCoord, IChunkData, IChunkInfo, IChunkLoadRequest, IChunkBounds } from '../types';
import { EChunkState, EChunkPriority } from '../types';
import { SpatialHashGrid } from './SpatialHashGrid';
import type { IChunkSerializer } from './ChunkSerializer';
import { ChunkSerializer } from './ChunkSerializer';
import { ChunkComponent } from '../components/ChunkComponent';
/**
* 区块数据提供者接口
*
* Interface for chunk data loading/saving.
*/
export interface IChunkDataProvider {
loadChunkData(coord: IChunkCoord): Promise<IChunkData | null>;
saveChunkData(data: IChunkData): Promise<void>;
}
/**
* 区块管理器事件
*
* Events emitted by ChunkManager.
*/
export interface IChunkManagerEvents {
onChunkLoaded?: (coord: IChunkCoord, entities: Entity[]) => void;
onChunkUnloaded?: (coord: IChunkCoord) => void;
onChunkLoadFailed?: (coord: IChunkCoord, error: Error) => void;
}
/**
* 区块管理器
*
* Manages chunk lifecycle, loading queue, and spatial queries.
*
* 区块管理器负责区块生命周期、加载队列和空间查询。
*/
export class ChunkManager implements IService {
private _chunkGrid: SpatialHashGrid<IChunkInfo>;
private _loadQueue: IChunkLoadRequest[] = [];
private _unloadQueue: Array<{ coord: IChunkCoord; scheduledTime: number }> = [];
private _scene: IScene | null = null;
private _dataProvider: IChunkDataProvider | null = null;
private _serializer: IChunkSerializer;
private _events: IChunkManagerEvents = {};
private _chunkSize: number;
constructor(chunkSize: number = 512, serializer?: IChunkSerializer) {
this._chunkSize = chunkSize;
this._chunkGrid = new SpatialHashGrid<IChunkInfo>(chunkSize);
this._serializer = serializer ?? new ChunkSerializer();
}
get chunkSize(): number {
return this._chunkSize;
}
get loadedChunkCount(): number {
return this._chunkGrid.size;
}
get pendingLoadCount(): number {
return this._loadQueue.length;
}
get pendingUnloadCount(): number {
return this._unloadQueue.length;
}
/**
* 设置场景
*
* Set the scene for entity creation.
*/
setScene(scene: IScene): void {
this._scene = scene;
}
/**
* 设置数据提供者
*
* Set the chunk data provider.
*/
setDataProvider(provider: IChunkDataProvider): void {
this._dataProvider = provider;
}
/**
* 设置事件回调
*
* Set event callbacks.
*/
setEvents(events: IChunkManagerEvents): void {
this._events = events;
}
/**
* 请求加载区块
*
* Request a chunk to be loaded.
*/
requestLoad(coord: IChunkCoord, priority: EChunkPriority = EChunkPriority.Normal): void {
const existing = this._chunkGrid.get(coord);
if (existing && existing.state !== EChunkState.Unloaded && existing.state !== EChunkState.Failed) {
if (existing.state === EChunkState.Loaded) {
existing.lastAccessTime = Date.now();
}
return;
}
const existingRequest = this._loadQueue.find(
(r) => r.coord.x === coord.x && r.coord.y === coord.y
);
if (existingRequest) {
if (priority < existingRequest.priority) {
existingRequest.priority = priority;
}
return;
}
this._loadQueue.push({
coord,
priority,
timestamp: Date.now()
});
this.sortLoadQueue();
}
/**
* 请求卸载区块
*
* Request a chunk to be unloaded.
*/
requestUnload(coord: IChunkCoord, delay: number = 0): void {
const chunk = this._chunkGrid.get(coord);
if (!chunk || chunk.state !== EChunkState.Loaded) {
return;
}
const existingRequest = this._unloadQueue.find(
(r) => r.coord.x === coord.x && r.coord.y === coord.y
);
if (!existingRequest) {
this._unloadQueue.push({
coord,
scheduledTime: Date.now() + delay
});
}
}
/**
* 取消卸载请求
*
* Cancel a pending unload request.
*/
cancelUnload(coord: IChunkCoord): void {
const index = this._unloadQueue.findIndex(
(r) => r.coord.x === coord.x && r.coord.y === coord.y
);
if (index >= 0) {
this._unloadQueue.splice(index, 1);
}
}
/**
* 处理加载队列
*
* Process pending load requests.
*/
async processLoads(maxCount: number): Promise<void> {
let processed = 0;
while (processed < maxCount && this._loadQueue.length > 0) {
const request = this._loadQueue.shift()!;
await this.loadChunk(request.coord);
processed++;
}
}
/**
* 处理卸载队列
*
* Process pending unload requests.
*/
processUnloads(maxCount: number): void {
const now = Date.now();
let processed = 0;
const readyToUnload = this._unloadQueue.filter((r) => r.scheduledTime <= now);
for (const request of readyToUnload) {
if (processed >= maxCount) break;
this.unloadChunk(request.coord);
processed++;
const index = this._unloadQueue.indexOf(request);
if (index >= 0) {
this._unloadQueue.splice(index, 1);
}
}
}
/**
* 加载区块
*
* Load a single chunk.
*/
private async loadChunk(coord: IChunkCoord): Promise<void> {
if (!this._scene) {
console.warn('[ChunkManager] No scene set');
return;
}
const bounds = this.getChunkBounds(coord);
const chunkInfo: IChunkInfo = {
coord,
state: EChunkState.Loading,
priority: EChunkPriority.Normal,
entities: [],
bounds,
lastAccessTime: Date.now(),
distanceSq: 0
};
this._chunkGrid.set(coord, chunkInfo);
try {
let entities: Entity[];
if (this._dataProvider) {
const data = await this._dataProvider.loadChunkData(coord);
if (data) {
entities = this._serializer.deserialize(data, this._scene);
} else {
entities = this.createEmptyChunk(coord, bounds);
}
} else {
entities = this.createEmptyChunk(coord, bounds);
}
chunkInfo.entities = entities;
chunkInfo.state = EChunkState.Loaded;
chunkInfo.lastAccessTime = Date.now();
this._events.onChunkLoaded?.(coord, entities);
} catch (error) {
chunkInfo.state = EChunkState.Failed;
this._events.onChunkLoadFailed?.(coord, error as Error);
}
}
/**
* 卸载区块
*
* Unload a single chunk.
*/
private unloadChunk(coord: IChunkCoord): void {
const chunk = this._chunkGrid.get(coord);
if (!chunk) return;
chunk.state = EChunkState.Unloading;
for (const entity of chunk.entities) {
entity.destroy();
}
this._chunkGrid.delete(coord);
this._events.onChunkUnloaded?.(coord);
}
/**
* 创建空区块
*
* Create an empty chunk entity.
*/
private createEmptyChunk(coord: IChunkCoord, bounds: IChunkBounds): Entity[] {
if (!this._scene) return [];
const chunkEntity = this._scene.createEntity(`Chunk_${coord.x}_${coord.y}`);
const chunkComponent = chunkEntity.addComponent(new ChunkComponent());
chunkComponent.initialize(coord, bounds);
chunkComponent.setState(EChunkState.Loaded);
return [chunkEntity];
}
/**
* 获取区块边界
*
* Get world-space bounds for a chunk.
*/
getChunkBounds(coord: IChunkCoord): IChunkBounds {
return {
minX: coord.x * this._chunkSize,
minY: coord.y * this._chunkSize,
maxX: (coord.x + 1) * this._chunkSize,
maxY: (coord.y + 1) * this._chunkSize
};
}
/**
* 世界坐标转区块坐标
*
* Convert world position to chunk coordinates.
*/
worldToChunk(worldX: number, worldY: number): IChunkCoord {
return {
x: Math.floor(worldX / this._chunkSize),
y: Math.floor(worldY / this._chunkSize)
};
}
/**
* 获取区块信息
*
* Get chunk info by coordinates.
*/
getChunk(coord: IChunkCoord): IChunkInfo | undefined {
return this._chunkGrid.get(coord);
}
/**
* 检查区块是否已加载
*
* Check if a chunk is loaded.
*/
isChunkLoaded(coord: IChunkCoord): boolean {
const chunk = this._chunkGrid.get(coord);
return chunk !== undefined && chunk.state === EChunkState.Loaded;
}
/**
* 获取需要加载的区块坐标
*
* Get chunk coordinates that need to be loaded within radius.
*/
getMissingChunks(centerCoord: IChunkCoord, radius: number): IChunkCoord[] {
const missing: IChunkCoord[] = [];
for (let dx = -radius; dx <= radius; dx++) {
for (let dy = -radius; dy <= radius; dy++) {
const coord = { x: centerCoord.x + dx, y: centerCoord.y + dy };
if (!this.isChunkLoaded(coord)) {
missing.push(coord);
}
}
}
return missing;
}
/**
* 遍历所有已加载区块
* Iterate over all loaded chunks
*
* @param callback 回调函数 | Callback function
*/
forEachChunk(callback: (info: IChunkInfo, coord: IChunkCoord) => void): void {
this._chunkGrid.forEach(callback);
}
/**
* 获取超出范围的已加载区块
*
* Get loaded chunks outside the given radius.
*/
getChunksOutsideRadius(centerCoord: IChunkCoord, radius: number): IChunkCoord[] {
const outside: IChunkCoord[] = [];
this._chunkGrid.forEach((_info, coord) => {
const dx = Math.abs(coord.x - centerCoord.x);
const dy = Math.abs(coord.y - centerCoord.y);
if (dx > radius || dy > radius) {
outside.push(coord);
}
});
return outside;
}
/**
* 排序加载队列
*
* Sort load queue by priority and timestamp.
*/
private sortLoadQueue(): void {
this._loadQueue.sort((a, b) => {
if (a.priority !== b.priority) {
return a.priority - b.priority;
}
return a.timestamp - b.timestamp;
});
}
/**
* 清空所有区块
*
* Unload all chunks.
*/
clear(): void {
this._chunkGrid.forEach((_info, coord) => {
this.unloadChunk(coord);
});
this._loadQueue = [];
this._unloadQueue = [];
}
/**
* 释放资源
*
* Dispose resources (IService interface).
*/
dispose(): void {
this.clear();
this._scene = null;
this._dataProvider = null;
this._events = {};
}
}

View File

@@ -0,0 +1,167 @@
import type { Entity, IScene } from '@esengine/ecs-framework';
import type { IPositionable } from '@esengine/spatial';
import type { IChunkCoord, IChunkData, ISerializedEntity, IChunkBounds } from '../types';
/**
* 区块序列化器接口
*
* Interface for chunk serialization/deserialization.
*/
export interface IChunkSerializer {
serialize(coord: IChunkCoord, entities: Entity[], bounds: IChunkBounds): IChunkData;
deserialize(data: IChunkData, scene: IScene): Entity[];
}
/**
* 默认区块序列化器
*
* Default chunk serializer implementation.
* Override for custom serialization logic.
*/
export class ChunkSerializer implements IChunkSerializer {
private static readonly DATA_VERSION = 1;
/**
* 序列化区块
*
* Serialize entities within a chunk.
*/
serialize(coord: IChunkCoord, entities: Entity[], bounds: IChunkBounds): IChunkData {
const serializedEntities: ISerializedEntity[] = [];
for (const entity of entities) {
const positionable = this.getPositionable(entity);
if (!positionable) continue;
const serialized: ISerializedEntity = {
name: entity.name,
localPosition: {
x: positionable.position.x - bounds.minX,
y: positionable.position.y - bounds.minY
},
components: this.serializeComponents(entity)
};
serializedEntities.push(serialized);
}
return {
coord,
entities: serializedEntities,
version: ChunkSerializer.DATA_VERSION
};
}
/**
* 获取实体的可定位组件
*
* Get positionable component from entity.
* Override to use custom position component.
*/
protected getPositionable(entity: Entity): IPositionable | null {
for (const component of entity.components) {
if ('position' in component && typeof (component as IPositionable).position === 'object') {
return component as IPositionable;
}
}
return null;
}
/**
* 反序列化区块
*
* Deserialize chunk data and create entities.
* Override setEntityPosition to set position on your custom component.
*/
deserialize(data: IChunkData, scene: IScene): Entity[] {
const entities: Entity[] = [];
const bounds = this.calculateBounds(data.coord);
for (const entityData of data.entities) {
const entity = scene.createEntity(entityData.name);
const worldX = bounds.minX + entityData.localPosition.x;
const worldY = bounds.minY + entityData.localPosition.y;
this.setEntityPosition(entity, worldX, worldY);
this.deserializeComponents(entity, entityData.components);
entities.push(entity);
}
return entities;
}
/**
* 设置实体位置
*
* Set entity position after deserialization.
* Override to use your custom position component.
*/
protected setEntityPosition(_entity: Entity, _x: number, _y: number): void {
// Override in subclass to set position on your position component
}
/**
* 序列化实体组件
*
* Serialize entity components.
*/
protected serializeComponents(entity: Entity): Record<string, unknown> {
const componentsData: Record<string, unknown> = {};
for (const component of entity.components) {
const componentName = component.constructor.name;
if (this.shouldSerializeComponent(componentName)) {
componentsData[componentName] = this.serializeComponent(component);
}
}
return componentsData;
}
/**
* 反序列化组件数据
*
* Deserialize component data to entity.
*/
protected deserializeComponents(_entity: Entity, _components: Record<string, unknown>): void {
// Override in subclass to handle specific component types
}
/**
* 检查组件是否需要序列化
*
* Check if component should be serialized.
*/
protected shouldSerializeComponent(componentName: string): boolean {
const excludeList = ['ChunkComponent', 'StreamingAnchorComponent'];
return !excludeList.includes(componentName);
}
/**
* 序列化单个组件
*
* Serialize a single component.
*/
protected serializeComponent(component: unknown): unknown {
if (typeof component === 'object' && component !== null && 'toJSON' in component) {
return (component as { toJSON: () => unknown }).toJSON();
}
return {};
}
/**
* 计算区块边界
*
* Calculate chunk bounds from coordinates.
*/
private calculateBounds(coord: IChunkCoord, chunkSize: number = 512): IChunkBounds {
return {
minX: coord.x * chunkSize,
minY: coord.y * chunkSize,
maxX: (coord.x + 1) * chunkSize,
maxY: (coord.y + 1) * chunkSize
};
}
}

View File

@@ -0,0 +1,173 @@
import type { IChunkCoord } from '../types';
/**
* 空间哈希网格
*
* Spatial hash grid for fast chunk lookups.
*
* 用于快速查询指定位置或范围内的区块。
*/
export class SpatialHashGrid<T> {
private _cells: Map<string, T> = new Map();
private _cellSize: number;
constructor(cellSize: number) {
this._cellSize = cellSize;
}
get cellSize(): number {
return this._cellSize;
}
get size(): number {
return this._cells.size;
}
/**
* 生成网格键
*
* Generate hash key from coordinates.
*/
private getKey(x: number, y: number): string {
return `${x},${y}`;
}
/**
* 设置单元格数据
*
* Set data at grid coordinates.
*/
set(coord: IChunkCoord, value: T): void {
this._cells.set(this.getKey(coord.x, coord.y), value);
}
/**
* 获取单元格数据
*
* Get data at grid coordinates.
*/
get(coord: IChunkCoord): T | undefined {
return this._cells.get(this.getKey(coord.x, coord.y));
}
/**
* 检查单元格是否存在
*
* Check if data exists at coordinates.
*/
has(coord: IChunkCoord): boolean {
return this._cells.has(this.getKey(coord.x, coord.y));
}
/**
* 删除单元格
*
* Delete data at coordinates.
*/
delete(coord: IChunkCoord): boolean {
return this._cells.delete(this.getKey(coord.x, coord.y));
}
/**
* 清空网格
*
* Clear all cells.
*/
clear(): void {
this._cells.clear();
}
/**
* 世界坐标转网格坐标
*
* Convert world position to grid coordinates.
*/
worldToGrid(worldX: number, worldY: number): IChunkCoord {
return {
x: Math.floor(worldX / this._cellSize),
y: Math.floor(worldY / this._cellSize)
};
}
/**
* 查询范围内的所有单元格
*
* Query all cells within a range.
*/
queryRange(centerCoord: IChunkCoord, radius: number): T[] {
const results: T[] = [];
for (let dx = -radius; dx <= radius; dx++) {
for (let dy = -radius; dy <= radius; dy++) {
const value = this.get({ x: centerCoord.x + dx, y: centerCoord.y + dy });
if (value !== undefined) {
results.push(value);
}
}
}
return results;
}
/**
* 获取范围内需要加载的坐标
*
* Get coordinates within range that need loading.
*/
getMissingInRange(centerCoord: IChunkCoord, radius: number): IChunkCoord[] {
const missing: IChunkCoord[] = [];
for (let dx = -radius; dx <= radius; dx++) {
for (let dy = -radius; dy <= radius; dy++) {
const coord = { x: centerCoord.x + dx, y: centerCoord.y + dy };
if (!this.has(coord)) {
missing.push(coord);
}
}
}
return missing;
}
/**
* 获取范围外的所有单元格
*
* Get all cells outside a given range.
*/
getOutsideRange(centerCoord: IChunkCoord, radius: number): Array<{ coord: IChunkCoord; value: T }> {
const outside: Array<{ coord: IChunkCoord; value: T }> = [];
for (const [key, value] of this._cells) {
const [x, y] = key.split(',').map(Number);
const dx = Math.abs(x - centerCoord.x);
const dy = Math.abs(y - centerCoord.y);
if (dx > radius || dy > radius) {
outside.push({ coord: { x, y }, value });
}
}
return outside;
}
/**
* 遍历所有单元格
*
* Iterate over all cells.
*/
forEach(callback: (value: T, coord: IChunkCoord) => void): void {
for (const [key, value] of this._cells) {
const [x, y] = key.split(',').map(Number);
callback(value, { x, y });
}
}
/**
* 获取所有值
*
* Get all values.
*/
values(): IterableIterator<T> {
return this._cells.values();
}
}

View File

@@ -0,0 +1,5 @@
export { SpatialHashGrid } from './SpatialHashGrid';
export { ChunkSerializer } from './ChunkSerializer';
export type { IChunkSerializer } from './ChunkSerializer';
export { ChunkManager } from './ChunkManager';
export type { IChunkDataProvider, IChunkManagerEvents } from './ChunkManager';