116 lines
3.7 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|