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)
- Main Menu
- Game World (single large map)
- Pause Menu
- Cutscenes
Super Mario World (Platformer)
- Main Menu
- Overworld Map
- Game World (levels)
- End of Level Screen (maybe part of the level)
- Cutscenes
Dragon Age Origins (Role Playing Game)
- Main Menu
- Character Creation
- Camp
- Field Map / Area
- Battle
- Dialogue
- Cutscenes
- Inventory*
Minecraft (First Person Sandbox)
- Main Menu
- World Select
- Game World
- 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.