feat(world-streaming): 添加世界流式加载系统 (#288)

实现基于区块的世界流式加载系统,支持开放世界游戏:

运行时包 (@esengine/world-streaming):
- ChunkComponent: 区块实体组件,包含坐标、边界、状态
- StreamingAnchorComponent: 流式锚点组件(玩家/摄像机)
- ChunkLoaderComponent: 流式加载配置组件
- ChunkStreamingSystem: 区块加载/卸载调度系统
- ChunkCullingSystem: 区块可见性剔除系统
- ChunkManager: 区块生命周期管理服务
- SpatialHashGrid: 空间哈希网格
- ChunkSerializer: 区块序列化

编辑器包 (@esengine/world-streaming-editor):
- ChunkVisualizer: 区块可视化覆盖层
- ChunkLoaderInspectorProvider: 区块加载器检视器
- StreamingAnchorInspectorProvider: 流式锚点检视器
- WorldStreamingPlugin: 完整插件导出
This commit is contained in:
YHH
2025-12-06 13:56:01 +08:00
committed by GitHub
parent 3cbfa1e4cb
commit 0c03b13d74
31 changed files with 2802 additions and 0 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
);
}
}