Created
May 6, 2026 16:09
-
-
Save Stanko/ea2e4ec26226759fa57865e43f69a3f6 to your computer and use it in GitHub Desktop.
Example of Kaplay object pooling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| }, | |
| }; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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