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,108 @@
import { EntitySystem, Matcher, ECSSystem } from '@esengine/ecs-framework';
import type { Entity } from '@esengine/ecs-framework';
import { ChunkComponent } from '../components/ChunkComponent';
import { EChunkState } from '../types';
/**
* 区块裁剪系统
*
* Handles visibility culling for chunk entities.
*
* 处理区块实体的可见性裁剪。
*/
@ECSSystem('ChunkCulling', { updateOrder: -40 })
export class ChunkCullingSystem extends EntitySystem {
private _viewMinX: number = 0;
private _viewMinY: number = 0;
private _viewMaxX: number = 1920;
private _viewMaxY: number = 1080;
private _padding: number = 100;
constructor() {
super(Matcher.all(ChunkComponent));
}
/**
* 设置视口边界
*
* Set viewport bounds for culling.
*/
setViewBounds(minX: number, minY: number, maxX: number, maxY: number): void {
this._viewMinX = minX;
this._viewMinY = minY;
this._viewMaxX = maxX;
this._viewMaxY = maxY;
}
/**
* 设置裁剪边距
*
* Set padding for culling bounds.
*/
setPadding(padding: number): void {
this._padding = padding;
}
protected process(entities: readonly Entity[]): void {
const cullMinX = this._viewMinX - this._padding;
const cullMinY = this._viewMinY - this._padding;
const cullMaxX = this._viewMaxX + this._padding;
const cullMaxY = this._viewMaxY + this._padding;
for (const entity of entities) {
const chunk = entity.getComponent(ChunkComponent);
if (!chunk) continue;
if (chunk.state !== EChunkState.Loaded) continue;
const bounds = chunk.bounds;
const isVisible = this.boundsIntersect(
bounds.minX,
bounds.minY,
bounds.maxX,
bounds.maxY,
cullMinX,
cullMinY,
cullMaxX,
cullMaxY
);
entity.enabled = isVisible;
}
}
/**
* 检查边界是否相交
*
* Check if two axis-aligned bounds intersect.
*/
private boundsIntersect(
aMinX: number,
aMinY: number,
aMaxX: number,
aMaxY: number,
bMinX: number,
bMinY: number,
bMaxX: number,
bMaxY: number
): boolean {
return aMinX < bMaxX && aMaxX > bMinX && aMinY < bMaxY && aMaxY > bMinY;
}
/**
* 更新视口(从相机)
*
* Update viewport from camera position and size.
*/
updateFromCamera(cameraX: number, cameraY: number, viewWidth: number, viewHeight: number): void {
const halfWidth = viewWidth * 0.5;
const halfHeight = viewHeight * 0.5;
this.setViewBounds(
cameraX - halfWidth,
cameraY - halfHeight,
cameraX + halfWidth,
cameraY + halfHeight
);
}
}

View File

@@ -0,0 +1,268 @@
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework';
import type { Entity, Scene } from '@esengine/ecs-framework';
import { StreamingAnchorComponent } from '../components/StreamingAnchorComponent';
import { ChunkLoaderComponent } from '../components/ChunkLoaderComponent';
import { ChunkManager } from '../services/ChunkManager';
import { EChunkPriority } from '../types';
import type { IChunkCoord } from '../types';
/**
* 区块流式加载系统
*
* Manages chunk loading/unloading based on streaming anchors.
*
* 根据流式锚点位置管理区块的加载和卸载。
*/
@ECSSystem('ChunkStreaming', { updateOrder: -50 })
export class ChunkStreamingSystem extends EntitySystem {
private _chunkManager: ChunkManager | null = null;
private _loaderEntity: Entity | null = null;
private _lastAnchorChunks: Map<Entity, IChunkCoord> = new Map();
constructor() {
super(Matcher.all(StreamingAnchorComponent));
}
/**
* 设置区块管理器
*
* Set the chunk manager instance.
*/
setChunkManager(manager: ChunkManager): void {
this._chunkManager = manager;
}
/**
* 获取区块管理器
*
* Get the chunk manager instance.
*/
get chunkManager(): ChunkManager | null {
return this._chunkManager;
}
initialize(): void {
super.initialize();
if (!this._chunkManager) {
this._chunkManager = new ChunkManager();
}
const scene = this.scene;
if (scene) {
this._chunkManager.setScene(scene);
this.findLoaderEntity(scene);
}
}
protected process(entities: readonly Entity[]): void {
if (!this._chunkManager) {
return;
}
const loader = this.getLoaderComponent();
if (!loader) {
return;
}
const deltaTime = Time.deltaTime;
this.updateAnchors(entities, deltaTime);
this.updateChunkRequests(entities, loader);
this._chunkManager.processLoads(loader.maxLoadsPerFrame);
this._chunkManager.processUnloads(loader.maxUnloadsPerFrame);
}
/**
* 更新锚点速度
*
* Update anchor velocities.
*/
private updateAnchors(entities: readonly Entity[], deltaTime: number): void {
for (const entity of entities) {
const anchor = entity.getComponent(StreamingAnchorComponent);
if (!anchor) continue;
const currentX = anchor.x;
const currentY = anchor.y;
if (deltaTime > 0) {
anchor.velocityX = (currentX - anchor.previousX) / deltaTime;
anchor.velocityY = (currentY - anchor.previousY) / deltaTime;
}
anchor.previousX = currentX;
anchor.previousY = currentY;
}
}
/**
* 更新区块加载/卸载请求
*
* Update chunk load/unload requests based on anchor positions.
*/
private updateChunkRequests(entities: readonly Entity[], loader: ChunkLoaderComponent): void {
if (!this._chunkManager) return;
const centerCoords: IChunkCoord[] = [];
for (const entity of entities) {
const anchor = entity.getComponent(StreamingAnchorComponent);
if (!anchor) continue;
const coord = loader.worldToChunk(anchor.x, anchor.y);
centerCoords.push(coord);
const lastCoord = this._lastAnchorChunks.get(entity);
const hasMovedChunk = !lastCoord || lastCoord.x !== coord.x || lastCoord.y !== coord.y;
if (hasMovedChunk) {
this._lastAnchorChunks.set(entity, coord);
}
this.requestChunksForAnchor(entity, coord, loader);
}
this.requestUnloadsOutsideRange(centerCoords, loader);
}
/**
* 请求锚点周围的区块
*
* Request chunks around an anchor point.
*/
private requestChunksForAnchor(
entity: Entity,
centerCoord: IChunkCoord,
loader: ChunkLoaderComponent
): void {
if (!this._chunkManager) return;
const anchor = entity.getComponent(StreamingAnchorComponent);
if (!anchor) return;
const effectiveRadius = Math.ceil(loader.loadRadius * anchor.weight);
for (let dx = -effectiveRadius; dx <= effectiveRadius; dx++) {
for (let dy = -effectiveRadius; dy <= effectiveRadius; dy++) {
const coord = { x: centerCoord.x + dx, y: centerCoord.y + dy };
const distSq = dx * dx + dy * dy;
let priority: EChunkPriority;
if (distSq === 0) {
priority = EChunkPriority.Immediate;
} else if (distSq <= 1) {
priority = EChunkPriority.High;
} else if (distSq <= 4) {
priority = EChunkPriority.Normal;
} else {
priority = EChunkPriority.Low;
}
this._chunkManager.requestLoad(coord, priority);
this._chunkManager.cancelUnload(coord);
}
}
if (loader.bEnablePrefetch && anchor.bEnablePrefetch) {
this.requestPrefetchChunks(anchor, centerCoord, loader);
}
}
/**
* 请求预加载区块
*
* Request prefetch chunks based on movement direction.
*/
private requestPrefetchChunks(
anchor: StreamingAnchorComponent,
centerCoord: IChunkCoord,
loader: ChunkLoaderComponent
): void {
if (!this._chunkManager) return;
const velocityMagnitude = Math.sqrt(
anchor.velocityX * anchor.velocityX + anchor.velocityY * anchor.velocityY
);
if (velocityMagnitude < 10) return;
const dirX = anchor.velocityX / velocityMagnitude;
const dirY = anchor.velocityY / velocityMagnitude;
const chunkDirX = Math.round(dirX);
const chunkDirY = Math.round(dirY);
if (chunkDirX === 0 && chunkDirY === 0) return;
for (let i = 1; i <= loader.prefetchRadius; i++) {
const coord = {
x: centerCoord.x + chunkDirX * (loader.loadRadius + i),
y: centerCoord.y + chunkDirY * (loader.loadRadius + i)
};
this._chunkManager.requestLoad(coord, EChunkPriority.Prefetch);
}
}
/**
* 请求卸载超出范围的区块
*
* Request unload for chunks outside all anchors' ranges.
*/
private requestUnloadsOutsideRange(
centerCoords: IChunkCoord[],
loader: ChunkLoaderComponent
): void {
if (!this._chunkManager || centerCoords.length === 0) return;
// 使用公共接口遍历区块 | Use public interface to iterate chunks
this._chunkManager.forEachChunk((_info, coord) => {
let isInRange = false;
for (const center of centerCoords) {
const dx = Math.abs(coord.x - center.x);
const dy = Math.abs(coord.y - center.y);
if (dx <= loader.unloadRadius && dy <= loader.unloadRadius) {
isInRange = true;
break;
}
}
if (!isInRange) {
this._chunkManager!.requestUnload(coord, loader.unloadDelay);
}
});
}
/**
* 查找 ChunkLoader 实体
*
* Find the chunk loader entity.
*/
private findLoaderEntity(scene: Scene): void {
const result = scene.queryAll(ChunkLoaderComponent);
if (result.entities.length > 0) {
this._loaderEntity = result.entities[0] as Entity;
}
}
/**
* 获取 ChunkLoaderComponent
*
* Get the chunk loader component.
*/
private getLoaderComponent(): ChunkLoaderComponent | null {
if (this._loaderEntity && !this._loaderEntity.isDestroyed) {
return this._loaderEntity.getComponent(ChunkLoaderComponent);
}
const scene = this.scene;
if (!scene) return null;
this.findLoaderEntity(scene);
return this._loaderEntity?.getComponent(ChunkLoaderComponent) ?? null;
}
}

View File

@@ -0,0 +1,2 @@
export { ChunkStreamingSystem } from './ChunkStreamingSystem';
export { ChunkCullingSystem } from './ChunkCullingSystem';