PVG
8. Introduction to Scenes — Program Video Games

8. Introduction to Scenes

[[programvideogames]]What is a scene? That depends on who you ask.

An answer you'll probably get nowadays is:
A scene is a collection of game objects, environment data, scripts, and UI elements.

I think this is definition is fine if you are using a pre-made engine like Unity. That's exactly what a scene is.

But what is a scene when we boil it down?
Different stuff shown on the screen, different ways to interact.
It's a different code-path.

That means we can simplify our scenes to exactly what fit our game.

Let's go through a few of examples.

Super Metroid (Metroidvania)

  1. Main Menu
  2. Game World (single large map)
  3. Pause Menu
  4. Cutscenes

Super Mario World (Platformer)

  1. Main Menu
  2. Overworld Map
  3. Game World (levels)
  4. End of Level Screen (maybe part of the level)
  5. Cutscenes

Dragon Age Origins (Role Playing Game)

  1. Main Menu
  2. Character Creation
  3. Camp
  4. Field Map / Area
  5. Battle
  6. Dialogue
  7. Cutscenes
  8. Inventory*

Minecraft (First Person Sandbox)

  1. Main Menu
  2. World Select
  3. Game World
  4. Inventory/Crafting*

* Overlay

For most games, the idea of implementing a complicated scene graph that allows generic "game objects" to be plugged in is unnecessary.

Scenes can be as simple as another Code Cycle that runs inside the main game loop.

Some "scenes" should be composited on top of the current one.
For example: Often an inventory is displayed over the top of current game scene.

For a simple exercise: Create two scenes with the ability to switch between them.
Main Menu: A menu with a button to start the "game".
Game: The scene we have been working on, with the moving rock.

hint:

Scene :: enum {
    Main_Menu,
    Game,
}

my answer:

package main

import "core:fmt"
import "core:strings"
import rl "vendor:raylib"
import "core:math/linalg"

WINDOW_WIDTH :: 1280
WINDOW_HEIGHT :: 720
ZOOM :: 2
BG_COLOR :: rl.Color{113, 202, 211, 255}

Scene :: enum {
    Menu,
    Game,
}

scene_menu :: proc() -> Scene {
    for !rl.WindowShouldClose() {
        free_all(context.temp_allocator)

        rl.BeginDrawing()
        rl.ClearBackground(rl.BLACK)

        text := fmt.ctprint("Start Game")
        text_width := rl.MeasureText(text, 20)
        button_rect := rl.Rectangle{
            f32(WINDOW_WIDTH / 2 - text_width / 2 - 4), 46,
            f32(text_width + 8), 28,
        }

        if rl.CheckCollisionPointRec(rl.GetMousePosition(), button_rect) {
            rl.SetMouseCursor(.POINTING_HAND)
            rl.DrawRectangleRec(button_rect, rl.YELLOW)

            if rl.IsMouseButtonPressed(.LEFT) {
                return .Game
            }
        } else {
            rl.SetMouseCursor(.DEFAULT)
            rl.DrawRectangleRec(button_rect, rl.WHITE)
        }

        rl.DrawText(text, WINDOW_WIDTH / 2 - text_width / 2, 50, 20, rl.BLACK)

        rl.EndDrawing()
    }

    return .Menu
}

scene_game :: proc() -> Scene {
    texture_background := rl.LoadTexture("assets/textures/bg_clouds_back.png")
    texture_tileset := rl.LoadTexture("assets/textures/tileset.png")
    defer rl.UnloadTexture(texture_background)
    defer rl.UnloadTexture(texture_tileset)

    camera := rl.Camera2D{
        zoom = ZOOM,
    }

    pos: rl.Vector2

    for !rl.WindowShouldClose() {
        free_all(context.temp_allocator)

        if rl.IsKeyPressed(.Q) do return .Menu

        delta := rl.GetFrameTime()
        input_vector: rl.Vector2

        if rl.IsKeyDown(.LEFT) do input_vector.x -= 1
        if rl.IsKeyDown(.RIGHT) do input_vector.x += 1
        if rl.IsKeyDown(.UP) do input_vector.y -= 1
        if rl.IsKeyDown(.DOWN) do input_vector.y += 1

        input_vector = linalg.normalize0(input_vector)

        pos += input_vector * 100 * delta

        rl.BeginDrawing()
        rl.BeginMode2D(camera)
        rl.ClearBackground(BG_COLOR)

        rl.DrawTexture(texture_background, 0, 0, rl.WHITE)
        rl.DrawTextureRec(texture_tileset, {0, 0, 16, 16}, pos, rl.WHITE)

        static_rect := rl.Rectangle{128, 128, 128, 128}
        moving_rect := rl.Rectangle{pos.x, pos.y, 32, 32}
        collision_rect := rl.GetCollisionRec(static_rect, moving_rect)

        resolved_rect: rl.Rectangle

        if collision_rect != {} {
            normal: rl.Vector2
            // Copy values so we can just update the relevant ones
            resolved_rect = moving_rect

            // Determine the direction of collision
            center_static := rl.Vector2{
                static_rect.x + static_rect.width / 2,
                static_rect.y + static_rect.height / 2,
            }
            center_moving := rl.Vector2{
                moving_rect.x + moving_rect.width / 2,
                moving_rect.y + moving_rect.height / 2,
            }
            dist := center_moving - center_static

            // Calculate the normal
            // Rectangles always have either X or Y as 0
            if abs(dist.x) > abs(dist.y) {
                normal.x = 1 if dist.x > 0 else -1
            } else {
                normal.y = 1 if dist.y > 0 else -1
            }

            if normal.x < 0 {
                // Left
                resolved_rect.x = static_rect.x - moving_rect.width
            } else if normal.x > 0 {
                // Right
                resolved_rect.x = static_rect.x + static_rect.width
            } else if normal.y < 0 {
                // Up
                resolved_rect.y = static_rect.y - moving_rect.height
            } else if normal.y > 0 {
                // Down
                resolved_rect.y = static_rect.y + static_rect.height
            }
        }

        rl.DrawRectangleLinesEx(static_rect, 1, rl.BLACK)
        rl.DrawRectangleLinesEx(moving_rect, 1, rl.BLACK)
        rl.DrawRectangleLinesEx(collision_rect, 1, rl.RED)
        if resolved_rect != {} {
            rl.DrawRectangleLinesEx(resolved_rect, 1, rl.ORANGE)
        }

        circle_pos := rl.Vector2{384, 128}
        circle_radius: f32 = 32
        if rl.CheckCollisionCircleRec(circle_pos, circle_radius, moving_rect) {
            rl.DrawCircleV(circle_pos, circle_radius, rl.RED)
        } else {
            rl.DrawCircleV(circle_pos, circle_radius, rl.DARKPURPLE)
        }

        mouse_position := rl.GetMousePosition() / 2

        if rl.CheckCollisionPointRec(mouse_position, static_rect) {
            rl.DrawCircleV(mouse_position, 8, rl.RED)
        }

        rl.DrawText(fmt.ctprintf("Position: %v", pos), 8, 8, 20, rl.WHITE)

        rl.EndMode2D()
        rl.EndDrawing()
    }

    return .Game
}

main :: proc() {
    rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "Program Video Games!")

    scene: Scene

    for !rl.WindowShouldClose() {
        if scene == .Menu {
            scene = scene_menu()
        } else {
            scene = scene_game()
        }
    }
}

Yours may be different, and that's fine.

Use whatever method you think makes sense.

I've used a new keyword in this code: defer

defer will defer the execution of the line until the end of the scope.
You can defer lines and blocks, check here for more info: https://odin-lang.org/docs/overview/#defer-statement
In this case, we release the texture data from our graphics memory if the scene changes (return).

In this lecture we build only the minimum of what we required to show the result we care about.

If we were going into this thinking we needed to build a scene hierarchy and have "game objects", events and other such things - it'd take us hours, days or weeks rather than minutes.

Good job getting through the introduction part of this course.

If you have any questions or comments, I encourage you to leave them in the Skool Community tab under the Vertical Slice and Dice category.