You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.8 KiB
202 lines
6.8 KiB
|
3 weeks ago
|
import type { EntityId } from 'db://assets/mx/module/ecs/EntityId';
|
||
|
|
import { ECS_INSTANCE_TYPE, type EcsComponent, type ComponentCtor } from 'db://assets/mx/module/ecs/Component';
|
||
|
|
import { View1, View2 } from 'db://assets/mx/module/ecs/View';
|
||
|
|
import { createMembershipSet, membershipAdd, membershipForEach, membershipHas, type MembershipSet } from '../../tools/MembershipSet';
|
||
|
|
|
||
|
|
const COMPONENT_CTOR_PREFIX = '__mx_ecs_ctor__';
|
||
|
|
const _duplicateCtorWarned = createMembershipSet<string>();
|
||
|
|
|
||
|
|
export function componentTypeKey(ctor: ComponentCtor<EcsComponent>): string {
|
||
|
|
return ctor.ecsTypeId ?? ctor.name ?? 'UnknownComponent';
|
||
|
|
}
|
||
|
|
|
||
|
|
function canonicalComponentCtor<T extends EcsComponent>(ctor: ComponentCtor<T>): ComponentCtor<T> {
|
||
|
|
const typeKey = componentTypeKey(ctor);
|
||
|
|
const g = globalThis as unknown as Record<string, ComponentCtor<EcsComponent> | undefined>;
|
||
|
|
const key = `${COMPONENT_CTOR_PREFIX}${typeKey}`;
|
||
|
|
const existing = g[key];
|
||
|
|
if (!existing) {
|
||
|
|
g[key] = ctor;
|
||
|
|
return ctor;
|
||
|
|
}
|
||
|
|
if (existing !== ctor && !membershipHas(_duplicateCtorWarned, typeKey)) {
|
||
|
|
membershipAdd(_duplicateCtorWarned, typeKey);
|
||
|
|
console.log('[World] duplicate component ctor merged', typeKey);
|
||
|
|
}
|
||
|
|
return existing as ComponentCtor<T>;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 单类型组件存储桶 */
|
||
|
|
export class ComponentStorage<T extends EcsComponent> {
|
||
|
|
private readonly _map = new Map<EntityId, T>();
|
||
|
|
|
||
|
|
set(id: EntityId, comp: T): void {
|
||
|
|
this._map.set(id, comp);
|
||
|
|
}
|
||
|
|
|
||
|
|
get(id: EntityId): T | undefined {
|
||
|
|
return this._map.get(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
has(id: EntityId): boolean {
|
||
|
|
return this._map.has(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
delete(id: EntityId): void {
|
||
|
|
this._map.delete(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 遍历桶内所有组件(含已销毁实体的残留,调用方需自行 isAlive 过滤) */
|
||
|
|
forEach(fn: (id: EntityId, comp: T) => void): void {
|
||
|
|
this._map.forEach((comp, id) => fn(id, comp));
|
||
|
|
}
|
||
|
|
|
||
|
|
get size(): number {
|
||
|
|
return this._map.size;
|
||
|
|
}
|
||
|
|
|
||
|
|
clear(): void {
|
||
|
|
this._map.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 实体世界:组件分桶存储 + 实体生命周期 + 查询 */
|
||
|
|
export class World {
|
||
|
|
private _nextId: EntityId = 1;
|
||
|
|
private readonly _alive = createMembershipSet<EntityId>();
|
||
|
|
private readonly _storagesByTypeId = new Map<string, ComponentStorage<any>>();
|
||
|
|
|
||
|
|
/** 下一实体 ID(快照恢复用) */
|
||
|
|
get nextEntityId(): EntityId {
|
||
|
|
return this._nextId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 设置下一实体 ID(快照恢复前调用,须 ≥ 已有最大实体 ID + 1) */
|
||
|
|
setNextEntityId(next: EntityId): void {
|
||
|
|
this._nextId = Math.max(1, next);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 创建新实体 */
|
||
|
|
spawn(): EntityId {
|
||
|
|
const id = this._nextId++;
|
||
|
|
membershipAdd(this._alive, id);
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 以指定 ID 复活实体(战斗快照冷恢复) */
|
||
|
|
spawnWithId(id: EntityId): EntityId {
|
||
|
|
if (id <= 0) return this.spawn();
|
||
|
|
membershipAdd(this._alive, id);
|
||
|
|
if (id >= this._nextId) this._nextId = id + 1;
|
||
|
|
return id;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 销毁实体(组件数据保留以便本帧查询,下帧清理) */
|
||
|
|
destroy(id: EntityId): void {
|
||
|
|
this._alive.delete(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 实体是否存活 */
|
||
|
|
isAlive(id: EntityId): boolean {
|
||
|
|
return id !== 0 && this._alive.has(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 清理已销毁实体的残留组件(可定期调用) */
|
||
|
|
gc(): void {
|
||
|
|
const alive = this._alive;
|
||
|
|
this._storagesByTypeId.forEach((storage) => {
|
||
|
|
storage.forEach((id) => {
|
||
|
|
if (!alive.has(id)) storage.delete(id);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private _storageForTypeId<T extends EcsComponent>(typeId: string): ComponentStorage<T> {
|
||
|
|
let s = this._storagesByTypeId.get(typeId);
|
||
|
|
if (!s) {
|
||
|
|
s = new ComponentStorage<T>();
|
||
|
|
this._storagesByTypeId.set(typeId, s);
|
||
|
|
}
|
||
|
|
return s as ComponentStorage<T>;
|
||
|
|
}
|
||
|
|
|
||
|
|
private _storage<T extends EcsComponent>(ctor: ComponentCtor<T>): ComponentStorage<T> {
|
||
|
|
const canonical = canonicalComponentCtor(ctor);
|
||
|
|
return this._storageForTypeId(componentTypeKey(canonical));
|
||
|
|
}
|
||
|
|
|
||
|
|
private _tagInstanceType(comp: EcsComponent, ctor: ComponentCtor<EcsComponent>): void {
|
||
|
|
(comp as Record<symbol, string>)[ECS_INSTANCE_TYPE] = componentTypeKey(ctor);
|
||
|
|
}
|
||
|
|
|
||
|
|
private _matchesInstanceType(comp: EcsComponent, expectedTypeId: string): boolean {
|
||
|
|
const actual = (comp as Record<symbol, string | undefined>)[ECS_INSTANCE_TYPE];
|
||
|
|
if (actual == null) return true;
|
||
|
|
if (actual === expectedTypeId) return true;
|
||
|
|
console.error(`[World] component type mismatch expected=${expectedTypeId} actual=${actual}`);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 添加组件(覆盖已有) */
|
||
|
|
addComponent<T extends EcsComponent>(id: EntityId, comp: T): T {
|
||
|
|
const ctor = canonicalComponentCtor(comp.constructor as ComponentCtor<T>);
|
||
|
|
this._tagInstanceType(comp, comp.constructor as ComponentCtor<T>);
|
||
|
|
this._storage(ctor).set(id, comp);
|
||
|
|
return comp;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 读取组件,不存在返回 undefined */
|
||
|
|
getComponent<T extends EcsComponent>(id: EntityId, ctor: ComponentCtor<T>): T | undefined {
|
||
|
|
const typeId = componentTypeKey(canonicalComponentCtor(ctor));
|
||
|
|
const comp = this._storage(ctor).get(id);
|
||
|
|
if (!comp) return undefined;
|
||
|
|
if (!this._matchesInstanceType(comp, typeId)) return undefined;
|
||
|
|
return comp as T;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 读取组件,不存在抛错 */
|
||
|
|
requireComponent<T extends EcsComponent>(id: EntityId, ctor: ComponentCtor<T>): T {
|
||
|
|
const comp = this.getComponent(id, ctor);
|
||
|
|
if (!comp) throw new Error(`[World] entity ${id} missing component ${componentTypeKey(ctor)}`);
|
||
|
|
return comp;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 是否拥有某组件 */
|
||
|
|
hasComponent<T extends EcsComponent>(id: EntityId, ctor: ComponentCtor<T>): boolean {
|
||
|
|
return this.getComponent(id, ctor) != null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 移除组件 */
|
||
|
|
removeComponent<T extends EcsComponent>(id: EntityId, ctor: ComponentCtor<T>): void {
|
||
|
|
this._storage(ctor).delete(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 单组件查询视图 */
|
||
|
|
view<C extends EcsComponent>(ctor: ComponentCtor<C>): View1<C> {
|
||
|
|
return new View1(this._storage(ctor), (id) => this.isAlive(id));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 双组件查询视图(按 ctorA 桶遍历 + 过滤 ctorB) */
|
||
|
|
view2<A extends EcsComponent, B extends EcsComponent>(
|
||
|
|
ctorA: ComponentCtor<A>,
|
||
|
|
ctorB: ComponentCtor<B>,
|
||
|
|
): View2<A, B> {
|
||
|
|
return new View2(
|
||
|
|
this._storage(ctorA),
|
||
|
|
ctorB,
|
||
|
|
(id, ctor) => this.getComponent(id, ctor),
|
||
|
|
(id) => this.isAlive(id),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 遍历所有存活实体 */
|
||
|
|
forEachAlive(fn: (id: EntityId) => void): void {
|
||
|
|
membershipForEach(this._alive, fn);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 存活实体数量 */
|
||
|
|
get aliveCount(): number {
|
||
|
|
return this._alive.size;
|
||
|
|
}
|
||
|
|
}
|