feat(ecs): ECS 网络状态同步系统 | add ECS network state synchronization (#390)
## @esengine/ecs-framework 新增 @sync 装饰器和二进制编解码器,支持基于 Component 的网络状态同步: - `sync` 装饰器标记需要同步的字段 - `ChangeTracker` 组件变更追踪 - 二进制编解码器 (BinaryWriter/BinaryReader) - `encodeSnapshot`/`decodeSnapshot` 批量编解码 - `encodeSpawn`/`decodeSpawn` 实体生成编解码 - `encodeDespawn`/`processDespawn` 实体销毁编解码 将以下方法标记为 @internal,用户应通过 Core.update() 驱动更新: - Scene.update() - SceneManager.update() - WorldManager.updateAll() ## @esengine/network - 新增 ComponentSyncSystem 基于 @sync 自动同步组件状态 - 将 ecs-framework 从 devDependencies 移到 peerDependencies ## @esengine/server 新增 ECSRoom,带有 ECS World 支持的房间基类: - 每个 ECSRoom 在 Core.worldManager 中创建独立的 World - Core.update() 统一更新 Time 和所有 World - onTick() 只处理状态同步逻辑 - 自动创建/销毁玩家实体 - 增量状态广播
This commit is contained in:
@@ -508,7 +508,9 @@ export class Scene implements IScene {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新场景
|
||||
* @zh 更新场景
|
||||
* @en Update scene
|
||||
* @internal 由 SceneManager 或 World 调用,用户不应直接调用
|
||||
*/
|
||||
public update() {
|
||||
this.epochManager.increment();
|
||||
|
||||
@@ -240,18 +240,9 @@ export class SceneManager implements IService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新场景
|
||||
*
|
||||
* 应该在每帧的游戏循环中调用。
|
||||
* 会自动处理延迟场景切换。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* function gameLoop(deltaTime: number) {
|
||||
* Core.update(deltaTime);
|
||||
* sceneManager.update(); // 每帧调用
|
||||
* }
|
||||
* ```
|
||||
* @zh 更新场景
|
||||
* @en Update scene
|
||||
* @internal 由 Core.update() 调用,用户不应直接调用
|
||||
*/
|
||||
public update(): void {
|
||||
// 处理延迟场景切换
|
||||
|
||||
125
packages/framework/core/src/ECS/Sync/ChangeTracker.ts
Normal file
125
packages/framework/core/src/ECS/Sync/ChangeTracker.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @zh 组件变更追踪器
|
||||
* @en Component change tracker
|
||||
*
|
||||
* @zh 用于追踪 @sync 标记字段的变更,支持增量同步
|
||||
* @en Tracks changes to @sync marked fields for delta synchronization
|
||||
*/
|
||||
export class ChangeTracker {
|
||||
/**
|
||||
* @zh 脏字段索引集合
|
||||
* @en Set of dirty field indices
|
||||
*/
|
||||
private _dirtyFields: Set<number> = new Set();
|
||||
|
||||
/**
|
||||
* @zh 是否有任何变更
|
||||
* @en Whether there are any changes
|
||||
*/
|
||||
private _hasChanges: boolean = false;
|
||||
|
||||
/**
|
||||
* @zh 上次同步的时间戳
|
||||
* @en Last sync timestamp
|
||||
*/
|
||||
private _lastSyncTime: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 标记字段为脏
|
||||
* @en Mark field as dirty
|
||||
*
|
||||
* @param fieldIndex - @zh 字段索引 @en Field index
|
||||
*/
|
||||
public setDirty(fieldIndex: number): void {
|
||||
this._dirtyFields.add(fieldIndex);
|
||||
this._hasChanges = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否有变更
|
||||
* @en Check if there are any changes
|
||||
*/
|
||||
public hasChanges(): boolean {
|
||||
return this._hasChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查特定字段是否脏
|
||||
* @en Check if a specific field is dirty
|
||||
*
|
||||
* @param fieldIndex - @zh 字段索引 @en Field index
|
||||
*/
|
||||
public isDirty(fieldIndex: number): boolean {
|
||||
return this._dirtyFields.has(fieldIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有脏字段索引
|
||||
* @en Get all dirty field indices
|
||||
*/
|
||||
public getDirtyFields(): number[] {
|
||||
return Array.from(this._dirtyFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取脏字段数量
|
||||
* @en Get number of dirty fields
|
||||
*/
|
||||
public getDirtyCount(): number {
|
||||
return this._dirtyFields.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有变更标记
|
||||
* @en Clear all change marks
|
||||
*/
|
||||
public clear(): void {
|
||||
this._dirtyFields.clear();
|
||||
this._hasChanges = false;
|
||||
this._lastSyncTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除特定字段的变更标记
|
||||
* @en Clear change mark for a specific field
|
||||
*
|
||||
* @param fieldIndex - @zh 字段索引 @en Field index
|
||||
*/
|
||||
public clearField(fieldIndex: number): void {
|
||||
this._dirtyFields.delete(fieldIndex);
|
||||
if (this._dirtyFields.size === 0) {
|
||||
this._hasChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取上次同步时间
|
||||
* @en Get last sync time
|
||||
*/
|
||||
public get lastSyncTime(): number {
|
||||
return this._lastSyncTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 标记所有字段为脏(用于首次同步)
|
||||
* @en Mark all fields as dirty (for initial sync)
|
||||
*
|
||||
* @param fieldCount - @zh 字段数量 @en Field count
|
||||
*/
|
||||
public markAllDirty(fieldCount: number): void {
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
this._dirtyFields.add(i);
|
||||
}
|
||||
this._hasChanges = fieldCount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置追踪器
|
||||
* @en Reset tracker
|
||||
*/
|
||||
public reset(): void {
|
||||
this._dirtyFields.clear();
|
||||
this._hasChanges = false;
|
||||
this._lastSyncTime = 0;
|
||||
}
|
||||
}
|
||||
217
packages/framework/core/src/ECS/Sync/decorators.ts
Normal file
217
packages/framework/core/src/ECS/Sync/decorators.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @zh 网络同步装饰器
|
||||
* @en Network synchronization decorators
|
||||
*
|
||||
* @zh 提供 @sync 装饰器,用于标记需要网络同步的 Component 字段
|
||||
* @en Provides @sync decorator to mark Component fields for network synchronization
|
||||
*/
|
||||
|
||||
import type { SyncType, SyncFieldMetadata, SyncMetadata } from './types';
|
||||
import { SYNC_METADATA, CHANGE_TRACKER } from './types';
|
||||
import { ChangeTracker } from './ChangeTracker';
|
||||
|
||||
/**
|
||||
* @zh 获取或创建组件的同步元数据
|
||||
* @en Get or create sync metadata for a component class
|
||||
*
|
||||
* @param target - @zh 组件类的原型 @en Component class prototype
|
||||
* @returns @zh 同步元数据 @en Sync metadata
|
||||
*/
|
||||
function getOrCreateSyncMetadata(target: any): SyncMetadata {
|
||||
const constructor = target.constructor;
|
||||
|
||||
// Check if has own metadata (not inherited)
|
||||
const hasOwnMetadata = Object.prototype.hasOwnProperty.call(constructor, SYNC_METADATA);
|
||||
|
||||
if (hasOwnMetadata) {
|
||||
return constructor[SYNC_METADATA];
|
||||
}
|
||||
|
||||
// Check for inherited metadata
|
||||
const inheritedMetadata: SyncMetadata | undefined = constructor[SYNC_METADATA];
|
||||
|
||||
// Create new metadata (copy from inherited if exists)
|
||||
const metadata: SyncMetadata = {
|
||||
typeId: constructor.name,
|
||||
fields: inheritedMetadata ? [...inheritedMetadata.fields] : [],
|
||||
fieldIndexMap: inheritedMetadata ? new Map(inheritedMetadata.fieldIndexMap) : new Map()
|
||||
};
|
||||
|
||||
constructor[SYNC_METADATA] = metadata;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 同步字段装饰器
|
||||
* @en Sync field decorator
|
||||
*
|
||||
* @zh 标记 Component 字段为可网络同步。被标记的字段会自动追踪变更,
|
||||
* 并在值修改时触发变更追踪器。
|
||||
* @en Marks a Component field for network synchronization. Marked fields
|
||||
* automatically track changes and trigger the change tracker on modification.
|
||||
*
|
||||
* @param type - @zh 字段的同步类型 @en Sync type of the field
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
* import { sync } from '@esengine/ecs-framework';
|
||||
*
|
||||
* @ECSComponent('Player')
|
||||
* class PlayerComponent extends Component {
|
||||
* @sync("string") name: string = "";
|
||||
* @sync("uint16") score: number = 0;
|
||||
* @sync("float32") x: number = 0;
|
||||
* @sync("float32") y: number = 0;
|
||||
*
|
||||
* // 不带 @sync 的字段不会同步
|
||||
* // Fields without @sync will not be synchronized
|
||||
* localData: any;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function sync(type: SyncType) {
|
||||
return function (target: any, propertyKey: string) {
|
||||
const metadata = getOrCreateSyncMetadata(target);
|
||||
|
||||
// Assign field index (auto-increment based on field count)
|
||||
const fieldIndex = metadata.fields.length;
|
||||
|
||||
// Create field metadata
|
||||
const fieldMeta: SyncFieldMetadata = {
|
||||
index: fieldIndex,
|
||||
name: propertyKey,
|
||||
type: type
|
||||
};
|
||||
|
||||
// Register field
|
||||
metadata.fields.push(fieldMeta);
|
||||
metadata.fieldIndexMap.set(propertyKey, fieldIndex);
|
||||
|
||||
// Store original property key for getter/setter
|
||||
const privateKey = `_sync_${propertyKey}`;
|
||||
|
||||
// Define getter/setter to intercept value changes
|
||||
Object.defineProperty(target, propertyKey, {
|
||||
get() {
|
||||
return this[privateKey];
|
||||
},
|
||||
set(value: any) {
|
||||
const oldValue = this[privateKey];
|
||||
if (oldValue !== value) {
|
||||
this[privateKey] = value;
|
||||
// Trigger change tracker if exists
|
||||
const tracker = this[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||
if (tracker) {
|
||||
tracker.setDirty(fieldIndex);
|
||||
}
|
||||
}
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取组件类的同步元数据
|
||||
* @en Get sync metadata for a component class
|
||||
*
|
||||
* @param componentClass - @zh 组件类或组件实例 @en Component class or instance
|
||||
* @returns @zh 同步元数据,如果不存在则返回 null @en Sync metadata, or null if not exists
|
||||
*/
|
||||
export function getSyncMetadata(componentClass: any): SyncMetadata | null {
|
||||
if (!componentClass) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const constructor = typeof componentClass === 'function'
|
||||
? componentClass
|
||||
: componentClass.constructor;
|
||||
|
||||
return constructor[SYNC_METADATA] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查组件是否有同步字段
|
||||
* @en Check if a component has sync fields
|
||||
*
|
||||
* @param component - @zh 组件类或组件实例 @en Component class or instance
|
||||
* @returns @zh 如果有同步字段返回 true @en Returns true if has sync fields
|
||||
*/
|
||||
export function hasSyncFields(component: any): boolean {
|
||||
const metadata = getSyncMetadata(component);
|
||||
return metadata !== null && metadata.fields.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取组件实例的变更追踪器
|
||||
* @en Get change tracker of a component instance
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
* @returns @zh 变更追踪器,如果不存在则返回 null @en Change tracker, or null if not exists
|
||||
*/
|
||||
export function getChangeTracker(component: any): ChangeTracker | null {
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
return component[CHANGE_TRACKER] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 为组件实例初始化变更追踪器
|
||||
* @en Initialize change tracker for a component instance
|
||||
*
|
||||
* @zh 这个函数应该在组件首次添加到实体时调用。
|
||||
* 它会创建变更追踪器并标记所有字段为脏(用于首次同步)。
|
||||
* @en This function should be called when a component is first added to an entity.
|
||||
* It creates the change tracker and marks all fields as dirty (for initial sync).
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
* @returns @zh 变更追踪器 @en Change tracker
|
||||
*/
|
||||
export function initChangeTracker(component: any): ChangeTracker {
|
||||
const metadata = getSyncMetadata(component);
|
||||
if (!metadata) {
|
||||
throw new Error('Component does not have sync metadata. Use @sync decorator on fields.');
|
||||
}
|
||||
|
||||
let tracker = component[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||
if (!tracker) {
|
||||
tracker = new ChangeTracker();
|
||||
component[CHANGE_TRACKER] = tracker;
|
||||
}
|
||||
|
||||
// Mark all fields as dirty for initial sync
|
||||
tracker.markAllDirty(metadata.fields.length);
|
||||
|
||||
return tracker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除组件实例的变更标记
|
||||
* @en Clear change marks for a component instance
|
||||
*
|
||||
* @zh 通常在同步完成后调用,清除所有脏标记
|
||||
* @en Usually called after sync is complete, clears all dirty marks
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
*/
|
||||
export function clearChanges(component: any): void {
|
||||
const tracker = getChangeTracker(component);
|
||||
if (tracker) {
|
||||
tracker.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查组件是否有变更
|
||||
* @en Check if a component has changes
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
* @returns @zh 如果有变更返回 true @en Returns true if has changes
|
||||
*/
|
||||
export function hasChanges(component: any): boolean {
|
||||
const tracker = getChangeTracker(component);
|
||||
return tracker ? tracker.hasChanges() : false;
|
||||
}
|
||||
285
packages/framework/core/src/ECS/Sync/encoding/BinaryReader.ts
Normal file
285
packages/framework/core/src/ECS/Sync/encoding/BinaryReader.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* @zh 二进制读取器
|
||||
* @en Binary Reader
|
||||
*
|
||||
* @zh 提供高效的二进制数据读取功能
|
||||
* @en Provides efficient binary data reading
|
||||
*/
|
||||
|
||||
import { decodeVarint } from './varint';
|
||||
|
||||
/**
|
||||
* @zh 文本解码器(使用浏览器原生 API)
|
||||
* @en Text decoder (using browser native API)
|
||||
*/
|
||||
const textDecoder = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
||||
|
||||
/**
|
||||
* @zh 二进制读取器
|
||||
* @en Binary reader for decoding data
|
||||
*/
|
||||
export class BinaryReader {
|
||||
/**
|
||||
* @zh 数据缓冲区
|
||||
* @en Data buffer
|
||||
*/
|
||||
private _buffer: Uint8Array;
|
||||
|
||||
/**
|
||||
* @zh DataView 用于读取数值
|
||||
* @en DataView for reading numbers
|
||||
*/
|
||||
private _view: DataView;
|
||||
|
||||
/**
|
||||
* @zh 当前读取位置
|
||||
* @en Current read position
|
||||
*/
|
||||
private _offset: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 创建二进制读取器
|
||||
* @en Create binary reader
|
||||
*
|
||||
* @param buffer - @zh 要读取的数据 @en Data to read
|
||||
*/
|
||||
constructor(buffer: Uint8Array) {
|
||||
this._buffer = buffer;
|
||||
this._view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前读取位置
|
||||
* @en Get current read position
|
||||
*/
|
||||
public get offset(): number {
|
||||
return this._offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置读取位置
|
||||
* @en Set read position
|
||||
*/
|
||||
public set offset(value: number) {
|
||||
this._offset = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取剩余可读字节数
|
||||
* @en Get remaining readable bytes
|
||||
*/
|
||||
public get remaining(): number {
|
||||
return this._buffer.length - this._offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否有更多数据可读
|
||||
* @en Check if there's more data to read
|
||||
*/
|
||||
public hasMore(): boolean {
|
||||
return this._offset < this._buffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取单个字节
|
||||
* @en Read single byte
|
||||
*/
|
||||
public readUint8(): number {
|
||||
this.checkBounds(1);
|
||||
return this._buffer[this._offset++]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取有符号字节
|
||||
* @en Read signed byte
|
||||
*/
|
||||
public readInt8(): number {
|
||||
this.checkBounds(1);
|
||||
return this._view.getInt8(this._offset++);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取布尔值
|
||||
* @en Read boolean
|
||||
*/
|
||||
public readBoolean(): boolean {
|
||||
return this.readUint8() !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取 16 位无符号整数(小端序)
|
||||
* @en Read 16-bit unsigned integer (little-endian)
|
||||
*/
|
||||
public readUint16(): number {
|
||||
this.checkBounds(2);
|
||||
const value = this._view.getUint16(this._offset, true);
|
||||
this._offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取 16 位有符号整数(小端序)
|
||||
* @en Read 16-bit signed integer (little-endian)
|
||||
*/
|
||||
public readInt16(): number {
|
||||
this.checkBounds(2);
|
||||
const value = this._view.getInt16(this._offset, true);
|
||||
this._offset += 2;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取 32 位无符号整数(小端序)
|
||||
* @en Read 32-bit unsigned integer (little-endian)
|
||||
*/
|
||||
public readUint32(): number {
|
||||
this.checkBounds(4);
|
||||
const value = this._view.getUint32(this._offset, true);
|
||||
this._offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取 32 位有符号整数(小端序)
|
||||
* @en Read 32-bit signed integer (little-endian)
|
||||
*/
|
||||
public readInt32(): number {
|
||||
this.checkBounds(4);
|
||||
const value = this._view.getInt32(this._offset, true);
|
||||
this._offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取 32 位浮点数(小端序)
|
||||
* @en Read 32-bit float (little-endian)
|
||||
*/
|
||||
public readFloat32(): number {
|
||||
this.checkBounds(4);
|
||||
const value = this._view.getFloat32(this._offset, true);
|
||||
this._offset += 4;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取 64 位浮点数(小端序)
|
||||
* @en Read 64-bit float (little-endian)
|
||||
*/
|
||||
public readFloat64(): number {
|
||||
this.checkBounds(8);
|
||||
const value = this._view.getFloat64(this._offset, true);
|
||||
this._offset += 8;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取变长整数
|
||||
* @en Read variable-length integer
|
||||
*/
|
||||
public readVarint(): number {
|
||||
const [value, newOffset] = decodeVarint(this._buffer, this._offset);
|
||||
this._offset = newOffset;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取字符串(UTF-8 编码,带长度前缀)
|
||||
* @en Read string (UTF-8 encoded with length prefix)
|
||||
*/
|
||||
public readString(): string {
|
||||
const length = this.readVarint();
|
||||
this.checkBounds(length);
|
||||
|
||||
const bytes = this._buffer.subarray(this._offset, this._offset + length);
|
||||
this._offset += length;
|
||||
|
||||
if (textDecoder) {
|
||||
return textDecoder.decode(bytes);
|
||||
} else {
|
||||
return this.utf8BytesToString(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 读取原始字节
|
||||
* @en Read raw bytes
|
||||
*
|
||||
* @param length - @zh 要读取的字节数 @en Number of bytes to read
|
||||
*/
|
||||
public readBytes(length: number): Uint8Array {
|
||||
this.checkBounds(length);
|
||||
const bytes = this._buffer.slice(this._offset, this._offset + length);
|
||||
this._offset += length;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 查看下一个字节但不移动读取位置
|
||||
* @en Peek next byte without advancing read position
|
||||
*/
|
||||
public peekUint8(): number {
|
||||
this.checkBounds(1);
|
||||
return this._buffer[this._offset]!;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 跳过指定字节数
|
||||
* @en Skip specified number of bytes
|
||||
*/
|
||||
public skip(count: number): void {
|
||||
this.checkBounds(count);
|
||||
this._offset += count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查边界
|
||||
* @en Check bounds
|
||||
*/
|
||||
private checkBounds(size: number): void {
|
||||
if (this._offset + size > this._buffer.length) {
|
||||
throw new Error(`BinaryReader: buffer overflow (offset=${this._offset}, size=${size}, bufferLength=${this._buffer.length})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh UTF-8 字节转字符串(后备方案)
|
||||
* @en UTF-8 bytes to string (fallback)
|
||||
*/
|
||||
private utf8BytesToString(bytes: Uint8Array): string {
|
||||
let result = '';
|
||||
let i = 0;
|
||||
|
||||
while (i < bytes.length) {
|
||||
let charCode: number;
|
||||
const byte1 = bytes[i++]!;
|
||||
|
||||
if (byte1 < 0x80) {
|
||||
charCode = byte1;
|
||||
} else if (byte1 < 0xE0) {
|
||||
const byte2 = bytes[i++]!;
|
||||
charCode = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
|
||||
} else if (byte1 < 0xF0) {
|
||||
const byte2 = bytes[i++]!;
|
||||
const byte3 = bytes[i++]!;
|
||||
charCode = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
|
||||
} else {
|
||||
const byte2 = bytes[i++]!;
|
||||
const byte3 = bytes[i++]!;
|
||||
const byte4 = bytes[i++]!;
|
||||
charCode = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) |
|
||||
((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
||||
|
||||
// Convert to surrogate pair
|
||||
if (charCode > 0xFFFF) {
|
||||
charCode -= 0x10000;
|
||||
result += String.fromCharCode(0xD800 + (charCode >> 10));
|
||||
charCode = 0xDC00 + (charCode & 0x3FF);
|
||||
}
|
||||
}
|
||||
|
||||
result += String.fromCharCode(charCode);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
257
packages/framework/core/src/ECS/Sync/encoding/BinaryWriter.ts
Normal file
257
packages/framework/core/src/ECS/Sync/encoding/BinaryWriter.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* @zh 二进制写入器
|
||||
* @en Binary Writer
|
||||
*
|
||||
* @zh 提供高效的二进制数据写入功能,支持自动扩容
|
||||
* @en Provides efficient binary data writing with auto-expansion
|
||||
*/
|
||||
|
||||
import { encodeVarint, varintSize } from './varint';
|
||||
|
||||
/**
|
||||
* @zh 文本编码器(使用浏览器原生 API)
|
||||
* @en Text encoder (using browser native API)
|
||||
*/
|
||||
const textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
||||
|
||||
/**
|
||||
* @zh 二进制写入器
|
||||
* @en Binary writer for encoding data
|
||||
*/
|
||||
export class BinaryWriter {
|
||||
/**
|
||||
* @zh 内部缓冲区
|
||||
* @en Internal buffer
|
||||
*/
|
||||
private _buffer: Uint8Array;
|
||||
|
||||
/**
|
||||
* @zh DataView 用于写入数值
|
||||
* @en DataView for writing numbers
|
||||
*/
|
||||
private _view: DataView;
|
||||
|
||||
/**
|
||||
* @zh 当前写入位置
|
||||
* @en Current write position
|
||||
*/
|
||||
private _offset: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 创建二进制写入器
|
||||
* @en Create binary writer
|
||||
*
|
||||
* @param initialCapacity - @zh 初始容量 @en Initial capacity
|
||||
*/
|
||||
constructor(initialCapacity: number = 256) {
|
||||
this._buffer = new Uint8Array(initialCapacity);
|
||||
this._view = new DataView(this._buffer.buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前写入位置
|
||||
* @en Get current write position
|
||||
*/
|
||||
public get offset(): number {
|
||||
return this._offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取写入的数据
|
||||
* @en Get written data
|
||||
*
|
||||
* @returns @zh 包含写入数据的 Uint8Array @en Uint8Array containing written data
|
||||
*/
|
||||
public toUint8Array(): Uint8Array {
|
||||
return this._buffer.slice(0, this._offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置写入器(清空数据但保留缓冲区)
|
||||
* @en Reset writer (clear data but keep buffer)
|
||||
*/
|
||||
public reset(): void {
|
||||
this._offset = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 确保有足够空间
|
||||
* @en Ensure enough space
|
||||
*
|
||||
* @param size - @zh 需要的额外字节数 @en Extra bytes needed
|
||||
*/
|
||||
private ensureCapacity(size: number): void {
|
||||
const required = this._offset + size;
|
||||
if (required > this._buffer.length) {
|
||||
// Double the buffer size or use required size, whichever is larger
|
||||
const newSize = Math.max(this._buffer.length * 2, required);
|
||||
const newBuffer = new Uint8Array(newSize);
|
||||
newBuffer.set(this._buffer);
|
||||
this._buffer = newBuffer;
|
||||
this._view = new DataView(this._buffer.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入单个字节
|
||||
* @en Write single byte
|
||||
*/
|
||||
public writeUint8(value: number): void {
|
||||
this.ensureCapacity(1);
|
||||
this._buffer[this._offset++] = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入有符号字节
|
||||
* @en Write signed byte
|
||||
*/
|
||||
public writeInt8(value: number): void {
|
||||
this.ensureCapacity(1);
|
||||
this._view.setInt8(this._offset++, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入布尔值
|
||||
* @en Write boolean
|
||||
*/
|
||||
public writeBoolean(value: boolean): void {
|
||||
this.writeUint8(value ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入 16 位无符号整数(小端序)
|
||||
* @en Write 16-bit unsigned integer (little-endian)
|
||||
*/
|
||||
public writeUint16(value: number): void {
|
||||
this.ensureCapacity(2);
|
||||
this._view.setUint16(this._offset, value, true);
|
||||
this._offset += 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入 16 位有符号整数(小端序)
|
||||
* @en Write 16-bit signed integer (little-endian)
|
||||
*/
|
||||
public writeInt16(value: number): void {
|
||||
this.ensureCapacity(2);
|
||||
this._view.setInt16(this._offset, value, true);
|
||||
this._offset += 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入 32 位无符号整数(小端序)
|
||||
* @en Write 32-bit unsigned integer (little-endian)
|
||||
*/
|
||||
public writeUint32(value: number): void {
|
||||
this.ensureCapacity(4);
|
||||
this._view.setUint32(this._offset, value, true);
|
||||
this._offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入 32 位有符号整数(小端序)
|
||||
* @en Write 32-bit signed integer (little-endian)
|
||||
*/
|
||||
public writeInt32(value: number): void {
|
||||
this.ensureCapacity(4);
|
||||
this._view.setInt32(this._offset, value, true);
|
||||
this._offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入 32 位浮点数(小端序)
|
||||
* @en Write 32-bit float (little-endian)
|
||||
*/
|
||||
public writeFloat32(value: number): void {
|
||||
this.ensureCapacity(4);
|
||||
this._view.setFloat32(this._offset, value, true);
|
||||
this._offset += 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入 64 位浮点数(小端序)
|
||||
* @en Write 64-bit float (little-endian)
|
||||
*/
|
||||
public writeFloat64(value: number): void {
|
||||
this.ensureCapacity(8);
|
||||
this._view.setFloat64(this._offset, value, true);
|
||||
this._offset += 8;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入变长整数
|
||||
* @en Write variable-length integer
|
||||
*/
|
||||
public writeVarint(value: number): void {
|
||||
this.ensureCapacity(varintSize(value));
|
||||
this._offset = encodeVarint(value, this._buffer, this._offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入字符串(UTF-8 编码,带长度前缀)
|
||||
* @en Write string (UTF-8 encoded with length prefix)
|
||||
*/
|
||||
public writeString(value: string): void {
|
||||
if (textEncoder) {
|
||||
const encoded = textEncoder.encode(value);
|
||||
this.writeVarint(encoded.length);
|
||||
this.ensureCapacity(encoded.length);
|
||||
this._buffer.set(encoded, this._offset);
|
||||
this._offset += encoded.length;
|
||||
} else {
|
||||
// Fallback for environments without TextEncoder
|
||||
const bytes = this.stringToUtf8Bytes(value);
|
||||
this.writeVarint(bytes.length);
|
||||
this.ensureCapacity(bytes.length);
|
||||
this._buffer.set(bytes, this._offset);
|
||||
this._offset += bytes.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 写入原始字节
|
||||
* @en Write raw bytes
|
||||
*/
|
||||
public writeBytes(data: Uint8Array): void {
|
||||
this.ensureCapacity(data.length);
|
||||
this._buffer.set(data, this._offset);
|
||||
this._offset += data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 字符串转 UTF-8 字节(后备方案)
|
||||
* @en String to UTF-8 bytes (fallback)
|
||||
*/
|
||||
private stringToUtf8Bytes(str: string): Uint8Array {
|
||||
const bytes: number[] = [];
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
let charCode = str.charCodeAt(i);
|
||||
|
||||
// Handle surrogate pairs
|
||||
if (charCode >= 0xD800 && charCode <= 0xDBFF && i + 1 < str.length) {
|
||||
const next = str.charCodeAt(i + 1);
|
||||
if (next >= 0xDC00 && next <= 0xDFFF) {
|
||||
charCode = 0x10000 + ((charCode - 0xD800) << 10) + (next - 0xDC00);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (charCode < 0x80) {
|
||||
bytes.push(charCode);
|
||||
} else if (charCode < 0x800) {
|
||||
bytes.push(0xC0 | (charCode >> 6));
|
||||
bytes.push(0x80 | (charCode & 0x3F));
|
||||
} else if (charCode < 0x10000) {
|
||||
bytes.push(0xE0 | (charCode >> 12));
|
||||
bytes.push(0x80 | ((charCode >> 6) & 0x3F));
|
||||
bytes.push(0x80 | (charCode & 0x3F));
|
||||
} else {
|
||||
bytes.push(0xF0 | (charCode >> 18));
|
||||
bytes.push(0x80 | ((charCode >> 12) & 0x3F));
|
||||
bytes.push(0x80 | ((charCode >> 6) & 0x3F));
|
||||
bytes.push(0x80 | (charCode & 0x3F));
|
||||
}
|
||||
}
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
}
|
||||
404
packages/framework/core/src/ECS/Sync/encoding/Decoder.ts
Normal file
404
packages/framework/core/src/ECS/Sync/encoding/Decoder.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
/**
|
||||
* @zh 组件状态解码器
|
||||
* @en Component state decoder
|
||||
*
|
||||
* @zh 从二进制格式解码并应用到 ECS Component
|
||||
* @en Decodes binary format and applies to ECS Components
|
||||
*/
|
||||
|
||||
import type { Entity } from '../../Entity';
|
||||
import type { Component } from '../../Component';
|
||||
import type { Scene } from '../../Scene';
|
||||
import type { SyncType, SyncMetadata } from '../types';
|
||||
import { SyncOperation, SYNC_METADATA } from '../types';
|
||||
import { BinaryReader } from './BinaryReader';
|
||||
|
||||
/**
|
||||
* @zh 组件类型注册表
|
||||
* @en Component type registry
|
||||
*/
|
||||
const componentRegistry = new Map<string, new () => Component>();
|
||||
|
||||
/**
|
||||
* @zh 注册组件类型
|
||||
* @en Register component type
|
||||
*
|
||||
* @param typeId - @zh 组件类型 ID @en Component type ID
|
||||
* @param componentClass - @zh 组件类 @en Component class
|
||||
*/
|
||||
export function registerSyncComponent<T extends Component>(
|
||||
typeId: string,
|
||||
componentClass: new () => T
|
||||
): void {
|
||||
componentRegistry.set(typeId, componentClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从 @ECSComponent 装饰器自动注册
|
||||
* @en Auto-register from @ECSComponent decorator
|
||||
*
|
||||
* @param componentClass - @zh 组件类 @en Component class
|
||||
*/
|
||||
export function autoRegisterSyncComponent(componentClass: new () => Component): void {
|
||||
const metadata: SyncMetadata | undefined = (componentClass as any)[SYNC_METADATA];
|
||||
if (metadata) {
|
||||
componentRegistry.set(metadata.typeId, componentClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码字段值
|
||||
* @en Decode field value
|
||||
*/
|
||||
function decodeFieldValue(reader: BinaryReader, type: SyncType): any {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return reader.readBoolean();
|
||||
case 'int8':
|
||||
return reader.readInt8();
|
||||
case 'uint8':
|
||||
return reader.readUint8();
|
||||
case 'int16':
|
||||
return reader.readInt16();
|
||||
case 'uint16':
|
||||
return reader.readUint16();
|
||||
case 'int32':
|
||||
return reader.readInt32();
|
||||
case 'uint32':
|
||||
return reader.readUint32();
|
||||
case 'float32':
|
||||
return reader.readFloat32();
|
||||
case 'float64':
|
||||
return reader.readFloat64();
|
||||
case 'string':
|
||||
return reader.readString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码并应用组件数据
|
||||
* @en Decode and apply component data
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
* @param metadata - @zh 组件同步元数据 @en Component sync metadata
|
||||
* @param reader - @zh 二进制读取器 @en Binary reader
|
||||
*/
|
||||
export function decodeComponent(
|
||||
component: Component,
|
||||
metadata: SyncMetadata,
|
||||
reader: BinaryReader
|
||||
): void {
|
||||
const fieldCount = reader.readVarint();
|
||||
|
||||
for (let i = 0; i < fieldCount; i++) {
|
||||
const fieldIndex = reader.readUint8();
|
||||
const field = metadata.fields[fieldIndex];
|
||||
|
||||
if (field) {
|
||||
const value = decodeFieldValue(reader, field.type);
|
||||
// Directly set the private backing field to avoid triggering change tracking
|
||||
(component as any)[`_sync_${field.name}`] = value;
|
||||
} else {
|
||||
// Unknown field, skip based on type info in metadata
|
||||
console.warn(`Unknown sync field index: ${fieldIndex}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码实体快照结果
|
||||
* @en Decode entity snapshot result
|
||||
*/
|
||||
export interface DecodeEntityResult {
|
||||
/**
|
||||
* @zh 实体 ID
|
||||
* @en Entity ID
|
||||
*/
|
||||
entityId: number;
|
||||
|
||||
/**
|
||||
* @zh 是否为新实体
|
||||
* @en Whether it's a new entity
|
||||
*/
|
||||
isNew: boolean;
|
||||
|
||||
/**
|
||||
* @zh 解码的组件类型列表
|
||||
* @en List of decoded component types
|
||||
*/
|
||||
componentTypes: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码并应用实体数据
|
||||
* @en Decode and apply entity data
|
||||
*
|
||||
* @param scene - @zh 场景 @en Scene
|
||||
* @param reader - @zh 二进制读取器 @en Binary reader
|
||||
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||
* @returns @zh 解码结果 @en Decode result
|
||||
*/
|
||||
export function decodeEntity(
|
||||
scene: Scene,
|
||||
reader: BinaryReader,
|
||||
entityMap?: Map<number, Entity>
|
||||
): DecodeEntityResult {
|
||||
const entityId = reader.readUint32();
|
||||
const componentCount = reader.readVarint();
|
||||
const componentTypes: string[] = [];
|
||||
|
||||
// Find or create entity
|
||||
let entity: Entity | null | undefined = entityMap?.get(entityId);
|
||||
let isNew = false;
|
||||
|
||||
if (!entity) {
|
||||
entity = scene.findEntityById(entityId);
|
||||
}
|
||||
|
||||
if (!entity) {
|
||||
// Entity doesn't exist, create it
|
||||
entity = scene.createEntity(`entity_${entityId}`);
|
||||
isNew = true;
|
||||
entityMap?.set(entityId, entity);
|
||||
}
|
||||
|
||||
for (let i = 0; i < componentCount; i++) {
|
||||
const typeId = reader.readString();
|
||||
componentTypes.push(typeId);
|
||||
|
||||
// Find component class from registry
|
||||
const componentClass = componentRegistry.get(typeId);
|
||||
if (!componentClass) {
|
||||
console.warn(`Unknown component type: ${typeId}`);
|
||||
// Skip component data - we need to read it to advance the reader
|
||||
const fieldCount = reader.readVarint();
|
||||
for (let j = 0; j < fieldCount; j++) {
|
||||
reader.readUint8(); // fieldIndex
|
||||
// We can't skip properly without knowing the type, so this is a problem
|
||||
// For now, log error and break
|
||||
console.error(`Cannot skip unknown component type: ${typeId}`);
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata: SyncMetadata | undefined = (componentClass as any)[SYNC_METADATA];
|
||||
if (!metadata) {
|
||||
console.warn(`Component ${typeId} has no sync metadata`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find or add component
|
||||
let component = entity.getComponent(componentClass);
|
||||
if (!component) {
|
||||
component = entity.addComponent(new componentClass());
|
||||
}
|
||||
|
||||
// Decode component data
|
||||
decodeComponent(component, metadata, reader);
|
||||
}
|
||||
|
||||
return { entityId, isNew, componentTypes };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码快照结果
|
||||
* @en Decode snapshot result
|
||||
*/
|
||||
export interface DecodeSnapshotResult {
|
||||
/**
|
||||
* @zh 操作类型
|
||||
* @en Operation type
|
||||
*/
|
||||
operation: SyncOperation;
|
||||
|
||||
/**
|
||||
* @zh 解码的实体列表
|
||||
* @en List of decoded entities
|
||||
*/
|
||||
entities: DecodeEntityResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码状态快照
|
||||
* @en Decode state snapshot
|
||||
*
|
||||
* @param scene - @zh 场景 @en Scene
|
||||
* @param data - @zh 二进制数据 @en Binary data
|
||||
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||
* @returns @zh 解码结果 @en Decode result
|
||||
*/
|
||||
export function decodeSnapshot(
|
||||
scene: Scene,
|
||||
data: Uint8Array,
|
||||
entityMap?: Map<number, Entity>
|
||||
): DecodeSnapshotResult {
|
||||
const reader = new BinaryReader(data);
|
||||
const operation = reader.readUint8() as SyncOperation;
|
||||
const entityCount = reader.readVarint();
|
||||
const entities: DecodeEntityResult[] = [];
|
||||
|
||||
const map = entityMap || new Map<number, Entity>();
|
||||
|
||||
for (let i = 0; i < entityCount; i++) {
|
||||
const result = decodeEntity(scene, reader, map);
|
||||
entities.push(result);
|
||||
}
|
||||
|
||||
return { operation, entities };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码生成消息结果
|
||||
* @en Decode spawn message result
|
||||
*/
|
||||
export interface DecodeSpawnResult {
|
||||
/**
|
||||
* @zh 实体
|
||||
* @en Entity
|
||||
*/
|
||||
entity: Entity;
|
||||
|
||||
/**
|
||||
* @zh 预制体类型
|
||||
* @en Prefab type
|
||||
*/
|
||||
prefabType: string;
|
||||
|
||||
/**
|
||||
* @zh 解码的组件类型列表
|
||||
* @en List of decoded component types
|
||||
*/
|
||||
componentTypes: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码实体生成消息
|
||||
* @en Decode entity spawn message
|
||||
*
|
||||
* @param scene - @zh 场景 @en Scene
|
||||
* @param data - @zh 二进制数据 @en Binary data
|
||||
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||
* @returns @zh 解码结果,如果不是 SPAWN 消息则返回 null @en Decode result, or null if not a SPAWN message
|
||||
*/
|
||||
export function decodeSpawn(
|
||||
scene: Scene,
|
||||
data: Uint8Array,
|
||||
entityMap?: Map<number, Entity>
|
||||
): DecodeSpawnResult | null {
|
||||
const reader = new BinaryReader(data);
|
||||
const operation = reader.readUint8();
|
||||
|
||||
if (operation !== SyncOperation.SPAWN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entityId = reader.readUint32();
|
||||
const prefabType = reader.readString();
|
||||
const componentCount = reader.readVarint();
|
||||
const componentTypes: string[] = [];
|
||||
|
||||
// Create entity
|
||||
const entity = scene.createEntity(`entity_${entityId}`);
|
||||
entityMap?.set(entityId, entity);
|
||||
|
||||
for (let i = 0; i < componentCount; i++) {
|
||||
const typeId = reader.readString();
|
||||
componentTypes.push(typeId);
|
||||
|
||||
const componentClass = componentRegistry.get(typeId);
|
||||
if (!componentClass) {
|
||||
console.warn(`Unknown component type: ${typeId}`);
|
||||
// Try to skip
|
||||
const fieldCount = reader.readVarint();
|
||||
for (let j = 0; j < fieldCount; j++) {
|
||||
reader.readUint8();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const metadata: SyncMetadata | undefined = (componentClass as any)[SYNC_METADATA];
|
||||
if (!metadata) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const component = entity.addComponent(new componentClass());
|
||||
decodeComponent(component, metadata, reader);
|
||||
}
|
||||
|
||||
return { entity, prefabType, componentTypes };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码销毁消息结果
|
||||
* @en Decode despawn message result
|
||||
*/
|
||||
export interface DecodeDespawnResult {
|
||||
/**
|
||||
* @zh 销毁的实体 ID 列表
|
||||
* @en List of despawned entity IDs
|
||||
*/
|
||||
entityIds: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码实体销毁消息
|
||||
* @en Decode entity despawn message
|
||||
*
|
||||
* @param data - @zh 二进制数据 @en Binary data
|
||||
* @returns @zh 解码结果,如果不是 DESPAWN 消息则返回 null @en Decode result, or null if not a DESPAWN message
|
||||
*/
|
||||
export function decodeDespawn(data: Uint8Array): DecodeDespawnResult | null {
|
||||
const reader = new BinaryReader(data);
|
||||
const operation = reader.readUint8();
|
||||
|
||||
if (operation !== SyncOperation.DESPAWN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entityIds: number[] = [];
|
||||
|
||||
// Check if it's a single entity or batch
|
||||
if (reader.remaining === 4) {
|
||||
// Single entity
|
||||
entityIds.push(reader.readUint32());
|
||||
} else {
|
||||
// Batch
|
||||
const count = reader.readVarint();
|
||||
for (let i = 0; i < count; i++) {
|
||||
entityIds.push(reader.readUint32());
|
||||
}
|
||||
}
|
||||
|
||||
return { entityIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理销毁消息(从场景中移除实体)
|
||||
* @en Process despawn message (remove entities from scene)
|
||||
*
|
||||
* @param scene - @zh 场景 @en Scene
|
||||
* @param data - @zh 二进制数据 @en Binary data
|
||||
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||
* @returns @zh 移除的实体 ID 列表 @en List of removed entity IDs
|
||||
*/
|
||||
export function processDespawn(
|
||||
scene: Scene,
|
||||
data: Uint8Array,
|
||||
entityMap?: Map<number, Entity>
|
||||
): number[] {
|
||||
const result = decodeDespawn(data);
|
||||
if (!result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const entityId of result.entityIds) {
|
||||
const entity = entityMap?.get(entityId) || scene.findEntityById(entityId);
|
||||
if (entity) {
|
||||
entity.destroy();
|
||||
entityMap?.delete(entityId);
|
||||
}
|
||||
}
|
||||
|
||||
return result.entityIds;
|
||||
}
|
||||
291
packages/framework/core/src/ECS/Sync/encoding/Encoder.ts
Normal file
291
packages/framework/core/src/ECS/Sync/encoding/Encoder.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* @zh 组件状态编码器
|
||||
* @en Component state encoder
|
||||
*
|
||||
* @zh 将 ECS Component 的 @sync 字段编码为二进制格式
|
||||
* @en Encodes @sync fields of ECS Components to binary format
|
||||
*/
|
||||
|
||||
import type { Entity } from '../../Entity';
|
||||
import type { Component } from '../../Component';
|
||||
import type { SyncType, SyncMetadata } from '../types';
|
||||
import { SyncOperation, SYNC_METADATA, CHANGE_TRACKER } from '../types';
|
||||
import type { ChangeTracker } from '../ChangeTracker';
|
||||
import { BinaryWriter } from './BinaryWriter';
|
||||
|
||||
/**
|
||||
* @zh 编码单个字段值
|
||||
* @en Encode a single field value
|
||||
*/
|
||||
function encodeFieldValue(writer: BinaryWriter, value: any, type: SyncType): void {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
writer.writeBoolean(value);
|
||||
break;
|
||||
case 'int8':
|
||||
writer.writeInt8(value);
|
||||
break;
|
||||
case 'uint8':
|
||||
writer.writeUint8(value);
|
||||
break;
|
||||
case 'int16':
|
||||
writer.writeInt16(value);
|
||||
break;
|
||||
case 'uint16':
|
||||
writer.writeUint16(value);
|
||||
break;
|
||||
case 'int32':
|
||||
writer.writeInt32(value);
|
||||
break;
|
||||
case 'uint32':
|
||||
writer.writeUint32(value);
|
||||
break;
|
||||
case 'float32':
|
||||
writer.writeFloat32(value);
|
||||
break;
|
||||
case 'float64':
|
||||
writer.writeFloat64(value);
|
||||
break;
|
||||
case 'string':
|
||||
writer.writeString(value ?? '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码组件的完整状态
|
||||
* @en Encode full state of a component
|
||||
*
|
||||
* @zh 格式: [fieldCount: varint] ([fieldIndex: uint8] [value])...
|
||||
* @en Format: [fieldCount: varint] ([fieldIndex: uint8] [value])...
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
* @param metadata - @zh 组件同步元数据 @en Component sync metadata
|
||||
* @param writer - @zh 二进制写入器 @en Binary writer
|
||||
*/
|
||||
export function encodeComponentFull(
|
||||
component: Component,
|
||||
metadata: SyncMetadata,
|
||||
writer: BinaryWriter
|
||||
): void {
|
||||
const fields = metadata.fields;
|
||||
writer.writeVarint(fields.length);
|
||||
|
||||
for (const field of fields) {
|
||||
writer.writeUint8(field.index);
|
||||
const value = (component as any)[field.name];
|
||||
encodeFieldValue(writer, value, field.type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码组件的增量状态(只编码脏字段)
|
||||
* @en Encode delta state of a component (only dirty fields)
|
||||
*
|
||||
* @zh 格式: [dirtyCount: varint] ([fieldIndex: uint8] [value])...
|
||||
* @en Format: [dirtyCount: varint] ([fieldIndex: uint8] [value])...
|
||||
*
|
||||
* @param component - @zh 组件实例 @en Component instance
|
||||
* @param metadata - @zh 组件同步元数据 @en Component sync metadata
|
||||
* @param tracker - @zh 变更追踪器 @en Change tracker
|
||||
* @param writer - @zh 二进制写入器 @en Binary writer
|
||||
* @returns @zh 是否有数据编码 @en Whether any data was encoded
|
||||
*/
|
||||
export function encodeComponentDelta(
|
||||
component: Component,
|
||||
metadata: SyncMetadata,
|
||||
tracker: ChangeTracker,
|
||||
writer: BinaryWriter
|
||||
): boolean {
|
||||
if (!tracker.hasChanges()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const dirtyFields = tracker.getDirtyFields();
|
||||
writer.writeVarint(dirtyFields.length);
|
||||
|
||||
for (const fieldIndex of dirtyFields) {
|
||||
const field = metadata.fields[fieldIndex];
|
||||
if (field) {
|
||||
writer.writeUint8(field.index);
|
||||
const value = (component as any)[field.name];
|
||||
encodeFieldValue(writer, value, field.type);
|
||||
}
|
||||
}
|
||||
|
||||
return dirtyFields.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码实体的所有同步组件
|
||||
* @en Encode all sync components of an entity
|
||||
*
|
||||
* @zh 格式:
|
||||
* [entityId: uint32]
|
||||
* [componentCount: varint]
|
||||
* ([typeIdLength: varint] [typeId: string] [componentData])...
|
||||
*
|
||||
* @en Format:
|
||||
* [entityId: uint32]
|
||||
* [componentCount: varint]
|
||||
* ([typeIdLength: varint] [typeId: string] [componentData])...
|
||||
*
|
||||
* @param entity - @zh 实体 @en Entity
|
||||
* @param writer - @zh 二进制写入器 @en Binary writer
|
||||
* @param deltaOnly - @zh 只编码增量 @en Only encode delta
|
||||
* @returns @zh 编码的组件数量 @en Number of components encoded
|
||||
*/
|
||||
export function encodeEntity(
|
||||
entity: Entity,
|
||||
writer: BinaryWriter,
|
||||
deltaOnly: boolean = false
|
||||
): number {
|
||||
writer.writeUint32(entity.id);
|
||||
|
||||
const components = entity.components;
|
||||
const syncComponents: Array<{
|
||||
component: Component;
|
||||
metadata: SyncMetadata;
|
||||
tracker: ChangeTracker | undefined;
|
||||
}> = [];
|
||||
|
||||
// Collect components with sync metadata
|
||||
for (const component of components) {
|
||||
const constructor = component.constructor as any;
|
||||
const metadata: SyncMetadata | undefined = constructor[SYNC_METADATA];
|
||||
if (metadata && metadata.fields.length > 0) {
|
||||
const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||
|
||||
// For delta encoding, only include components with changes
|
||||
if (deltaOnly && tracker && !tracker.hasChanges()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
syncComponents.push({ component, metadata, tracker });
|
||||
}
|
||||
}
|
||||
|
||||
writer.writeVarint(syncComponents.length);
|
||||
|
||||
for (const { component, metadata, tracker } of syncComponents) {
|
||||
// Write component type ID
|
||||
writer.writeString(metadata.typeId);
|
||||
|
||||
if (deltaOnly && tracker) {
|
||||
encodeComponentDelta(component, metadata, tracker, writer);
|
||||
} else {
|
||||
encodeComponentFull(component, metadata, writer);
|
||||
}
|
||||
}
|
||||
|
||||
return syncComponents.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码状态快照(多个实体)
|
||||
* @en Encode state snapshot (multiple entities)
|
||||
*
|
||||
* @zh 格式:
|
||||
* [operation: uint8] (FULL=0, DELTA=1, SPAWN=2, DESPAWN=3)
|
||||
* [entityCount: varint]
|
||||
* (entityData)...
|
||||
*
|
||||
* @en Format:
|
||||
* [operation: uint8] (FULL=0, DELTA=1, SPAWN=2, DESPAWN=3)
|
||||
* [entityCount: varint]
|
||||
* (entityData)...
|
||||
*
|
||||
* @param entities - @zh 要编码的实体数组 @en Entities to encode
|
||||
* @param operation - @zh 同步操作类型 @en Sync operation type
|
||||
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||
*/
|
||||
export function encodeSnapshot(
|
||||
entities: Entity[],
|
||||
operation: SyncOperation = SyncOperation.FULL
|
||||
): Uint8Array {
|
||||
const writer = new BinaryWriter(1024);
|
||||
|
||||
writer.writeUint8(operation);
|
||||
writer.writeVarint(entities.length);
|
||||
|
||||
const deltaOnly = operation === SyncOperation.DELTA;
|
||||
|
||||
for (const entity of entities) {
|
||||
encodeEntity(entity, writer, deltaOnly);
|
||||
}
|
||||
|
||||
return writer.toUint8Array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码实体生成消息
|
||||
* @en Encode entity spawn message
|
||||
*
|
||||
* @param entity - @zh 生成的实体 @en Spawned entity
|
||||
* @param prefabType - @zh 预制体类型(可选)@en Prefab type (optional)
|
||||
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||
*/
|
||||
export function encodeSpawn(entity: Entity, prefabType?: string): Uint8Array {
|
||||
const writer = new BinaryWriter(256);
|
||||
|
||||
writer.writeUint8(SyncOperation.SPAWN);
|
||||
writer.writeUint32(entity.id);
|
||||
writer.writeString(prefabType || '');
|
||||
|
||||
// Encode all sync components for initial state
|
||||
const components = entity.components;
|
||||
const syncComponents: Array<{ component: Component; metadata: SyncMetadata }> = [];
|
||||
|
||||
for (const component of components) {
|
||||
const constructor = component.constructor as any;
|
||||
const metadata: SyncMetadata | undefined = constructor[SYNC_METADATA];
|
||||
if (metadata && metadata.fields.length > 0) {
|
||||
syncComponents.push({ component, metadata });
|
||||
}
|
||||
}
|
||||
|
||||
writer.writeVarint(syncComponents.length);
|
||||
|
||||
for (const { component, metadata } of syncComponents) {
|
||||
writer.writeString(metadata.typeId);
|
||||
encodeComponentFull(component, metadata, writer);
|
||||
}
|
||||
|
||||
return writer.toUint8Array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码实体销毁消息
|
||||
* @en Encode entity despawn message
|
||||
*
|
||||
* @param entityId - @zh 销毁的实体 ID @en Despawned entity ID
|
||||
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||
*/
|
||||
export function encodeDespawn(entityId: number): Uint8Array {
|
||||
const writer = new BinaryWriter(8);
|
||||
|
||||
writer.writeUint8(SyncOperation.DESPAWN);
|
||||
writer.writeUint32(entityId);
|
||||
|
||||
return writer.toUint8Array();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码批量实体销毁消息
|
||||
* @en Encode batch entity despawn message
|
||||
*
|
||||
* @param entityIds - @zh 销毁的实体 ID 数组 @en Despawned entity IDs
|
||||
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||
*/
|
||||
export function encodeDespawnBatch(entityIds: number[]): Uint8Array {
|
||||
const writer = new BinaryWriter(8 + entityIds.length * 4);
|
||||
|
||||
writer.writeUint8(SyncOperation.DESPAWN);
|
||||
writer.writeVarint(entityIds.length);
|
||||
|
||||
for (const id of entityIds) {
|
||||
writer.writeUint32(id);
|
||||
}
|
||||
|
||||
return writer.toUint8Array();
|
||||
}
|
||||
52
packages/framework/core/src/ECS/Sync/encoding/index.ts
Normal file
52
packages/framework/core/src/ECS/Sync/encoding/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @zh 二进制编解码模块
|
||||
* @en Binary encoding/decoding module
|
||||
*
|
||||
* @zh 提供 ECS Component 状态的二进制序列化和反序列化功能
|
||||
* @en Provides binary serialization and deserialization for ECS Component state
|
||||
*/
|
||||
|
||||
// Variable-length integer encoding
|
||||
export {
|
||||
varintSize,
|
||||
encodeVarint,
|
||||
decodeVarint,
|
||||
zigzagEncode,
|
||||
zigzagDecode,
|
||||
encodeSignedVarint,
|
||||
decodeSignedVarint
|
||||
} from './varint';
|
||||
|
||||
// Binary writer/reader
|
||||
export { BinaryWriter } from './BinaryWriter';
|
||||
export { BinaryReader } from './BinaryReader';
|
||||
|
||||
// Encoder
|
||||
export {
|
||||
encodeComponentFull,
|
||||
encodeComponentDelta,
|
||||
encodeEntity,
|
||||
encodeSnapshot,
|
||||
encodeSpawn,
|
||||
encodeDespawn,
|
||||
encodeDespawnBatch
|
||||
} from './Encoder';
|
||||
|
||||
// Decoder
|
||||
export {
|
||||
registerSyncComponent,
|
||||
autoRegisterSyncComponent,
|
||||
decodeComponent,
|
||||
decodeEntity,
|
||||
decodeSnapshot,
|
||||
decodeSpawn,
|
||||
decodeDespawn,
|
||||
processDespawn
|
||||
} from './Decoder';
|
||||
|
||||
export type {
|
||||
DecodeEntityResult,
|
||||
DecodeSnapshotResult,
|
||||
DecodeSpawnResult,
|
||||
DecodeDespawnResult
|
||||
} from './Decoder';
|
||||
137
packages/framework/core/src/ECS/Sync/encoding/varint.ts
Normal file
137
packages/framework/core/src/ECS/Sync/encoding/varint.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* @zh 变长整数编解码
|
||||
* @en Variable-length integer encoding/decoding
|
||||
*
|
||||
* @zh 使用 LEB128 编码方式,可变长度编码正整数。
|
||||
* 小数值使用更少字节,大数值使用更多字节。
|
||||
* @en Uses LEB128 encoding for variable-length integer encoding.
|
||||
* Small values use fewer bytes, large values use more bytes.
|
||||
*
|
||||
* | 值范围 | 字节数 |
|
||||
* |--------|--------|
|
||||
* | 0-127 | 1 |
|
||||
* | 128-16383 | 2 |
|
||||
* | 16384-2097151 | 3 |
|
||||
* | 2097152-268435455 | 4 |
|
||||
* | 268435456+ | 5 |
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 计算变长整数所需的字节数
|
||||
* @en Calculate bytes needed for a varint
|
||||
*
|
||||
* @param value - @zh 整数值 @en Integer value
|
||||
* @returns @zh 所需字节数 @en Bytes needed
|
||||
*/
|
||||
export function varintSize(value: number): number {
|
||||
if (value < 0) {
|
||||
throw new Error('Varint only supports non-negative integers');
|
||||
}
|
||||
if (value < 128) return 1;
|
||||
if (value < 16384) return 2;
|
||||
if (value < 2097152) return 3;
|
||||
if (value < 268435456) return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码变长整数到字节数组
|
||||
* @en Encode varint to byte array
|
||||
*
|
||||
* @param value - @zh 要编码的整数 @en Integer to encode
|
||||
* @param buffer - @zh 目标缓冲区 @en Target buffer
|
||||
* @param offset - @zh 写入偏移 @en Write offset
|
||||
* @returns @zh 写入后的新偏移 @en New offset after writing
|
||||
*/
|
||||
export function encodeVarint(value: number, buffer: Uint8Array, offset: number): number {
|
||||
if (value < 0) {
|
||||
throw new Error('Varint only supports non-negative integers');
|
||||
}
|
||||
|
||||
while (value >= 0x80) {
|
||||
buffer[offset++] = (value & 0x7F) | 0x80;
|
||||
value >>>= 7;
|
||||
}
|
||||
buffer[offset++] = value;
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从字节数组解码变长整数
|
||||
* @en Decode varint from byte array
|
||||
*
|
||||
* @param buffer - @zh 源缓冲区 @en Source buffer
|
||||
* @param offset - @zh 读取偏移 @en Read offset
|
||||
* @returns @zh [解码值, 新偏移] @en [decoded value, new offset]
|
||||
*/
|
||||
export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
|
||||
let result = 0;
|
||||
let shift = 0;
|
||||
let byte: number;
|
||||
|
||||
do {
|
||||
if (offset >= buffer.length) {
|
||||
throw new Error('Varint decode: buffer overflow');
|
||||
}
|
||||
byte = buffer[offset++]!;
|
||||
result |= (byte & 0x7F) << shift;
|
||||
shift += 7;
|
||||
} while (byte >= 0x80);
|
||||
|
||||
return [result, offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码有符号整数(ZigZag 编码)
|
||||
* @en Encode signed integer (ZigZag encoding)
|
||||
*
|
||||
* @zh ZigZag 编码将有符号整数映射到无符号整数:
|
||||
* 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...
|
||||
* 这样小的负数也能用较少字节表示。
|
||||
* @en ZigZag encoding maps signed integers to unsigned:
|
||||
* 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...
|
||||
* This allows small negative numbers to use fewer bytes.
|
||||
*
|
||||
* @param value - @zh 有符号整数 @en Signed integer
|
||||
* @returns @zh ZigZag 编码后的值 @en ZigZag encoded value
|
||||
*/
|
||||
export function zigzagEncode(value: number): number {
|
||||
return (value << 1) ^ (value >> 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码有符号整数(ZigZag 解码)
|
||||
* @en Decode signed integer (ZigZag decoding)
|
||||
*
|
||||
* @param value - @zh ZigZag 编码的值 @en ZigZag encoded value
|
||||
* @returns @zh 原始有符号整数 @en Original signed integer
|
||||
*/
|
||||
export function zigzagDecode(value: number): number {
|
||||
return (value >>> 1) ^ -(value & 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 编码有符号变长整数
|
||||
* @en Encode signed varint
|
||||
*
|
||||
* @param value - @zh 有符号整数 @en Signed integer
|
||||
* @param buffer - @zh 目标缓冲区 @en Target buffer
|
||||
* @param offset - @zh 写入偏移 @en Write offset
|
||||
* @returns @zh 写入后的新偏移 @en New offset after writing
|
||||
*/
|
||||
export function encodeSignedVarint(value: number, buffer: Uint8Array, offset: number): number {
|
||||
return encodeVarint(zigzagEncode(value), buffer, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 解码有符号变长整数
|
||||
* @en Decode signed varint
|
||||
*
|
||||
* @param buffer - @zh 源缓冲区 @en Source buffer
|
||||
* @param offset - @zh 读取偏移 @en Read offset
|
||||
* @returns @zh [解码值, 新偏移] @en [decoded value, new offset]
|
||||
*/
|
||||
export function decodeSignedVarint(buffer: Uint8Array, offset: number): [number, number] {
|
||||
const [encoded, newOffset] = decodeVarint(buffer, offset);
|
||||
return [zigzagDecode(encoded), newOffset];
|
||||
}
|
||||
55
packages/framework/core/src/ECS/Sync/index.ts
Normal file
55
packages/framework/core/src/ECS/Sync/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @zh ECS 网络同步模块
|
||||
* @en ECS Network Synchronization Module
|
||||
*
|
||||
* @zh 提供基于 ECS Component 的网络状态同步功能:
|
||||
* - @sync 装饰器:标记需要同步的字段
|
||||
* - ChangeTracker:追踪字段级变更
|
||||
* - 二进制编解码器:高效的网络序列化
|
||||
*
|
||||
* @en Provides network state synchronization based on ECS Components:
|
||||
* - @sync decorator: Mark fields for synchronization
|
||||
* - ChangeTracker: Track field-level changes
|
||||
* - Binary encoder/decoder: Efficient network serialization
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Component, ECSComponent, sync } from '@esengine/ecs-framework';
|
||||
*
|
||||
* @ECSComponent('Player')
|
||||
* class PlayerComponent extends Component {
|
||||
* @sync("string") name: string = "";
|
||||
* @sync("uint16") score: number = 0;
|
||||
* @sync("float32") x: number = 0;
|
||||
* @sync("float32") y: number = 0;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Types
|
||||
export {
|
||||
SyncType,
|
||||
SyncFieldMetadata,
|
||||
SyncMetadata,
|
||||
SyncOperation,
|
||||
TYPE_SIZES,
|
||||
SYNC_METADATA,
|
||||
CHANGE_TRACKER
|
||||
} from './types';
|
||||
|
||||
// Change Tracker
|
||||
export { ChangeTracker } from './ChangeTracker';
|
||||
|
||||
// Decorators
|
||||
export {
|
||||
sync,
|
||||
getSyncMetadata,
|
||||
hasSyncFields,
|
||||
getChangeTracker,
|
||||
initChangeTracker,
|
||||
clearChanges,
|
||||
hasChanges
|
||||
} from './decorators';
|
||||
|
||||
// Encoding
|
||||
export * from './encoding';
|
||||
127
packages/framework/core/src/ECS/Sync/types.ts
Normal file
127
packages/framework/core/src/ECS/Sync/types.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @zh 网络同步类型定义
|
||||
* @en Network synchronization type definitions
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 支持的同步数据类型
|
||||
* @en Supported sync data types
|
||||
*/
|
||||
export type SyncType =
|
||||
| 'boolean'
|
||||
| 'int8'
|
||||
| 'uint8'
|
||||
| 'int16'
|
||||
| 'uint16'
|
||||
| 'int32'
|
||||
| 'uint32'
|
||||
| 'float32'
|
||||
| 'float64'
|
||||
| 'string';
|
||||
|
||||
/**
|
||||
* @zh 同步字段元数据
|
||||
* @en Sync field metadata
|
||||
*/
|
||||
export interface SyncFieldMetadata {
|
||||
/**
|
||||
* @zh 字段索引(用于二进制编码)
|
||||
* @en Field index (for binary encoding)
|
||||
*/
|
||||
index: number;
|
||||
|
||||
/**
|
||||
* @zh 字段名称
|
||||
* @en Field name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* @zh 字段类型
|
||||
* @en Field type
|
||||
*/
|
||||
type: SyncType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 组件同步元数据
|
||||
* @en Component sync metadata
|
||||
*/
|
||||
export interface SyncMetadata {
|
||||
/**
|
||||
* @zh 组件类型 ID
|
||||
* @en Component type ID
|
||||
*/
|
||||
typeId: string;
|
||||
|
||||
/**
|
||||
* @zh 同步字段列表(按索引排序)
|
||||
* @en Sync fields list (sorted by index)
|
||||
*/
|
||||
fields: SyncFieldMetadata[];
|
||||
|
||||
/**
|
||||
* @zh 字段名到索引的映射
|
||||
* @en Field name to index mapping
|
||||
*/
|
||||
fieldIndexMap: Map<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 同步操作类型
|
||||
* @en Sync operation type
|
||||
*/
|
||||
export enum SyncOperation {
|
||||
/**
|
||||
* @zh 完整快照
|
||||
* @en Full snapshot
|
||||
*/
|
||||
FULL = 0,
|
||||
|
||||
/**
|
||||
* @zh 增量更新
|
||||
* @en Delta update
|
||||
*/
|
||||
DELTA = 1,
|
||||
|
||||
/**
|
||||
* @zh 实体生成
|
||||
* @en Entity spawn
|
||||
*/
|
||||
SPAWN = 2,
|
||||
|
||||
/**
|
||||
* @zh 实体销毁
|
||||
* @en Entity despawn
|
||||
*/
|
||||
DESPAWN = 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 各类型的字节大小
|
||||
* @en Byte size for each type
|
||||
*/
|
||||
export const TYPE_SIZES: Record<SyncType, number> = {
|
||||
boolean: 1,
|
||||
int8: 1,
|
||||
uint8: 1,
|
||||
int16: 2,
|
||||
uint16: 2,
|
||||
int32: 4,
|
||||
uint32: 4,
|
||||
float32: 4,
|
||||
float64: 8,
|
||||
string: -1, // 动态长度 | dynamic length
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 同步元数据的 Symbol 键
|
||||
* @en Symbol key for sync metadata
|
||||
*/
|
||||
export const SYNC_METADATA = Symbol('SyncMetadata');
|
||||
|
||||
/**
|
||||
* @zh 变更追踪器的 Symbol 键
|
||||
* @en Symbol key for change tracker
|
||||
*/
|
||||
export const CHANGE_TRACKER = Symbol('ChangeTracker');
|
||||
@@ -317,9 +317,7 @@ export class WorldManager implements IService {
|
||||
/**
|
||||
* @zh 更新所有活跃的World
|
||||
* @en Update all active Worlds
|
||||
*
|
||||
* @zh 应该在每帧的游戏循环中调用
|
||||
* @en Should be called in each frame of game loop
|
||||
* @internal 由 Core.update() 调用,用户不应直接调用
|
||||
*/
|
||||
public updateAll(): void {
|
||||
if (!this._isRunning) return;
|
||||
|
||||
@@ -57,3 +57,6 @@ export { EpochManager } from './Core/EpochManager';
|
||||
// Compiled Query
|
||||
export { CompiledQuery } from './Core/Query/CompiledQuery';
|
||||
export type { InstanceTypes } from './Core/Query/CompiledQuery';
|
||||
|
||||
// Network Synchronization
|
||||
export * from './Sync';
|
||||
|
||||
Reference in New Issue
Block a user