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

@@ -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)
}

View File

@@ -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",

View 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();

View File

@@ -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 };
}
/**
*
*

View File

@@ -51,3 +51,4 @@ export type {
// Module
export { WorldStreamingModule, worldStreamingModule } from './WorldStreamingModule';
export type { IWorldStreamingSetupOptions } from './WorldStreamingModule';

View File

@@ -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];
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();