package game import "core:strings" import "core:math/rand" import rl "vendor:raylib" VIEWPORT_W :: 640 VIEWPORT_H :: 360 WINDOW_TITLE :: "Pong" Resolution :: struct { width: i32, height: i32, label: cstring, } // Preset window dimensions RESOLUTIONS := [?]Resolution{ {1280, 720, "1280x720"}, {1920, 1080, "1920x1080"}, {2560, 1440, "2560x1440"}, {3840, 2160, "3840x2160"}, } Game :: struct { viewport: rl.RenderTexture2D, bg_texture: rl.Texture2D, crt_shader: rl.Shader, scene_stack: [dynamic]Scene, should_close: bool, audio: Audio_Manager, // Camera state for screen effects / screen shake camera: rl.Camera2D, shake_intensity: f32, shake_timer: f32, // resolution state window_w: i32, window_h: i32, res_index: int } // AO: lets me load this at compile time! gamepad_compat := #load("../gamecontrollerdb.txt", string) // ------------------------------------------------------------------------------ // Game Lifecycle // ------------------------------------------------------------------------------ game_init :: proc(game: ^Game) { rl.SetConfigFlags({.WINDOW_ALWAYS_RUN, .MSAA_4X_HINT}) game.res_index = 2 game.window_w = RESOLUTIONS[game.res_index].width game.window_h = RESOLUTIONS[game.res_index].height rl.InitWindow(game.window_w, game.window_h, WINDOW_TITLE) rl.SetTargetFPS(60) rl.SetExitKey(.KEY_NULL) gamepad_cstr := strings.clone_to_cstring(gamepad_compat) defer delete(gamepad_cstr) rl.SetGamepadMappings(gamepad_cstr) // init any global managers audio_init(&game.audio) // allocate some event capacity to stop early dynamic reallocations GLOBAL_EVENTS = make([dynamic]Event, 0, 16) game.scene_stack = make([dynamic]Scene, 0, 8) game.viewport = rl.LoadRenderTexture(VIEWPORT_W, VIEWPORT_H) // Look into texture filters rl.SetTextureFilter(game.viewport.texture, .BILINEAR) // Generate soft CRT background radial texture once on startup bg_center := rl.Color{22, 28, 36, 255} bg_corner := rl.Color{6, 8, 12, 255} bg_img := rl.GenImageGradientRadial(VIEWPORT_W, VIEWPORT_H, 0.3, bg_center, bg_corner) game.bg_texture = rl.LoadTextureFromImage(bg_img) rl.UnloadImage(bg_img) // Free CPU memory game.crt_shader = rl.LoadShader(nil, "assets/shaders/crt.fs") // Setup 2D camera matching the 640x360 viewport center game.camera = rl.Camera2D{ target = rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0}, offset = rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0}, rotation = 0.0, zoom = 1.0, } game.should_close = false set_root_scene(game, make_main_menu()) } game_update :: proc(game: ^Game) { dt := rl.GetFrameTime() input := poll_input() // ------------------------------------------------------------------------- // Update Camera Shake // ------------------------------------------------------------------------- base_center := rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0} if game.shake_timer > 0 { game.shake_timer -= dt // Generate random offset within current intensity bounds offset_x := (f32(rand.float32()) * 2.0 - 1.0) * game.shake_intensity offset_y := (f32(rand.float32()) * 2.0 - 1.0) * game.shake_intensity game.camera.target = base_center + rl.Vector2{offset_x, offset_y} if game.shake_timer <= 0 { game.shake_intensity = 0.0 game.camera.target = base_center } } else { game.camera.target = base_center } active_scene := get_active_scene(game) if active_scene != nil { scene_update(active_scene, game, input, dt) } // background music buffer streaming audio_update(&game.audio, dt) // flush any events triggered in this loop flush_events() } game_render :: proc(game: ^Game) { rl.BeginDrawing() rl.ClearBackground(rl.BLACK) rl.BeginTextureMode(game.viewport) rl.DrawTexture(game.bg_texture, 0, 0, rl.WHITE) // World/Scene objects rendered through 2D camera space rl.BeginMode2D(game.camera) // Render all active items on the stack from bottom to top for &scene in game.scene_stack { scene_render(&scene) } rl.EndMode2D() rl.EndTextureMode() render_game_to_window(game) rl.DrawFPS(10, game.window_h - 20) rl.EndDrawing() } game_quit :: proc(game: ^Game) { audio_manager_quit(&game.audio) scene_manager_quit(game) event_manager_quit(game) rl.UnloadShader(game.crt_shader) rl.UnloadTexture(game.bg_texture) rl.UnloadRenderTexture(game.viewport) rl.CloseWindow() } game_should_run :: proc(game: Game) -> bool { return !game.should_close && !rl.WindowShouldClose() } game_trigger_shake :: proc(game: ^Game, intensity: f32, duration: f32 = 0.15) { game.shake_intensity = max(game.shake_intensity, intensity) game.shake_timer = duration } // ------------------------------------------------------------------------------ // Window Upscale Render // ------------------------------------------------------------------------------ game_set_resolution :: proc(game: ^Game, res_index: int) { game.res_index = clamp(res_index, 0, len(RESOLUTIONS) - 1) target_w := RESOLUTIONS[game.res_index].width target_h := RESOLUTIONS[game.res_index].height monitor := rl.GetCurrentMonitor() screen_w := rl.GetMonitorWidth(monitor) screen_h := rl.GetMonitorHeight(monitor) // Clamp window size so it doesn't exceed physical screen limits game.window_w = min(target_w, screen_w) game.window_h = min(target_h, screen_h) rl.SetWindowSize(game.window_w, game.window_h) } render_game_to_window :: proc(game: ^Game) { texture := game.viewport.texture rl.BeginShaderMode(game.crt_shader) rl.DrawTexturePro( texture, // src: inverted y coordinate flips opengl texture buffer correctly rl.Rectangle{0, 0, f32(texture.width), -f32(texture.height)}, // dst: scales viewport canvas to fill window resolution rl.Rectangle{0, 0, f32(game.window_w), f32(game.window_h)}, rl.Vector2{}, 0, rl.WHITE, ) rl.EndShaderMode() }