From 952247def04fed8a7aee457880defd5b0e778f3b Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 30 Sep 2025 22:26:44 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=20Component=E5=9F=BA?= =?UTF-8?q?=E7=B1=BB=E8=BF=9D=E5=8F=8DECS=E7=BA=AF=E7=B2=B9=E6=80=A7?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/ECS/Component.ts | 149 ++++------------- packages/core/src/ECS/Entity.ts | 33 +--- packages/core/src/ECS/IScene.ts | 9 - packages/core/src/ECS/Scene.ts | 10 +- packages/core/src/ECS/Utils/EntityList.ts | 49 +----- packages/core/src/Types/index.ts | 20 +-- packages/core/tests/ECS/Component.test.ts | 190 ++++------------------ packages/core/tests/ECS/Entity.test.ts | 4 +- 8 files changed, 81 insertions(+), 383 deletions(-) diff --git a/packages/core/src/ECS/Component.ts b/packages/core/src/ECS/Component.ts index b60d8d9e..bc03773d 100644 --- a/packages/core/src/ECS/Component.ts +++ b/packages/core/src/ECS/Component.ts @@ -1,21 +1,30 @@ import type { IComponent } from '../Types'; -import type { Entity } from './Entity'; /** * 游戏组件基类 - * - * ECS架构中的组件(Component),用于实现具体的游戏功能。 - * 组件包含数据和行为,可以被添加到实体上以扩展实体的功能。 - * + * + * ECS架构中的组件(Component)应该是纯数据容器。 + * 所有游戏逻辑应该在 EntitySystem 中实现,而不是在组件内部。 + * * @example + * 推荐做法:纯数据组件 * ```typescript * class HealthComponent extends Component { * public health: number = 100; - * - * public takeDamage(damage: number): void { - * this.health -= damage; - * if (this.health <= 0) { - * this.entity.destroy(); + * public maxHealth: number = 100; + * } + * ``` + * + * @example + * 推荐做法:在 System 中处理逻辑 + * ```typescript + * class HealthSystem extends EntitySystem { + * process(entities: Entity[]): void { + * for (const entity of entities) { + * const health = entity.getComponent(HealthComponent); + * if (health && health.health <= 0) { + * entity.destroy(); + * } * } * } * } @@ -24,141 +33,49 @@ import type { Entity } from './Entity'; export abstract class Component implements IComponent { /** * 组件ID生成器 - * + * * 用于为每个组件分配唯一的ID。 */ public static _idGenerator: number = 0; - + /** * 组件唯一标识符 - * + * * 在整个游戏生命周期中唯一的数字ID。 */ public readonly id: number; - - /** - * 组件所属的实体 - * - * 指向拥有此组件的实体实例。 - */ - public entity!: Entity; - - /** - * 组件启用状态 - * - * 控制组件是否参与更新循环。 - */ - private _enabled: boolean = true; - - /** - * 更新顺序 - * - * 决定组件在更新循环中的执行顺序。 - * - * @see EntitySystem - */ - private _updateOrder: number = 0; /** * 创建组件实例 - * + * * 自动分配唯一ID给组件。 */ constructor() { this.id = Component._idGenerator++; } - /** - * 获取组件启用状态 - * - * 组件的实际启用状态取决于自身状态和所属实体的状态。 - * - * @deprecated 不符合ECS架构规范,建议自己实现DisabledComponent标记组件替代 - * @returns 如果组件和所属实体都启用则返回true - */ - public get enabled(): boolean { - return this.entity ? this.entity.enabled && this._enabled : this._enabled; - } - - /** - * 设置组件启用状态 - * - * 当状态改变时会触发相应的生命周期回调。 - * - * @deprecated 不符合ECS架构规范,建议自己实现DisabledComponent标记组件替代 - * @param value - 新的启用状态 - */ - public set enabled(value: boolean) { - if (this._enabled !== value) { - this._enabled = value; - if (this._enabled) { - this.onEnabled(); - } else { - this.onDisabled(); - } - } - } - - /** - * 获取更新顺序 - * - * @deprecated 不符合ECS架构规范,更新顺序应该由EntitySystem管理 - * @see EntitySystem - * @returns 组件的更新顺序值 - */ - public get updateOrder(): number { - return this._updateOrder; - } - - /** - * 设置更新顺序 - * - * @deprecated 不符合ECS架构规范,更新顺序应该由EntitySystem管理 - * @see EntitySystem - * @param value - 新的更新顺序值 - */ - public set updateOrder(value: number) { - this._updateOrder = value; - } - /** * 组件添加到实体时的回调 - * + * * 当组件被添加到实体时调用,可以在此方法中进行初始化操作。 + * + * @remarks + * 这是一个生命周期钩子,用于组件的初始化逻辑。 + * 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。 */ public onAddedToEntity(): void { } /** * 组件从实体移除时的回调 - * + * * 当组件从实体中移除时调用,可以在此方法中进行清理操作。 + * + * @remarks + * 这是一个生命周期钩子,用于组件的清理逻辑。 + * 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。 */ public onRemovedFromEntity(): void { } - /** - * 组件启用时的回调 - * - * 当组件被启用时调用。 - */ - public onEnabled(): void { - } - - /** - * 组件禁用时的回调 - * - * 当组件被禁用时调用。 - */ - public onDisabled(): void { - } - - /** - * 更新组件 - * - * @deprecated 不符合ECS架构规范,建议使用EntitySystem来处理更新逻辑 - */ - public update(): void { - } - } \ No newline at end of file diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index af136260..cc679365 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -104,12 +104,7 @@ export class Entity { * 所属场景引用 */ public scene: IScene | null = null; - - /** - * 更新间隔 - */ - public updateInterval: number = 1; - + /** * 销毁状态标志 */ @@ -334,8 +329,6 @@ export class Entity { const typeId = ComponentRegistry.getBitIndex(componentType); - component.entity = this; - this._componentsByTypeId[typeId] = component; const denseIndex = this.components.length; @@ -528,8 +521,6 @@ export class Entity { component: component }); } - - component.entity = null as any; // 通知所有相关的QuerySystem组件已变动 Entity.notifyQuerySystems(this); @@ -568,8 +559,6 @@ export class Entity { } component.onRemovedFromEntity(); - - component.entity = null as any; } this.components.length = 0; @@ -833,26 +822,6 @@ export class Entity { } } - /** - * 更新实体 - * - * 调用所有组件的更新方法,并更新子实体。 - */ - public update(): void { - if (!this.activeInHierarchy || this._isDestroyed) { - return; - } - - for (const component of this.components) { - if (component.enabled) { - component.update(); - } - } - - for (const child of this._children) { - child.update(); - } - } /** * 销毁实体 diff --git a/packages/core/src/ECS/IScene.ts b/packages/core/src/ECS/IScene.ts index 8ea21b28..51ff9a98 100644 --- a/packages/core/src/ECS/IScene.ts +++ b/packages/core/src/ECS/IScene.ts @@ -152,13 +152,4 @@ export interface ISceneConfig { * 场景名称 */ name?: string; - /** - * 调试配置 - */ - debug?: boolean; - /** - * 是否启用实体直接更新 - * @default false - */ - enableEntityDirectUpdate?: boolean; } \ No newline at end of file diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 55ad6efc..2f97e9bc 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -95,11 +95,6 @@ export class Scene implements IScene { this.name = config.name; } - // 配置实体直接更新选项 - if (config?.enableEntityDirectUpdate !== undefined) { - this.entities.setEnableEntityDirectUpdate(config.enableEntityDirectUpdate); - } - if (!Entity.eventBus) { Entity.eventBus = new EventBus(false); } @@ -180,16 +175,13 @@ export class Scene implements IScene { * 更新场景 */ public update() { - // 更新实体列表 + // 更新实体列表(处理延迟操作) this.entities.updateLists(); // 更新实体处理器 if (this.entityProcessors != null) this.entityProcessors.update(); - // 更新实体 - this.entities.update(); - // 更新实体处理器后处理 if (this.entityProcessors != null) this.entityProcessors.lateUpdate(); diff --git a/packages/core/src/ECS/Utils/EntityList.ts b/packages/core/src/ECS/Utils/EntityList.ts index 6214c329..1a923e18 100644 --- a/packages/core/src/ECS/Utils/EntityList.ts +++ b/packages/core/src/ECS/Utils/EntityList.ts @@ -16,10 +16,6 @@ export class EntityList { // 延迟操作队列 private _entitiesToAdd: Entity[] = []; private _entitiesToRemove: Entity[] = []; - private _isUpdating = false; - - // 是否启用实体直接更新 - private _enableEntityDirectUpdate = false; public get count(): number { return this.buffer.length; @@ -30,23 +26,11 @@ export class EntityList { } /** - * 设置是否启用实体直接更新 - */ - public setEnableEntityDirectUpdate(enabled: boolean): void { - this._enableEntityDirectUpdate = enabled; - } - - /** - * 添加实体(立即添加或延迟添加) + * 添加实体 * @param entity 要添加的实体 */ public add(entity: Entity): void { - if (this._isUpdating) { - // 如果正在更新中,延迟添加 - this._entitiesToAdd.push(entity); - } else { - this.addImmediate(entity); - } + this.addImmediate(entity); } /** @@ -67,16 +51,11 @@ export class EntityList { } /** - * 移除实体(立即移除或延迟移除) + * 移除实体 * @param entity 要移除的实体 */ public remove(entity: Entity): void { - if (this._isUpdating) { - // 如果正在更新中,延迟移除 - this._entitiesToRemove.push(entity); - } else { - this.removeImmediate(entity); - } + this.removeImmediate(entity); } /** @@ -147,25 +126,11 @@ export class EntityList { } /** - * 更新实体列表和实体 + * 更新实体列表 + * + * 处理延迟操作(添加/删除实体) */ public update(): void { - this._isUpdating = true; - - try { - // 只有启用实体直接更新时才遍历更新实体 - if (this._enableEntityDirectUpdate) { - for (let i = 0; i < this.buffer.length; i++) { - const entity = this.buffer[i]; - if (entity.enabled && !entity.isDestroyed) { - entity.update(); - } - } - } - } finally { - this._isUpdating = false; - } - // 处理延迟操作 this.updateLists(); } diff --git a/packages/core/src/Types/index.ts b/packages/core/src/Types/index.ts index 390e5a2c..7e687e1f 100644 --- a/packages/core/src/Types/index.ts +++ b/packages/core/src/Types/index.ts @@ -4,32 +4,20 @@ /** * 组件接口 - * - * 定义组件的基本契约,所有组件都应该实现此接口 + * + * 定义组件的基本契约。 + * 在 ECS 架构中,组件应该是纯数据容器,不包含业务逻辑。 */ export interface IComponent { /** 组件唯一标识符 */ readonly id: number; /** 组件所属的实体ID */ entityId?: string | number; - /** 组件启用状态 */ - enabled: boolean; - /** 更新顺序 */ - updateOrder: number; - + /** 组件添加到实体时的回调 */ onAddedToEntity(): void; /** 组件从实体移除时的回调 */ onRemovedFromEntity(): void; - /** 组件启用时的回调 */ - onEnabled(): void; - /** 组件禁用时的回调 */ - onDisabled(): void; - /** - * 更新组件 - * @deprecated 不符合ECS架构规范,建议使用EntitySystem来处理更新逻辑 - */ - update(): void; } /** diff --git a/packages/core/tests/ECS/Component.test.ts b/packages/core/tests/ECS/Component.test.ts index f55a0d35..4531d15d 100644 --- a/packages/core/tests/ECS/Component.test.ts +++ b/packages/core/tests/ECS/Component.test.ts @@ -6,9 +6,6 @@ class TestComponent extends Component { public value: number = 100; public onAddedCalled = false; public onRemovedCalled = false; - public onEnabledCalled = false; - public onDisabledCalled = false; - public updateCalled = false; public override onAddedToEntity(): void { this.onAddedCalled = true; @@ -17,18 +14,6 @@ class TestComponent extends Component { public override onRemovedFromEntity(): void { this.onRemovedCalled = true; } - - public override onEnabled(): void { - this.onEnabledCalled = true; - } - - public override onDisabled(): void { - this.onDisabledCalled = true; - } - - public override update(): void { - this.updateCalled = true; - } } class AnotherTestComponent extends Component { @@ -73,87 +58,10 @@ describe('Component - 组件基类测试', () => { }); }); - describe('启用状态管理', () => { - test('组件默认应该是启用的', () => { - expect(component.enabled).toBe(true); - }); - - test('设置组件禁用状态应该工作', () => { - component.enabled = false; - expect(component.enabled).toBe(false); - expect(component.onDisabledCalled).toBe(true); - }); - - test('重新启用组件应该工作', () => { - component.enabled = false; - component.onDisabledCalled = false; - component.onEnabledCalled = false; - - component.enabled = true; - expect(component.enabled).toBe(true); - expect(component.onEnabledCalled).toBe(true); - }); - - test('设置相同的状态不应该触发回调', () => { - component.enabled = true; // 已经是true - expect(component.onEnabledCalled).toBe(false); - - component.enabled = false; - component.onDisabledCalled = false; - - component.enabled = false; // 已经是false - expect(component.onDisabledCalled).toBe(false); - }); - - test('组件启用状态应该受实体状态影响', () => { - entity.addComponent(component); - expect(component.enabled).toBe(true); - - // 禁用实体应该让组件表现为禁用 - entity.enabled = false; - expect(component.enabled).toBe(false); - - // 重新启用实体 - entity.enabled = true; - expect(component.enabled).toBe(true); - }); - - test('组件自身禁用时即使实体启用也应该是禁用的', () => { - entity.addComponent(component); - - component.enabled = false; - entity.enabled = true; - - expect(component.enabled).toBe(false); - }); - - test('没有实体时组件状态应该只取决于自身', () => { - // 组件还没有添加到实体 - expect(component.enabled).toBe(true); - - component.enabled = false; - expect(component.enabled).toBe(false); - }); - }); - - describe('更新顺序', () => { - test('组件默认更新顺序应该是0', () => { - expect(component.updateOrder).toBe(0); - }); - - test('应该能够设置更新顺序', () => { - component.updateOrder = 10; - expect(component.updateOrder).toBe(10); - - component.updateOrder = -5; - expect(component.updateOrder).toBe(-5); - }); - }); - describe('生命周期回调', () => { test('添加到实体时应该调用onAddedToEntity', () => { expect(component.onAddedCalled).toBe(false); - + entity.addComponent(component); expect(component.onAddedCalled).toBe(true); }); @@ -161,106 +69,73 @@ describe('Component - 组件基类测试', () => { test('从实体移除时应该调用onRemovedFromEntity', () => { entity.addComponent(component); expect(component.onRemovedCalled).toBe(false); - + entity.removeComponent(component); expect(component.onRemovedCalled).toBe(true); }); - test('启用时应该调用onEnabled', () => { - component.enabled = false; - component.onEnabledCalled = false; - - component.enabled = true; - expect(component.onEnabledCalled).toBe(true); - }); - - test('禁用时应该调用onDisabled', () => { - expect(component.onDisabledCalled).toBe(false); - - component.enabled = false; - expect(component.onDisabledCalled).toBe(true); - }); - }); - - describe('更新方法', () => { - test('update方法应该可以被调用', () => { - expect(component.updateCalled).toBe(false); - - component.update(); - expect(component.updateCalled).toBe(true); - }); - test('基类的默认生命周期方法应该安全调用', () => { const baseComponent = new (class extends Component {})(); - + // 这些方法不应该抛出异常 expect(() => { baseComponent.onAddedToEntity(); baseComponent.onRemovedFromEntity(); - baseComponent.onEnabled(); - baseComponent.onDisabled(); - baseComponent.update(); }).not.toThrow(); }); }); - describe('实体关联', () => { - test('组件应该能够访问其所属的实体', () => { - entity.addComponent(component); - expect(component.entity).toBe(entity); + describe('实体-组件关系', () => { + test('组件可以被添加到实体', () => { + expect(() => { + entity.addComponent(component); + }).not.toThrow(); + + expect(entity.hasComponent(TestComponent)).toBe(true); + expect(entity.getComponent(TestComponent)).toBe(component); }); - test('组件移除后entity引用行为', () => { + test('组件可以从实体移除', () => { entity.addComponent(component); - expect(component.entity).toBe(entity); - - entity.removeComponent(component); - // 移除后entity引用可能被清空,这是正常行为 - // 具体行为取决于实现,这里只测试不会抛出异常 + expect(() => { - const _ = component.entity; + entity.removeComponent(component); }).not.toThrow(); + + expect(entity.hasComponent(TestComponent)).toBe(false); }); }); describe('边界情况', () => { - test('多次启用禁用应该工作正常', () => { - for (let i = 0; i < 10; i++) { - component.enabled = false; - expect(component.enabled).toBe(false); - - component.enabled = true; - expect(component.enabled).toBe(true); - } - }); - - test('极端更新顺序值应该被接受', () => { - component.updateOrder = 999999; - expect(component.updateOrder).toBe(999999); - - component.updateOrder = -999999; - expect(component.updateOrder).toBe(-999999); - }); - test('大量组件创建应该有不同的ID', () => { const components: Component[] = []; const count = 1000; - + for (let i = 0; i < count; i++) { components.push(new TestComponent()); } - + // 检查所有ID都不同 const ids = new Set(components.map(c => c.id)); expect(ids.size).toBe(count); }); + + test('组件应该是纯数据容器', () => { + // 验证组件只有数据字段 + const comp = new TestComponent(); + expect(comp.value).toBe(100); + + // 可以修改数据 + comp.value = 200; + expect(comp.value).toBe(200); + }); }); describe('继承和多态', () => { test('不同类型的组件应该都继承自Component', () => { const test1 = new TestComponent(); const test2 = new AnotherTestComponent(); - + expect(test1).toBeInstanceOf(Component); expect(test2).toBeInstanceOf(Component); expect(test1).toBeInstanceOf(TestComponent); @@ -269,9 +144,10 @@ describe('Component - 组件基类测试', () => { test('组件应该能够重写基类方法', () => { const test = new TestComponent(); - test.update(); - - expect(test.updateCalled).toBe(true); + + // 重写生命周期方法应该工作 + entity.addComponent(test); + expect(test.onAddedCalled).toBe(true); }); }); }); \ No newline at end of file diff --git a/packages/core/tests/ECS/Entity.test.ts b/packages/core/tests/ECS/Entity.test.ts index eef2136f..c94469b5 100644 --- a/packages/core/tests/ECS/Entity.test.ts +++ b/packages/core/tests/ECS/Entity.test.ts @@ -68,7 +68,7 @@ describe('Entity - 组件缓存优化测试', () => { expect(addedComponent).toBe(position); expect(entity.components.length).toBe(1); expect(entity.components[0]).toBe(position); - expect(position.entity).toBe(entity); + expect(entity.hasComponent(TestPositionComponent)).toBe(true); }); test('应该能够获取组件', () => { @@ -96,7 +96,7 @@ describe('Entity - 组件缓存优化测试', () => { entity.removeComponent(position); expect(entity.components.length).toBe(0); expect(entity.hasComponent(TestPositionComponent)).toBe(false); - expect(position.entity).toBeNull(); + expect(entity.getComponent(TestPositionComponent)).toBeNull(); }); });