Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

2D Camera system

examples/core/2d_camera.livemd

2D Camera system

Mix.install([
  {:zexray, github: "jn-jairo/zexray", depth: 1}
])

Code example

Example complexity rating: [★★☆☆] 2/4

defmodule Example do
  use Zexray.Enum
  use Zexray.Type

  @screen_width 800
  @screen_height 450
  @title "zexray [core] example - 2d camera"

  @max_buildings 100

  def init do
    # Initialize window
    Zexray.Window.with_window(@screen_width, @screen_height, @title, fn ->
      # Set our game to run at 60 frames-per-second
      Zexray.Timing.set_target_fps(60)

      # Manage resources loading/unloading
      Zexray.Resource.with_resource(
        fn ->
          player = type_rectangle(x: 400, y: 280, width: 40, height: 40)

          {_spacing, buildings} =
            0..@max_buildings
            |> Enum.reduce({0, []}, fn _, {spacing, buildings} ->
              width = get_random_value(50, 200)
              height = get_random_value(100, 800)

              building =
                type_rectangle(
                  width: width,
                  height: height,
                  y: @screen_height - 130 - height,
                  x: -6_000 + spacing
                )

              build_color =
                type_color(
                  r: trunc(get_random_value(200, 240)),
                  g: trunc(get_random_value(200, 240)),
                  b: trunc(get_random_value(200, 250)),
                  a: 255
                )

              {spacing + width, [{building, build_color} | buildings]}
            end)

          camera =
            type_camera_2d(
              target:
                type_vector2(
                  x: type_rectangle(player, :x) + 20,
                  y: type_rectangle(player, :y) + 20
                ),
              offset:
                type_vector2(
                  x: @screen_width / 2,
                  y: @screen_height / 2
                ),
              rotation: 0,
              zoom: 1
            )

          {player, camera, buildings}
        end,
        &loop/1
      )
    end)
  end

  defp get_random_value(min, max) do
    :rand.uniform() * (max - min) + min
  end

  defp loop({player, camera, buildings}) do
    # Detect window close button or ESC key
    if Zexray.Window.should_close?() do
      :ok
    else
      # Update
      type_rectangle(x: player_x, y: player_y) = player

      type_camera_2d(
        rotation: camera_rotation,
        zoom: camera_zoom
      ) = camera

      # Player movement
      player_x =
        cond do
          Zexray.Keyboard.down?(enum_keyboard_key(:right)) -> player_x + 2
          Zexray.Keyboard.down?(enum_keyboard_key(:left)) -> player_x - 2
          true -> player_x
        end

      # Camera target follows player
      camera_target = type_vector2(x: player_x + 20, y: player_y + 20)

      # Camera rotation controls
      camera_rotation =
        cond do
          Zexray.Keyboard.down?(enum_keyboard_key(:a)) -> camera_rotation - 1
          Zexray.Keyboard.down?(enum_keyboard_key(:s)) -> camera_rotation + 1
          true -> camera_rotation
        end

      # Limit camera rotation to 80 degrees (-40 to 40)
      camera_rotation =
        cond do
          camera_rotation > 40 -> 40
          camera_rotation < -40 -> -40
          true -> camera_rotation
        end

      # Camera zoom controls
      # Uses log scaling to provide consistent zoom speed
      camera_zoom = :math.exp(:math.log(camera_zoom) + Zexray.Mouse.get_wheel_move() * 0.1)

      camera_zoom =
        cond do
          camera_zoom > 3 -> 3
          camera_zoom < 0.1 -> 0.1
          true -> camera_zoom
        end

      # Camera reset (zoom and rotation)
      {camera_zoom, camera_rotation} =
        if Zexray.Keyboard.pressed?(enum_keyboard_key(:r)) do
          {1, 0}
        else
          {camera_zoom, camera_rotation}
        end

      player = type_rectangle(player, x: player_x)

      camera =
        type_camera_2d(camera,
          target: camera_target,
          rotation: camera_rotation,
          zoom: camera_zoom
        )

      # Draw

      Zexray.Drawing.with_drawing(fn ->
        Zexray.Drawing.clear_background(enum_color(:raywhite))

        Zexray.Drawing.with_mode_2d(camera, fn ->
          Zexray.Shape.draw_rectangle(-6_000, 320, 13_000, 8_000, enum_color(:darkgray))

          buildings
          |> Enum.each(fn {building, build_color} ->
            Zexray.Shape.draw_rectangle_rec(building, build_color)
          end)

          Zexray.Shape.draw_rectangle_rec(player, enum_color(:red))

          type_vector2(x: camera_target_x, y: camera_target_y) = camera_target

          Zexray.Shape.draw_line(
            trunc(camera_target_x),
            @screen_height * -10,
            trunc(camera_target_x),
            @screen_width * 10,
            enum_color(:green)
          )

          Zexray.Shape.draw_line(
            @screen_width * -10,
            trunc(camera_target_y),
            @screen_width * 10,
            trunc(camera_target_y),
            enum_color(:green)
          )
        end)

        Zexray.Text.draw("SCREEN AREA", 640, 10, 20, enum_color(:red))

        Zexray.Shape.draw_rectangle_lines_ex(
          type_rectangle(
            x: 0,
            y: 0,
            width: @screen_width,
            height: @screen_height
          ),
          5,
          enum_color(:red)
        )

        Zexray.Shape.draw_rectangle(10, 10, 250, 113, type_color(enum_color(:skyblue), a: 127))
        Zexray.Shape.draw_rectangle_lines(10, 10, 250, 113, enum_color(:blue))

        Zexray.Text.draw("Free 2d camera controls:", 20, 20, 10, enum_color(:black))
        Zexray.Text.draw("- Right/Left to move Offset", 40, 40, 10, enum_color(:darkgray))
        Zexray.Text.draw("- Mouse Wheel to Zoom in-out", 40, 60, 10, enum_color(:darkgray))
        Zexray.Text.draw("- A / S to Rotate", 40, 80, 10, enum_color(:darkgray))
        Zexray.Text.draw("- R to reset Zoom and Rotation", 40, 100, 10, enum_color(:darkgray))
      end)

      {player, camera, buildings}
      |> loop()
    end
  end
end
Example.init()