Files
KateLegend2_proj/assets/scripts/common/ObjectPool.ts
T
2026-05-06 08:17:32 +08:00

116 lines
3.7 KiB
TypeScript

/**
* Generic object pool used by damage effects, bullets, enemies and VFX
* (requirement 18.5). Pure-TS, platform-agnostic, Jest-testable.
*
* Design notes:
* - `factory` creates a brand-new instance when the free list is empty.
* - `resetter` is invoked on every released object, letting the caller wipe
* transient state (position, timers, listeners) before it goes back to
* the pool.
* - `maxSize` caps the retained instances; objects released beyond the cap
* are dropped (letting the GC collect them) to bound memory usage
* (requirement 18.4: memory peak ≤ 200MB).
* - Double-release is silently ignored but reported through `onDoubleRelease`
* so tests / Logger can assert correctness.
*/
export type ObjectFactory<T> = () => T;
export type ObjectResetter<T> = (obj: T) => void;
export interface ObjectPoolOptions<T> {
/** Required creator invoked when the pool is empty. */
factory: ObjectFactory<T>;
/** Optional cleaner invoked on every `release`. */
resetter?: ObjectResetter<T>;
/** Max retained objects; excess releases are discarded. Default 128. */
maxSize?: number;
/** Optional pre-warm count (creates this many objects upfront). Default 0. */
preAlloc?: number;
/** Optional diagnostic hook. */
onDoubleRelease?: (obj: T) => void;
}
export class ObjectPool<T> {
private readonly free: T[] = [];
private readonly borrowed = new Set<T>();
private readonly factory: ObjectFactory<T>;
private readonly resetter?: ObjectResetter<T>;
private readonly maxSize: number;
private readonly onDoubleRelease?: (obj: T) => void;
// Diagnostics
private _acquiredTotal = 0;
private _recycledTotal = 0;
private _createdTotal = 0;
constructor(options: ObjectPoolOptions<T>) {
this.factory = options.factory;
this.resetter = options.resetter;
this.maxSize = options.maxSize ?? 128;
this.onDoubleRelease = options.onDoubleRelease;
const preAlloc = options.preAlloc ?? 0;
for (let i = 0; i < preAlloc; i++) {
const inst = this.factory();
this._createdTotal++;
this.free.push(inst);
}
}
/** Acquire an object from the pool (creates one if empty). */
public acquire(): T {
this._acquiredTotal++;
const inst = this.free.pop();
if (inst !== undefined) {
this.borrowed.add(inst);
return inst;
}
const created = this.factory();
this._createdTotal++;
this.borrowed.add(created);
return created;
}
/** Release an object back to the pool. Double-releases are ignored. */
public release(obj: T): void {
if (!this.borrowed.has(obj)) {
this.onDoubleRelease?.(obj);
return;
}
this.borrowed.delete(obj);
this.resetter?.(obj);
if (this.free.length < this.maxSize) {
this.free.push(obj);
this._recycledTotal++;
}
// else: drop the object so the GC can reclaim it.
}
/** Number of objects currently held in the free list. */
public get freeCount(): number {
return this.free.length;
}
/** Number of objects that are currently out on loan. */
public get borrowedCount(): number {
return this.borrowed.size;
}
/** Drop everything. Used on scene unload. */
public drain(): void {
this.free.length = 0;
this.borrowed.clear();
}
/** Diagnostic stats (used by Logger / perf BI埋点). */
public stats() {
return {
free: this.freeCount,
borrowed: this.borrowedCount,
acquired: this._acquiredTotal,
recycled: this._recycledTotal,
created: this._createdTotal,
};
}
}