Compare commits

..
6 Commits
Author SHA1 Message Date
jasonhilder 5038bc5673 Setup game controller status on game state. 2026-10-21 07:28:49 +02:00
jasonhilder 7fbdb9f5cf Updated readme for credits. 2026-10-19 07:19:10 +02:00
jasonhilder b1bab7ece5 Screenshots v1.0 2026-10-15 06:59:13 +02:00
jasonhilder ba410861e6 Initial detailed Readme V1.0 2026-10-15 06:57:14 +02:00
jasonhilder 1ee4ff920b Make screen shake only happen on score. 2026-10-15 06:56:53 +02:00
jasonhilder f9987788bf Cleanups and consitency. 2026-10-15 06:56:44 +02:00
10 changed files with 121 additions and 32 deletions
+85 -2
View File
@@ -1,3 +1,86 @@
# pong_raylib
# PONG 2026
Classic pong implemented in Odin and Raylib
A deliberately over engineered Pong clone in [Odin](https://odin-lang.org) with [raylib](https://www.raylib.com).
Retro line-based graphics with a CRT post-processing shader.
## Screenshots
![Main menu](screenshots/main_menu.png)
![Settings menu](screenshots/settings_menu.png)
![Gameplay](screenshots/gameplay.png)
## Why Over-Engineered?
While the game could have been done in a single file, the goal was to structure the project like a small production game instead:
- **Manager-per-subsystem**: audio, input, event, level, particle, and scene managers, each in its own file
- **Event system**: decoupled communication between systems (`event_manager.odin`) instead of direct calls
- **Scene management**: state/scene stack for menus, gameplay, etc. (`scene_manager.odin`)
- **Entity layer**: shared base entity with ball, player, and opponent specializations
- **Input abstraction**: keyboard and gamepad behind one interface
- **Data-driven assets**: audio and shaders loaded from `assets/`, not embedded
The idea behind breaking it into systems is so that it can help build my foundations into more complex games.
## Features
- Single-player vs AI opponent
- CRT fragment shader (`assets/shaders/crt.fs`)
- Particle effects, sound effects, music
- Gamepad support via SDL controller mappings
## Controls
| Action | Keyboard | Gamepad |
|--------|----------|---------|
| Move | W(up) S(down) | Left Joystick |
| Select | Enter | A button |
| Pause | Spacebar | Start button |
## Requirements
Please note, I have not built this on MacOs or Windows, only linux.
While in theory if you have the odin compiler it should just work as there is no external libs besides those bundled with Odin.
- Odin compiler (version dev-2026-10:e87445a58)
- raylib (bundled with Odin as `vendor:raylib`)
- GPU/driver with OpenGL 3.3 support (shader uses `#version 330`)
## Build & Run
./build.sh # see build.sh for flags and options
./out/pong
## Project Layout
assets/
audio/ music, SFX, UI sounds
shaders/ crt.fs
screenshots/ README images
src/
main.odin entry point
game.odin game state and loop
*_manager.odin audio, event, input, level, particle, scene
entity*.odin base entity, ball, player, opponent
out/ build output
build.sh build script
## Credits
Sourced from the web, not mine.
| File | Title | Author | Source | License |
|------|-------|--------|--------|---------|
| `UI, gameplay effects` | Impact Sounds 1.0 + Interface Sounds 1.0 | Kenney | https://www.kenney.nl | [CC0 1.0](http://creativecommons.org/publicdomain/zero/1.0/) |
| `Background Music` | 5 Action Chiptunes By Juhani Junkala | Juhani Junkala | <download page URL> | CC0 1.0 |
### Third-Party
- `gamecontrollerdb.txt`: [SDL_GameControllerDB](https://github.com/mdqinc/SDL_GameControllerDB) (zlib)
- [raylib](https://github.com/raysan5/raylib) by Ramon Santamaria (@raysan5), zlib
- [Odin](https://github.com/odin-lang/Odin), BSD-3-Clause
## License
MIT, see [LICENSE](LICENSE).
Any third-party assets retain their original licenses, listed above.
Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 KiB

+1
View File
@@ -120,6 +120,7 @@ check_ball_paddle_collision :: proc(ball: ^Entity, paddle: Entity) -> bool {
}
increment_score :: proc(score_side: ^int, max_score: int, player: bool, ball: ^Entity) {
emit_event(.Score_Change)
score_side^ += 1
if score_side^ == max_score {
emit_event(Game_Over_Data{player})
+3 -8
View File
@@ -58,14 +58,6 @@ process_events :: proc(game: ^Game) {
push_scene(game, make_game_over_overlay(e.winner_is_player))
case Ball_Collision_Data:
// normalize the speed between 0.0 -> 1.0
// map that value to the camera displacement
min_speed: f32 = 200.0
max_speed: f32 = 550.0
speed_factor := clamp((e.speed - min_speed) / (max_speed - min_speed), 0, 1.0)
intensity := 0.5 + (speed_factor * 4.5)
game_trigger_shake(game, intensity, duration = 0.15)
audio_play_sfx(&game.audio, .Ball_Collision)
case Simple_Event:
@@ -81,6 +73,9 @@ process_events :: proc(game: ^Game) {
case .Game_Resume:
pop_scene(game)
case .Score_Change:
game_trigger_shake(game, 10, duration = 0.20)
case .Game_Settings:
parent: Maybe(Scene_Type) = nil
if active := get_active_scene(game); active != nil {
+11 -6
View File
@@ -6,7 +6,7 @@ import rl "vendor:raylib"
VIEWPORT_W :: 640
VIEWPORT_H :: 360
WINDOW_TITLE :: "Pong"
WINDOW_TITLE :: "PONG 2026"
Resolution :: struct {
width: i32,
@@ -49,7 +49,11 @@ Game :: struct {
// resolution state
window_w: i32,
window_h: i32,
res_index: int
res_index: int,
// Controller state.
controller_gid: i32,
controller_connected: bool,
}
// AO: lets me load this at compile time!
@@ -60,6 +64,7 @@ gamepad_compat := #load("../gamecontrollerdb.txt", string)
// ------------------------------------------------------------------------------
game_init :: proc(game: ^Game) {
rl.SetConfigFlags({.WINDOW_ALWAYS_RUN, .MSAA_4X_HINT, .WINDOW_RESIZABLE})
game.controller_gid = -1
game.res_index = 0
game.window_w = RESOLUTIONS[game.res_index].width
game.window_h = RESOLUTIONS[game.res_index].height
@@ -67,9 +72,10 @@ game_init :: proc(game: ^Game) {
rl.InitWindow(game.window_w, game.window_h, WINDOW_TITLE)
rl.SetTargetFPS(60)
rl.SetExitKey(.KEY_NULL)
rl.ToggleBorderlessWindowed()
rl.HideCursor()
game_set_resolution(game, game.res_index)
gamepad_cstr := strings.clone_to_cstring(gamepad_compat)
defer delete(gamepad_cstr)
rl.SetGamepadMappings(gamepad_cstr)
@@ -115,7 +121,7 @@ game_init :: proc(game: ^Game) {
game_update :: proc(game: ^Game) {
dt := rl.GetFrameTime()
input := poll_input()
input := poll_input(game)
// -------------------------------------------------------------------------
// Update Camera Shake
@@ -183,8 +189,7 @@ game_render :: proc(game: ^Game) {
render_game_to_window(game)
when DEBUG do rl.DrawFPS(10, rl.GetScreenHeight() - 20)
// when DEBUG do rl.DrawFPS(10, rl.GetScreenHeight() - 20)
rl.EndDrawing()
}
+18 -12
View File
@@ -1,5 +1,6 @@
package game
import "core:strings"
import rl "vendor:raylib"
// ------------------------------------------------------------------------------
@@ -21,35 +22,40 @@ Input_Event :: struct {
}
// Currently active controller
active_gamepad_id: i32 = -1
is_real_pad :: proc(i: i32) -> bool {
return rl.IsGamepadAvailable(i) &&
!strings.contains(string(rl.GetGamepadName(i)), "Mouse") &&
!strings.contains(string(rl.GetGamepadName(i)), "Keyboard")
}
update_active_gamepad :: proc() {
update_active_gamepad :: proc(game: ^Game) {
// is there currently a controller, if so check if its still connected.
if active_gamepad_id != -1 {
if rl.IsGamepadAvailable(active_gamepad_id) {
if game.controller_gid != -1 {
if rl.IsGamepadAvailable(game.controller_gid) {
// do nothing still connected
return
}
// else we lost connection
active_gamepad_id = -1
rl.TraceLog(.WARNING, "Controller disconnected!!")
game.controller_gid = -1
game.controller_connected = false
}
for i: i32 = 0; i < 4; i += 1 {
if rl.IsGamepadAvailable(i) {
active_gamepad_id = i
if rl.IsGamepadAvailable(i) && is_real_pad(i) {
game.controller_connected = true
game.controller_gid = i
return
}
}
}
poll_input :: proc() -> Input_Event {
poll_input :: proc(game: ^Game) -> Input_Event {
input_event := Input_Event{}
input_dir := rl.Vector2{0, 0}
update_active_gamepad()
gamepad_active := active_gamepad_id != -1
gid := active_gamepad_id
update_active_gamepad(game)
gamepad_active := game.controller_connected
gid := game.controller_gid
// --------------------------------------------------------------------------
// Continuous Input (keyboard + gamepad d-pad)
+2 -2
View File
@@ -120,7 +120,7 @@ render_hud :: proc(level: Level) {
p1_size := rl.MeasureTextEx(font, "PLAYER 1", HUD_FONT_SIZE, HUD_FONT_SPACING)
p1_pos := rl.Vector2{HUD_PADDING, HUD_PADDING}
rl.DrawTextEx(font, "PLAYER 1", p1_pos, HUD_FONT_SIZE, HUD_FONT_SPACING, rl.WHITE)
rl.DrawTextEx(font, "PLAYER", p1_pos, HUD_FONT_SIZE, HUD_FONT_SPACING, rl.WHITE)
opp_size := rl.MeasureTextEx(font, "COMPUTER", HUD_FONT_SIZE, HUD_FONT_SPACING)
opp_pos := rl.Vector2{f32(VIEWPORT_W) - HUD_PADDING - opp_size.x, HUD_PADDING}
@@ -130,4 +130,4 @@ render_hud :: proc(level: Level) {
score_size := rl.MeasureTextEx(font, score_text, HUD_FONT_SIZE, HUD_FONT_SPACING)
score_pos := rl.Vector2{(f32(VIEWPORT_W) * 0.5) - (score_size.x * 0.5), HUD_PADDING}
rl.DrawTextEx(font, score_text, score_pos, HUD_FONT_SIZE, HUD_FONT_SPACING, rl.WHITE)
}
}
+1 -2
View File
@@ -1,6 +1,5 @@
package game
import "core:strings"
import "core:fmt"
import rl "vendor:raylib"
@@ -251,7 +250,7 @@ render_main_menu :: proc(scene: Scene) {
menu, menu_ok := canvas.menu.?
assert(menu_ok, "render_main_menu: Canvas is missing a UI_Menu component")
t_text: cstring = "PONG"
t_text: cstring = WINDOW_TITLE
t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE)
t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2)
rl.DrawText(t_text, t_x, i32(canvas.padding), UI_TITLE_FONT_SIZE, rl.WHITE)