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,83 @@
/**
* ChunkLoader Inspector Provider
*
* Custom inspector for ChunkLoaderComponent with streaming configuration.
*/
import React, { useState, useCallback } from 'react';
import { Settings, Play, Pause, RefreshCw } from 'lucide-react';
import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core';
import type { ChunkLoaderComponent } from '@esengine/world-streaming';
interface ChunkLoaderInspectorData {
entityId: string;
component: ChunkLoaderComponent;
}
export class ChunkLoaderInspectorProvider implements IInspectorProvider<ChunkLoaderInspectorData> {
readonly id = 'chunk-loader-inspector';
readonly name = 'Chunk Loader Inspector';
readonly priority = 100;
canHandle(target: unknown): target is ChunkLoaderInspectorData {
if (typeof target !== 'object' || target === null) return false;
const obj = target as Record<string, unknown>;
return 'entityId' in obj && 'component' in obj &&
obj.component !== null &&
typeof obj.component === 'object' &&
'chunkSize' in (obj.component as Record<string, unknown>) &&
'loadRadius' in (obj.component as Record<string, unknown>);
}
render(data: ChunkLoaderInspectorData, _context: InspectorContext): React.ReactElement {
const { component } = data;
return (
<div className="entity-inspector">
<div className="inspector-section">
<div className="section-title">
<Settings size={14} style={{ marginRight: '6px' }} />
Streaming Configuration
</div>
<div className="property-row">
<label>Chunk Size</label>
<span>{component.chunkSize} units</span>
</div>
<div className="property-row">
<label>Load Radius</label>
<span>{component.loadRadius} chunks</span>
</div>
<div className="property-row">
<label>Unload Radius</label>
<span>{component.unloadRadius} chunks</span>
</div>
<div className="property-row">
<label>Max Loads/Frame</label>
<span>{component.maxLoadsPerFrame}</span>
</div>
<div className="property-row">
<label>Unload Delay</label>
<span>{component.unloadDelay}ms</span>
</div>
<div className="property-row">
<label>Prefetch</label>
<span>{component.bEnablePrefetch ? 'Enabled' : 'Disabled'}</span>
</div>
{component.bEnablePrefetch && (
<div className="property-row">
<label>Prefetch Radius</label>
<span>{component.prefetchRadius} chunks</span>
</div>
)}
</div>
</div>
);
}
}

View File

@@ -0,0 +1,81 @@
/**
* StreamingAnchor Inspector Provider
*
* Custom inspector for StreamingAnchorComponent.
*/
import React from 'react';
import { Anchor, Navigation } from 'lucide-react';
import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core';
import type { StreamingAnchorComponent } from '@esengine/world-streaming';
interface StreamingAnchorInspectorData {
entityId: string;
component: StreamingAnchorComponent;
}
export class StreamingAnchorInspectorProvider implements IInspectorProvider<StreamingAnchorInspectorData> {
readonly id = 'streaming-anchor-inspector';
readonly name = 'Streaming Anchor Inspector';
readonly priority = 100;
canHandle(target: unknown): target is StreamingAnchorInspectorData {
if (typeof target !== 'object' || target === null) return false;
const obj = target as Record<string, unknown>;
return 'entityId' in obj && 'component' in obj &&
obj.component !== null &&
typeof obj.component === 'object' &&
'weight' in (obj.component as Record<string, unknown>) &&
'bEnablePrefetch' in (obj.component as Record<string, unknown>) &&
'velocityX' in (obj.component as Record<string, unknown>);
}
render(data: StreamingAnchorInspectorData, _context: InspectorContext): React.ReactElement {
const { component } = data;
const velocity = Math.sqrt(
component.velocityX * component.velocityX +
component.velocityY * component.velocityY
);
return (
<div className="entity-inspector">
<div className="inspector-section">
<div className="section-title">
<Anchor size={14} style={{ marginRight: '6px' }} />
Streaming Anchor
</div>
<div className="property-row">
<label>Weight</label>
<span>{component.weight.toFixed(2)}</span>
</div>
<div className="property-row">
<label>Prefetch</label>
<span>{component.bEnablePrefetch ? 'Enabled' : 'Disabled'}</span>
</div>
</div>
<div className="inspector-section">
<div className="section-title">
<Navigation size={14} style={{ marginRight: '6px' }} />
Movement (Runtime)
</div>
<div className="property-row">
<label>Velocity</label>
<span>{velocity.toFixed(1)} u/s</span>
</div>
<div className="property-row">
<label>Direction</label>
<span>
({component.velocityX.toFixed(1)}, {component.velocityY.toFixed(1)})
</span>
</div>
</div>
</div>
);
}
}