Object pools are a classic game programming technique: instead of creating and destroying objects every frame, you pre-allocate a fixed array and mark slots as active or inactive. This keeps memory flat, avoids allocation overhead, and plays well with Chasm's arena system.
The Pattern
The core idea is a struct with an active flag and a fixed array of them:
defstruct Bullet do
x :: float = 0.0
y :: float = 0.0
vx :: float = 0.0
vy :: float = 0.0
active :: int = 0 # 0 = slot is free, 1 = slot is live
end
@bullets :: script = array_fixed(128, Bullet{})array_fixed(128, Bullet{}) reserves exactly 128 Bullet slots up front. No allocation ever happens at spawn time, you just flip active to 1.
Spawning
Find the first free slot and write into it:
defp spawn_bullet(x :: float, y :: float, vx :: float, vy :: float) do
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 0 do
@bullets.set(i, b with { x: x, y: y, vx: vx, vy: vy, active: 1 })
return 0
end
i = i + 1
end
# Pool is full, silently drop the spawn.
# This is intentional: it caps bullet count without crashing.
return 0
endreturn 0 is used because Chasm functions need a return value; the result is discarded by the caller.
Updating and Despawning
Update every active bullet, and deactivate any that should be removed:
defp update_bullets(dt :: float) do
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 1 do
b = b with { x: b.x + b.vx * dt, y: b.y + b.vy * dt }
# Despawn when off-screen
if b.x < 0.0 or b.x > screen_width() or b.y < 0.0 or b.y > screen_height() do
b = b with { active: 0 }
end
@bullets.set(i, b)
end
i = i + 1
end
endBecause with produces a new struct value (Chasm structs are value types), you always write back with .set.
Drawing
Only draw active bullets:
defp draw_bullets() do
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 1 do
draw_rectangle(b.x - 3.0, b.y - 3.0, 6.0, 6.0, 0xffff00ff)
end
i = i + 1
end
endFull Example
Putting it together with a player that fires on spacebar:
defstruct Bullet do
x :: float = 0.0
y :: float = 0.0
vx :: float = 0.0
vy :: float = 0.0
active :: int = 0
end
@bullets :: script = array_fixed(128, Bullet{})
@player_x :: script = 400.0
@player_y :: script = 500.0
@fire_timer :: script = 0.0
defp spawn_bullet(x :: float, y :: float, vx :: float, vy :: float) do
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 0 do
@bullets.set(i, b with { x: x, y: y, vx: vx, vy: vy, active: 1 })
return 0
end
i = i + 1
end
return 0
end
defp update_bullets(dt :: float) do
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 1 do
b = b with { x: b.x + b.vx * dt, y: b.y + b.vy * dt }
if b.y < 0.0 do
b = b with { active: 0 }
end
@bullets.set(i, b)
end
i = i + 1
end
end
def on_tick(dt :: float) do
if key_down(:left) do @player_x = @player_x - 300.0 * dt end
if key_down(:right) do @player_x = @player_x + 300.0 * dt end
@fire_timer = @fire_timer - dt
if key_down(:space) and @fire_timer <= 0.0 do
spawn_bullet(@player_x, @player_y, 0.0, -600.0)
@fire_timer = 0.1 # fire rate: 10 bullets/sec
end
update_bullets(dt)
end
def on_draw() do
clear_background(0x101018ff)
draw_rectangle(@player_x - 12.0, @player_y - 8.0, 24.0, 16.0, 0x44aaff ff)
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 1 do
draw_rectangle(b.x - 3.0, b.y - 6.0, 6.0, 12.0, 0xffff00ff)
end
i = i + 1
end
endCounting Active Objects
Sometimes you need to know how many slots are in use, for scoring, UI, or logic:
defp count_active() :: int do
count = 0
i = 0
while i < @bullets.len do
b = @bullets.get(i)
if b.active == 1 do
count = count + 1
end
i = i + 1
end
return count
endMultiple Pools
Use separate arrays for different object types:
@bullets :: script = array_fixed(128, Bullet{})
@enemies :: script = array_fixed(32, Enemy{})
@particles :: script = array_fixed(256, Particle{})Each pool is independently sized for its expected maximum. Choosing pool sizes conservatively (a bit larger than you expect) is cheaper than dynamic allocation, these are just stack-allocated arrays in the script arena.
Why Not array_new?
array_new grows dynamically on demand, which is convenient but:
- Allocates from the script arena on every push
- Cannot reclaim memory from individual elements, only a full arena reset frees it
For objects that spawn and despawn frequently (bullets, particles, enemies), a fixed pool with an active flag is the right choice. Use array_new for lists that grow once and don't need per-element deallocation.