Compare commits
23
Commits
cd1d9ef0e4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5038bc5673 | ||
|
|
7fbdb9f5cf | ||
|
|
b1bab7ece5 | ||
|
|
ba410861e6 | ||
|
|
1ee4ff920b | ||
|
|
f9987788bf | ||
|
|
ec2efe79f0 | ||
|
|
73b04fc80c | ||
|
|
9b9c777515 | ||
|
|
f1ef1075e7 | ||
|
|
72eb4d5447 | ||
|
|
d5b3588cb3 | ||
|
|
a7ada07466 | ||
|
|
79d9ba147c | ||
|
|
bb35f185b8 | ||
|
|
dda388868c | ||
|
|
fb7bfd6bf3 | ||
|
|
5395225cc2 | ||
|
|
200e05f2dc | ||
|
|
404c320973 | ||
|
|
d6c52cecda | ||
|
|
25bd7a85a1 | ||
|
|
0f393af931 |
@@ -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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,48 @@
|
||||
#version 330
|
||||
|
||||
in vec2 fragTexCoord;
|
||||
in vec4 fragColor;
|
||||
|
||||
out vec4 finalColor;
|
||||
|
||||
uniform sampler2D texture0;
|
||||
uniform vec4 colDiffuse;
|
||||
|
||||
// Adjust these values to tweak the aesthetic:
|
||||
const float SCANLINE_INTENSITY = 0.25; // Lower value = softer lines (0.08 to 0.20)
|
||||
const float BLOOM_AMOUNT = 0.10; // Slight phosphor bleed/glow
|
||||
const float CRT_CURVATURE = 0.03; // Subtle screen bulge (0.0 = flat)
|
||||
|
||||
void main() {
|
||||
// 1. Subtle screen curvature distortion
|
||||
vec2 uv = fragTexCoord;
|
||||
vec2 dc = abs(0.5 - uv);
|
||||
uv.x += uv.x * (dc.y * dc.y) * CRT_CURVATURE;
|
||||
uv.y += uv.y * (dc.x * dc.x) * CRT_CURVATURE;
|
||||
|
||||
// Discard pixels outside the curved screen bounds
|
||||
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
|
||||
finalColor = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Sample pixel with slight horizontal blur (Phosphor bleed)
|
||||
vec4 col = texture(texture0, uv);
|
||||
vec4 col_left = texture(texture0, uv - vec2(0.0008, 0.0));
|
||||
vec4 col_right = texture(texture0, uv + vec2(0.0008, 0.0));
|
||||
col.rgb = mix(col.rgb, (col_left.rgb + col_right.rgb) * 0.5, BLOOM_AMOUNT);
|
||||
|
||||
// 3. Gaussian scanline profile matching 360 vertical lines
|
||||
float scanline = sin(uv.y * 360.0 * 3.14159);
|
||||
// Smooth power curve prevents harsh pitch-black cutoffs
|
||||
float scanline_factor = pow(0.5 + 0.5 * scanline, 1.8);
|
||||
|
||||
// Apply scanlines smoothly
|
||||
col.rgb *= mix(1.0 - SCANLINE_INTENSITY, 1.0, scanline_factor);
|
||||
|
||||
// 4. Subtle corner vignette
|
||||
float vig = (16.0 * uv.x * uv.y * (1.0 - uv.x) * (1.0 - uv.y));
|
||||
col.rgb *= pow(vig, 0.05);
|
||||
|
||||
finalColor = col * colDiffuse * fragColor;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -eo pipefail
|
||||
|
||||
# Find the odin binary
|
||||
export PATH="$HOME/.odin:$PATH"
|
||||
@@ -9,26 +9,72 @@ SRC_DIR="src"
|
||||
OUT_DIR="out"
|
||||
OUT_NAME="pong"
|
||||
|
||||
# flags for every odin build; unix-style errors (file:line:col: msg) are what the vis error picker parses
|
||||
ODIN_FLAGS=(-error-pos-style:unix)
|
||||
|
||||
# ---- ctags ------------------------------------------------------------
|
||||
# tags = project tags (-t, vis runs it on every .odin save)
|
||||
# + stdlib tags (-T, once, or after an Odin update), shared by all projects
|
||||
TAGS_FILE="tags"
|
||||
STD_TAGS="${XDG_CACHE_HOME:-$HOME/.cache}/odin-tags/std.tags"
|
||||
|
||||
# --languages=Odin keeps vendor's bundled C/JS out of the index;
|
||||
# --fields=+n adds the line:N field the vis pickers jump to
|
||||
CTAGS_OPTS=(--languages=Odin --fields=+n)
|
||||
# -----------------------------------------------------------------------
|
||||
|
||||
BUILD_ONLY=0
|
||||
OPTIMIZED=0
|
||||
WEB=0
|
||||
CLEAN=0
|
||||
DEBUGGER=0
|
||||
VALGRIND=0
|
||||
TAGS=""
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [-b] [-o] [-w] [-d] [-v] [-c] [-h]"
|
||||
echo "Usage: $0 [-b] [-o] [-w] [-d] [-v] [-c] [-t] [-T] [-h]"
|
||||
echo " -b build only, don't run"
|
||||
echo " -o optimized release build (-o:speed, no debug info)"
|
||||
echo " -w web/WASM build (js_wasm32 target)"
|
||||
echo " -d build then launch in debugger instead of running"
|
||||
echo " -v build then run under Valgrind"
|
||||
echo " -c clean build artifacts"
|
||||
echo " -t re-index $SRC_DIR and merge it with the stdlib tags"
|
||||
echo " -T index Odin core + vendor (once, or after an Odin update)"
|
||||
echo " -h show this help"
|
||||
exit 1
|
||||
}
|
||||
|
||||
while getopts "bowdvch" opt; do
|
||||
# stdlib tags, header lines stripped so the merged file keeps only one set
|
||||
generate_std_tags() {
|
||||
local root
|
||||
root="$(odin root 2>/dev/null || echo "$HOME/.odin")"
|
||||
root="${root%/}"
|
||||
|
||||
mkdir -p "$(dirname "$STD_TAGS")"
|
||||
ctags -R -f - "${CTAGS_OPTS[@]}" "$root/core" "$root/vendor" | grep -v '^!_TAG_' > "$STD_TAGS.tmp"
|
||||
mv -f "$STD_TAGS.tmp" "$STD_TAGS"
|
||||
echo "Stdlib tags written -> $STD_TAGS"
|
||||
}
|
||||
|
||||
# project tags merged with the stdlib tags; both inputs are byte-sorted
|
||||
generate_tags() {
|
||||
if [ ! -f "$STD_TAGS" ]; then
|
||||
echo "No stdlib tags; run $0 -T first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# $$ keeps two overlapping on-save runs from sharing temp files
|
||||
local src_tags="$TAGS_FILE.src.$$"
|
||||
local merged="$TAGS_FILE.tmp.$$"
|
||||
|
||||
ctags -R -f "$src_tags" "${CTAGS_OPTS[@]}" "$SRC_DIR"
|
||||
LC_ALL=C sort -m -u "$src_tags" "$STD_TAGS" > "$merged"
|
||||
mv -f "$merged" "$TAGS_FILE"
|
||||
rm -f "$src_tags"
|
||||
}
|
||||
|
||||
while getopts "bowdvctTh" opt; do
|
||||
case $opt in
|
||||
b) BUILD_ONLY=1 ;;
|
||||
o) OPTIMIZED=1 ;;
|
||||
@@ -36,11 +82,18 @@ while getopts "bowdvch" opt; do
|
||||
d) DEBUGGER=1 ;;
|
||||
v) VALGRIND=1 ;;
|
||||
c) CLEAN=1 ;;
|
||||
t) TAGS=src ;;
|
||||
T) TAGS=std ;;
|
||||
h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$TAGS" in
|
||||
src) generate_tags; exit 0 ;;
|
||||
std) generate_std_tags; exit 0 ;;
|
||||
esac
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
if [ "$CLEAN" -eq 1 ]; then
|
||||
@@ -51,7 +104,7 @@ fi
|
||||
|
||||
if [ "$WEB" -eq 1 ]; then
|
||||
echo "Building web (WASM)..."
|
||||
odin build "$SRC_DIR" \
|
||||
odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
|
||||
-target:js_wasm32 \
|
||||
-out:"$OUT_DIR/$OUT_NAME.wasm" \
|
||||
$( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" )
|
||||
@@ -67,15 +120,16 @@ fi
|
||||
|
||||
if [ "$OPTIMIZED" -eq 1 ]; then
|
||||
echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
|
||||
odin build "$SRC_DIR" \
|
||||
odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
|
||||
-out:"$OUT_DIR/$OUT_NAME" \
|
||||
-o:speed u
|
||||
-no-bounds-check
|
||||
-o:speed \
|
||||
-no-bounds-check
|
||||
else
|
||||
echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
|
||||
odin build "$SRC_DIR" \
|
||||
odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
|
||||
-out:"$OUT_DIR/$OUT_NAME" \
|
||||
-define:DEBUG_MODE=true
|
||||
-debug \
|
||||
-define:DEBUG_MODE=true
|
||||
fi
|
||||
|
||||
echo "Build complete."
|
||||
@@ -87,6 +141,7 @@ if [ "$BUILD_ONLY" -eq 0 ]; then
|
||||
lldb "$OUT_DIR/$OUT_NAME"
|
||||
|
||||
elif [ "$VALGRIND" -eq 1 ]; then
|
||||
VG_STATUS=0
|
||||
valgrind \
|
||||
--leak-check=full \
|
||||
--show-leak-kinds=definite,possible \
|
||||
@@ -94,11 +149,13 @@ if [ "$BUILD_ONLY" -eq 0 ]; then
|
||||
--errors-for-leak-kinds=definite,possible \
|
||||
--error-exitcode=1 \
|
||||
--log-file="$OUT_DIR/valgrind.log" \
|
||||
"./$OUT_DIR/$OUT_NAME"
|
||||
"./$OUT_DIR/$OUT_NAME" || VG_STATUS=$?
|
||||
|
||||
echo
|
||||
echo "Valgrind report written to $OUT_DIR/valgrind.log"
|
||||
[ "$VG_STATUS" -ne 0 ] && echo "Valgrind reported errors (exit $VG_STATUS)"
|
||||
exit "$VG_STATUS"
|
||||
else
|
||||
"./$OUT_DIR/$OUT_NAME"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"build_systems":
|
||||
[
|
||||
{
|
||||
"name": "Run Build Script",
|
||||
"cmd": ["/bin/bash", "build.sh"],
|
||||
"working_dir": "$project_path",
|
||||
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
|
||||
"variants":
|
||||
[
|
||||
{
|
||||
"name": "Run with Arguments...",
|
||||
"cmd": ["/bin/bash", "build.sh", "$select_args"]
|
||||
},
|
||||
{
|
||||
"name": "Build Only (-b)",
|
||||
"cmd": ["/bin/bash", "build.sh", "-b"]
|
||||
},
|
||||
{
|
||||
"name": "Optimized (-o)",
|
||||
"cmd": ["/bin/bash", "build.sh", "-o"]
|
||||
},
|
||||
{
|
||||
"name": "Debug in LLDB (-d)",
|
||||
"cmd": ["/bin/bash", "build.sh", "-d"]
|
||||
},
|
||||
{
|
||||
"name": "Valgrind (-v)",
|
||||
"cmd": ["/bin/bash", "build.sh", "-v"]
|
||||
},
|
||||
{
|
||||
"name": "Clean (-c)",
|
||||
"cmd": ["/bin/bash", "build.sh", "-c"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
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 |
+10
-10
@@ -8,7 +8,7 @@ Fade_State :: enum {
|
||||
Out,
|
||||
}
|
||||
|
||||
// setup sound IDs so gameplay code never
|
||||
// setup sound IDs so gameplay code never
|
||||
// touches raw Raylib sound handles
|
||||
Sound_ID :: enum {
|
||||
Ball_Collision,
|
||||
@@ -20,12 +20,12 @@ Sound_ID :: enum {
|
||||
Audio_Manager :: struct {
|
||||
sounds: map[Sound_ID]rl.Sound,
|
||||
music: rl.Music,
|
||||
music_loaded: bool,
|
||||
|
||||
music_loaded: bool, //
|
||||
|
||||
sfx_vol: f32, // User SFX volume setting
|
||||
user_music_vol: f32, // User Music volume setting
|
||||
current_music_vol: f32, // Live interpolated output volume
|
||||
|
||||
|
||||
// Runtime Fade State
|
||||
fade_state: Fade_State,
|
||||
fade_elapsed_sec: f32,
|
||||
@@ -40,13 +40,13 @@ audio_init :: proc(audio: ^Audio_Manager) {
|
||||
audio.sounds = make(map[Sound_ID]rl.Sound)
|
||||
|
||||
// Load SFX
|
||||
audio.sfx_vol = 0.3
|
||||
audio.sfx_vol = 0.4
|
||||
audio.sounds[.UI_Select] = rl.LoadSound("assets/audio/ui/select.ogg")
|
||||
audio.sounds[.Ball_Collision] = rl.LoadSound("assets/audio/gameplay/ballhit.ogg")
|
||||
|
||||
// Load Music Stream
|
||||
audio.user_music_vol = 0.1
|
||||
audio.current_music_vol = 0.1
|
||||
audio.user_music_vol = 0.4
|
||||
audio.current_music_vol = 0.4
|
||||
audio.fade_state = .None
|
||||
audio.music = rl.LoadMusicStream("assets/audio/gameplay/music/track_1.wav")
|
||||
audio.music_loaded = true
|
||||
@@ -91,7 +91,7 @@ audio_update :: proc(audio: ^Audio_Manager, dt: f32) {
|
||||
|
||||
audio_manager_quit :: proc(audio: ^Audio_Manager) {
|
||||
for _, sound in audio.sounds {
|
||||
rl.UnloadSound(sound)
|
||||
rl.UnloadSound(sound)
|
||||
}
|
||||
delete(audio.sounds)
|
||||
rl.UnloadMusicStream(audio.music)
|
||||
@@ -119,7 +119,7 @@ audio_stop_music :: proc(audio: ^Audio_Manager) {
|
||||
audio_play_sfx :: proc(audio: ^Audio_Manager, id: Sound_ID) {
|
||||
if sound, ok := audio.sounds[id]; ok {
|
||||
rl.SetSoundVolume(sound, audio.sfx_vol)
|
||||
rl.PlaySound(sound)
|
||||
rl.PlaySound(sound)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,4 +146,4 @@ audio_fade_out :: proc(audio: ^Audio_Manager, duration: f32 = 1.0) {
|
||||
audio.fade_state = .Out
|
||||
audio.fade_duration_sec = max(0.001, duration)
|
||||
audio.fade_elapsed_sec = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -6,14 +6,16 @@ import rl "vendor:raylib"
|
||||
|
||||
SPEED_INC :: 30
|
||||
EFFECTS_SPEED :: 300
|
||||
INITIAL_SPEED :: 200
|
||||
MAX_SPEED :: 550
|
||||
|
||||
init_ball :: proc() -> Entity {
|
||||
return Entity{
|
||||
type = .Ball,
|
||||
active = true,
|
||||
pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)},
|
||||
speed = 200,
|
||||
max_speed = 550,
|
||||
speed = INITIAL_SPEED,
|
||||
max_speed = MAX_SPEED,
|
||||
vel = rl.Vector2{0,0},
|
||||
shape = .Circle,
|
||||
size = rl.Vector2{8, 0},
|
||||
@@ -21,9 +23,13 @@ init_ball :: proc() -> Entity {
|
||||
}
|
||||
}
|
||||
|
||||
serve_ball :: proc(ball: ^Entity) {
|
||||
serve_ball :: proc(ball: ^Entity, serve_dir: u8) {
|
||||
dir_x: f32 = 0.0
|
||||
// Randomize initial direction: -1.0 or 1.0
|
||||
dir_x := f32(rand.float32() > 0.5 ? 1 : -1)
|
||||
if serve_dir == 0 do dir_x = f32(rand.float32() > 0.5 ? 1 : -1)
|
||||
if serve_dir == 1 do dir_x = 1.0
|
||||
if serve_dir == 2 do dir_x = -1.0
|
||||
|
||||
dir_y := rand.choice([]f32{-0.1, 0.1}) // Slightly angled launch
|
||||
ball.vel = rl.Vector2Normalize({dir_x, dir_y}) * ball.speed
|
||||
}
|
||||
@@ -31,7 +37,9 @@ serve_ball :: proc(ball: ^Entity) {
|
||||
reset_ball :: proc(ball: ^Entity) {
|
||||
ball.pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)}
|
||||
ball.vel = rl.Vector2{0,0}
|
||||
ball.speed = 300
|
||||
ball.speed = INITIAL_SPEED
|
||||
ball.trail_positions = {}
|
||||
ball.trail_count = 0
|
||||
}
|
||||
|
||||
update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
|
||||
@@ -45,7 +53,7 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
|
||||
level.serve_timer -= dt
|
||||
|
||||
if level.serve_timer <= 0 {
|
||||
serve_ball(ball)
|
||||
serve_ball(ball, level.serve_dir)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -86,12 +94,14 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
|
||||
increment_score(&level.score_player, level.score_win, true, ball)
|
||||
reset_ball(ball)
|
||||
level.serve_timer = level.wait_time
|
||||
level.serve_dir = 1
|
||||
}
|
||||
|
||||
if ball.pos.x <= field_bounds.x + ball.size.x {
|
||||
increment_score(&level.score_enemy, level.score_win, false, ball)
|
||||
reset_ball(ball)
|
||||
level.serve_timer = level.wait_time
|
||||
level.serve_dir = 2
|
||||
}
|
||||
|
||||
ball.pos += ball.vel * dt
|
||||
@@ -110,10 +120,10 @@ 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})
|
||||
ball.trail_count = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ update_opponent :: proc(self: ^Entity, ball: Entity, bounds: rl.Rectangle, dt: f
|
||||
// center of ball - center of paddle gives the target
|
||||
self.target_y = (ball.pos.y + (ball.size.y / 2)) - (self.size.y / 2) + error_offset
|
||||
|
||||
// TODO: make this and the deadzone change with difficulty
|
||||
self.reaction_timer = 0.15
|
||||
}
|
||||
} else {
|
||||
|
||||
+35
-12
@@ -58,35 +58,54 @@ 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:
|
||||
#partial switch e {
|
||||
case .Game_Start:
|
||||
set_root_scene(game, make_gameplay_scene())
|
||||
game.transition = {true, 0, 0.2}
|
||||
audio_fade_in(&game.audio, duration = 1.5)
|
||||
|
||||
case .Game_Pause:
|
||||
push_scene(game, make_pause_overlay())
|
||||
|
||||
case .Game_Resume, .Game_Back:
|
||||
case .Game_Resume:
|
||||
pop_scene(game)
|
||||
|
||||
case .Score_Change:
|
||||
game_trigger_shake(game, 10, duration = 0.20)
|
||||
|
||||
case .Game_Settings:
|
||||
push_scene(game, make_settings_menu(&game.audio, RESOLUTIONS[game.res_index].label))
|
||||
parent: Maybe(Scene_Type) = nil
|
||||
if active := get_active_scene(game); active != nil {
|
||||
parent = active.type
|
||||
}
|
||||
|
||||
pop_scene(game)
|
||||
push_scene(game, make_settings_menu(parent, &game.audio, RESOLUTIONS[game.res_index].label))
|
||||
|
||||
case .Game_Main_Menu:
|
||||
set_root_scene(game, make_main_menu())
|
||||
game.transition = {true, 0, 0.3}
|
||||
audio_fade_out(&game.audio, duration = 1.0)
|
||||
|
||||
case .Game_Back:
|
||||
// Read parent from active scene
|
||||
if active := get_active_scene(game); active != nil {
|
||||
if parent, ok := active.parent.?; ok {
|
||||
pop_scene(game)
|
||||
|
||||
#partial switch parent {
|
||||
case .Pause_Menu: push_scene(game, make_pause_overlay())
|
||||
case .Main_Menu: push_scene(game, make_main_menu())
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// still pop when no parent linked ie resuming Gameplay
|
||||
pop_scene(game)
|
||||
|
||||
case .Game_Quit:
|
||||
game.should_close = true
|
||||
@@ -97,4 +116,8 @@ process_events :: proc(game: ^Game) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_manager_quit :: proc(game: ^Game) {
|
||||
delete(GLOBAL_EVENTS)
|
||||
}
|
||||
|
||||
+71
-20
@@ -1,4 +1,4 @@
|
||||
package game
|
||||
package game
|
||||
|
||||
import "core:strings"
|
||||
import "core:math/rand"
|
||||
@@ -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,
|
||||
@@ -16,16 +16,27 @@ Resolution :: struct {
|
||||
|
||||
// Preset window dimensions
|
||||
RESOLUTIONS := [?]Resolution{
|
||||
{1280, 720, "1280x720"},
|
||||
{0, 0, "Fullscreen"},
|
||||
{1024, 768, "1024x768"},
|
||||
{1280, 720, "1280x720"},
|
||||
{1366, 768, "1366x768"},
|
||||
{1600, 900, "1600x900"},
|
||||
{1920, 1080, "1920x1080"},
|
||||
{2560, 1440, "2560x1440"},
|
||||
{3840, 2160, "3840x2160"},
|
||||
}
|
||||
|
||||
Transition :: struct {
|
||||
active: bool,
|
||||
elapsed: f32,
|
||||
duration: f32,
|
||||
}
|
||||
|
||||
Game :: struct {
|
||||
viewport: rl.RenderTexture2D,
|
||||
bg_texture: rl.Texture2D,
|
||||
crt_shader: rl.Shader,
|
||||
transition: Transition,
|
||||
scene_stack: [dynamic]Scene,
|
||||
should_close: bool,
|
||||
audio: Audio_Manager,
|
||||
@@ -38,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!
|
||||
@@ -48,14 +63,18 @@ 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
|
||||
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
|
||||
|
||||
rl.InitWindow(game.window_w, game.window_h, WINDOW_TITLE)
|
||||
rl.SetTargetFPS(60)
|
||||
rl.SetExitKey(.KEY_NULL)
|
||||
rl.HideCursor()
|
||||
|
||||
game_set_resolution(game, game.res_index)
|
||||
|
||||
gamepad_cstr := strings.clone_to_cstring(gamepad_compat)
|
||||
defer delete(gamepad_cstr)
|
||||
@@ -79,9 +98,14 @@ game_init :: proc(game: ^Game) {
|
||||
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")
|
||||
|
||||
// some ray gui color updates for now
|
||||
rl.GuiSetStyle(.SLIDER, i32(rl.GuiControlProperty.BASE_COLOR_NORMAL), 0x00000000)
|
||||
rl.GuiSetStyle(.SLIDER, i32(rl.GuiControlProperty.BORDER_WIDTH), 0x00000000)
|
||||
rl.GuiSetStyle(.SLIDER, i32(rl.GuiControlProperty.BASE_COLOR_PRESSED), 0x66BFFFFF)
|
||||
rl.GuiSetStyle(.SLIDER, i32(rl.GuiControlProperty.TEXT_COLOR_FOCUSED), 0x66BFFFFF)
|
||||
|
||||
// Setup 2D camera matching the 640x360 viewport center
|
||||
game.camera = rl.Camera2D{
|
||||
target = rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0},
|
||||
@@ -90,13 +114,14 @@ game_init :: proc(game: ^Game) {
|
||||
zoom = 1.0,
|
||||
}
|
||||
|
||||
game.transition = {true, 0, 0.2}
|
||||
game.should_close = false
|
||||
set_root_scene(game, make_main_menu())
|
||||
}
|
||||
|
||||
game_update :: proc(game: ^Game) {
|
||||
dt := rl.GetFrameTime()
|
||||
input := poll_input()
|
||||
input := poll_input(game)
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Update Camera Shake
|
||||
@@ -121,10 +146,15 @@ game_update :: proc(game: ^Game) {
|
||||
}
|
||||
|
||||
active_scene := get_active_scene(game)
|
||||
if active_scene != nil {
|
||||
|
||||
if game.transition.active {
|
||||
game.transition.elapsed += dt
|
||||
if game.transition.elapsed >= game.transition.duration do game.transition.active = false
|
||||
} else if active_scene != nil {
|
||||
scene_update(active_scene, game, input, dt)
|
||||
}
|
||||
|
||||
|
||||
// background music buffer streaming
|
||||
audio_update(&game.audio, dt)
|
||||
|
||||
@@ -137,36 +167,41 @@ game_render :: proc(game: ^Game) {
|
||||
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)
|
||||
|
||||
// @note: fine for now but probably not a good idea...
|
||||
// Render all scenes in the stack from bottom to top
|
||||
for i in 0 ..< len(game.scene_stack) {
|
||||
scene_render(&game.scene_stack[i])
|
||||
// Render all active items on the stack from bottom to top
|
||||
for &scene in game.scene_stack {
|
||||
scene_render(&scene)
|
||||
}
|
||||
|
||||
// when game transition active increment its elapsed time
|
||||
// render will check and display accordingly
|
||||
if game.transition.active {
|
||||
a := u8(255 * (1 - clamp(game.transition.elapsed / game.transition.duration, 0, 1)))
|
||||
rl.DrawRectangle(0, 0, VIEWPORT_W, VIEWPORT_H, {0, 0, 0, a})
|
||||
}
|
||||
rl.EndMode2D()
|
||||
|
||||
rl.EndTextureMode()
|
||||
|
||||
render_game_to_window(game)
|
||||
|
||||
rl.DrawFPS(10, game.window_h - 20)
|
||||
// when DEBUG do rl.DrawFPS(10, rl.GetScreenHeight() - 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)
|
||||
delete(GLOBAL_EVENTS)
|
||||
delete(game.scene_stack)
|
||||
|
||||
rl.CloseWindow()
|
||||
}
|
||||
|
||||
@@ -184,6 +219,17 @@ game_trigger_shake :: proc(game: ^Game, intensity: f32, duration: f32 = 0.15) {
|
||||
// ------------------------------------------------------------------------------
|
||||
game_set_resolution :: proc(game: ^Game, res_index: int) {
|
||||
game.res_index = clamp(res_index, 0, len(RESOLUTIONS) - 1)
|
||||
|
||||
borderless := rl.IsWindowState({.BORDERLESS_WINDOWED_MODE})
|
||||
if game.res_index == 0 {
|
||||
if !borderless do rl.ToggleBorderlessWindowed()
|
||||
return
|
||||
}
|
||||
|
||||
if borderless {
|
||||
rl.ToggleBorderlessWindowed()
|
||||
rl.RestoreWindow()
|
||||
}
|
||||
|
||||
target_w := RESOLUTIONS[game.res_index].width
|
||||
target_h := RESOLUTIONS[game.res_index].height
|
||||
@@ -202,16 +248,21 @@ game_set_resolution :: proc(game: ^Game, res_index: int) {
|
||||
render_game_to_window :: proc(game: ^Game) {
|
||||
texture := game.viewport.texture
|
||||
|
||||
w, h := f32(rl.GetScreenWidth()), f32(rl.GetScreenHeight())
|
||||
s := min(w / VIEWPORT_W, h / VIEWPORT_H)
|
||||
dst := rl.Rectangle{(w - VIEWPORT_W*s)/2, (h - VIEWPORT_H*s)/2, VIEWPORT_W*s, VIEWPORT_H*s}
|
||||
|
||||
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.Rectangle{0, 0, f32(game.window_w), f32(game.window_h)},
|
||||
dst,
|
||||
rl.Vector2{},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
rl.EndShaderMode()
|
||||
}
|
||||
}
|
||||
|
||||
+20
-14
@@ -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)
|
||||
@@ -86,11 +92,11 @@ poll_input :: proc() -> Input_Event {
|
||||
input_event.right_pressed = rl.IsKeyPressed(.D) || rl.IsKeyPressed(.RIGHT) ||
|
||||
(gamepad_active && rl.IsGamepadButtonPressed(gid, .LEFT_FACE_RIGHT))
|
||||
|
||||
input_event.confirm_pressed = rl.IsKeyPressed(.ENTER) || rl.IsKeyPressed(.SPACE) ||
|
||||
input_event.confirm_pressed = rl.IsKeyPressed(.ENTER) ||
|
||||
(gamepad_active && rl.IsGamepadButtonPressed(gid, .RIGHT_FACE_DOWN))
|
||||
|
||||
input_event.pause_pressed = rl.IsKeyPressed(.SPACE) ||
|
||||
(gamepad_active && rl.IsGamepadButtonPressed(gid, .MIDDLE_RIGHT))
|
||||
|
||||
return input_event
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ Level :: struct {
|
||||
player: Entity,
|
||||
opponent: Entity,
|
||||
ball: Entity,
|
||||
serve_dir: u8,
|
||||
serve_timer: f32,
|
||||
wait_time: f32,
|
||||
score_player: int,
|
||||
@@ -55,7 +56,7 @@ init_level :: proc() -> Level {
|
||||
ball = init_ball(),
|
||||
serve_timer = SERVE_WAIT_TIME,
|
||||
wait_time = SERVE_WAIT_TIME,
|
||||
score_win = 1,
|
||||
score_win = 3,
|
||||
score_player = 0,
|
||||
score_enemy = 0,
|
||||
particles = make([dynamic]Particle),
|
||||
@@ -119,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}
|
||||
@@ -129,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)
|
||||
}
|
||||
}
|
||||
|
||||
+26
-7
@@ -16,6 +16,7 @@ Scene_Type :: enum {
|
||||
|
||||
Scene :: struct {
|
||||
type: Scene_Type,
|
||||
parent: Maybe(Scene_Type),
|
||||
level: Maybe(Level),
|
||||
canvas: Maybe(Canvas),
|
||||
winner_is_player: bool,
|
||||
@@ -26,7 +27,7 @@ push_scene :: proc(game: ^Game, scene: Scene) {
|
||||
}
|
||||
|
||||
pop_scene :: proc(game: ^Game) {
|
||||
if len(game.scene_stack) > 1 {
|
||||
if len(game.scene_stack) > 0 {
|
||||
top_index := len(game.scene_stack) - 1
|
||||
destroy_scene(&game.scene_stack[top_index])
|
||||
pop(&game.scene_stack)
|
||||
@@ -101,6 +102,13 @@ scene_render :: proc(scene: ^Scene) {
|
||||
}
|
||||
}
|
||||
|
||||
scene_manager_quit :: proc(game: ^Game) {
|
||||
for &scene in game.scene_stack {
|
||||
destroy_scene(&scene)
|
||||
}
|
||||
delete(game.scene_stack)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
// UI Layout & Styling Constants
|
||||
// ------------------------------------------------------------------------------
|
||||
@@ -169,8 +177,8 @@ list_menu_items :: proc(canvas: Canvas, menu: UI_Menu) {
|
||||
item_rec := get_canvas_item_rec(canvas, i, top_margin = UI_TOP_MARGIN_LIST)
|
||||
|
||||
is_selected := (i == menu.selected_index)
|
||||
border_color := is_selected ? rl.ORANGE : rl.DARKGRAY
|
||||
text_color := is_selected ? rl.ORANGE : rl.WHITE
|
||||
border_color := is_selected ? rl.SKYBLUE : rl.DARKGRAY
|
||||
text_color := is_selected ? rl.SKYBLUE : rl.WHITE
|
||||
|
||||
rl.DrawRectangleLinesEx(item_rec, UI_ITEM_BORDER_THICK, border_color)
|
||||
|
||||
@@ -183,7 +191,7 @@ list_menu_items :: proc(canvas: Canvas, menu: UI_Menu) {
|
||||
|
||||
case UI_Slider:
|
||||
slider_rec := item_rec
|
||||
text_width := rl.MeasureText(v.label, UI_LABEL_FONT_SIZE)
|
||||
text_width := 31
|
||||
slider_rec.x = slider_rec.x + f32(text_width * 2)
|
||||
slider_rec.width = (slider_rec.width - f32(text_width * 2))
|
||||
|
||||
@@ -220,6 +228,7 @@ make_main_menu :: proc() -> Scene {
|
||||
|
||||
return Scene{
|
||||
type = .Main_Menu,
|
||||
parent = nil,
|
||||
canvas = Canvas{
|
||||
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
|
||||
padding = UI_TITLE_PADDING,
|
||||
@@ -241,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)
|
||||
@@ -289,6 +298,7 @@ make_pause_overlay :: proc() -> Scene {
|
||||
|
||||
return Scene{
|
||||
type = .Pause_Menu,
|
||||
parent = nil,
|
||||
canvas = Canvas{
|
||||
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
|
||||
padding = UI_TITLE_PADDING,
|
||||
@@ -356,7 +366,7 @@ update_pause_menu :: proc(scene: ^Scene, input: Input_Event) {
|
||||
// ------------------------------------------------------------------------------
|
||||
// Settings Menu
|
||||
// ------------------------------------------------------------------------------
|
||||
make_settings_menu :: proc(audio: ^Audio_Manager, current_res_label: cstring) -> Scene {
|
||||
make_settings_menu :: proc(parent: Maybe(Scene_Type), audio: ^Audio_Manager, current_res_label: cstring) -> Scene {
|
||||
ui_items:[8]UI_Item = {
|
||||
0 = UI_Button{"Back"},
|
||||
1 = UI_Selector{
|
||||
@@ -377,6 +387,7 @@ make_settings_menu :: proc(audio: ^Audio_Manager, current_res_label: cstring) ->
|
||||
|
||||
return Scene{
|
||||
type = .Settings_Menu,
|
||||
parent = parent,
|
||||
canvas = Canvas{
|
||||
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
|
||||
padding = UI_TITLE_PADDING,
|
||||
@@ -398,6 +409,12 @@ render_settings_menu :: proc(scene: Scene) {
|
||||
menu, menu_ok := canvas.menu.?
|
||||
assert(menu_ok, "render_settings_menu: Canvas is missing a UI_Menu component")
|
||||
|
||||
|
||||
if scene.parent != .Main_Menu {
|
||||
// Draw dark overlay so gameplay/background stays visible behind Settings
|
||||
rl.DrawRectangleRec(canvas.bounds, rl.Color{0, 0, 0, 200})
|
||||
}
|
||||
|
||||
t_text: cstring = "SETTINGS"
|
||||
t_width := rl.MeasureText(t_text, UI_TITLE_FONT_SIZE)
|
||||
t_x := i32(canvas.bounds.width * 0.5) - (t_width / 2)
|
||||
@@ -465,6 +482,7 @@ make_game_over_overlay :: proc(winner_is_player: bool) -> Scene {
|
||||
|
||||
return Scene{
|
||||
type = .Game_Over,
|
||||
parent = nil,
|
||||
winner_is_player = winner_is_player,
|
||||
canvas = Canvas{
|
||||
bounds = rl.Rectangle{0, 0, VIEWPORT_W, VIEWPORT_H},
|
||||
@@ -530,6 +548,7 @@ update_game_over :: proc(scene: ^Scene, input: Input_Event) {
|
||||
make_gameplay_scene :: proc() -> Scene {
|
||||
return Scene{
|
||||
type = .Gameplay,
|
||||
parent = nil,
|
||||
level = init_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user