Camera & Viewport

Camera follow, smoothing, shake, and world-to-screen transforms in Chasm.

A camera in a 2D game is just an offset applied to every draw call. Store the camera position as script-lifetime attributes and subtract it from world coordinates before drawing.

Basic Camera Follow

The simplest camera: center on the player every frame.

@cam_x :: script = 0.0
@cam_y :: script = 0.0

defp update_camera(target_x :: float, target_y :: float) do
  sw :: frame = to_float(screen_width())
  sh :: frame = to_float(screen_height())
  @cam_x = copy_to_script(target_x - sw * 0.5)
  @cam_y = copy_to_script(target_y - sh * 0.5)
end

defp world_to_screen_x(wx :: float) :: float do
  return wx - @cam_x
end

defp world_to_screen_y(wy :: float) :: float do
  return wy - @cam_y
end

Use world_to_screen_x / world_to_screen_y on every draw call:

def on_draw() do
  draw_rectangle(
    world_to_screen_x(@player_x),
    world_to_screen_y(@player_y),
    16.0, 16.0, :white
  )
end

Smooth Follow

Hard-locking the camera to the player feels jarring. Lerp toward the target instead:

@cam_x :: script = 0.0
@cam_y :: script = 0.0

defp update_camera(target_x :: float, target_y :: float, dt :: float) do
  sw      :: frame = to_float(screen_width())
  sh      :: frame = to_float(screen_height())
  goal_x  :: frame = target_x - sw * 0.5
  goal_y  :: frame = target_y - sh * 0.5
  speed   :: frame = 6.0
  @cam_x = copy_to_script(lerp(@cam_x, goal_x, speed * dt))
  @cam_y = copy_to_script(lerp(@cam_y, goal_y, speed * dt))
end

Increase speed for a tighter follow, decrease it for a floatier feel.

Camera Shake

Store a shake magnitude and decay it each frame. Add a random offset to the camera when drawing.

@cam_x         :: script = 0.0
@cam_y         :: script = 0.0
@shake_mag     :: script = 0.0
@shake_decay   :: script = 8.0

defp add_shake(amount :: float) do
  @shake_mag = copy_to_script(@shake_mag + amount)
end

defp update_shake(dt :: float) do
  if @shake_mag > 0.0 do
    @shake_mag = copy_to_script(@shake_mag - @shake_decay * dt)
    if @shake_mag < 0.0 do
      @shake_mag = copy_to_script(0.0)
    end
  end
end

defp shake_offset_x() :: float do
  if @shake_mag <= 0.0 do return 0.0 end
  return (random_float() * 2.0 - 1.0) * @shake_mag
end

defp shake_offset_y() :: float do
  if @shake_mag <= 0.0 do return 0.0 end
  return (random_float() * 2.0 - 1.0) * @shake_mag
end

Then offset draw calls by shake_offset_x() / shake_offset_y() on top of the camera transform.

Camera Bounds

Clamp the camera so it never shows outside the world:

defp clamp_camera(world_w :: float, world_h :: float) do
  sw    :: frame = to_float(screen_width())
  sh    :: frame = to_float(screen_height())
  max_x :: frame = world_w - sw
  max_y :: frame = world_h - sh
  @cam_x = copy_to_script(clamp(@cam_x, 0.0, max_x))
  @cam_y = copy_to_script(clamp(@cam_y, 0.0, max_y))
end

Call clamp_camera after update_camera each tick.