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,103 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
import type { IChunkCoord, IChunkBounds } from '../types';
import { EChunkState } from '../types';
/**
* 区块组件
*
* Attached to chunk root entities. Tracks chunk state and bounds.
*
* 区块组件挂载在区块根实体上,用于管理区块状态和边界信息。
*/
@ECSComponent('Chunk')
@Serializable({ version: 1, typeId: 'Chunk' })
export class ChunkComponent extends Component {
@Serialize()
@Property({ type: 'integer', label: 'Coord X', readOnly: true })
private _coordX: number = 0;
@Serialize()
@Property({ type: 'integer', label: 'Coord Y', readOnly: true })
private _coordY: number = 0;
@Serialize()
@Property({ type: 'string', label: 'State', readOnly: true })
private _state: EChunkState = EChunkState.Unloaded;
@Serialize()
private _minX: number = 0;
@Serialize()
private _minY: number = 0;
@Serialize()
private _maxX: number = 0;
@Serialize()
private _maxY: number = 0;
private _lastAccessTime: number = 0;
get coord(): IChunkCoord {
return { x: this._coordX, y: this._coordY };
}
get bounds(): IChunkBounds {
return {
minX: this._minX,
minY: this._minY,
maxX: this._maxX,
maxY: this._maxY
};
}
get state(): EChunkState {
return this._state;
}
get lastAccessTime(): number {
return this._lastAccessTime;
}
/**
* 初始化区块
*
* Initialize chunk with coordinates and bounds.
*/
initialize(coord: IChunkCoord, bounds: IChunkBounds): void {
this._coordX = coord.x;
this._coordY = coord.y;
this._minX = bounds.minX;
this._minY = bounds.minY;
this._maxX = bounds.maxX;
this._maxY = bounds.maxY;
this._lastAccessTime = Date.now();
}
/**
* 设置区块状态
*
* Set chunk state.
*/
setState(state: EChunkState): void {
this._state = state;
}
/**
* 更新访问时间
*
* Update last access time for LRU tracking.
*/
touch(): void {
this._lastAccessTime = Date.now();
}
/**
* 检查点是否在区块内
*
* Check if a point is within chunk bounds.
*/
containsPoint(x: number, y: number): boolean {
return x >= this._minX && x < this._maxX && y >= this._minY && y < this._maxY;
}
}

View File

@@ -0,0 +1,133 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
import type { IChunkCoord, IStreamingConfig } from '../types';
import { DEFAULT_STREAMING_CONFIG } from '../types';
/**
* 区块加载器组件
*
* Singleton component that manages streaming configuration.
* Attach to a manager entity in the scene.
*
* 单例组件,管理流式加载配置。挂载在场景管理实体上。
*/
@ECSComponent('ChunkLoader')
@Serializable({ version: 1, typeId: 'ChunkLoader' })
export class ChunkLoaderComponent extends Component {
@Serialize()
@Property({ type: 'integer', label: 'Chunk Size', min: 64, max: 4096 })
private _chunkSize: number = DEFAULT_STREAMING_CONFIG.chunkSize;
@Serialize()
@Property({ type: 'integer', label: 'Load Radius', min: 1, max: 10 })
private _loadRadius: number = DEFAULT_STREAMING_CONFIG.loadRadius;
@Serialize()
@Property({ type: 'integer', label: 'Unload Radius', min: 2, max: 20 })
private _unloadRadius: number = DEFAULT_STREAMING_CONFIG.unloadRadius;
@Serialize()
@Property({ type: 'integer', label: 'Max Loads Per Frame', min: 1, max: 10 })
private _maxLoadsPerFrame: number = DEFAULT_STREAMING_CONFIG.maxLoadsPerFrame;
@Serialize()
@Property({ type: 'integer', label: 'Max Unloads Per Frame', min: 1, max: 10 })
private _maxUnloadsPerFrame: number = DEFAULT_STREAMING_CONFIG.maxUnloadsPerFrame;
@Serialize()
@Property({ type: 'integer', label: 'Unload Delay (ms)', min: 0, max: 30000 })
private _unloadDelay: number = DEFAULT_STREAMING_CONFIG.unloadDelay;
@Serialize()
@Property({ type: 'boolean', label: 'Enable Prefetch' })
private _bEnablePrefetch: boolean = DEFAULT_STREAMING_CONFIG.bEnablePrefetch;
@Serialize()
@Property({ type: 'integer', label: 'Prefetch Radius', min: 0, max: 5 })
private _prefetchRadius: number = DEFAULT_STREAMING_CONFIG.prefetchRadius;
get chunkSize(): number {
return this._chunkSize;
}
get loadRadius(): number {
return this._loadRadius;
}
get unloadRadius(): number {
return this._unloadRadius;
}
get maxLoadsPerFrame(): number {
return this._maxLoadsPerFrame;
}
get maxUnloadsPerFrame(): number {
return this._maxUnloadsPerFrame;
}
get unloadDelay(): number {
return this._unloadDelay;
}
get bEnablePrefetch(): boolean {
return this._bEnablePrefetch;
}
get prefetchRadius(): number {
return this._prefetchRadius;
}
/**
* 应用配置
*
* Apply streaming configuration.
*/
applyConfig(config: Partial<IStreamingConfig>): void {
if (config.chunkSize !== undefined) this._chunkSize = config.chunkSize;
if (config.loadRadius !== undefined) this._loadRadius = config.loadRadius;
if (config.unloadRadius !== undefined) this._unloadRadius = config.unloadRadius;
if (config.maxLoadsPerFrame !== undefined) this._maxLoadsPerFrame = config.maxLoadsPerFrame;
if (config.maxUnloadsPerFrame !== undefined) this._maxUnloadsPerFrame = config.maxUnloadsPerFrame;
if (config.unloadDelay !== undefined) this._unloadDelay = config.unloadDelay;
if (config.bEnablePrefetch !== undefined) this._bEnablePrefetch = config.bEnablePrefetch;
if (config.prefetchRadius !== undefined) this._prefetchRadius = config.prefetchRadius;
}
/**
* 世界坐标转区块坐标
*
* 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)
};
}
/**
* 区块坐标转世界坐标(返回区块中心)
*
* Convert chunk coordinates to world position (chunk center).
*/
chunkToWorld(coord: IChunkCoord): { x: number; y: number } {
return {
x: coord.x * this._chunkSize + this._chunkSize * 0.5,
y: coord.y * this._chunkSize + this._chunkSize * 0.5
};
}
/**
* 获取区块边界
*
* Get chunk world-space bounds.
*/
getChunkBounds(coord: IChunkCoord): { minX: number; minY: number; maxX: number; maxY: number } {
return {
minX: coord.x * this._chunkSize,
minY: coord.y * this._chunkSize,
maxX: (coord.x + 1) * this._chunkSize,
maxY: (coord.y + 1) * this._chunkSize
};
}
}

View File

@@ -0,0 +1,61 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
/**
* 流式锚点组件
*
* Marks an entity as a streaming anchor point.
* Chunks are loaded/unloaded based on distance to anchors.
*
* 标记实体作为流式加载锚点。通常挂载在玩家或摄像机实体上,
* 系统会根据锚点位置加载/卸载周围区块。
*/
@ECSComponent('StreamingAnchor')
@Serializable({ version: 1, typeId: 'StreamingAnchor' })
export class StreamingAnchorComponent extends Component {
/**
* 锚点权重
*
* Weight multiplier for this anchor's load radius.
* Higher values mean larger load radius around this anchor.
*/
@Serialize()
@Property({ type: 'number', label: 'Weight', min: 0.1, max: 10 })
weight: number = 1.0;
/**
* 是否启用预加载
*
* Enable directional prefetching based on movement.
*/
@Serialize()
@Property({ type: 'boolean', label: 'Enable Prefetch' })
bEnablePrefetch: boolean = true;
/**
* 上一帧位置 X
*
* Previous frame X position for velocity calculation.
*/
previousX: number = 0;
/**
* 上一帧位置 Y
*
* Previous frame Y position for velocity calculation.
*/
previousY: number = 0;
/**
* 速度 X 分量
*
* X component of velocity (units per second).
*/
velocityX: number = 0;
/**
* 速度 Y 分量
*
* Y component of velocity (units per second).
*/
velocityY: number = 0;
}

View File

@@ -0,0 +1,3 @@
export { ChunkComponent } from './ChunkComponent';
export { StreamingAnchorComponent } from './StreamingAnchorComponent';
export { ChunkLoaderComponent } from './ChunkLoaderComponent';