Compare commits
29
Commits
2fc3bd07a2
...
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 | ||
|
|
cd1d9ef0e4 | ||
|
|
5f38503b49 | ||
|
|
29c88a67ad | ||
|
|
f345b0a5f4 | ||
|
|
ce142a2d02 | ||
|
|
4a1df958e4 |
@@ -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,106 +1,161 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -eo pipefail
|
||||
|
||||
# Find the odin binary
|
||||
export PATH="$HOME/.odin:$PATH"
|
||||
|
||||
|
||||
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 " -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 " -h show this help"
|
||||
exit 1
|
||||
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
|
||||
case $opt in
|
||||
b) BUILD_ONLY=1 ;;
|
||||
o) OPTIMIZED=1 ;;
|
||||
w) WEB=1 ;;
|
||||
d) DEBUGGER=1 ;;
|
||||
v) VALGRIND=1 ;;
|
||||
c) CLEAN=1 ;;
|
||||
h) usage ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
# 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 ;;
|
||||
w) WEB=1 ;;
|
||||
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
|
||||
echo "Cleaning..."
|
||||
rm -rf "${OUT_DIR:?}"/*
|
||||
exit 0
|
||||
echo "Cleaning..."
|
||||
rm -rf "${OUT_DIR:?}"/*
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$WEB" -eq 1 ]; then
|
||||
echo "Building web (WASM)..."
|
||||
odin build "$SRC_DIR" \
|
||||
-target:js_wasm32 \
|
||||
-out:"$OUT_DIR/$OUT_NAME.wasm" \
|
||||
$( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" )
|
||||
echo "Building web (WASM)..."
|
||||
odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
|
||||
-target:js_wasm32 \
|
||||
-out:"$OUT_DIR/$OUT_NAME.wasm" \
|
||||
$( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" )
|
||||
|
||||
# Copy the HTML shell if one exists
|
||||
if [ -f "web/index_template.html" ]; then
|
||||
cp web/index_template.html "$OUT_DIR/index.html"
|
||||
fi
|
||||
# Copy the HTML shell if one exists
|
||||
if [ -f "web/index_template.html" ]; then
|
||||
cp web/index_template.html "$OUT_DIR/index.html"
|
||||
fi
|
||||
|
||||
echo "Web build complete -> $OUT_DIR/$OUT_NAME.wasm"
|
||||
exit 0
|
||||
echo "Web build complete -> $OUT_DIR/$OUT_NAME.wasm"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$OPTIMIZED" -eq 1 ]; then
|
||||
echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
|
||||
odin build "$SRC_DIR" \
|
||||
-out:"$OUT_DIR/$OUT_NAME" \
|
||||
-o:speed \
|
||||
-no-bounds-check
|
||||
echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
|
||||
odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
|
||||
-out:"$OUT_DIR/$OUT_NAME" \
|
||||
-o:speed \
|
||||
-no-bounds-check
|
||||
else
|
||||
echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
|
||||
odin build "$SRC_DIR" \
|
||||
-out:"$OUT_DIR/$OUT_NAME" \
|
||||
-define:DEBUG_MODE=true
|
||||
echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
|
||||
odin build "$SRC_DIR" "${ODIN_FLAGS[@]}" \
|
||||
-out:"$OUT_DIR/$OUT_NAME" \
|
||||
-debug \
|
||||
-define:DEBUG_MODE=true
|
||||
fi
|
||||
|
||||
echo "Build complete."
|
||||
|
||||
if [ "$BUILD_ONLY" -eq 0 ]; then
|
||||
echo "----------------------------------------"
|
||||
echo "----------------------------------------"
|
||||
|
||||
if [ "$DEBUGGER" -eq 1 ]; then
|
||||
lldb "$OUT_DIR/$OUT_NAME"
|
||||
if [ "$DEBUGGER" -eq 1 ]; then
|
||||
lldb "$OUT_DIR/$OUT_NAME"
|
||||
|
||||
elif [ "$VALGRIND" -eq 1 ]; then
|
||||
# --show-leak-kinds=definite,possible reduces tons of noise
|
||||
# still shows the leaks you actually need to fix.
|
||||
# to audit whole process, temporarily switch back to --show-leak-kinds=all
|
||||
valgrind \
|
||||
elif [ "$VALGRIND" -eq 1 ]; then
|
||||
VG_STATUS=0
|
||||
valgrind \
|
||||
--leak-check=full \
|
||||
--show-leak-kinds=definite,possible \
|
||||
--track-origins=yes \
|
||||
--errors-for-leak-kinds=definite,possible \
|
||||
--error-exitcode=1 \
|
||||
--log-file="$OUT_DIR/valgrind.log" \
|
||||
"./$OUT_DIR/$OUT_NAME"
|
||||
--log-file="$OUT_DIR/valgrind.log" \
|
||||
"./$OUT_DIR/$OUT_NAME" || VG_STATUS=$?
|
||||
|
||||
echo
|
||||
echo "Valgrind report written to $OUT_DIR/valgrind.log"
|
||||
else
|
||||
"./$OUT_DIR/$OUT_NAME"
|
||||
fi
|
||||
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
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,491 +0,0 @@
|
||||
{
|
||||
"auto_complete":
|
||||
{
|
||||
"selected_items":
|
||||
[
|
||||
]
|
||||
},
|
||||
"buffers":
|
||||
[
|
||||
{
|
||||
"file": "pong_odin.sublime-project",
|
||||
"settings":
|
||||
{
|
||||
"buffer_size": 807,
|
||||
"encoding": "UTF-8",
|
||||
"line_ending": "Unix"
|
||||
},
|
||||
"undo_stack":
|
||||
[
|
||||
[
|
||||
7,
|
||||
1,
|
||||
"revert",
|
||||
null,
|
||||
"AgAAAAAAAAAAAAAAAAAAAAAAAAAmAwAAewoJImZvbGRlcnMiOgoJWwoJCXsKCQkJInBhdGgiOiAiLiIKCQl9CgldLAoJImJ1aWxkX3N5c3RlbXMiOgoJWwoJCXsKCQkJIm5hbWUiOiAiUnVuIEJ1aWxkIFNjcmlwdCIsCgkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCJdLAoJCQkid29ya2luZ19kaXIiOiAiJHByb2plY3RfcGF0aCIsCgkJCSJmaWxlX3JlZ2V4IjogIl5bIF0qRmlsZSBcIiguLi4qPylcIiwgbGluZSAoWzAtOV0qKSIsCgkJCSJ2YXJpYW50cyI6CgkJCVsKCQkJCXsKCQkJCQkibmFtZSI6ICJSdW4gd2l0aCBBcmd1bWVudHMuLi4iLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICIkc2VsZWN0X2FyZ3MiXQoJCQkJfSwKCQkJCXsKCQkJCQkibmFtZSI6ICJCdWlsZCBPbmx5ICgtYikiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItYiJdCgkJCQl9LAoJCQkJewoJCQkJCSJuYW1lIjogIk9wdGltaXplZCAoLW8pIiwKCQkJCQkiY21kIjogWyIvYmluL2Jhc2giLCAiYnVpbGQuc2giLCAiLW8iXQoJCQkJfSwKCQkJCXsKCQkJCQkibmFtZSI6ICJEZWJ1ZyBpbiBMTERCICgtZCkiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItZCJdCgkJCQl9LAoJCQkJewoJCQkJCSJuYW1lIjogIlZhbGdyaW5kICgtdikiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItdiJdCgkJCQl9LAoJCQkJewoJCQkJCSJuYW1lIjogIkNsZWFuICgtYykiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItYyJdCgkJCQl9CgkJCV0KCQl9CgldCn0AAAAAAAAAAC0AAAAAAAAAAAAAAA",
|
||||
"AgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPC/AAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8L8"
|
||||
],
|
||||
[
|
||||
11,
|
||||
1,
|
||||
"nv_vi_paste",
|
||||
{
|
||||
"before_cursor": false,
|
||||
"count": 1,
|
||||
"mode": "mode_visual_line",
|
||||
"register": "\""
|
||||
},
|
||||
"AQAAAAAAAAAAAAAAJwMAAAAAAAAtAAAAewoJImZvbGRlcnMiOgoJWwoJCXsKCQkJInBhdGgiOiAiLiIKCQl9CgldCn0K",
|
||||
"AgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPC/AAAAAAEAAAAAAAAAAAAAAC0AAAAAAAAAAAAAAAAA8L8"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"contents": "",
|
||||
"settings":
|
||||
{
|
||||
"buffer_size": 0,
|
||||
"line_ending": "Unix"
|
||||
},
|
||||
"undo_stack":
|
||||
[
|
||||
[
|
||||
2,
|
||||
1,
|
||||
"nv_vi_paste",
|
||||
{
|
||||
"before_cursor": false,
|
||||
"count": 1,
|
||||
"mode": "mode_internal_normal",
|
||||
"register": "\""
|
||||
},
|
||||
"AQAAAAAAAAAAAAAAJwMAAAAAAAAAAAAA",
|
||||
"AgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPC/AAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8L8"
|
||||
],
|
||||
[
|
||||
6,
|
||||
1,
|
||||
"nv_vi_d",
|
||||
{
|
||||
"count": 1,
|
||||
"mode": "mode_visual_line",
|
||||
"register": "\""
|
||||
},
|
||||
"AQAAAAAAAAAAAAAAAAAAAAAAAAAnAwAAewoJImZvbGRlcnMiOgoJWwoJCXsKCQkJInBhdGgiOiAiLiIKCQl9CgldLAoJImJ1aWxkX3N5c3RlbXMiOgoJWwoJCXsKCQkJIm5hbWUiOiAiUnVuIEJ1aWxkIFNjcmlwdCIsCgkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCJdLAoJCQkid29ya2luZ19kaXIiOiAiJHByb2plY3RfcGF0aCIsCgkJCSJmaWxlX3JlZ2V4IjogIl5bIF0qRmlsZSBcIiguLi4qPylcIiwgbGluZSAoWzAtOV0qKSIsCgkJCSJ2YXJpYW50cyI6CgkJCVsKCQkJCXsKCQkJCQkibmFtZSI6ICJSdW4gd2l0aCBBcmd1bWVudHMuLi4iLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICIkc2VsZWN0X2FyZ3MiXQoJCQkJfSwKCQkJCXsKCQkJCQkibmFtZSI6ICJCdWlsZCBPbmx5ICgtYikiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItYiJdCgkJCQl9LAoJCQkJewoJCQkJCSJuYW1lIjogIk9wdGltaXplZCAoLW8pIiwKCQkJCQkiY21kIjogWyIvYmluL2Jhc2giLCAiYnVpbGQuc2giLCAiLW8iXQoJCQkJfSwKCQkJCXsKCQkJCQkibmFtZSI6ICJEZWJ1ZyBpbiBMTERCICgtZCkiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItZCJdCgkJCQl9LAoJCQkJewoJCQkJCSJuYW1lIjogIlZhbGdyaW5kICgtdikiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItdiJdCgkJCQl9LAoJCQkJewoJCQkJCSJuYW1lIjogIkNsZWFuICgtYykiLAoJCQkJCSJjbWQiOiBbIi9iaW4vYmFzaCIsICJidWlsZC5zaCIsICItYyJdCgkJCQl9CgkJCV0KCQl9CgldCn0K",
|
||||
"AgAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPC/AAAAAAEAAAAAAAAAAAAAACcDAAAAAAAAAAAAAAAA8L8"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"build_system": "",
|
||||
"build_system_choices":
|
||||
[
|
||||
[
|
||||
[
|
||||
[
|
||||
"Run Build Script",
|
||||
""
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
"Run with Arguments..."
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
"Build Only (-b)"
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
"Optimized (-o)"
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
"Debug in LLDB (-d)"
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
"Valgrind (-v)"
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
"Clean (-c)"
|
||||
]
|
||||
],
|
||||
[
|
||||
"Run Build Script",
|
||||
""
|
||||
]
|
||||
]
|
||||
],
|
||||
"build_varint": "",
|
||||
"command_palette":
|
||||
{
|
||||
"height": 0.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"sett",
|
||||
"Preferences: Settings"
|
||||
],
|
||||
[
|
||||
"lsp res",
|
||||
"LSP: Restart Server"
|
||||
],
|
||||
[
|
||||
"lsp",
|
||||
"Preferences: LSP Server Configurations"
|
||||
],
|
||||
[
|
||||
"setting",
|
||||
"Preferences: Settings"
|
||||
],
|
||||
[
|
||||
"set",
|
||||
"Selection: Expand to Block"
|
||||
],
|
||||
[
|
||||
"lsp se",
|
||||
"Preferences: LSP Server Configurations"
|
||||
],
|
||||
[
|
||||
"lsp server",
|
||||
"Preferences: LSP Server Configurations"
|
||||
],
|
||||
[
|
||||
"inst",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"key",
|
||||
"Preferences: Key Bindings"
|
||||
],
|
||||
[
|
||||
"remo",
|
||||
"Package Control: Remove Package"
|
||||
],
|
||||
[
|
||||
"color",
|
||||
"UI: Select Color Scheme"
|
||||
],
|
||||
[
|
||||
"insta",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"remove",
|
||||
"Package Control: Remove Package"
|
||||
],
|
||||
[
|
||||
"theme",
|
||||
"UI: Customize Theme"
|
||||
],
|
||||
[
|
||||
"instal",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"color scheme",
|
||||
"UI: Select Color Scheme"
|
||||
],
|
||||
[
|
||||
"cust",
|
||||
"UI: Customize Color Scheme"
|
||||
],
|
||||
[
|
||||
"settings",
|
||||
"Preferences: Settings"
|
||||
],
|
||||
[
|
||||
"pack",
|
||||
"Package Control: Install Package"
|
||||
],
|
||||
[
|
||||
"list pac",
|
||||
"Package Control: List Packages"
|
||||
],
|
||||
[
|
||||
"package inst",
|
||||
"Package Control: Install Package"
|
||||
]
|
||||
],
|
||||
"width": 0.0
|
||||
},
|
||||
"console":
|
||||
{
|
||||
"height": 124.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"distraction_free":
|
||||
{
|
||||
"menu_visible": true,
|
||||
"show_minimap": false,
|
||||
"show_open_files": false,
|
||||
"show_tabs": false,
|
||||
"side_bar_visible": false,
|
||||
"status_bar_visible": false
|
||||
},
|
||||
"expanded_folders":
|
||||
[
|
||||
"/home/jason/Projects/pong_raylib"
|
||||
],
|
||||
"file_history":
|
||||
[
|
||||
"/home/jason/Projects/Games/pong_raylib/src/input_manager.odin",
|
||||
"/home/jason/Projects/Games/pong_raylib/todo",
|
||||
"/home/jason/Projects/Games/pong_raylib/src/main.odin",
|
||||
"/home/jason/Projects/Games/pong_raylib/pong_odin.sublime-project",
|
||||
"/home/jason/Projects/Games/pong_raylib/src/entity_player.odin",
|
||||
"/home/jason/Projects/Games/pong_raylib/src/audio_manager.odin",
|
||||
"/home/jason/.config/sublime-text/Packages/User/LanguageServers.sublime-settings",
|
||||
"/home/jason/.config/sublime-text/Packages/LSP/LanguageServers.sublime-settings",
|
||||
"/home/jason/.config/sublime-text/Packages/User/Mariana.sublime-color-scheme",
|
||||
"/home/jason/.config/sublime-text/Packages/Color Scheme - Default/Mariana.sublime-color-scheme"
|
||||
],
|
||||
"find":
|
||||
{
|
||||
"height": 43.0
|
||||
},
|
||||
"find_in_files":
|
||||
{
|
||||
"height": 158.0,
|
||||
"where_history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"find_state":
|
||||
{
|
||||
"case_sensitive": false,
|
||||
"find_history":
|
||||
[
|
||||
],
|
||||
"highlight": true,
|
||||
"in_selection": false,
|
||||
"preserve_case": false,
|
||||
"regex": false,
|
||||
"replace_history":
|
||||
[
|
||||
],
|
||||
"reverse": false,
|
||||
"scrollbar_highlights": true,
|
||||
"show_context": true,
|
||||
"use_buffer2": true,
|
||||
"use_gitignore": true,
|
||||
"whole_word": false,
|
||||
"wrap": true
|
||||
},
|
||||
"groups":
|
||||
[
|
||||
{
|
||||
"sheets":
|
||||
[
|
||||
{
|
||||
"buffer": 0,
|
||||
"file": "pong_odin.sublime-project",
|
||||
"selected": true,
|
||||
"semi_transient": false,
|
||||
"settings":
|
||||
{
|
||||
"buffer_size": 807,
|
||||
"regions":
|
||||
{
|
||||
},
|
||||
"selection":
|
||||
[
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
"_nv_visual_sel_mode": "mode_visual_line",
|
||||
"lsp_uri": "file:///home/jason/Projects/pong_raylib/pong_odin.sublime-project",
|
||||
"syntax": "Packages/JSON/JSON.sublime-syntax",
|
||||
"tab_size": 4,
|
||||
"translate_tabs_to_spaces": false
|
||||
},
|
||||
"translation.x": 0.0,
|
||||
"translation.y": 0.0,
|
||||
"zoom_level": 1.0
|
||||
},
|
||||
"stack_index": 0,
|
||||
"stack_multiselect": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"buffer": 1,
|
||||
"semi_transient": false,
|
||||
"settings":
|
||||
{
|
||||
"buffer_size": 0,
|
||||
"regions":
|
||||
{
|
||||
},
|
||||
"selection":
|
||||
[
|
||||
[
|
||||
0,
|
||||
0
|
||||
]
|
||||
],
|
||||
"settings":
|
||||
{
|
||||
"_nv_visual_sel_mode": "mode_visual_line",
|
||||
"auto_name": "",
|
||||
"lsp_uri": "buffer:41",
|
||||
"syntax": "Packages/Text/Plain text.tmLanguage"
|
||||
},
|
||||
"translation.x": 0.0,
|
||||
"translation.y": 0.0,
|
||||
"zoom_level": 1.0
|
||||
},
|
||||
"stack_index": 1,
|
||||
"stack_multiselect": false,
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"incremental_find":
|
||||
{
|
||||
"height": 43.0
|
||||
},
|
||||
"input":
|
||||
{
|
||||
"height": 59.0
|
||||
},
|
||||
"layout":
|
||||
{
|
||||
"cells":
|
||||
[
|
||||
[
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1
|
||||
]
|
||||
],
|
||||
"cols":
|
||||
[
|
||||
0.0,
|
||||
1.0
|
||||
],
|
||||
"rows":
|
||||
[
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
"menu_visible": true,
|
||||
"output.LSP Log Panel":
|
||||
{
|
||||
"height": 0.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"output.diagnostics":
|
||||
{
|
||||
"height": 0.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"output.exec":
|
||||
{
|
||||
"height": 1123.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"output.find_results":
|
||||
{
|
||||
"height": 0.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"output.mdpopups":
|
||||
{
|
||||
"height": 0.0,
|
||||
"history":
|
||||
[
|
||||
]
|
||||
},
|
||||
"pinned_build_system": "",
|
||||
"project": "pong_odin.sublime-project",
|
||||
"replace":
|
||||
{
|
||||
"height": 82.0
|
||||
},
|
||||
"save_all_on_build": true,
|
||||
"select_file":
|
||||
{
|
||||
"height": 0.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"input",
|
||||
"src/input_manager.odin"
|
||||
],
|
||||
[
|
||||
"todo",
|
||||
"todo"
|
||||
],
|
||||
[
|
||||
"main",
|
||||
"src/main.odin"
|
||||
],
|
||||
[
|
||||
"ene",
|
||||
"src/entity_player.odin"
|
||||
]
|
||||
],
|
||||
"width": 0.0
|
||||
},
|
||||
"select_project":
|
||||
{
|
||||
"height": 500.0,
|
||||
"last_filter": "dot",
|
||||
"selected_items":
|
||||
[
|
||||
[
|
||||
"dot",
|
||||
"~/Projects/dotfiles/dotfiles.sublime-project"
|
||||
],
|
||||
[
|
||||
"pong",
|
||||
"~/Projects/pong_raylib/pong_odin.sublime-project"
|
||||
]
|
||||
],
|
||||
"width": 380.0
|
||||
},
|
||||
"select_symbol":
|
||||
{
|
||||
"height": 0.0,
|
||||
"last_filter": "",
|
||||
"selected_items":
|
||||
[
|
||||
],
|
||||
"width": 0.0
|
||||
},
|
||||
"selected_group": 0,
|
||||
"settings":
|
||||
{
|
||||
"_vintageous_reset_during_init": true,
|
||||
"lsp_show_inlay_hints": false
|
||||
},
|
||||
"show_minimap": true,
|
||||
"show_open_files": false,
|
||||
"show_tabs": false,
|
||||
"side_bar_visible": true,
|
||||
"side_bar_width": 328.0,
|
||||
"status_bar_visible": true,
|
||||
"template_settings":
|
||||
{
|
||||
}
|
||||
}
|
||||
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 |
@@ -20,7 +20,7 @@ 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
|
||||
@@ -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
|
||||
|
||||
+29
-15
@@ -4,13 +4,18 @@ import "core:math"
|
||||
import "core:math/rand"
|
||||
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 = 100,
|
||||
max_speed = 550,
|
||||
speed = INITIAL_SPEED,
|
||||
max_speed = MAX_SPEED,
|
||||
vel = rl.Vector2{0,0},
|
||||
shape = .Circle,
|
||||
size = rl.Vector2{8, 0},
|
||||
@@ -18,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
|
||||
}
|
||||
@@ -28,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) {
|
||||
@@ -42,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
|
||||
@@ -59,16 +70,17 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
|
||||
level.player.color = rl.Color{255,255,255,255}
|
||||
ball.tween_elapsed = 0.0
|
||||
resolve_collision(ball, level.player, true)
|
||||
spawn_max_speed_sparks(&level.particles, level.player, ball.pos.y, true)
|
||||
if ball.speed >= EFFECTS_SPEED {
|
||||
spawn_max_speed_sparks(&level.particles, level.player, ball.pos.y, true)
|
||||
}
|
||||
|
||||
// if ball.speed >= ball.max_speed - 100 {
|
||||
// spawn_max_speed_sparks(&level.particles, ball.pos, true)
|
||||
// }
|
||||
} else if check_ball_paddle_collision(ball, level.opponent) {
|
||||
level.opponent.color = rl.Color{255,255,255,255}
|
||||
ball.tween_elapsed = 0.0
|
||||
resolve_collision(ball, level.opponent, false)
|
||||
spawn_max_speed_sparks(&level.particles, level.opponent, ball.pos.y, false)
|
||||
if ball.speed >= EFFECTS_SPEED {
|
||||
spawn_max_speed_sparks(&level.particles, level.opponent, ball.pos.y, false)
|
||||
}
|
||||
} else {
|
||||
level.opponent.color = rl.Color{255,255,255,0}
|
||||
level.player.color = rl.Color{255,255,255,0}
|
||||
@@ -82,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
|
||||
@@ -101,21 +115,21 @@ check_ball_paddle_collision :: proc(ball: ^Entity, paddle: Entity) -> bool {
|
||||
height = paddle.size.y,
|
||||
}
|
||||
collided := rl.CheckCollisionCircleRec(ball.pos, ball.size.x, paddle_rec)
|
||||
if collided do emit_event(.Ball_Collision)
|
||||
if collided do emit_event(Ball_Collision_Data{speed = ball.speed})
|
||||
return collided
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
increment_speed :: proc(ball: ^Entity) {
|
||||
if ball.speed <= ball.max_speed {
|
||||
ball.speed += 45
|
||||
ball.speed += SPEED_INC
|
||||
ball.vel = rl.Vector2Normalize({ball.vel.x, ball.vel.y}) * ball.speed
|
||||
}
|
||||
}
|
||||
@@ -148,7 +162,7 @@ resolve_collision :: proc(ball: ^Entity, paddle: Entity, is_player: bool) {
|
||||
}
|
||||
|
||||
update_ball_trail :: proc(ball: ^Entity, dt: f32) {
|
||||
if ball.speed < 300 {
|
||||
if ball.speed < EFFECTS_SPEED {
|
||||
ball.trail_count = 0
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+40
-6
@@ -1,7 +1,8 @@
|
||||
package game
|
||||
|
||||
Change_Resolution_Data :: struct { res_dir: int }
|
||||
Game_Over_Data :: struct { winner_is_player: bool }
|
||||
Ball_Collision_Data :: struct { speed: f32, }
|
||||
Game_Over_Data :: struct { winner_is_player: bool }
|
||||
|
||||
Simple_Event :: enum {
|
||||
Game_Start,
|
||||
@@ -14,13 +15,13 @@ Simple_Event :: enum {
|
||||
Game_Quit,
|
||||
UI_Select,
|
||||
Score_Change,
|
||||
Ball_Collision,
|
||||
}
|
||||
|
||||
Event :: union {
|
||||
Simple_Event,
|
||||
Change_Resolution_Data,
|
||||
Game_Over_Data,
|
||||
Ball_Collision_Data,
|
||||
}
|
||||
|
||||
// global event storage
|
||||
@@ -56,34 +57,67 @@ process_events :: proc(game: ^Game) {
|
||||
case Game_Over_Data:
|
||||
push_scene(game, make_game_over_overlay(e.winner_is_player))
|
||||
|
||||
case Ball_Collision_Data:
|
||||
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
|
||||
|
||||
case .UI_Select:
|
||||
audio_play_sfx(&game.audio, .UI_Select)
|
||||
|
||||
case .Ball_Collision:
|
||||
audio_play_sfx(&game.audio, .Ball_Collision)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event_manager_quit :: proc(game: ^Game) {
|
||||
delete(GLOBAL_EVENTS)
|
||||
}
|
||||
|
||||
+129
-31
@@ -1,13 +1,12 @@
|
||||
package game
|
||||
|
||||
import "core:strings"
|
||||
import "core:math/rand"
|
||||
import rl "vendor:raylib"
|
||||
|
||||
// VIEWPORT_W :: 1280
|
||||
// VIEWPORT_H :: 720
|
||||
VIEWPORT_W :: 640
|
||||
VIEWPORT_H :: 360
|
||||
WINDOW_TITLE :: "Pong"
|
||||
WINDOW_TITLE :: "PONG 2026"
|
||||
|
||||
Resolution :: struct {
|
||||
width: i32,
|
||||
@@ -17,23 +16,44 @@ 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,
|
||||
|
||||
// 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
|
||||
res_index: int,
|
||||
|
||||
// Controller state.
|
||||
controller_gid: i32,
|
||||
controller_connected: bool,
|
||||
}
|
||||
|
||||
// AO: lets me load this at compile time!
|
||||
@@ -43,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)
|
||||
@@ -65,8 +89,8 @@ game_init :: proc(game: ^Game) {
|
||||
game.scene_stack = make([dynamic]Scene, 0, 8)
|
||||
game.viewport = rl.LoadRenderTexture(VIEWPORT_W, VIEWPORT_H)
|
||||
|
||||
// Force nearest neighbor point filtering on the viewport texture
|
||||
rl.SetTextureFilter(game.viewport.texture, .POINT)
|
||||
// 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}
|
||||
@@ -74,21 +98,63 @@ 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
|
||||
// rl.SetTextureFilter(game.bg_texture, .BILINEAR)
|
||||
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},
|
||||
offset = rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0},
|
||||
rotation = 0.0,
|
||||
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
|
||||
// -------------------------------------------------------------------------
|
||||
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 {
|
||||
|
||||
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)
|
||||
|
||||
@@ -101,31 +167,41 @@ game_render :: proc(game: ^Game) {
|
||||
rl.ClearBackground(rl.BLACK)
|
||||
|
||||
rl.BeginTextureMode(game.viewport)
|
||||
|
||||
rl.DrawTexture(game.bg_texture, 0, 0, rl.WHITE)
|
||||
|
||||
// @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])
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -133,12 +209,28 @@ 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)
|
||||
|
||||
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
|
||||
|
||||
@@ -156,15 +248,21 @@ game_set_resolution :: proc(game: ^Game, res_index: int) {
|
||||
render_game_to_window :: proc(game: ^Game) {
|
||||
texture := game.viewport.texture
|
||||
|
||||
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,
|
||||
)
|
||||
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)},
|
||||
dst,
|
||||
rl.Vector2{},
|
||||
0,
|
||||
rl.WHITE,
|
||||
)
|
||||
rl.EndShaderMode()
|
||||
}
|
||||
+19
-13
@@ -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,7 +92,7 @@ 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) ||
|
||||
|
||||
@@ -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}
|
||||
|
||||
+25
-6
@@ -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