From 06b3f920071fbcb05676461910cccb94f68c6ce0 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Wed, 8 Oct 2025 18:34:15 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=BA=8F=E5=88=97=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/ECS/IScene.ts | 21 +- packages/core/src/ECS/Scene.ts | 54 +- .../ECS/Serialization/ComponentSerializer.ts | 336 ++++++++++ .../Serialization/ComponentTypeRegistry.ts | 194 ++++++ .../src/ECS/Serialization/EntitySerializer.ts | 223 +++++++ .../src/ECS/Serialization/SceneSerializer.ts | 529 ++++++++++++++++ .../Serialization/SerializationDecorators.ts | 254 ++++++++ .../src/ECS/Serialization/VersionMigration.ts | 371 +++++++++++ packages/core/src/ECS/Serialization/index.ts | 54 ++ packages/core/src/ECS/index.ts | 3 +- .../ECS/Serialization/Serialization.test.ts | 575 ++++++++++++++++++ 11 files changed, 2610 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/ECS/Serialization/ComponentSerializer.ts create mode 100644 packages/core/src/ECS/Serialization/ComponentTypeRegistry.ts create mode 100644 packages/core/src/ECS/Serialization/EntitySerializer.ts create mode 100644 packages/core/src/ECS/Serialization/SceneSerializer.ts create mode 100644 packages/core/src/ECS/Serialization/SerializationDecorators.ts create mode 100644 packages/core/src/ECS/Serialization/VersionMigration.ts create mode 100644 packages/core/src/ECS/Serialization/index.ts create mode 100644 packages/core/tests/ECS/Serialization/Serialization.test.ts diff --git a/packages/core/src/ECS/IScene.ts b/packages/core/src/ECS/IScene.ts index 51ff9a98..0cdd50b8 100644 --- a/packages/core/src/ECS/IScene.ts +++ b/packages/core/src/ECS/IScene.ts @@ -9,7 +9,7 @@ import { TypeSafeEventSystem } from './Core/EventSystem'; /** * 场景接口定义 - * + * * 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。 */ export interface IScene { @@ -18,6 +18,25 @@ export interface IScene { */ name: string; + /** + * 场景自定义数据 + * + * 用于存储场景级别的配置和状态数据,例如: + * - 天气状态 + * - 时间设置 + * - 游戏难度 + * - 音频配置 + * - 关卡检查点 + * + * @example + * ```typescript + * scene.sceneData.set('weather', 'rainy'); + * scene.sceneData.set('timeOfDay', 14.5); + * scene.sceneData.set('checkpoint', { x: 100, y: 200 }); + * ``` + */ + readonly sceneData: Map; + /** * 场景中的实体集合 */ diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 4b4bdcf0..2901aea3 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -10,6 +10,7 @@ import { EventBus } from './Core/EventBus'; import { IScene, ISceneConfig } from './IScene'; import { getComponentInstanceTypeName, getSystemInstanceTypeName } from './Decorators'; import { TypedQueryBuilder } from './Core/Query/TypedQuery'; +import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer'; /** * 游戏场景默认实现类 @@ -20,14 +21,21 @@ import { TypedQueryBuilder } from './Core/Query/TypedQuery'; export class Scene implements IScene { /** * 场景名称 - * + * * 用于标识和调试的友好名称。 */ public name: string = ""; + /** + * 场景自定义数据 + * + * 用于存储场景级别的配置和状态数据。 + */ + public readonly sceneData: Map = new Map(); + /** * 场景中的实体集合 - * + * * 管理场景内所有实体的生命周期。 */ public readonly entities: EntityList; @@ -489,4 +497,46 @@ export class Scene implements IScene { componentStats: this.componentStorageManager.getAllStats() }; } + + /** + * 序列化场景 + * + * 将场景及其所有实体、组件序列化为JSON字符串 + * + * @param options 序列化选项 + * @returns 序列化后的JSON字符串 + * + * @example + * ```typescript + * const saveData = scene.serialize({ + * components: [PlayerComponent, PositionComponent], + * format: 'json', + * pretty: true + * }); + * ``` + */ + public serialize(options?: SceneSerializationOptions): string { + return SceneSerializer.serialize(this, options); + } + + /** + * 反序列化场景 + * + * 从序列化数据恢复场景状态 + * + * @param saveData 序列化的数据 + * @param options 反序列化选项 + * + * @example + * ```typescript + * scene.deserialize(saveData, { + * strategy: 'replace', + * preserveIds: false, + * componentRegistry: ComponentTypeRegistry.getRegistry() + * }); + * ``` + */ + public deserialize(saveData: string, options?: SceneDeserializationOptions): void { + SceneSerializer.deserialize(this, saveData, options); + } } \ No newline at end of file diff --git a/packages/core/src/ECS/Serialization/ComponentSerializer.ts b/packages/core/src/ECS/Serialization/ComponentSerializer.ts new file mode 100644 index 00000000..2038a438 --- /dev/null +++ b/packages/core/src/ECS/Serialization/ComponentSerializer.ts @@ -0,0 +1,336 @@ +/** + * 组件序列化器 + * + * 负责组件的序列化和反序列化操作 + */ + +import { Component } from '../Component'; +import { ComponentType } from '../Core/ComponentStorage'; +import { getComponentTypeName } from '../Decorators'; +import { + getSerializationMetadata, + isSerializable, + SerializationMetadata +} from './SerializationDecorators'; + +/** + * 序列化后的组件数据 + */ +export interface SerializedComponent { + /** + * 组件类型名称 + */ + type: string; + + /** + * 序列化版本 + */ + version: number; + + /** + * 组件数据 + */ + data: Record; +} + +/** + * 组件序列化器类 + */ +export class ComponentSerializer { + /** + * 序列化单个组件 + * + * @param component 要序列化的组件实例 + * @returns 序列化后的组件数据,如果组件不可序列化则返回null + */ + public static serialize(component: Component): SerializedComponent | null { + const metadata = getSerializationMetadata(component); + + if (!metadata) { + // 组件没有使用@Serializable装饰器,不可序列化 + return null; + } + + const componentType = component.constructor as ComponentType; + const typeName = metadata.options.typeId || getComponentTypeName(componentType); + const data: Record = {}; + + // 序列化标记的字段 + for (const [fieldName, options] of metadata.fields) { + const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName; + const value = (component as any)[fieldName]; + + // 跳过忽略的字段 + if (metadata.ignoredFields.has(fieldName)) { + continue; + } + + // 使用自定义序列化器或默认序列化 + const serializedValue = options.serializer + ? options.serializer(value) + : this.serializeValue(value); + + // 使用别名或原始字段名 + const key = options.alias || fieldKey; + data[key] = serializedValue; + } + + return { + type: typeName, + version: metadata.options.version, + data + }; + } + + /** + * 反序列化组件 + * + * @param serializedData 序列化的组件数据 + * @param componentRegistry 组件类型注册表 (类型名 -> 构造函数) + * @returns 反序列化后的组件实例,如果失败则返回null + */ + public static deserialize( + serializedData: SerializedComponent, + componentRegistry: Map + ): Component | null { + const componentClass = componentRegistry.get(serializedData.type); + + if (!componentClass) { + console.warn(`未找到组件类型: ${serializedData.type}`); + return null; + } + + const metadata = getSerializationMetadata(componentClass); + + if (!metadata) { + console.warn(`组件 ${serializedData.type} 不可序列化`); + return null; + } + + // 创建组件实例 + const component = new componentClass(); + + // 反序列化字段 + for (const [fieldName, options] of metadata.fields) { + const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName; + const key = options.alias || fieldKey; + const serializedValue = serializedData.data[key]; + + if (serializedValue === undefined) { + continue; // 字段不存在于序列化数据中 + } + + // 使用自定义反序列化器或默认反序列化 + const value = options.deserializer + ? options.deserializer(serializedValue) + : this.deserializeValue(serializedValue); + + (component as any)[fieldName] = value; + } + + return component; + } + + /** + * 批量序列化组件 + * + * @param components 组件数组 + * @returns 序列化后的组件数据数组 + */ + public static serializeComponents(components: Component[]): SerializedComponent[] { + const result: SerializedComponent[] = []; + + for (const component of components) { + const serialized = this.serialize(component); + if (serialized) { + result.push(serialized); + } + } + + return result; + } + + /** + * 批量反序列化组件 + * + * @param serializedComponents 序列化的组件数据数组 + * @param componentRegistry 组件类型注册表 + * @returns 反序列化后的组件数组 + */ + public static deserializeComponents( + serializedComponents: SerializedComponent[], + componentRegistry: Map + ): Component[] { + const result: Component[] = []; + + for (const serialized of serializedComponents) { + const component = this.deserialize(serialized, componentRegistry); + if (component) { + result.push(component); + } + } + + return result; + } + + /** + * 默认值序列化 + * + * 处理基本类型、数组、对象等的序列化 + */ + private static serializeValue(value: any): any { + if (value === null || value === undefined) { + return value; + } + + // 基本类型 + const type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean') { + return value; + } + + // 日期 + if (value instanceof Date) { + return { + __type: 'Date', + value: value.toISOString() + }; + } + + // 数组 + if (Array.isArray(value)) { + return value.map(item => this.serializeValue(item)); + } + + // Map (如果没有使用@SerializeMap装饰器) + if (value instanceof Map) { + return { + __type: 'Map', + value: Array.from(value.entries()) + }; + } + + // Set + if (value instanceof Set) { + return { + __type: 'Set', + value: Array.from(value) + }; + } + + // 普通对象 + if (type === 'object') { + const result: Record = {}; + for (const key in value) { + if (value.hasOwnProperty(key)) { + result[key] = this.serializeValue(value[key]); + } + } + return result; + } + + // 其他类型(函数等)不序列化 + return undefined; + } + + /** + * 默认值反序列化 + */ + private static deserializeValue(value: any): any { + if (value === null || value === undefined) { + return value; + } + + // 基本类型直接返回 + const type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean') { + return value; + } + + // 处理特殊类型标记 + if (type === 'object' && value.__type) { + switch (value.__type) { + case 'Date': + return new Date(value.value); + case 'Map': + return new Map(value.value); + case 'Set': + return new Set(value.value); + } + } + + // 数组 + if (Array.isArray(value)) { + return value.map(item => this.deserializeValue(item)); + } + + // 普通对象 + if (type === 'object') { + const result: Record = {}; + for (const key in value) { + if (value.hasOwnProperty(key)) { + result[key] = this.deserializeValue(value[key]); + } + } + return result; + } + + return value; + } + + /** + * 验证序列化数据的版本 + * + * @param serializedData 序列化数据 + * @param expectedVersion 期望的版本号 + * @returns 版本是否匹配 + */ + public static validateVersion( + serializedData: SerializedComponent, + expectedVersion: number + ): boolean { + return serializedData.version === expectedVersion; + } + + /** + * 获取组件的序列化信息 + * + * @param component 组件实例或组件类 + * @returns 序列化信息对象,包含类型名、版本、可序列化字段列表 + */ + public static getSerializationInfo(component: Component | ComponentType): { + type: string; + version: number; + fields: string[]; + ignoredFields: string[]; + isSerializable: boolean; + } | null { + const metadata = getSerializationMetadata(component); + + if (!metadata) { + return { + type: 'unknown', + version: 0, + fields: [], + ignoredFields: [], + isSerializable: false + }; + } + + const componentType = typeof component === 'function' + ? component + : (component.constructor as ComponentType); + + return { + type: metadata.options.typeId || getComponentTypeName(componentType), + version: metadata.options.version, + fields: Array.from(metadata.fields.keys()).map(k => + typeof k === 'symbol' ? k.toString() : k + ), + ignoredFields: Array.from(metadata.ignoredFields).map(k => + typeof k === 'symbol' ? k.toString() : k + ), + isSerializable: true + }; + } +} diff --git a/packages/core/src/ECS/Serialization/ComponentTypeRegistry.ts b/packages/core/src/ECS/Serialization/ComponentTypeRegistry.ts new file mode 100644 index 00000000..3cf03cd6 --- /dev/null +++ b/packages/core/src/ECS/Serialization/ComponentTypeRegistry.ts @@ -0,0 +1,194 @@ +/** + * 全局组件类型注册表 + * + * 用于序列化系统的组件类型查找和管理 + */ + +import { Component } from '../Component'; +import { ComponentType } from '../Core/ComponentStorage'; +import { getComponentTypeName } from '../Decorators'; + +/** + * 全局组件类型注册表 + * + * 维护组件类型名称到构造函数的映射,用于序列化/反序列化 + */ +export class ComponentTypeRegistry { + /** + * 组件类型映射表 + * Map<类型名称, 构造函数> + */ + private static registry = new Map(); + + /** + * 注册组件类型 + * + * @param componentClass 组件构造函数 + * @param typeName 组件类型名称(可选,默认使用类名或@ECSComponent装饰器指定的名称) + * + * @example + * ```typescript + * @ECSComponent('Player') + * @Serializable({ version: 1 }) + * class PlayerComponent extends Component { + * @Serialize() name: string = ''; + * } + * + * // 注册组件 + * ComponentTypeRegistry.register(PlayerComponent); + * ``` + */ + public static register(componentClass: ComponentType, typeName?: string): void { + const name = typeName || getComponentTypeName(componentClass); + + if (this.registry.has(name)) { + console.warn(`Component type "${name}" is already registered, overwriting...`); + } + + this.registry.set(name, componentClass); + } + + /** + * 批量注册组件类型 + * + * @param componentClasses 组件构造函数数组 + * + * @example + * ```typescript + * ComponentTypeRegistry.registerMany([ + * PlayerComponent, + * PositionComponent, + * VelocityComponent + * ]); + * ``` + */ + public static registerMany(componentClasses: ComponentType[]): void { + for (const componentClass of componentClasses) { + this.register(componentClass); + } + } + + /** + * 获取组件类型 + * + * @param typeName 组件类型名称 + * @returns 组件构造函数,如果未找到则返回undefined + */ + public static get(typeName: string): ComponentType | undefined { + return this.registry.get(typeName); + } + + /** + * 检查组件类型是否已注册 + * + * @param typeName 组件类型名称 + * @returns 如果已注册返回true + */ + public static has(typeName: string): boolean { + return this.registry.has(typeName); + } + + /** + * 取消注册组件类型 + * + * @param typeName 组件类型名称 + * @returns 如果成功取消注册返回true + */ + public static unregister(typeName: string): boolean { + return this.registry.delete(typeName); + } + + /** + * 清空注册表 + */ + public static clear(): void { + this.registry.clear(); + } + + /** + * 获取所有已注册的组件类型名称 + * + * @returns 组件类型名称数组 + */ + public static getAllTypeNames(): string[] { + return Array.from(this.registry.keys()); + } + + /** + * 获取所有已注册的组件类型 + * + * @returns 组件构造函数数组 + */ + public static getAllTypes(): ComponentType[] { + return Array.from(this.registry.values()); + } + + /** + * 获取注册表的Map副本 + * + * @returns 组件类型注册表的副本 + */ + public static getRegistry(): Map { + return new Map(this.registry); + } + + /** + * 获取注册的组件数量 + * + * @returns 已注册的组件类型数量 + */ + public static get size(): number { + return this.registry.size; + } + + /** + * 从组件实例获取类型名称 + * + * @param component 组件实例 + * @returns 组件类型名称 + */ + public static getTypeName(component: Component): string { + return getComponentTypeName(component.constructor as ComponentType); + } + + /** + * 根据组件类查找已注册的类型名称 + * + * @param componentClass 组件构造函数 + * @returns 类型名称,如果未注册则返回undefined + */ + public static findTypeName(componentClass: ComponentType): string | undefined { + const typeName = getComponentTypeName(componentClass); + + // 检查是否已注册 + if (this.registry.get(typeName) === componentClass) { + return typeName; + } + + // 遍历查找 + for (const [name, cls] of this.registry) { + if (cls === componentClass) { + return name; + } + } + + return undefined; + } + + /** + * 自动发现并注册所有装饰的组件 + * + * 注意:此方法需要组件类已经被加载到内存中 + * + * @param components 组件类数组 + */ + public static autoRegister(components: ComponentType[]): void { + for (const component of components) { + try { + this.register(component); + } catch (error) { + console.error(`Failed to auto-register component ${component.name}:`, error); + } + } + } +} diff --git a/packages/core/src/ECS/Serialization/EntitySerializer.ts b/packages/core/src/ECS/Serialization/EntitySerializer.ts new file mode 100644 index 00000000..c502448f --- /dev/null +++ b/packages/core/src/ECS/Serialization/EntitySerializer.ts @@ -0,0 +1,223 @@ +/** + * 实体序列化器 + * + * 负责实体的序列化和反序列化操作 + */ + +import { Entity } from '../Entity'; +import { Component } from '../Component'; +import { ComponentType } from '../Core/ComponentStorage'; +import { ComponentSerializer, SerializedComponent } from './ComponentSerializer'; + +/** + * 序列化后的实体数据 + */ +export interface SerializedEntity { + /** + * 实体ID + */ + id: number; + + /** + * 实体名称 + */ + name: string; + + /** + * 实体标签 + */ + tag: number; + + /** + * 激活状态 + */ + active: boolean; + + /** + * 启用状态 + */ + enabled: boolean; + + /** + * 更新顺序 + */ + updateOrder: number; + + /** + * 组件列表 + */ + components: SerializedComponent[]; + + /** + * 子实体列表 + */ + children: SerializedEntity[]; + + /** + * 父实体ID(如果有) + */ + parentId?: number; +} + +/** + * 实体序列化器类 + */ +export class EntitySerializer { + /** + * 序列化单个实体 + * + * @param entity 要序列化的实体 + * @param includeChildren 是否包含子实体(默认true) + * @returns 序列化后的实体数据 + */ + public static serialize(entity: Entity, includeChildren: boolean = true): SerializedEntity { + const serializedComponents = ComponentSerializer.serializeComponents( + Array.from(entity.components) + ); + + const serializedEntity: SerializedEntity = { + id: entity.id, + name: entity.name, + tag: entity.tag, + active: entity.active, + enabled: entity.enabled, + updateOrder: entity.updateOrder, + components: serializedComponents, + children: [] + }; + + // 序列化父实体引用 + if (entity.parent) { + serializedEntity.parentId = entity.parent.id; + } + + // 序列化子实体 + if (includeChildren) { + for (const child of entity.children) { + serializedEntity.children.push(this.serialize(child, true)); + } + } + + return serializedEntity; + } + + /** + * 反序列化实体 + * + * @param serializedEntity 序列化的实体数据 + * @param componentRegistry 组件类型注册表 + * @param idGenerator 实体ID生成器(用于生成新ID或保持原ID) + * @param preserveIds 是否保持原始ID(默认false) + * @returns 反序列化后的实体 + */ + public static deserialize( + serializedEntity: SerializedEntity, + componentRegistry: Map, + idGenerator: () => number, + preserveIds: boolean = false + ): Entity { + // 创建实体(使用原始ID或新生成的ID) + const entityId = preserveIds ? serializedEntity.id : idGenerator(); + const entity = new Entity(serializedEntity.name, entityId); + + // 恢复实体属性 + entity.tag = serializedEntity.tag; + entity.active = serializedEntity.active; + entity.enabled = serializedEntity.enabled; + entity.updateOrder = serializedEntity.updateOrder; + + // 反序列化组件 + const components = ComponentSerializer.deserializeComponents( + serializedEntity.components, + componentRegistry + ); + + for (const component of components) { + entity.addComponent(component); + } + + // 反序列化子实体 + for (const childData of serializedEntity.children) { + const childEntity = this.deserialize( + childData, + componentRegistry, + idGenerator, + preserveIds + ); + entity.addChild(childEntity); + } + + return entity; + } + + /** + * 批量序列化实体 + * + * @param entities 实体数组 + * @param includeChildren 是否包含子实体 + * @returns 序列化后的实体数据数组 + */ + public static serializeEntities( + entities: Entity[], + includeChildren: boolean = true + ): SerializedEntity[] { + const result: SerializedEntity[] = []; + + for (const entity of entities) { + // 只序列化顶层实体(没有父实体的实体) + // 子实体会在父实体序列化时一并处理 + if (!entity.parent || !includeChildren) { + result.push(this.serialize(entity, includeChildren)); + } + } + + return result; + } + + /** + * 批量反序列化实体 + * + * @param serializedEntities 序列化的实体数据数组 + * @param componentRegistry 组件类型注册表 + * @param idGenerator 实体ID生成器 + * @param preserveIds 是否保持原始ID + * @returns 反序列化后的实体数组 + */ + public static deserializeEntities( + serializedEntities: SerializedEntity[], + componentRegistry: Map, + idGenerator: () => number, + preserveIds: boolean = false + ): Entity[] { + const result: Entity[] = []; + + for (const serialized of serializedEntities) { + const entity = this.deserialize( + serialized, + componentRegistry, + idGenerator, + preserveIds + ); + result.push(entity); + } + + return result; + } + + /** + * 创建实体的深拷贝 + * + * @param entity 要拷贝的实体 + * @param componentRegistry 组件类型注册表 + * @param idGenerator ID生成器 + * @returns 拷贝后的新实体 + */ + public static clone( + entity: Entity, + componentRegistry: Map, + idGenerator: () => number + ): Entity { + const serialized = this.serialize(entity, true); + return this.deserialize(serialized, componentRegistry, idGenerator, false); + } +} diff --git a/packages/core/src/ECS/Serialization/SceneSerializer.ts b/packages/core/src/ECS/Serialization/SceneSerializer.ts new file mode 100644 index 00000000..1aa43082 --- /dev/null +++ b/packages/core/src/ECS/Serialization/SceneSerializer.ts @@ -0,0 +1,529 @@ +/** + * 场景序列化器 + * + * 负责整个场景的序列化和反序列化,包括实体、组件等 + */ + +import type { IScene } from '../IScene'; +import { Entity } from '../Entity'; +import { Component } from '../Component'; +import { ComponentType } from '../Core/ComponentStorage'; +import { EntitySerializer, SerializedEntity } from './EntitySerializer'; +import { getComponentTypeName } from '../Decorators'; +import { getSerializationMetadata } from './SerializationDecorators'; +import { ComponentTypeRegistry } from './ComponentTypeRegistry'; + +/** + * 场景序列化格式 + */ +export type SerializationFormat = 'json' | 'binary'; + +/** + * 场景序列化策略 + */ +export type DeserializationStrategy = 'merge' | 'replace'; + +/** + * 版本迁移函数 + */ +export type MigrationFunction = ( + oldVersion: number, + newVersion: number, + data: any +) => any; + +/** + * 场景序列化选项 + */ +export interface SceneSerializationOptions { + /** + * 要序列化的组件类型列表 + * 如果未指定,则序列化所有可序列化的组件 + */ + components?: ComponentType[]; + + /** + * 是否序列化系统状态(当前不支持) + */ + systems?: boolean; + + /** + * 序列化格式 + */ + format?: SerializationFormat; + + /** + * 是否美化JSON输出(仅在format='json'时有效) + */ + pretty?: boolean; + + /** + * 是否包含元数据(如序列化时间、版本等) + */ + includeMetadata?: boolean; +} + +/** + * 场景反序列化选项 + */ +export interface SceneDeserializationOptions { + /** + * 反序列化策略 + * - 'merge': 合并到现有场景 + * - 'replace': 替换现有场景内容 + */ + strategy?: DeserializationStrategy; + + /** + * 版本迁移函数 + */ + migration?: MigrationFunction; + + /** + * 是否保持原始实体ID + */ + preserveIds?: boolean; + + /** + * 组件类型注册表 + * 如果未提供,将尝试从全局注册表获取 + */ + componentRegistry?: Map; +} + +/** + * 序列化后的场景数据 + */ +export interface SerializedScene { + /** + * 场景名称 + */ + name: string; + + /** + * 序列化版本 + */ + version: number; + + /** + * 序列化时间戳 + */ + timestamp?: number; + + /** + * 场景自定义数据 + * + * 存储场景级别的配置和状态 + */ + sceneData?: Record; + + /** + * 实体列表 + */ + entities: SerializedEntity[]; + + /** + * 元数据 + */ + metadata?: { + entityCount: number; + componentTypeCount: number; + serializationOptions?: SceneSerializationOptions; + }; + + /** + * 组件类型注册信息 + */ + componentTypeRegistry: Array<{ + typeName: string; + version: number; + }>; +} + +/** + * 场景序列化器类 + */ +export class SceneSerializer { + /** + * 当前序列化版本 + */ + private static readonly SERIALIZATION_VERSION = 1; + + /** + * 序列化场景 + * + * @param scene 要序列化的场景 + * @param options 序列化选项 + * @returns 序列化后的数据(JSON字符串或二进制数据) + */ + public static serialize(scene: IScene, options?: SceneSerializationOptions): string { + const opts: SceneSerializationOptions = { + systems: false, + format: 'json', + pretty: true, + includeMetadata: true, + ...options + }; + + // 过滤实体和组件 + const entities = this.filterEntities(scene, opts); + + // 序列化实体 + const serializedEntities = EntitySerializer.serializeEntities(entities, true); + + // 收集组件类型信息 + const componentTypeRegistry = this.buildComponentTypeRegistry(entities); + + // 序列化场景自定义数据 + const sceneData = this.serializeSceneData(scene.sceneData); + + // 构建序列化数据 + const serializedScene: SerializedScene = { + name: scene.name, + version: this.SERIALIZATION_VERSION, + entities: serializedEntities, + componentTypeRegistry + }; + + // 添加场景数据(如果有) + if (sceneData && Object.keys(sceneData).length > 0) { + serializedScene.sceneData = sceneData; + } + + // 添加元数据 + if (opts.includeMetadata) { + serializedScene.timestamp = Date.now(); + serializedScene.metadata = { + entityCount: serializedEntities.length, + componentTypeCount: componentTypeRegistry.length, + serializationOptions: opts + }; + } + + // 根据格式返回数据 + if (opts.format === 'json') { + return opts.pretty + ? JSON.stringify(serializedScene, null, 2) + : JSON.stringify(serializedScene); + } else { + // 二进制格式(未来实现) + throw new Error('Binary serialization format is not yet implemented'); + } + } + + /** + * 反序列化场景 + * + * @param scene 目标场景 + * @param saveData 序列化的数据 + * @param options 反序列化选项 + */ + public static deserialize( + scene: IScene, + saveData: string, + options?: SceneDeserializationOptions + ): void { + const opts: SceneDeserializationOptions = { + strategy: 'replace', + preserveIds: false, + ...options + }; + + // 解析数据 + let serializedScene: SerializedScene; + try { + serializedScene = JSON.parse(saveData); + } catch (error) { + throw new Error(`Failed to parse save data: ${error}`); + } + + // 版本迁移 + if (opts.migration && serializedScene.version !== this.SERIALIZATION_VERSION) { + serializedScene = opts.migration( + serializedScene.version, + this.SERIALIZATION_VERSION, + serializedScene + ); + } + + // 构建组件注册表 + const componentRegistry = opts.componentRegistry || this.getGlobalComponentRegistry(); + + // 根据策略处理场景 + if (opts.strategy === 'replace') { + // 清空场景 + scene.destroyAllEntities(); + } + + // ID生成器 + const idGenerator = () => scene.identifierPool.checkOut(); + + // 反序列化实体 + const entities = EntitySerializer.deserializeEntities( + serializedScene.entities, + componentRegistry, + idGenerator, + opts.preserveIds || false + ); + + // 将实体添加到场景 + for (const entity of entities) { + scene.addEntity(entity); + } + + // 反序列化场景自定义数据 + if (serializedScene.sceneData) { + this.deserializeSceneData(serializedScene.sceneData, scene.sceneData); + } + } + + /** + * 序列化场景自定义数据 + * + * 将 Map 转换为普通对象 + */ + private static serializeSceneData(sceneData: Map): Record { + const result: Record = {}; + + for (const [key, value] of sceneData) { + result[key] = this.serializeValue(value); + } + + return result; + } + + /** + * 反序列化场景自定义数据 + * + * 将普通对象还原为 Map + */ + private static deserializeSceneData( + data: Record, + targetMap: Map + ): void { + targetMap.clear(); + + for (const [key, value] of Object.entries(data)) { + targetMap.set(key, this.deserializeValue(value)); + } + } + + /** + * 序列化单个值 + */ + private static serializeValue(value: any): any { + if (value === null || value === undefined) { + return value; + } + + // 基本类型 + const type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean') { + return value; + } + + // Date + if (value instanceof Date) { + return { __type: 'Date', value: value.toISOString() }; + } + + // Map + if (value instanceof Map) { + return { __type: 'Map', value: Array.from(value.entries()) }; + } + + // Set + if (value instanceof Set) { + return { __type: 'Set', value: Array.from(value) }; + } + + // 数组 + if (Array.isArray(value)) { + return value.map(item => this.serializeValue(item)); + } + + // 普通对象 + if (type === 'object') { + const result: Record = {}; + for (const key in value) { + if (value.hasOwnProperty(key)) { + result[key] = this.serializeValue(value[key]); + } + } + return result; + } + + // 其他类型不序列化 + return undefined; + } + + /** + * 反序列化单个值 + */ + private static deserializeValue(value: any): any { + if (value === null || value === undefined) { + return value; + } + + // 基本类型 + const type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean') { + return value; + } + + // 处理特殊类型标记 + if (type === 'object' && value.__type) { + switch (value.__type) { + case 'Date': + return new Date(value.value); + case 'Map': + return new Map(value.value); + case 'Set': + return new Set(value.value); + } + } + + // 数组 + if (Array.isArray(value)) { + return value.map(item => this.deserializeValue(item)); + } + + // 普通对象 + if (type === 'object') { + const result: Record = {}; + for (const key in value) { + if (value.hasOwnProperty(key)) { + result[key] = this.deserializeValue(value[key]); + } + } + return result; + } + + return value; + } + + /** + * 过滤要序列化的实体和组件 + */ + private static filterEntities(scene: IScene, options: SceneSerializationOptions): Entity[] { + const entities = Array.from(scene.entities.buffer); + + // 如果指定了组件类型过滤 + if (options.components && options.components.length > 0) { + const componentTypeSet = new Set(options.components); + + // 只返回拥有指定组件的实体 + return entities.filter(entity => { + return Array.from(entity.components).some(component => + componentTypeSet.has(component.constructor as ComponentType) + ); + }); + } + + return entities; + } + + /** + * 构建组件类型注册表 + */ + private static buildComponentTypeRegistry( + entities: Entity[] + ): Array<{ typeName: string; version: number }> { + const registry = new Map(); + + for (const entity of entities) { + for (const component of entity.components) { + const componentType = component.constructor as ComponentType; + const typeName = getComponentTypeName(componentType); + const metadata = getSerializationMetadata(component); + + if (metadata && !registry.has(typeName)) { + registry.set(typeName, metadata.options.version); + } + } + } + + return Array.from(registry.entries()).map(([typeName, version]) => ({ + typeName, + version + })); + } + + /** + * 获取全局组件注册表 + * + * 从所有已注册的组件类型构建注册表 + */ + private static getGlobalComponentRegistry(): Map { + return ComponentTypeRegistry.getRegistry(); + } + + /** + * 验证保存数据的有效性 + * + * @param saveData 序列化的数据 + * @returns 验证结果 + */ + public static validate(saveData: string): { + valid: boolean; + version?: number; + errors?: string[]; + } { + const errors: string[] = []; + + try { + const data = JSON.parse(saveData); + + if (!data.version) { + errors.push('Missing version field'); + } + + if (!data.entities || !Array.isArray(data.entities)) { + errors.push('Missing or invalid entities field'); + } + + if (!data.componentTypeRegistry || !Array.isArray(data.componentTypeRegistry)) { + errors.push('Missing or invalid componentTypeRegistry field'); + } + + return { + valid: errors.length === 0, + version: data.version, + errors: errors.length > 0 ? errors : undefined + }; + } catch (error) { + return { + valid: false, + errors: [`JSON parse error: ${error}`] + }; + } + } + + /** + * 获取保存数据的信息(不完全反序列化) + * + * @param saveData 序列化的数据 + * @returns 保存数据的元信息 + */ + public static getInfo(saveData: string): { + name: string; + version: number; + timestamp?: number; + entityCount: number; + componentTypeCount: number; + } | null { + try { + const data: SerializedScene = JSON.parse(saveData); + + return { + name: data.name, + version: data.version, + timestamp: data.timestamp, + entityCount: data.metadata?.entityCount || data.entities.length, + componentTypeCount: data.componentTypeRegistry.length + }; + } catch (error) { + return null; + } + } +} diff --git a/packages/core/src/ECS/Serialization/SerializationDecorators.ts b/packages/core/src/ECS/Serialization/SerializationDecorators.ts new file mode 100644 index 00000000..e9969e7a --- /dev/null +++ b/packages/core/src/ECS/Serialization/SerializationDecorators.ts @@ -0,0 +1,254 @@ +/** + * 序列化装饰器 + * + * 提供组件级别的序列化支持,包括字段级装饰器和类级装饰器 + */ + +import { Component } from '../Component'; + +/** + * 序列化元数据的Symbol键 + */ +export const SERIALIZABLE_METADATA = Symbol('SerializableMetadata'); +export const SERIALIZE_FIELD = Symbol('SerializeField'); +export const SERIALIZE_OPTIONS = Symbol('SerializeOptions'); + +/** + * 可序列化配置选项 + */ +export interface SerializableOptions { + /** + * 序列化版本号,用于数据迁移 + */ + version: number; + + /** + * 组件类型标识符(可选,默认使用类名) + */ + typeId?: string; +} + +/** + * 字段序列化配置 + */ +export interface FieldSerializeOptions { + /** + * 自定义序列化器 + */ + serializer?: (value: any) => any; + + /** + * 自定义反序列化器 + */ + deserializer?: (value: any) => any; + + /** + * 字段别名(用于序列化后的键名) + */ + alias?: string; +} + +/** + * 序列化元数据 + */ +export interface SerializationMetadata { + options: SerializableOptions; + fields: Map; + ignoredFields: Set; +} + +/** + * 组件可序列化装饰器 + * + * 标记组件类为可序列化,必须与字段装饰器配合使用 + * + * @param options 序列化配置选项 + * + * @example + * ```typescript + * @ECSComponent('Player') + * @Serializable({ version: 1 }) + * class PlayerComponent extends Component { + * @Serialize() name: string = 'Player'; + * @Serialize() level: number = 1; + * } + * ``` + */ +export function Serializable(options: SerializableOptions) { + return function Component>(target: T): T { + if (!options || typeof options.version !== 'number') { + throw new Error('Serializable装饰器必须提供有效的版本号'); + } + + // 初始化或获取现有元数据 + let metadata: SerializationMetadata = (target as any)[SERIALIZABLE_METADATA]; + if (!metadata) { + metadata = { + options, + fields: new Map(), + ignoredFields: new Set() + }; + (target as any)[SERIALIZABLE_METADATA] = metadata; + } else { + metadata.options = options; + } + + return target; + }; +} + +/** + * 字段序列化装饰器 + * + * 标记字段为可序列化 + * + * @param options 字段序列化选项(可选) + * + * @example + * ```typescript + * @Serialize() + * name: string = 'Player'; + * + * @Serialize({ alias: 'hp' }) + * health: number = 100; + * ``` + */ +export function Serialize(options?: FieldSerializeOptions) { + return function (target: any, propertyKey: string | symbol) { + const constructor = target.constructor; + + // 获取或创建元数据 + let metadata: SerializationMetadata = constructor[SERIALIZABLE_METADATA]; + if (!metadata) { + metadata = { + options: { version: 1 }, // 默认版本 + fields: new Map(), + ignoredFields: new Set() + }; + constructor[SERIALIZABLE_METADATA] = metadata; + } + + // 记录字段 + metadata.fields.set(propertyKey, options || {}); + }; +} + +/** + * Map序列化装饰器 + * + * 专门用于序列化Map类型字段 + * + * @example + * ```typescript + * @SerializeAsMap() + * inventory: Map = new Map(); + * ``` + */ +export function SerializeAsMap() { + return function (target: any, propertyKey: string | symbol) { + Serialize({ + serializer: (value: Map) => { + if (!(value instanceof Map)) { + return null; + } + return Array.from(value.entries()); + }, + deserializer: (value: any) => { + if (!Array.isArray(value)) { + return new Map(); + } + return new Map(value); + } + })(target, propertyKey); + }; +} + +/** + * Set序列化装饰器 + * + * 专门用于序列化Set类型字段 + * + * @example + * ```typescript + * @SerializeAsSet() + * tags: Set = new Set(); + * ``` + */ +export function SerializeAsSet() { + return function (target: any, propertyKey: string | symbol) { + Serialize({ + serializer: (value: Set) => { + if (!(value instanceof Set)) { + return null; + } + return Array.from(value); + }, + deserializer: (value: any) => { + if (!Array.isArray(value)) { + return new Set(); + } + return new Set(value); + } + })(target, propertyKey); + }; +} + +/** + * 忽略序列化装饰器 + * + * 标记字段不参与序列化 + * + * @example + * ```typescript + * @IgnoreSerialization() + * tempCache: any = null; + * ``` + */ +export function IgnoreSerialization() { + return function (target: any, propertyKey: string | symbol) { + const constructor = target.constructor; + + // 获取或创建元数据 + let metadata: SerializationMetadata = constructor[SERIALIZABLE_METADATA]; + if (!metadata) { + metadata = { + options: { version: 1 }, + fields: new Map(), + ignoredFields: new Set() + }; + constructor[SERIALIZABLE_METADATA] = metadata; + } + + // 记录忽略字段 + metadata.ignoredFields.add(propertyKey); + }; +} + +/** + * 获取组件的序列化元数据 + * + * @param componentClass 组件类或组件实例 + * @returns 序列化元数据,如果组件不可序列化则返回null + */ +export function getSerializationMetadata(componentClass: any): SerializationMetadata | null { + if (!componentClass) { + return null; + } + + // 如果是实例,获取其构造函数 + const constructor = typeof componentClass === 'function' + ? componentClass + : componentClass.constructor; + + return constructor[SERIALIZABLE_METADATA] || null; +} + +/** + * 检查组件是否可序列化 + * + * @param component 组件类或组件实例 + * @returns 如果组件可序列化返回true + */ +export function isSerializable(component: any): boolean { + return getSerializationMetadata(component) !== null; +} diff --git a/packages/core/src/ECS/Serialization/VersionMigration.ts b/packages/core/src/ECS/Serialization/VersionMigration.ts new file mode 100644 index 00000000..6492bca3 --- /dev/null +++ b/packages/core/src/ECS/Serialization/VersionMigration.ts @@ -0,0 +1,371 @@ +/** + * 版本迁移系统 + * + * 提供组件和场景数据的版本迁移支持 + */ + +import { SerializedComponent } from './ComponentSerializer'; +import { SerializedScene } from './SceneSerializer'; + +/** + * 组件迁移函数 + */ +export type ComponentMigrationFunction = (data: any, fromVersion: number, toVersion: number) => any; + +/** + * 场景迁移函数 + */ +export type SceneMigrationFunction = ( + scene: SerializedScene, + fromVersion: number, + toVersion: number +) => SerializedScene; + +/** + * 版本迁移管理器 + */ +export class VersionMigrationManager { + /** + * 组件迁移函数注册表 + * Map<组件类型名, Map<版本号, 迁移函数>> + */ + private static componentMigrations = new Map>(); + + /** + * 场景迁移函数注册表 + * Map<版本号, 迁移函数> + */ + private static sceneMigrations = new Map(); + + /** + * 注册组件迁移函数 + * + * @param componentType 组件类型名称 + * @param fromVersion 源版本号 + * @param toVersion 目标版本号 + * @param migration 迁移函数 + * + * @example + * ```typescript + * // 从版本1迁移到版本2 + * VersionMigrationManager.registerComponentMigration( + * 'PlayerComponent', + * 1, + * 2, + * (data) => { + * // 添加新字段 + * data.experience = 0; + * return data; + * } + * ); + * ``` + */ + public static registerComponentMigration( + componentType: string, + fromVersion: number, + toVersion: number, + migration: ComponentMigrationFunction + ): void { + if (!this.componentMigrations.has(componentType)) { + this.componentMigrations.set(componentType, new Map()); + } + + const versionMap = this.componentMigrations.get(componentType)!; + + // 使用fromVersion作为key,表示"从这个版本迁移" + versionMap.set(fromVersion, migration); + } + + /** + * 注册场景迁移函数 + * + * @param fromVersion 源版本号 + * @param toVersion 目标版本号 + * @param migration 迁移函数 + * + * @example + * ```typescript + * VersionMigrationManager.registerSceneMigration( + * 1, + * 2, + * (scene) => { + * // 迁移场景结构 + * scene.metadata = { ...scene.metadata, migratedFrom: 1 }; + * return scene; + * } + * ); + * ``` + */ + public static registerSceneMigration( + fromVersion: number, + toVersion: number, + migration: SceneMigrationFunction + ): void { + this.sceneMigrations.set(fromVersion, migration); + } + + /** + * 迁移组件数据 + * + * @param component 序列化的组件数据 + * @param targetVersion 目标版本号 + * @returns 迁移后的组件数据 + */ + public static migrateComponent( + component: SerializedComponent, + targetVersion: number + ): SerializedComponent { + const currentVersion = component.version; + + if (currentVersion === targetVersion) { + return component; // 版本相同,无需迁移 + } + + const migrations = this.componentMigrations.get(component.type); + if (!migrations) { + console.warn(`No migration path found for component ${component.type}`); + return component; + } + + let migratedData = { ...component }; + let version = currentVersion; + + // 执行迁移链 + while (version < targetVersion) { + const migration = migrations.get(version); + + if (!migration) { + console.warn( + `Missing migration from version ${version} to ${version + 1} for ${component.type}` + ); + break; + } + + migratedData.data = migration(migratedData.data, version, version + 1); + version++; + } + + migratedData.version = version; + return migratedData; + } + + /** + * 迁移场景数据 + * + * @param scene 序列化的场景数据 + * @param targetVersion 目标版本号 + * @returns 迁移后的场景数据 + */ + public static migrateScene(scene: SerializedScene, targetVersion: number): SerializedScene { + const currentVersion = scene.version; + + if (currentVersion === targetVersion) { + return scene; // 版本相同,无需迁移 + } + + let migratedScene = { ...scene }; + let version = currentVersion; + + // 执行场景级迁移 + while (version < targetVersion) { + const migration = this.sceneMigrations.get(version); + + if (!migration) { + console.warn(`Missing scene migration from version ${version} to ${version + 1}`); + break; + } + + migratedScene = migration(migratedScene, version, version + 1); + version++; + } + + migratedScene.version = version; + + // 迁移所有组件 + migratedScene = this.migrateSceneComponents(migratedScene); + + return migratedScene; + } + + /** + * 迁移场景中所有组件的版本 + */ + private static migrateSceneComponents(scene: SerializedScene): SerializedScene { + const migratedScene = { ...scene }; + + migratedScene.entities = scene.entities.map(entity => ({ + ...entity, + components: entity.components.map(component => { + // 查找组件的目标版本 + const typeInfo = scene.componentTypeRegistry.find( + t => t.typeName === component.type + ); + + if (typeInfo && typeInfo.version !== component.version) { + return this.migrateComponent(component, typeInfo.version); + } + + return component; + }), + children: this.migrateEntitiesComponents(entity.children, scene.componentTypeRegistry) + })); + + return migratedScene; + } + + /** + * 递归迁移实体的组件 + */ + private static migrateEntitiesComponents( + entities: any[], + typeRegistry: Array<{ typeName: string; version: number }> + ): any[] { + return entities.map(entity => ({ + ...entity, + components: entity.components.map((component: SerializedComponent) => { + const typeInfo = typeRegistry.find(t => t.typeName === component.type); + + if (typeInfo && typeInfo.version !== component.version) { + return this.migrateComponent(component, typeInfo.version); + } + + return component; + }), + children: this.migrateEntitiesComponents(entity.children, typeRegistry) + })); + } + + /** + * 清除所有迁移函数 + */ + public static clearMigrations(): void { + this.componentMigrations.clear(); + this.sceneMigrations.clear(); + } + + /** + * 获取组件的迁移路径 + * + * @param componentType 组件类型名称 + * @returns 可用的迁移版本列表 + */ + public static getComponentMigrationPath(componentType: string): number[] { + const migrations = this.componentMigrations.get(componentType); + if (!migrations) { + return []; + } + + return Array.from(migrations.keys()).sort((a, b) => a - b); + } + + /** + * 获取场景的迁移路径 + * + * @returns 可用的场景迁移版本列表 + */ + public static getSceneMigrationPath(): number[] { + return Array.from(this.sceneMigrations.keys()).sort((a, b) => a - b); + } + + /** + * 检查是否可以迁移组件 + * + * @param componentType 组件类型名称 + * @param fromVersion 源版本 + * @param toVersion 目标版本 + * @returns 是否存在完整的迁移路径 + */ + public static canMigrateComponent( + componentType: string, + fromVersion: number, + toVersion: number + ): boolean { + if (fromVersion === toVersion) { + return true; + } + + const migrations = this.componentMigrations.get(componentType); + if (!migrations) { + return false; + } + + // 检查是否存在完整的迁移路径 + for (let v = fromVersion; v < toVersion; v++) { + if (!migrations.has(v)) { + return false; + } + } + + return true; + } + + /** + * 检查是否可以迁移场景 + * + * @param fromVersion 源版本 + * @param toVersion 目标版本 + * @returns 是否存在完整的迁移路径 + */ + public static canMigrateScene(fromVersion: number, toVersion: number): boolean { + if (fromVersion === toVersion) { + return true; + } + + // 检查是否存在完整的场景迁移路径 + for (let v = fromVersion; v < toVersion; v++) { + if (!this.sceneMigrations.has(v)) { + return false; + } + } + + return true; + } +} + +/** + * 便捷的迁移构建器 + * + * 提供链式API来定义迁移 + */ +export class MigrationBuilder { + private componentType?: string; + private fromVersion: number = 1; + private toVersion: number = 2; + + /** + * 设置组件类型 + */ + public forComponent(componentType: string): this { + this.componentType = componentType; + return this; + } + + /** + * 设置版本范围 + */ + public fromVersionToVersion(from: number, to: number): this { + this.fromVersion = from; + this.toVersion = to; + return this; + } + + /** + * 注册迁移函数 + */ + public migrate(migration: ComponentMigrationFunction | SceneMigrationFunction): void { + if (this.componentType) { + VersionMigrationManager.registerComponentMigration( + this.componentType, + this.fromVersion, + this.toVersion, + migration as ComponentMigrationFunction + ); + } else { + VersionMigrationManager.registerSceneMigration( + this.fromVersion, + this.toVersion, + migration as SceneMigrationFunction + ); + } + } +} diff --git a/packages/core/src/ECS/Serialization/index.ts b/packages/core/src/ECS/Serialization/index.ts new file mode 100644 index 00000000..1e13f2f5 --- /dev/null +++ b/packages/core/src/ECS/Serialization/index.ts @@ -0,0 +1,54 @@ +/** + * ECS序列化系统 + * + * 提供完整的场景、实体和组件序列化支持 + */ + +// 装饰器 +export { + Serializable, + Serialize, + SerializeAsMap, + SerializeAsSet, + IgnoreSerialization, + getSerializationMetadata, + isSerializable, + SERIALIZABLE_METADATA, + SERIALIZE_FIELD, + SERIALIZE_OPTIONS +} from './SerializationDecorators'; + +export type { + SerializableOptions, + FieldSerializeOptions, + SerializationMetadata +} from './SerializationDecorators'; + +// 组件序列化器 +export { ComponentSerializer } from './ComponentSerializer'; +export type { SerializedComponent } from './ComponentSerializer'; + +// 实体序列化器 +export { EntitySerializer } from './EntitySerializer'; +export type { SerializedEntity } from './EntitySerializer'; + +// 场景序列化器 +export { SceneSerializer } from './SceneSerializer'; +export type { + SerializedScene, + SerializationFormat, + DeserializationStrategy, + MigrationFunction, + SceneSerializationOptions, + SceneDeserializationOptions +} from './SceneSerializer'; + +// 版本迁移 +export { VersionMigrationManager, MigrationBuilder } from './VersionMigration'; +export type { + ComponentMigrationFunction, + SceneMigrationFunction +} from './VersionMigration'; + +// 组件类型注册表 +export { ComponentTypeRegistry } from './ComponentTypeRegistry'; diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index 25a5eaa5..01582a1a 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -11,4 +11,5 @@ export { WorldManager, IWorldManagerConfig } from './WorldManager'; export * from './Core/Events'; export * from './Core/Query'; export * from './Core/Storage'; -export * from './Core/StorageDecorators'; \ No newline at end of file +export * from './Core/StorageDecorators'; +export * from './Serialization'; \ No newline at end of file diff --git a/packages/core/tests/ECS/Serialization/Serialization.test.ts b/packages/core/tests/ECS/Serialization/Serialization.test.ts new file mode 100644 index 00000000..f185b408 --- /dev/null +++ b/packages/core/tests/ECS/Serialization/Serialization.test.ts @@ -0,0 +1,575 @@ +/** + * 序列化系统测试 + */ + +import { Component } from '../../../src/ECS/Component'; +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { + Serializable, + Serialize, + SerializeAsMap, + SerializeAsSet, + IgnoreSerialization, + ComponentSerializer, + EntitySerializer, + SceneSerializer, + ComponentTypeRegistry, + VersionMigrationManager, + MigrationBuilder +} from '../../../src/ECS/Serialization'; +import { ECSComponent } from '../../../src/ECS/Decorators'; + +// 测试组件定义 +@ECSComponent('Position') +@Serializable({ version: 1 }) +class PositionComponent extends Component { + @Serialize() + public x: number = 0; + + @Serialize() + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('Velocity') +@Serializable({ version: 1 }) +class VelocityComponent extends Component { + @Serialize() + public dx: number = 0; + + @Serialize() + public dy: number = 0; +} + +@ECSComponent('Player') +@Serializable({ version: 1 }) +class PlayerComponent extends Component { + @Serialize() + public name: string = ''; + + @Serialize() + public level: number = 1; + + @SerializeAsMap() + public inventory: Map = new Map(); + + @SerializeAsSet() + public tags: Set = new Set(); + + @IgnoreSerialization() + public tempCache: any = null; +} + +@ECSComponent('Health') +@Serializable({ version: 1 }) +class HealthComponent extends Component { + @Serialize() + public current: number = 100; + + @Serialize() + public max: number = 100; +} + +// 非可序列化组件 +class NonSerializableComponent extends Component { + public data: any = null; +} + +describe('ECS Serialization System', () => { + beforeEach(() => { + // 注册组件类型 + ComponentTypeRegistry.clear(); + ComponentTypeRegistry.registerMany([ + PositionComponent, + VelocityComponent, + PlayerComponent, + HealthComponent + ]); + }); + + describe('Component Serialization', () => { + it('should serialize a simple component', () => { + const position = new PositionComponent(100, 200); + const serialized = ComponentSerializer.serialize(position); + + expect(serialized).not.toBeNull(); + expect(serialized!.type).toBe('Position'); + expect(serialized!.version).toBe(1); + expect(serialized!.data.x).toBe(100); + expect(serialized!.data.y).toBe(200); + }); + + it('should deserialize a simple component', () => { + const serializedData = { + type: 'Position', + version: 1, + data: { x: 150, y: 250 } + }; + + const registry = ComponentTypeRegistry.getRegistry(); + const component = ComponentSerializer.deserialize(serializedData, registry); + + expect(component).not.toBeNull(); + expect(component).toBeInstanceOf(PositionComponent); + expect((component as PositionComponent).x).toBe(150); + expect((component as PositionComponent).y).toBe(250); + }); + + it('should serialize Map fields', () => { + const player = new PlayerComponent(); + player.name = 'Hero'; + player.level = 5; + player.inventory.set('sword', 1); + player.inventory.set('potion', 10); + + const serialized = ComponentSerializer.serialize(player); + + expect(serialized).not.toBeNull(); + expect(serialized!.data.inventory).toEqual([ + ['sword', 1], + ['potion', 10] + ]); + }); + + it('should deserialize Map fields', () => { + const serializedData = { + type: 'Player', + version: 1, + data: { + name: 'Hero', + level: 5, + inventory: [ + ['sword', 1], + ['potion', 10] + ], + tags: ['warrior', 'hero'] + } + }; + + const registry = ComponentTypeRegistry.getRegistry(); + const component = ComponentSerializer.deserialize( + serializedData, + registry + ) as PlayerComponent; + + expect(component).not.toBeNull(); + expect(component.inventory.get('sword')).toBe(1); + expect(component.inventory.get('potion')).toBe(10); + expect(component.tags.has('warrior')).toBe(true); + expect(component.tags.has('hero')).toBe(true); + }); + + it('should ignore fields marked with @IgnoreSerialization', () => { + const player = new PlayerComponent(); + player.tempCache = { foo: 'bar' }; + + const serialized = ComponentSerializer.serialize(player); + + expect(serialized).not.toBeNull(); + expect(serialized!.data.tempCache).toBeUndefined(); + }); + + it('should return null for non-serializable components', () => { + const nonSerializable = new NonSerializableComponent(); + const serialized = ComponentSerializer.serialize(nonSerializable); + + expect(serialized).toBeNull(); + }); + }); + + describe('Entity Serialization', () => { + it('should serialize an entity with components', () => { + const entity = new Entity('Player', 1); + entity.addComponent(new PositionComponent(50, 100)); + entity.addComponent(new VelocityComponent()); + entity.tag = 10; + + const serialized = EntitySerializer.serialize(entity); + + expect(serialized.id).toBe(1); + expect(serialized.name).toBe('Player'); + expect(serialized.tag).toBe(10); + expect(serialized.components.length).toBe(2); + }); + + it('should serialize entity hierarchy', () => { + const parent = new Entity('Parent', 1); + const child = new Entity('Child', 2); + + parent.addComponent(new PositionComponent(0, 0)); + child.addComponent(new PositionComponent(10, 10)); + parent.addChild(child); + + const serialized = EntitySerializer.serialize(parent); + + expect(serialized.children.length).toBe(1); + expect(serialized.children[0].id).toBe(2); + expect(serialized.children[0].name).toBe('Child'); + }); + + it('should deserialize an entity', () => { + const serializedEntity = { + id: 1, + name: 'TestEntity', + tag: 5, + active: true, + enabled: true, + updateOrder: 0, + components: [ + { + type: 'Position', + version: 1, + data: { x: 100, y: 200 } + } + ], + children: [] + }; + + const registry = ComponentTypeRegistry.getRegistry(); + let idCounter = 10; + const entity = EntitySerializer.deserialize( + serializedEntity, + registry, + () => idCounter++, + false + ); + + expect(entity.name).toBe('TestEntity'); + expect(entity.tag).toBe(5); + expect(entity.components.length).toBe(1); + }); + }); + + describe('Scene Serialization', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene({ name: 'TestScene' }); + }); + + afterEach(() => { + scene.end(); + }); + + it('should serialize a scene', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new PlayerComponent()); + + const saveData = scene.serialize({ format: 'json', pretty: true }); + + expect(saveData).toBeTruthy(); + + const parsed = JSON.parse(saveData); + expect(parsed.name).toBe('TestScene'); + expect(parsed.version).toBe(1); + expect(parsed.entities.length).toBe(2); + }); + + it('should deserialize a scene with replace strategy', () => { + // 创建初始实体 + const entity1 = scene.createEntity('Initial'); + entity1.addComponent(new PositionComponent(0, 0)); + + // 序列化 + const entity2 = scene.createEntity('ToSave'); + entity2.addComponent(new PositionComponent(100, 100)); + const saveData = scene.serialize(); + + // 清空并重新加载 + scene.deserialize(saveData, { + strategy: 'replace', + componentRegistry: ComponentTypeRegistry.getRegistry() + }); + + expect(scene.entities.count).toBeGreaterThan(0); + }); + + it('should filter components during serialization', () => { + const entity = scene.createEntity('Mixed'); + entity.addComponent(new PositionComponent(1, 2)); + entity.addComponent(new PlayerComponent()); + entity.addComponent(new HealthComponent()); + + const saveData = scene.serialize({ + components: [PositionComponent, PlayerComponent] + }); + + const parsed = JSON.parse(saveData); + expect(parsed.entities.length).toBeGreaterThan(0); + }); + + it('should preserve entity hierarchy', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + parent.addChild(child); + + parent.addComponent(new PositionComponent(0, 0)); + child.addComponent(new PositionComponent(10, 10)); + + const saveData = scene.serialize(); + const parsed = JSON.parse(saveData); + + // 只有父实体在顶层 + expect(parsed.entities.length).toBe(1); + expect(parsed.entities[0].children.length).toBe(1); + }); + + it('should validate save data', () => { + const entity = scene.createEntity('Test'); + entity.addComponent(new PositionComponent(5, 5)); + + const saveData = scene.serialize(); + const validation = SceneSerializer.validate(saveData); + + expect(validation.valid).toBe(true); + expect(validation.version).toBe(1); + }); + + it('should get save data info', () => { + const entity = scene.createEntity('InfoTest'); + entity.addComponent(new PositionComponent(1, 1)); + + const saveData = scene.serialize(); + const info = SceneSerializer.getInfo(saveData); + + expect(info).not.toBeNull(); + expect(info!.name).toBe('TestScene'); + expect(info!.version).toBe(1); + }); + }); + + describe('Version Migration', () => { + @ECSComponent('OldPlayer') + @Serializable({ version: 1 }) + class OldPlayerV1 extends Component { + @Serialize() + public name: string = ''; + + @Serialize() + public hp: number = 100; + } + + @ECSComponent('OldPlayer') + @Serializable({ version: 2 }) + class OldPlayerV2 extends Component { + @Serialize() + public name: string = ''; + + @Serialize() + public health: number = 100; // 重命名了字段 + + @Serialize() + public maxHealth: number = 100; // 新增字段 + } + + beforeEach(() => { + VersionMigrationManager.clearMigrations(); + }); + + it('should migrate component from v1 to v2', () => { + // 注册迁移 + VersionMigrationManager.registerComponentMigration( + 'OldPlayer', + 1, + 2, + (data) => { + return { + name: data.name, + health: data.hp, + maxHealth: data.hp + }; + } + ); + + const v1Data = { + type: 'OldPlayer', + version: 1, + data: { name: 'Hero', hp: 80 } + }; + + const migrated = VersionMigrationManager.migrateComponent(v1Data, 2); + + expect(migrated.version).toBe(2); + expect(migrated.data.health).toBe(80); + expect(migrated.data.maxHealth).toBe(80); + expect(migrated.data.hp).toBeUndefined(); + }); + + it('should use MigrationBuilder for component migration', () => { + new MigrationBuilder() + .forComponent('Player') + .fromVersionToVersion(1, 2) + .migrate((data: any) => { + data.experience = 0; + return data; + }); + + expect(VersionMigrationManager.canMigrateComponent('Player', 1, 2)).toBe(true); + }); + + it('should check migration path availability', () => { + VersionMigrationManager.registerComponentMigration('Test', 1, 2, (d) => d); + VersionMigrationManager.registerComponentMigration('Test', 2, 3, (d) => d); + + expect(VersionMigrationManager.canMigrateComponent('Test', 1, 3)).toBe(true); + expect(VersionMigrationManager.canMigrateComponent('Test', 1, 4)).toBe(false); + }); + + it('should get migration path', () => { + VersionMigrationManager.registerComponentMigration('PathTest', 1, 2, (d) => d); + VersionMigrationManager.registerComponentMigration('PathTest', 2, 3, (d) => d); + + const path = VersionMigrationManager.getComponentMigrationPath('PathTest'); + + expect(path).toEqual([1, 2]); + }); + }); + + describe('ComponentTypeRegistry', () => { + it('should register and retrieve component types', () => { + ComponentTypeRegistry.clear(); + ComponentTypeRegistry.register(PositionComponent); + + expect(ComponentTypeRegistry.has('Position')).toBe(true); + expect(ComponentTypeRegistry.get('Position')).toBe(PositionComponent); + }); + + it('should register multiple component types', () => { + ComponentTypeRegistry.clear(); + ComponentTypeRegistry.registerMany([ + PositionComponent, + VelocityComponent, + PlayerComponent + ]); + + expect(ComponentTypeRegistry.size).toBe(3); + }); + + it('should get all type names', () => { + ComponentTypeRegistry.clear(); + ComponentTypeRegistry.register(PositionComponent); + ComponentTypeRegistry.register(VelocityComponent); + + const typeNames = ComponentTypeRegistry.getAllTypeNames(); + + expect(typeNames).toContain('Position'); + expect(typeNames).toContain('Velocity'); + }); + + it('should unregister component types', () => { + ComponentTypeRegistry.clear(); + ComponentTypeRegistry.register(PositionComponent); + + expect(ComponentTypeRegistry.has('Position')).toBe(true); + + ComponentTypeRegistry.unregister('Position'); + + expect(ComponentTypeRegistry.has('Position')).toBe(false); + }); + }); + + describe('Integration Tests', () => { + it('should perform full save/load cycle', () => { + const scene1 = new Scene({ name: 'SaveTest' }); + + // 创建复杂实体 + const player = scene1.createEntity('Player'); + const playerComp = new PlayerComponent(); + playerComp.name = 'TestHero'; + playerComp.level = 10; + playerComp.inventory.set('sword', 1); + playerComp.inventory.set('shield', 1); + playerComp.tags.add('warrior'); + + player.addComponent(playerComp); + player.addComponent(new PositionComponent(100, 200)); + player.addComponent(new HealthComponent()); + + // 创建子实体 + const weapon = scene1.createEntity('Weapon'); + weapon.addComponent(new PositionComponent(5, 0)); + player.addChild(weapon); + + // 序列化 + const saveData = scene1.serialize(); + + // 新场景 + const scene2 = new Scene({ name: 'LoadTest' }); + + // 反序列化 + scene2.deserialize(saveData, { + strategy: 'replace', + componentRegistry: ComponentTypeRegistry.getRegistry() + }); + + // 验证 + const loadedPlayer = scene2.findEntity('Player'); + expect(loadedPlayer).not.toBeNull(); + + const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent; + expect(loadedPlayerComp).not.toBeNull(); + expect(loadedPlayerComp.name).toBe('TestHero'); + expect(loadedPlayerComp.level).toBe(10); + expect(loadedPlayerComp.inventory.get('sword')).toBe(1); + expect(loadedPlayerComp.tags.has('warrior')).toBe(true); + + // 验证层级结构 + expect(loadedPlayer!.childCount).toBe(1); + + scene1.end(); + scene2.end(); + }); + + it('should serialize and deserialize scene custom data', () => { + const scene1 = new Scene({ name: 'SceneDataTest' }); + + // 设置场景自定义数据 + scene1.sceneData.set('weather', 'rainy'); + scene1.sceneData.set('timeOfDay', 14.5); + scene1.sceneData.set('difficulty', 'hard'); + scene1.sceneData.set('checkpoint', { x: 100, y: 200 }); + scene1.sceneData.set('tags', new Set(['action', 'adventure'])); + scene1.sceneData.set('metadata', new Map([['author', 'test'], ['version', '1.0']])); + + // 序列化 + const saveData = scene1.serialize(); + + // 新场景 + const scene2 = new Scene({ name: 'LoadTest' }); + + // 反序列化 + scene2.deserialize(saveData, { + strategy: 'replace', + componentRegistry: ComponentTypeRegistry.getRegistry() + }); + + // 验证场景数据 + expect(scene2.sceneData.get('weather')).toBe('rainy'); + expect(scene2.sceneData.get('timeOfDay')).toBe(14.5); + expect(scene2.sceneData.get('difficulty')).toBe('hard'); + expect(scene2.sceneData.get('checkpoint')).toEqual({ x: 100, y: 200 }); + + const tags = scene2.sceneData.get('tags'); + expect(tags).toBeInstanceOf(Set); + expect(tags.has('action')).toBe(true); + expect(tags.has('adventure')).toBe(true); + + const metadata = scene2.sceneData.get('metadata'); + expect(metadata).toBeInstanceOf(Map); + expect(metadata.get('author')).toBe('test'); + expect(metadata.get('version')).toBe('1.0'); + + scene1.end(); + scene2.end(); + }); + }); +});