chore: update pathfinding, add rpc/world-streaming docs, refactor world-streaming location (#376)
This commit is contained in:
@@ -185,7 +185,7 @@ export async function createServer(config: ServerConfig = {}): Promise<GameServe
|
||||
|
||||
// 内置 RoomMessage 处理
|
||||
msgHandlersObj['RoomMessage'] = async (data: any, conn) => {
|
||||
const { type, payload } = data as { type: string; payload: unknown }
|
||||
const { type, data: payload } = data as { type: string; data: unknown }
|
||||
roomManager.handleMessage(conn.id, type, payload)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*"
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/spatial": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.0.1",
|
||||
@@ -26,7 +27,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*"
|
||||
"@esengine/ecs-framework-math": "workspace:*",
|
||||
"@esengine/spatial": "workspace:*"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
100
packages/framework/world-streaming/src/WorldStreamingModule.ts
Normal file
100
packages/framework/world-streaming/src/WorldStreamingModule.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
|
||||
import { ChunkComponent } from './components/ChunkComponent';
|
||||
import { StreamingAnchorComponent } from './components/StreamingAnchorComponent';
|
||||
import { ChunkLoaderComponent } from './components/ChunkLoaderComponent';
|
||||
import { ChunkStreamingSystem } from './systems/ChunkStreamingSystem';
|
||||
import { ChunkCullingSystem } from './systems/ChunkCullingSystem';
|
||||
import { ChunkManager } from './services/ChunkManager';
|
||||
|
||||
/**
|
||||
* 世界流式加载配置
|
||||
*
|
||||
* Configuration for world streaming setup.
|
||||
*/
|
||||
export interface IWorldStreamingSetupOptions {
|
||||
/**
|
||||
* 区块大小(世界单位)
|
||||
*
|
||||
* Chunk size in world units.
|
||||
*/
|
||||
chunkSize?: number;
|
||||
|
||||
/**
|
||||
* 是否添加 Culling 系统
|
||||
*
|
||||
* Whether to add the culling system.
|
||||
*/
|
||||
bEnableCulling?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 世界流式加载模块
|
||||
*
|
||||
* Helper class for setting up world streaming functionality.
|
||||
*
|
||||
* 提供世界流式加载功能的帮助类。
|
||||
*/
|
||||
export class WorldStreamingModule {
|
||||
private _chunkManager: ChunkManager | null = null;
|
||||
|
||||
get chunkManager(): ChunkManager | null {
|
||||
return this._chunkManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册组件到注册表
|
||||
*
|
||||
* Register streaming components to registry.
|
||||
*/
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(ChunkComponent);
|
||||
registry.register(StreamingAnchorComponent);
|
||||
registry.register(ChunkLoaderComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册服务到容器
|
||||
*
|
||||
* Register streaming services to container.
|
||||
*/
|
||||
registerServices(services: ServiceContainer, chunkSize?: number): void {
|
||||
this._chunkManager = new ChunkManager(chunkSize);
|
||||
services.registerInstance(ChunkManager, this._chunkManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并添加系统到场景
|
||||
*
|
||||
* Create and add streaming systems to scene.
|
||||
*/
|
||||
createSystems(scene: IScene, options?: IWorldStreamingSetupOptions): void {
|
||||
const streamingSystem = new ChunkStreamingSystem();
|
||||
if (this._chunkManager) {
|
||||
streamingSystem.setChunkManager(this._chunkManager);
|
||||
}
|
||||
scene.addSystem(streamingSystem);
|
||||
|
||||
if (options?.bEnableCulling !== false) {
|
||||
scene.addSystem(new ChunkCullingSystem());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键设置流式加载
|
||||
*
|
||||
* Setup world streaming in one call.
|
||||
*/
|
||||
setup(
|
||||
scene: IScene,
|
||||
services: ServiceContainer,
|
||||
registry: IComponentRegistry,
|
||||
options?: IWorldStreamingSetupOptions
|
||||
): ChunkManager {
|
||||
this.registerComponents(registry);
|
||||
this.registerServices(services, options?.chunkSize);
|
||||
this.createSystems(scene, options);
|
||||
return this._chunkManager!;
|
||||
}
|
||||
}
|
||||
|
||||
export const worldStreamingModule = new WorldStreamingModule();
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
import type { IPositionable } from '@esengine/spatial';
|
||||
import type { IVector2 } from '@esengine/ecs-framework-math';
|
||||
|
||||
/**
|
||||
* 流式锚点组件
|
||||
@@ -8,10 +10,39 @@ import { Component, ECSComponent, Serializable, Serialize, Property } from '@ese
|
||||
*
|
||||
* 标记实体作为流式加载锚点。通常挂载在玩家或摄像机实体上,
|
||||
* 系统会根据锚点位置加载/卸载周围区块。
|
||||
*
|
||||
* 用户需要在每帧更新此组件的 x/y 位置。
|
||||
* User must update the x/y position each frame.
|
||||
*/
|
||||
@ECSComponent('StreamingAnchor')
|
||||
@Serializable({ version: 1, typeId: 'StreamingAnchor' })
|
||||
export class StreamingAnchorComponent extends Component {
|
||||
export class StreamingAnchorComponent extends Component implements IPositionable {
|
||||
/**
|
||||
* 当前 X 位置
|
||||
*
|
||||
* Current X position in world units.
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'X' })
|
||||
x: number = 0;
|
||||
|
||||
/**
|
||||
* 当前 Y 位置
|
||||
*
|
||||
* Current Y position in world units.
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Y' })
|
||||
y: number = 0;
|
||||
|
||||
/**
|
||||
* 获取位置 (IPositionable 接口)
|
||||
*
|
||||
* Get position (IPositionable interface).
|
||||
*/
|
||||
get position(): IVector2 {
|
||||
return { x: this.x, y: this.y };
|
||||
}
|
||||
/**
|
||||
* 锚点权重
|
||||
*
|
||||
@@ -51,3 +51,4 @@ export type {
|
||||
|
||||
// Module
|
||||
export { WorldStreamingModule, worldStreamingModule } from './WorldStreamingModule';
|
||||
export type { IWorldStreamingSetupOptions } from './WorldStreamingModule';
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Entity, IScene, IService } from '@esengine/ecs-framework';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import type { IChunkCoord, IChunkData, IChunkInfo, IChunkLoadRequest, IChunkBounds } from '../types';
|
||||
import { EChunkState, EChunkPriority } from '../types';
|
||||
import { SpatialHashGrid } from './SpatialHashGrid';
|
||||
@@ -286,11 +285,6 @@ export class ChunkManager implements IService {
|
||||
chunkComponent.initialize(coord, bounds);
|
||||
chunkComponent.setState(EChunkState.Loaded);
|
||||
|
||||
const transform = chunkEntity.getComponent(TransformComponent);
|
||||
if (transform) {
|
||||
transform.setPosition(bounds.minX, bounds.minY);
|
||||
}
|
||||
|
||||
return [chunkEntity];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Entity, IScene } from '@esengine/ecs-framework';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import type { IPositionable } from '@esengine/spatial';
|
||||
import type { IChunkCoord, IChunkData, ISerializedEntity, IChunkBounds } from '../types';
|
||||
|
||||
/**
|
||||
@@ -30,14 +30,14 @@ export class ChunkSerializer implements IChunkSerializer {
|
||||
const serializedEntities: ISerializedEntity[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
const positionable = this.getPositionable(entity);
|
||||
if (!positionable) continue;
|
||||
|
||||
const serialized: ISerializedEntity = {
|
||||
name: entity.name,
|
||||
localPosition: {
|
||||
x: transform.position.x - bounds.minX,
|
||||
y: transform.position.y - bounds.minY
|
||||
x: positionable.position.x - bounds.minX,
|
||||
y: positionable.position.y - bounds.minY
|
||||
},
|
||||
components: this.serializeComponents(entity)
|
||||
};
|
||||
@@ -52,10 +52,26 @@ export class ChunkSerializer implements IChunkSerializer {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取实体的可定位组件
|
||||
*
|
||||
* 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[] = [];
|
||||
@@ -64,13 +80,9 @@ export class ChunkSerializer implements IChunkSerializer {
|
||||
for (const entityData of data.entities) {
|
||||
const entity = scene.createEntity(entityData.name);
|
||||
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (transform) {
|
||||
transform.setPosition(
|
||||
bounds.minX + entityData.localPosition.x,
|
||||
bounds.minY + entityData.localPosition.y
|
||||
);
|
||||
}
|
||||
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);
|
||||
@@ -79,6 +91,16 @@ export class ChunkSerializer implements IChunkSerializer {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化实体组件
|
||||
*
|
||||
@@ -113,7 +135,7 @@ export class ChunkSerializer implements IChunkSerializer {
|
||||
* Check if component should be serialized.
|
||||
*/
|
||||
protected shouldSerializeComponent(componentName: string): boolean {
|
||||
const excludeList = ['TransformComponent', 'ChunkComponent', 'StreamingAnchorComponent'];
|
||||
const excludeList = ['ChunkComponent', 'StreamingAnchorComponent'];
|
||||
return !excludeList.includes(componentName);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EntitySystem, Matcher, Time, ECSSystem } from '@esengine/ecs-framework';
|
||||
import type { Entity, Scene } from '@esengine/ecs-framework';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { StreamingAnchorComponent } from '../components/StreamingAnchorComponent';
|
||||
import { ChunkLoaderComponent } from '../components/ChunkLoaderComponent';
|
||||
import { ChunkManager } from '../services/ChunkManager';
|
||||
@@ -21,7 +20,7 @@ export class ChunkStreamingSystem extends EntitySystem {
|
||||
private _lastAnchorChunks: Map<Entity, IChunkCoord> = new Map();
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(StreamingAnchorComponent, TransformComponent));
|
||||
super(Matcher.all(StreamingAnchorComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,12 +82,10 @@ export class ChunkStreamingSystem extends EntitySystem {
|
||||
private updateAnchors(entities: readonly Entity[], deltaTime: number): void {
|
||||
for (const entity of entities) {
|
||||
const anchor = entity.getComponent(StreamingAnchorComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!anchor) continue;
|
||||
|
||||
if (!anchor || !transform) continue;
|
||||
|
||||
const currentX = transform.position.x;
|
||||
const currentY = transform.position.y;
|
||||
const currentX = anchor.x;
|
||||
const currentY = anchor.y;
|
||||
|
||||
if (deltaTime > 0) {
|
||||
anchor.velocityX = (currentX - anchor.previousX) / deltaTime;
|
||||
@@ -111,10 +108,10 @@ export class ChunkStreamingSystem extends EntitySystem {
|
||||
const centerCoords: IChunkCoord[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
if (!transform) continue;
|
||||
const anchor = entity.getComponent(StreamingAnchorComponent);
|
||||
if (!anchor) continue;
|
||||
|
||||
const coord = loader.worldToChunk(transform.position.x, transform.position.y);
|
||||
const coord = loader.worldToChunk(anchor.x, anchor.y);
|
||||
centerCoords.push(coord);
|
||||
|
||||
const lastCoord = this._lastAnchorChunks.get(entity);
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule, SystemContext } from '@esengine/engine-core';
|
||||
import { ChunkComponent } from './components/ChunkComponent';
|
||||
import { StreamingAnchorComponent } from './components/StreamingAnchorComponent';
|
||||
import { ChunkLoaderComponent } from './components/ChunkLoaderComponent';
|
||||
import { ChunkStreamingSystem } from './systems/ChunkStreamingSystem';
|
||||
import { ChunkCullingSystem } from './systems/ChunkCullingSystem';
|
||||
import { ChunkManager } from './services/ChunkManager';
|
||||
|
||||
/**
|
||||
* 世界流式加载模块
|
||||
*
|
||||
* Runtime module for world streaming functionality.
|
||||
*
|
||||
* 提供世界流式加载功能的运行时模块。
|
||||
*/
|
||||
export class WorldStreamingModule implements IRuntimeModule {
|
||||
private _chunkManager: ChunkManager | null = null;
|
||||
|
||||
get chunkManager(): ChunkManager | null {
|
||||
return this._chunkManager;
|
||||
}
|
||||
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(ChunkComponent);
|
||||
registry.register(StreamingAnchorComponent);
|
||||
registry.register(ChunkLoaderComponent);
|
||||
}
|
||||
|
||||
registerServices(services: ServiceContainer): void {
|
||||
this._chunkManager = new ChunkManager();
|
||||
services.registerInstance(ChunkManager, this._chunkManager);
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, _context: SystemContext): void {
|
||||
const streamingSystem = new ChunkStreamingSystem();
|
||||
if (this._chunkManager) {
|
||||
streamingSystem.setChunkManager(this._chunkManager);
|
||||
}
|
||||
scene.addSystem(streamingSystem);
|
||||
scene.addSystem(new ChunkCullingSystem());
|
||||
}
|
||||
|
||||
onSystemsCreated(_scene: IScene, _context: SystemContext): void {
|
||||
// No post-creation setup needed
|
||||
}
|
||||
}
|
||||
|
||||
export const worldStreamingModule = new WorldStreamingModule();
|
||||
Reference in New Issue
Block a user