From 37dc2ab2fc4a2708987fd52d3ea026449b4fc967 Mon Sep 17 00:00:00 2001 From: Jason Hilder Date: Wed, 16 Sep 2026 07:54:25 +0200 Subject: [PATCH] Setup all menus to use canvas. --- src/scene_manager.odin | 385 +++++++++++++++++++++++++---------------- 1 file changed, 234 insertions(+), 151 deletions(-) diff --git a/src/scene_manager.odin b/src/scene_manager.odin index aa37234..a2931da 100644 --- a/src/scene_manager.odin +++ b/src/scene_manager.odin @@ -22,12 +22,6 @@ Scene :: struct { // ------------------------------------------------------------------------------ // UI Definitions // ------------------------------------------------------------------------------ -UI_Menu :: struct { - items: [8]cstring, - count: int, - selected_index: int, -} - Canvas :: struct { bounds: rl.Rectangle, padding: f32, @@ -36,45 +30,33 @@ Canvas :: struct { menu: Maybe(UI_Menu), } +UI_Menu :: struct { + items: [8]cstring, + count: int, + selected_index: int, +} + get_canvas_item_rec :: proc(canvas: Canvas, index: int, top_margin: f32 = 120) -> rl.Rectangle { y_pos := (canvas.bounds.y + top_margin) + (f32(index) * (canvas.item_height + canvas.item_spaceing)) + width_value := (canvas.bounds.width * 0.5) return rl.Rectangle{ - x = canvas.bounds.x + canvas.padding, + x = (canvas.bounds.x) + width_value * 0.5, y = y_pos, - width = canvas.bounds.width - (canvas.padding * 2.0), - height = canvas.item_height + width = width_value, + height = canvas.item_height, } } -render_main_menu :: proc(scene: Scene) { - canvas, ok := scene.canvas.? - // Assert or log error if canvas is unexpectedly missing - if !ok { return } - - // Safely unwrap the embedded menu state - menu, menu_ok := canvas.menu.? - if !menu_ok { return } - - // draw canvas base background - rl.DrawRectangleRec(canvas.bounds, rl.BLACK) - - // draw the menu header - t_text: cstring = "PONG" - t_width := rl.MeasureText(t_text, 40) - t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) - rl.DrawText(t_text, t_x, i32(canvas.padding), 40, rl.WHITE) - - // Iterate up to menu.count using the data inside UI_Menu +list_menu_items :: proc(canvas: Canvas, menu: UI_Menu) { for i in 0 ..< menu.count { item := menu.items[i] item_rec := get_canvas_item_rec(canvas, i, top_margin = 150.0) text_width := rl.MeasureText(item, 20) - text_x := i32(item_rec.x + (item_rec.width * 0.5)) - (text_width / 2) + text_x := i32(item_rec.x + item_rec.width * 0.5) - (text_width / 2) text_y := i32(item_rec.y + (item_rec.height * 0.5)) - 10 - // Highlight selected item box & text is_selected := (i == menu.selected_index) border_color := is_selected ? rl.ORANGE : rl.DARKGRAY text_color := is_selected ? rl.ORANGE : rl.WHITE @@ -84,61 +66,8 @@ render_main_menu :: proc(scene: Scene) { } } -update_main_menu :: proc(scene: ^Scene, input: Input_Event) { - // pointer to Canvas - canvas, canvas_ok := &scene.canvas.? - if !canvas_ok do return - - // pointer to UI_Menu - menu, menu_ok := &canvas.menu.? - if !menu_ok do return - - // get some navigation events - if input.up_pressed { - menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count - } - if input.down_pressed { - menu.selected_index = (menu.selected_index + 1) % menu.count - } - - if input.confirm_pressed { - switch menu.selected_index { - case 0: // Start Game - emit_event(Event{ type = .Game_Start }) - case 1: // Options - // @todo open options scene/overlay - case 2: // Quit - emit_event(Event{ type = .Game_Quit }) - } - } -} - -// @todo -render_game_over :: proc(scene: Scene) { - rl.DrawRectangleRec( - rl.Rectangle{0, 0, f32(VIEWPORT_W), f32(VIEWPORT_H)}, - rl.Color{0, 0, 0, 200}, - ) - - msg :cstring = scene.winner_is_player ? "YOU WIN!" : "GAME OVER" - text_color := scene.winner_is_player ? rl.GREEN : rl.RED - - rl.DrawText(msg, VIEWPORT_W / 2 - 100, VIEWPORT_H / 2 - 40, 40, text_color) - rl.DrawText("Press P to Play Again", VIEWPORT_W / 2 - 110, VIEWPORT_H / 2 + 10, 20, rl.LIGHTGRAY) -} - -// @todo -render_pause_menu :: proc(scene: Scene) { - rl.DrawRectangleRec( - rl.Rectangle{0, 0, f32(VIEWPORT_W), f32(VIEWPORT_H)}, - rl.Color{0, 0, 0, 140}, - ) - rl.DrawText("PAUSED", VIEWPORT_W / 2 - 80, VIEWPORT_H / 2 - 40, 40, rl.WHITE) - rl.DrawText("Press P to Resume", VIEWPORT_W / 2 - 90, VIEWPORT_H / 2 + 10, 20, rl.LIGHTGRAY) -} - // ------------------------------------------------------------------------------ -// Scene Constructors +// Main Menu // ------------------------------------------------------------------------------ make_main_menu :: proc() -> Scene { return Scene{ @@ -157,7 +86,182 @@ make_main_menu :: proc() -> Scene { } } -// @todo +render_main_menu :: proc(scene: Scene) { + canvas, canvas_ok := scene.canvas.? + assert(canvas_ok, "render_main_menu: Scene is missing a Canvas component") + + menu, menu_ok := canvas.menu.? + assert(menu_ok, "render_main_menu: Canvas is missing a UI_Menu component") + + rl.DrawRectangleRec(canvas.bounds, rl.BLACK) + + t_text: cstring = "PONG" + t_width := rl.MeasureText(t_text, 40) + t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) + rl.DrawText(t_text, t_x, i32(canvas.padding), 40, rl.WHITE) + + list_menu_items(canvas, menu) +} + +update_main_menu :: proc(scene: ^Scene, input: Input_Event) { + canvas, canvas_ok := &scene.canvas.? + assert(canvas_ok, "update_main_menu: Scene is missing a Canvas component") + + menu, menu_ok := &canvas.menu.? + assert(menu_ok, "update_main_menu: Canvas is missing a UI_Menu component") + + if input.up_pressed { + menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count + } + if input.down_pressed { + menu.selected_index = (menu.selected_index + 1) % menu.count + } + + if input.confirm_pressed { + switch menu.selected_index { + case 0: + emit_event(Event{ type = .Game_Start }) + case 1: + // @todo open options + case 2: + emit_event(Event{ type = .Game_Quit }) + } + } +} + +// ------------------------------------------------------------------------------ +// Pause Menu +// ------------------------------------------------------------------------------ +make_pause_overlay :: proc() -> Scene { + return Scene{ + type = .Pause_Menu, + canvas = Canvas{ + bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, + padding = 20, + item_height = 40, + item_spaceing = 15, + menu = UI_Menu{ + items = {0 = "Resume Game", 1 = "Options", 2 = "Main Menu"}, + count = 3, + selected_index = 0, + }, + }, + } +} + +render_pause_menu :: proc(scene: Scene) { + canvas, canvas_ok := scene.canvas.? + assert(canvas_ok, "render_pause_menu: Scene is missing a Canvas component") + + menu, menu_ok := canvas.menu.? + assert(menu_ok, "render_pause_menu: Canvas is missing a UI_Menu component") + + rl.DrawRectangleRec(canvas.bounds, rl.Color{0, 0, 0, 220}) + + t_text: cstring = "PAUSE MENU" + t_width := rl.MeasureText(t_text, 40) + t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) + rl.DrawText(t_text, t_x, i32(canvas.padding), 40, rl.WHITE) + + list_menu_items(canvas, menu) +} + +update_pause_menu :: proc(scene: ^Scene, input: Input_Event) { + canvas, canvas_ok := &scene.canvas.? + assert(canvas_ok, "update_pause_menu: Scene is missing a Canvas component") + + menu, menu_ok := &canvas.menu.? + assert(menu_ok, "update_pause_menu: Canvas is missing a UI_Menu component") + + if input.up_pressed { + menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count + } + if input.down_pressed { + menu.selected_index = (menu.selected_index + 1) % menu.count + } + + if input.confirm_pressed { + switch menu.selected_index { + case 0: + emit_event(Event{ type = .Game_Resume }) + case 1: + // @todo open options + case 2: + emit_event(Event{ type = .Game_Main_Menu }) + } + } + + if input.pause_pressed { + emit_event(Event{ type = .Game_Resume }) + } +} + +// ------------------------------------------------------------------------------ +// Game Over Menu +// ------------------------------------------------------------------------------ +make_game_over_overlay :: proc(winner_is_player: bool) -> Scene { + return Scene{ + type = .Game_Over, + winner_is_player = winner_is_player, + canvas = Canvas{ + bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, + padding = 20, + item_height = 40, + item_spaceing = 15, + menu = UI_Menu{ + items = {0 = "Restart Game", 1 = "Main Menu"}, + count = 2, + selected_index = 0, + }, + }, + } +} + +render_game_over :: proc(scene: Scene) { + canvas, canvas_ok := scene.canvas.? + assert(canvas_ok, "render_pause_menu: Scene is missing a Canvas component") + + menu, menu_ok := canvas.menu.? + assert(menu_ok, "render_pause_menu: Canvas is missing a UI_Menu component") + + rl.DrawRectangleRec(canvas.bounds, rl.Color{0, 0, 0, 255}) + + t_text: cstring = scene.winner_is_player ? "YOU WIN!" : "GAME OVER" + t_width := rl.MeasureText(t_text, 40) + t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2) + text_color := scene.winner_is_player ? rl.GREEN : rl.RED + rl.DrawText(t_text, t_x, i32(canvas.padding), 40, text_color) + + list_menu_items(canvas, menu) +} + +update_game_over :: proc(scene: ^Scene, input: Input_Event) { + canvas, canvas_ok := &scene.canvas.? + assert(canvas_ok, "update_pause_menu: Scene is missing a Canvas component") + + menu, menu_ok := &canvas.menu.? + assert(menu_ok, "update_pause_menu: Canvas is missing a UI_Menu component") + + if input.up_pressed { + menu.selected_index = (menu.selected_index - 1 + menu.count) % menu.count + } + if input.down_pressed { + menu.selected_index = (menu.selected_index + 1) % menu.count + } + + if input.confirm_pressed { + switch menu.selected_index { + case 0: + emit_event(Event{ type = .Game_Start }) + case 1: + emit_event(Event{ type = .Game_Main_Menu }) + } + } +} + +// ------------------------------------------------------------------------------ +// Game Play Scene +// ------------------------------------------------------------------------------ make_gameplay_scene :: proc() -> Scene { return Scene{ type = .Gameplay, @@ -165,81 +269,15 @@ make_gameplay_scene :: proc() -> Scene { } } -// @todo -make_pause_overlay :: proc() -> Scene { - return Scene{ - type = .Pause_Menu, - canvas = Canvas{}, - } -} - -// @todo -make_game_over_overlay :: proc(winner_is_player: bool) -> Scene { - return Scene{ - type = .Game_Over, - canvas = Canvas{}, - winner_is_player = winner_is_player, - } -} - // ------------------------------------------------------------------------------ // Unified Scene Dispatchers // ------------------------------------------------------------------------------ -scene_update :: proc(scene: ^Scene, game: ^Game, input: Input_Event, dt: f32) { - // mutate level state through pointer if level exists - if level, ok := &scene.level.?; ok { - update_level(level, input, dt) - } - - // dispatch scene behavior - #partial switch scene.type { - case .Main_Menu: - update_main_menu(scene, input) - - case .Gameplay: - if input.pause_pressed { - game.overlay_scene = make_pause_overlay() - } - - case .Pause_Menu: - if input.pause_pressed { - game.overlay_scene = nil - } - - case .Game_Over: - // Press P or your start key to restart - if input.pause_pressed { - game.overlay_scene = nil - game.scene = make_gameplay_scene() - } - } - - // Loop over global frame events - for event in GLOBAL_EVENTS { - #partial switch event.type { - case .Game_Start: - // clear old state - game.overlay_scene = nil - - // swap to the gameplay scene - // @todo consider different levels here? - game.scene = make_gameplay_scene() - case .Game_Over: - game.overlay_scene = make_game_over_overlay(event.winner_is_player) - case .Game_Quit: - game.should_close = true - } - } -} - scene_render :: proc(scene: ^Scene) { - // render gameplay field & hud if present if level, ok := scene.level.?; ok { render_level(level) render_hud(level) } - // render scene/overlay ui elements #partial switch scene.type { case .Pause_Menu: render_pause_menu(scene^) @@ -250,3 +288,48 @@ scene_render :: proc(scene: ^Scene) { } } +scene_update :: proc(scene: ^Scene, game: ^Game, input: Input_Event, dt: f32) { + #partial switch scene.type { + case .Main_Menu: + update_main_menu(scene, input) + + case .Pause_Menu: + update_pause_menu(scene, input) + + case .Gameplay: + level, level_ok := &scene.level.? + assert(level_ok, "scene_update: .Gameplay scene is missing a Level component") + update_level(level, input, dt) + + if input.pause_pressed { + emit_event(Event{ type = .Game_Pause }) + } + + case .Game_Over: + update_game_over(scene, input) + } + + for event in GLOBAL_EVENTS { + #partial switch event.type { + case .Game_Start: + game.overlay_scene = nil + game.scene = make_gameplay_scene() + + case .Game_Pause: + game.overlay_scene = make_pause_overlay() + + case .Game_Resume: + game.overlay_scene = nil + + case .Game_Main_Menu: + game.overlay_scene = nil + game.scene = make_main_menu() + + case .Game_Over: + game.overlay_scene = make_game_over_overlay(event.winner_is_player) + + case .Game_Quit: + game.should_close = true + } + } +} \ No newline at end of file