Object Pools

Reusing fixed arrays as object pools to avoid per-frame allocation in Chasm.

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
end

return 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
end

Because 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
end

Full 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
end

Counting 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
end

Multiple 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.