Skip to content

Instantly share code, notes, and snippets.

@Stanko
Created May 6, 2026 16:09
Show Gist options
  • Select an option

  • Save Stanko/ea2e4ec26226759fa57865e43f69a3f6 to your computer and use it in GitHub Desktop.

Select an option

Save Stanko/ea2e4ec26226759fa57865e43f69a3f6 to your computer and use it in GitHub Desktop.
Example of Kaplay object pooling
import type { Comp, GameObj, KEventController } from "kaplay";
import type { Pool } from "../utils/pool";
export type RecycleListener = () => void;
export interface ObjectPoolComp extends Comp {
controllers: KEventController[];
onRecycle(listener: RecycleListener): () => void;
recycle(): void;
enable(): void;
}
interface ObjectPoolOptions {
onRecycle?: RecycleListener;
}
export const objectPool = (
pool: Pool,
options: ObjectPoolOptions = {},
): ObjectPoolComp => {
const { onRecycle: initialOnRecycle } = options;
let recycleListeners: RecycleListener[] = [];
return {
id: "object-pool",
controllers: [],
recycle() {
const obj = this as unknown as GameObj<ObjectPoolComp>;
pool.add(obj);
initialOnRecycle?.();
[...recycleListeners].forEach((listener) => listener());
recycleListeners = [];
},
onRecycle(listener) {
recycleListeners.push(listener);
return () => {
const index = recycleListeners.indexOf(listener);
if (index !== -1) {
recycleListeners.splice(index, 1);
}
};
},
enable() {
const obj = this as unknown as GameObj<ObjectPoolComp>;
obj.paused = false;
obj.hidden = false;
},
};
};
import type { GameObj } from "kaplay";
import type { ObjectPoolComp } from "../components/object-pool";
// This is my game layer object which gets paused, you can just use k.add instead of g.add below
import { g } from "../global/scenes";
interface PoolOptions {
max?: number;
}
type PoolItem = GameObj<ObjectPoolComp>;
type PoolRoot<T extends PoolItem> = GameObj<{ pool: T[] }>;
export class Pool<T extends PoolItem = PoolItem> {
type: string;
max: number;
root: PoolRoot<T> = this.createRoot();
create: () => T;
constructor(type: string, create: () => T, options: PoolOptions = {}) {
const { max = 100 } = options;
this.type = type;
this.create = create;
this.max = max;
}
add(item: T) {
if (!this.root.exists()) {
this.root = this.createRoot();
}
if (this.root.pool.length < this.max) {
item.tag("pooled");
item.paused = true;
item.hidden = true;
item.controllers.forEach((c) => c.cancel());
item.controllers = [];
this.root.pool.push(item);
} else {
item.destroy();
}
}
get(): T {
if (!this.root.exists()) {
return this.create();
}
// Debug, commented out on purpose
// console.log("pool", this.type, this.root.pool.length);
const item = this.root.pool.pop();
if (item) {
item.untag("pooled");
return item;
}
return this.create();
}
private createRoot(): PoolRoot<T> {
return g.add([`pool--${this.type}`, { pool: [] as T[] }]) as PoolRoot<T>;
}
}
type Bullet = {
export type Bullet = GameObj<
| string
| SpriteComp
| AreaComp
| AnchorComp
| PosComp
| ObjectPoolComp
| {
velocity: Vec2;
}
>;
}
// Method which will be used to create a "blank" bullet if there is none available in the pool
export const createBullet = (): Bullet => {
const bullet: Bullet = g.add([
k.sprite("bullet"),
k.area({
shape: new k.Rect(k.vec2(0, 0), 6, 6),
isSensor: true,
}),
k.anchor("bot"),
k.pos(),
objectPool(bullets, {
// Remove tags to avoid checking for collisions
onRecycle: () => {
bullet.untag("bullet");
bullet.untag("enemy-bullet");
},
}),
{
velocity: k.vec2(0, -230),
},
]);
return bullet;
};
// The pool
const bullets = new Pool<Bullet>("bullet", createBullet);
// Method to add a bullet to the game which gets them from the pool
export const addBullet = (pos: Vec2, velocity: Vec2 = k.vec2(0, -230)) => {
const bullet = bullets.get();
// Here you need to set all of the initial properties
bullet.tag("bullet");
bullet.velocity = velocity;
bullet.moveTo(pos);
const updateController = bullet.onUpdate(() => {
bullet.move(bullet.velocity.x, bullet.velocity.y);
if (isOutOfGameField(bullet.pos, 5)) {
// Use .recycle() instead of .destroy()
bullet.recycle();
}
});
const collideController = bullet.onCollide("enemy", (enemy) => {
// do your collision thing
});
// Make sure to add the controllers so they can be cancelled when the bullet is recycled
bullet.controllers.push(updateController, collideController);
// Re-enable the bullet (show and unpause it)
bullet.enable();
return bullet;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment