Compare commits

...
3 Commits
Author SHA1 Message Date
jasonhilder cb523a3d6f Initial simple event manager. 2026-09-15 05:42:12 +02:00
jasonhilder 106f8195e2 Added a winning score to levels. 2026-09-15 05:41:41 +02:00
jasonhilder 0000ca4f12 Consolidated scenes for neatness. 2026-09-15 05:41:25 +02:00
5 changed files with 169 additions and 95 deletions
+8 -3
View File
@@ -2,7 +2,6 @@ package game
import "core:math/rand" import "core:math/rand"
import rl "vendor:raylib" import rl "vendor:raylib"
import "core:log"
init_ball :: proc() -> Entity { init_ball :: proc() -> Entity {
return Entity{ return Entity{
@@ -57,13 +56,13 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
if ball.pos.y <= field_bounds.y + ball.size.x { ball.vel.y = -ball.vel.y } if ball.pos.y <= field_bounds.y + ball.size.x { ball.vel.y = -ball.vel.y }
if ball.pos.x >= (field_bounds.x + field_bounds.width) - ball.size.x { if ball.pos.x >= (field_bounds.x + field_bounds.width) - ball.size.x {
level.score_player += 1 increment_score(&level.score_player, level.score_win, true)
reset_ball(ball) reset_ball(ball)
level.serve_timer = level.wait_time level.serve_timer = level.wait_time
} }
if ball.pos.x <= field_bounds.x + ball.size.x { if ball.pos.x <= field_bounds.x + ball.size.x {
level.score_enemy += 1 increment_score(&level.score_enemy, level.score_win, false)
reset_ball(ball) reset_ball(ball)
level.serve_timer = level.wait_time level.serve_timer = level.wait_time
} }
@@ -79,6 +78,12 @@ check_ball_paddle_collision :: proc(ball: Entity, paddle: Entity) -> bool {
return rl.CheckCollisionCircleRec(ball.pos, ball.size.x, paddle_rec) return rl.CheckCollisionCircleRec(ball.pos, ball.size.x, paddle_rec)
} }
increment_score :: proc(score_side: ^int, max_score: int, player: bool) {
score_side^ += 1
if score_side^ == max_score do emit_event(Event{ .Game_Over, player })
}
render_ball :: proc(ball: Entity, field_bounds: rl.Rectangle) { render_ball :: proc(ball: Entity, field_bounds: rl.Rectangle) {
rl.DrawCircleV(ball.pos, ball.size.x, rl.MAROON) rl.DrawCircleV(ball.pos, ball.size.x, rl.MAROON)
} }
+25
View File
@@ -0,0 +1,25 @@
package game
// all possible game events
Event_Type :: enum {
Game_Over,
Score_Change,
Ball_Collision,
}
// optional details attached to an event
Event :: struct {
type: Event_Type,
winner_is_player: bool,
}
// global event storage
GLOBAL_EVENTS: [dynamic]Event
emit_event :: proc(event: Event) {
append(&GLOBAL_EVENTS, event)
}
flush_events :: proc() {
clear(&GLOBAL_EVENTS)
}
+20 -12
View File
@@ -10,10 +10,8 @@ WINDOW_TITLE :: "Pong"
Game :: struct { Game :: struct {
viewport: rl.RenderTexture2D, viewport: rl.RenderTexture2D,
// base scene (gameplay, mainmenu)
scene: Scene, scene: Scene,
// active overlay (pause, settings, or nil) overlay_scene: Maybe(Scene),
overlay_scene: Maybe(Overlay_Scene),
} }
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
@@ -24,33 +22,43 @@ game_init :: proc(game: ^Game) {
rl.SetTargetFPS(60) rl.SetTargetFPS(60)
game.viewport = rl.LoadRenderTexture(VIEWPORT_W, VIEWPORT_H) game.viewport = rl.LoadRenderTexture(VIEWPORT_W, VIEWPORT_H)
game.scene = init_gameplay_scene() game.scene = make_gameplay_scene()
// game.scene = make_main_menu()
debug_init()
} }
game_update :: proc(game: ^Game) { game_update :: proc(game: ^Game) {
dt := rl.GetFrameTime() dt := rl.GetFrameTime()
input := poll_input() input := poll_input()
// update the overlay if active, otherwise update base scene // Pass direct pointers (^Scene) so modifications persist
if game.overlay_scene != nil { if overlay, ok := &game.overlay_scene.?; ok {
overlay_update(game, input, dt) scene_update(overlay, game, input, dt)
} else { } else {
scene_update(game, input, dt) scene_update(&game.scene, game, input, dt)
} }
// flush any events triggered in this loop
flush_events()
} }
game_render :: proc(game: ^Game) { game_render :: proc(game: ^Game) {
rl.BeginDrawing() rl.BeginDrawing()
rl.ClearBackground(rl.BLACK) rl.ClearBackground(rl.BLACK)
// offscreen pass render everything into virtual 1280x720 target
rl.BeginTextureMode(game.viewport) rl.BeginTextureMode(game.viewport)
rl.ClearBackground(rl.BLACK) rl.ClearBackground(rl.BLACK)
scene_render(game) // Renders base scene (Level field or Main Menu)
overlay_render(game) // Renders active overlay on top if present // Render base scene
scene_render(&game.scene)
// Render active overlay on top if present
if overlay, ok := &game.overlay_scene.?; ok {
scene_render(overlay)
}
rl.EndTextureMode() rl.EndTextureMode()
// upscale pass stretch virtual buffer onto physical window display
render_game_to_window(game.viewport.texture) render_game_to_window(game.viewport.texture)
rl.EndDrawing() rl.EndDrawing()
+2 -3
View File
@@ -15,6 +15,7 @@ Level :: struct {
wait_time: f32, wait_time: f32,
score_player: int, score_player: int,
score_enemy: int, score_enemy: int,
score_win: int,
} }
FIELD_BORDER :: 3 FIELD_BORDER :: 3
@@ -50,6 +51,7 @@ init_level :: proc() -> Level {
wait_time = SERVE_WAIT_TIME, wait_time = SERVE_WAIT_TIME,
score_player = 0, score_player = 0,
score_enemy = 0, score_enemy = 0,
score_win = 2,
} }
} }
@@ -89,17 +91,14 @@ render_hud :: proc(level: Level) {
opp_box := rl.Rectangle{(VIEWPORT_W - 5 - opp_size.x), 5, (opp_size.x), (opp_size.y)} opp_box := rl.Rectangle{(VIEWPORT_W - 5 - opp_size.x), 5, (opp_size.x), (opp_size.y)}
opp_pos := rl.Vector2{opp_box.x, opp_box.y} opp_pos := rl.Vector2{opp_box.x, opp_box.y}
rl.DrawTextEx(font, "COMPUTER", opp_pos, 20, 2, rl.WHITE) rl.DrawTextEx(font, "COMPUTER", opp_pos, 20, 2, rl.WHITE)
// rl.DrawRectangleLinesEx(opp_box, 1.0, rl.RED)
// score box // score box
// DYNAMIC SCORE BOX
// Format the score dynamically into a C-string allocated on the temp allocator // Format the score dynamically into a C-string allocated on the temp allocator
score_text := fmt.ctprintf("%d : %d", level.score_player, level.score_enemy) score_text := fmt.ctprintf("%d : %d", level.score_player, level.score_enemy)
score_size := rl.MeasureTextEx(font, score_text, 20, 2) score_size := rl.MeasureTextEx(font, score_text, 20, 2)
score_box := rl.Rectangle{(VIEWPORT_W / 2 - score_size.x / 2), 5, (score_size.x), (score_size.y)} score_box := rl.Rectangle{(VIEWPORT_W / 2 - score_size.x / 2), 5, (score_size.x), (score_size.y)}
score_pos := rl.Vector2{score_box.x, score_box.y} score_pos := rl.Vector2{score_box.x, score_box.y}
rl.DrawTextEx(font, score_text, score_pos, 20, 2, rl.WHITE) rl.DrawTextEx(font, score_text, score_pos, 20, 2, rl.WHITE)
// rl.DrawRectangleLinesEx(score_box, 1.0, rl.RED)
// game name // game name
// rl.DrawRectangleLines(VIEWPORT_W / 2 - 50, VIEWPORT_H - 50, 100, 50, rl.RED) // rl.DrawRectangleLines(VIEWPORT_W / 2 - 50, VIEWPORT_H - 50, 100, 50, rl.RED)
+112 -75
View File
@@ -3,98 +3,135 @@ package game
import rl "vendor:raylib" import rl "vendor:raylib"
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
// Scene & Overlay Definitions // Scene Definitions
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
Scene_Gameplay :: struct { Scene_Type :: enum {
level: Level, Main_Menu,
Gameplay,
Pause_Menu,
Game_Over,
} }
Scene_Menu :: struct { Scene :: struct {
// @todo: ui_canvas layout state type: Scene_Type,
} level: Maybe(Level),
canvas: Maybe(UI_Canvas),
Scene_PauseMenu :: struct { winner_is_player: bool, // Store win state for rendering
// @todo: pause option state
}
Scene :: union {
Scene_Menu,
Scene_Gameplay,
}
Overlay_Scene :: union {
Scene_PauseMenu,
} }
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
// Base Scene Dispatchers // UI Definitions
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
scene_update :: proc(game: ^Game, input: Input_Event, dt: f32) { UI_Canvas :: struct {
switch &s in game.scene { bounds: rl.Rectangle,
case Scene_Menu: // update_menu(&s, game, input, dt) padding: f32,
case Scene_Gameplay: update_gameplay(&s, game, input, dt) item_height: f32,
} item_spaceing: f32,
}
scene_render :: proc(game: ^Game) {
switch &s in game.scene {
case Scene_Menu: // render_menu(s)
case Scene_Gameplay: render_gameplay(s)
}
} }
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
// Overlay Scene Dispatchers // Scene Constructors
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
overlay_update :: proc(game: ^Game, input: Input_Event, dt: f32) -> bool { make_gameplay_scene :: proc() -> Scene {
overlay := game.overlay_scene.? or_return return Scene{
type = .Gameplay,
switch &o in overlay {
case Scene_PauseMenu:
if input.pause_pressed {
game.overlay_scene = nil
return true
}
// @todo: handle overlay menu option selection (resume, settings, quit)
}
return true
}
overlay_render :: proc(game: ^Game) -> bool {
overlay := game.overlay_scene.? or_return
switch &o in overlay {
case Scene_PauseMenu:
// Translucent dark tint across virtual canvas space
rl.DrawRectangleRec(rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H}, rl.Color{0, 0, 0, 140})
// Pause Menu UI elements
rl.DrawText("PAUSED", VIEWPORT_W / 2 - 80, VIEWPORT_H / 2 - 40, 40, rl.WHITE)
rl.DrawText("Press Q to Quit to Menu", VIEWPORT_W / 2 - 110, VIEWPORT_H / 2 + 10, 20, rl.LIGHTGRAY)
}
return true
}
// ------------------------------------------------------------------------------
// Gameplay Scene Procedures
// ------------------------------------------------------------------------------
init_gameplay_scene :: proc() -> Scene_Gameplay {
return Scene_Gameplay{
level = init_level(), level = init_level(),
} }
} }
update_gameplay :: proc(scene: ^Scene_Gameplay, game: ^Game, input: Input_Event, dt: f32) { make_pause_overlay :: proc() -> Scene {
if input.pause_pressed { return Scene{
game.overlay_scene = Scene_PauseMenu{} type = .Pause_Menu,
return canvas = UI_Canvas{},
}
}
make_main_menu :: proc() -> Scene {
return Scene{
type = .Main_Menu,
canvas = UI_Canvas{},
}
}
make_game_over_overlay :: proc(winner_is_player: bool) -> Scene {
return Scene{
type = .Game_Over,
canvas = UI_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)
} }
update_level(&scene.level, input, dt) // dispatch scene behavior
#partial switch scene.type {
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:
if input.pause_pressed { // Press P or your start key to restart
game.overlay_scene = nil
game.scene = make_gameplay_scene() // Reset the base level
}
case .Main_Menu:
// @todo: handle menu selection / start game
}
// Loop over global frame events
for event in GLOBAL_EVENTS {
#partial switch event.type {
case .Game_Over:
game.overlay_scene = make_game_over_overlay(event.winner_is_player)
}
}
} }
render_gameplay :: proc(scene: Scene_Gameplay) { scene_render :: proc(scene: ^Scene) {
render_level(scene.level) // render gameplay field & hud if present
render_hud(scene.level) if level, ok := scene.level.?; ok {
render_level(level)
render_hud(level)
}
// render scene/overlay ui elements
#partial switch scene.type {
case .Pause_Menu:
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)
case .Game_Over:
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)
case .Main_Menu:
rl.DrawText("PONG", VIEWPORT_W / 2 - 50, VIEWPORT_H / 3, 40, rl.WHITE)
rl.DrawText("Press ENTER to Start", VIEWPORT_W / 2 - 110, VIEWPORT_H / 2, 20, rl.LIGHTGRAY)
}
} }