PVG
36. Camera Zoom Level Mode — Program Video Games

36. Camera Zoom Level Mode

[[programvideogames]]In this lesson we'll add the ability to zoom in and out in the editor as well as automatically entering "Level Mode" when zoomed out far enough.

Level Mode will be how we add extra levels, resize them, and connect them together.

We're also going to start to remove backward compatibility with the old LDtk system.

To that end, go ahead and delete all the LDtk types in main.odin as well as the entire level_parse_and_store proc.

// Also delete this from `game_init`
-       // Load level data
-       {
-               level_data, ok := os.read_entire_file("data/world.ldtk", allocator = context.allocator)
-               assert(ok, "Failed to load level data")
-
-               ldtk_data := new(LDtk_Data, context.temp_allocator)
-               err := json.unmarshal(level_data, ldtk_data, allocator = context.temp_allocator)
-               if err != nil {
-                       log.panicf("failed to parse json: %v", err)
-               }
-
-               for &level in ldtk_data.levels {
-                       level_parse_and_store(gs, &level)
-               }
-       }

While we are in main, add this code to game_update where we turn off Editor Mode:

if rl.IsKeyPressed(.F5) {
        gs.editor_enabled = !gs.editor_enabled
        gs.camera.zoom = ZOOM // New
}

And finally, remove the line that saves world data in the main proc:

world_data_save() // Remove this
world_data_load() // Keep this

Next, let's head over to editor.odin and add the new functionality:

Editor_Tool :: enum {
    // ...
    Level, // New
}

Editor_State :: struct {
    // ...
    previous_tool: Editor_Tool, // New
}

We are storing the previous tool when we auto-switch to Level Mode for a bit of Quality of Life.

Now we're going to add zooming and panning.

In editor_update we want to add this functionality at the top and move our tool changing code a bit:

scroll := rl.GetMouseWheelMove()
if scroll != 0 {
    mouse_pos := rl.GetMousePosition()

    // Use raylib's GetScreenToWorld2D to zoom in/out at the cursor position
    mouse_world_pos := rl.GetScreenToWorld2D(mouse_pos, gs.camera)

    // Clamp zoom
    gs.camera.zoom = clamp(gs.camera.zoom + scroll * 0.25, 0.25, 8)

    mouse_world_pos_new := rl.GetScreenToWorld2D(mouse_pos, gs.camera)

    gs.camera.target += (mouse_world_pos - mouse_world_pos_new)
}

if es.tool == .Level {
    if gs.camera.zoom >= 1 {
        es.tool = es.previous_tool
    }
} else {
    if rl.IsKeyPressed(.T) { // Moved
        es.tool = .Tile
    }

    if rl.IsKeyPressed(.S) { // Moved
        es.tool = .Spike
    }

    es.previous_tool = es.tool
    if gs.camera.zoom < 1 {
        es.tool = .Level
    }
}

// Panning
if rl.IsMouseButtonDown(.MIDDLE) {
    mouse_delta := rl.GetMouseDelta()
    gs.camera.target -= mouse_delta / gs.camera.zoom
}

Next we'll add the beginnings of Level creation code in the switch statement:

case .Level:
    if rl.IsMouseButtonPressed(.LEFT) || rl.IsMouseButtonPressed(.RIGHT) {
        es.area_begin = coords
    }

    if rl.IsMouseButtonDown(.LEFT) || rl.IsMouseButtonDown(.RIGHT) {
        es.area_end = coords
    }

There will be more to come here, but we need to switch topics for the next lesson. We'll need to add undo, redo, and save before we continue too far into the editor's features.

Down in editor_draw I have added more useful debug text:

    // Delete the previous text
    rl.DrawTextEx(
        gs.font_48,
        fmt.ctprintf(
            "Tool: %s
Camera.Zoom: %v
Camera.Target: %v",
            es.tool,
            gs.camera.zoom,
            gs.camera.target,
        ),
        {8, 8},
        24,
        0,
        rl.WHITE,
    )

I've also added a convenience feature to see the minimum level size for our chosen render and tile size.

Just below that code we draw the tool rectangle - we want to add this new block below the existing code:

if place || remove {
    // ... existing code remains the same ...

    if es.tool == .Level {
        one_screen_rect := rect
        one_screen_rect.width =
            math.ceil(f32(RENDER_WIDTH) / TILE_SIZE) * TILE_SIZE * gs.camera.zoom
        one_screen_rect.height =
            math.ceil(f32(RENDER_HEIGHT) / TILE_SIZE) * TILE_SIZE * gs.camera.zoom
        rl.DrawRectangleLinesEx(one_screen_rect, 1, rl.DARKGRAY)
    }
}

// Draw level bounds and name
// name is just "level_<id>"
for _, ld in gs.level_definitions {
    level_min := ld.level_min - gs.camera.target
    level_max := ld.level_max - gs.camera.target
    level_size := level_max - level_min
    level_rect := Rect{level_min.x, level_min.y, level_size.x, level_size.y}
    level_rect = rect_scale_all(level_rect, gs.camera.zoom)

    color := ld.id == gs.level.id ? rl.WHITE : rl.GRAY
    thickness := f32(1)

    if es.tool == .Level {
        thickness = 4
        text := fmt.ctprintf("level_%d", ld.id)
        text_size := rl.MeasureTextEx(gs.font_48, text, 48, 0)
        text_pos :=
            Vec2{level_rect.x, level_rect.y} +
            ({level_rect.width, level_rect.height} - text_size) / 2
        rl.DrawTextEx(gs.font_48, text, text_pos, 48, 0, {255, 255, 255, 128})
    }

    rl.DrawRectangleLinesEx(level_rect, thickness, color)
}

That's it for this one. In the next few lessons we'll be implementing undo, redo, and saving, so stay tuned!