Compare commits

..
6 Commits
5 changed files with 6457 additions and 109 deletions
Regular → Executable
+56 -58
View File
@@ -4,6 +4,7 @@ set -e
# Find the odin binary # Find the odin binary
export PATH="$HOME/.odin:$PATH" export PATH="$HOME/.odin:$PATH"
SRC_DIR="src" SRC_DIR="src"
OUT_DIR="out" OUT_DIR="out"
OUT_NAME="pong" OUT_NAME="pong"
@@ -16,91 +17,88 @@ DEBUGGER=0
VALGRIND=0 VALGRIND=0
usage() { usage() {
echo "Usage: $0 [-b] [-o] [-w] [-d] [-v] [-c] [-h]" echo "Usage: $0 [-b] [-o] [-w] [-d] [-v] [-c] [-h]"
echo " -b build only, don't run" echo " -b build only, don't run"
echo " -o optimized release build (-o:speed, no debug info)" echo " -o optimized release build (-o:speed, no debug info)"
echo " -w web/WASM build (js_wasm32 target)" echo " -w web/WASM build (js_wasm32 target)"
echo " -d build then launch in debugger instead of running" echo " -d build then launch in debugger instead of running"
echo " -v build then run under Valgrind" echo " -v build then run under Valgrind"
echo " -c clean build artifacts" echo " -c clean build artifacts"
echo " -h show this help" echo " -h show this help"
exit 1 exit 1
} }
while getopts "bowdvch" opt; do while getopts "bowdvch" opt; do
case $opt in case $opt in
b) BUILD_ONLY=1 ;; b) BUILD_ONLY=1 ;;
o) OPTIMIZED=1 ;; o) OPTIMIZED=1 ;;
w) WEB=1 ;; w) WEB=1 ;;
d) DEBUGGER=1 ;; d) DEBUGGER=1 ;;
v) VALGRIND=1 ;; v) VALGRIND=1 ;;
c) CLEAN=1 ;; c) CLEAN=1 ;;
h) usage ;; h) usage ;;
*) usage ;; *) usage ;;
esac esac
done done
mkdir -p "$OUT_DIR" mkdir -p "$OUT_DIR"
if [ "$CLEAN" -eq 1 ]; then if [ "$CLEAN" -eq 1 ]; then
echo "Cleaning..." echo "Cleaning..."
rm -rf "${OUT_DIR:?}"/* rm -rf "${OUT_DIR:?}"/*
exit 0 exit 0
fi fi
if [ "$WEB" -eq 1 ]; then if [ "$WEB" -eq 1 ]; then
echo "Building web (WASM)..." echo "Building web (WASM)..."
odin build "$SRC_DIR" \ odin build "$SRC_DIR" \
-target:js_wasm32 \ -target:js_wasm32 \
-out:"$OUT_DIR/$OUT_NAME.wasm" \ -out:"$OUT_DIR/$OUT_NAME.wasm" \
$( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" ) $( [ "$OPTIMIZED" -eq 1 ] && echo "-o:speed" || echo "-debug" )
# Copy the HTML shell if one exists # Copy the HTML shell if one exists
if [ -f "web/index_template.html" ]; then if [ -f "web/index_template.html" ]; then
cp web/index_template.html "$OUT_DIR/index.html" cp web/index_template.html "$OUT_DIR/index.html"
fi fi
echo "Web build complete -> $OUT_DIR/$OUT_NAME.wasm" echo "Web build complete -> $OUT_DIR/$OUT_NAME.wasm"
exit 0 exit 0
fi fi
if [ "$OPTIMIZED" -eq 1 ]; then if [ "$OPTIMIZED" -eq 1 ]; then
echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..." echo "Building optimized ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
odin build "$SRC_DIR" \ odin build "$SRC_DIR" \
-out:"$OUT_DIR/$OUT_NAME" \ -out:"$OUT_DIR/$OUT_NAME" \
-o:speed \ -o:speed u
-no-bounds-check -no-bounds-check
else else
echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..." echo "Building debug ($SRC_DIR -> $OUT_DIR/$OUT_NAME)..."
odin build "$SRC_DIR" \ odin build "$SRC_DIR" \
-out:"$OUT_DIR/$OUT_NAME" \ -out:"$OUT_DIR/$OUT_NAME" \
-define:DEBUG_MODE=true -define:DEBUG_MODE=true
fi fi
echo "Build complete." echo "Build complete."
if [ "$BUILD_ONLY" -eq 0 ]; then if [ "$BUILD_ONLY" -eq 0 ]; then
echo "----------------------------------------" echo "----------------------------------------"
if [ "$DEBUGGER" -eq 1 ]; then if [ "$DEBUGGER" -eq 1 ]; then
lldb "$OUT_DIR/$OUT_NAME" lldb "$OUT_DIR/$OUT_NAME"
elif [ "$VALGRIND" -eq 1 ]; then elif [ "$VALGRIND" -eq 1 ]; then
# --show-leak-kinds=definite,possible reduces tons of noise valgrind \
# still shows the leaks you actually need to fix.
# to audit whole process, temporarily switch back to --show-leak-kinds=all
valgrind \
--leak-check=full \ --leak-check=full \
--show-leak-kinds=definite,possible \ --show-leak-kinds=definite,possible \
--track-origins=yes \ --track-origins=yes \
--errors-for-leak-kinds=definite,possible \ --errors-for-leak-kinds=definite,possible \
--error-exitcode=1 \ --error-exitcode=1 \
--log-file="$OUT_DIR/valgrind.log" \ --log-file="$OUT_DIR/valgrind.log" \
"./$OUT_DIR/$OUT_NAME" "./$OUT_DIR/$OUT_NAME"
echo echo
echo "Valgrind report written to $OUT_DIR/valgrind.log" echo "Valgrind report written to $OUT_DIR/valgrind.log"
else else
"./$OUT_DIR/$OUT_NAME" "./$OUT_DIR/$OUT_NAME"
fi fi
fi fi
File diff suppressed because one or more lines are too long
+13 -9
View File
@@ -4,12 +4,15 @@ import "core:math"
import "core:math/rand" import "core:math/rand"
import rl "vendor:raylib" import rl "vendor:raylib"
SPEED_INC :: 30
EFFECTS_SPEED :: 300
init_ball :: proc() -> Entity { init_ball :: proc() -> Entity {
return Entity{ return Entity{
type = .Ball, type = .Ball,
active = true, active = true,
pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)}, pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)},
speed = 100, speed = 200,
max_speed = 550, max_speed = 550,
vel = rl.Vector2{0,0}, vel = rl.Vector2{0,0},
shape = .Circle, shape = .Circle,
@@ -59,16 +62,17 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) {
level.player.color = rl.Color{255,255,255,255} level.player.color = rl.Color{255,255,255,255}
ball.tween_elapsed = 0.0 ball.tween_elapsed = 0.0
resolve_collision(ball, level.player, true) 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) { } else if check_ball_paddle_collision(ball, level.opponent) {
level.opponent.color = rl.Color{255,255,255,255} level.opponent.color = rl.Color{255,255,255,255}
ball.tween_elapsed = 0.0 ball.tween_elapsed = 0.0
resolve_collision(ball, level.opponent, false) 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 { } else {
level.opponent.color = rl.Color{255,255,255,0} level.opponent.color = rl.Color{255,255,255,0}
level.player.color = rl.Color{255,255,255,0} level.player.color = rl.Color{255,255,255,0}
@@ -101,7 +105,7 @@ check_ball_paddle_collision :: proc(ball: ^Entity, paddle: Entity) -> bool {
height = paddle.size.y, height = paddle.size.y,
} }
collided := rl.CheckCollisionCircleRec(ball.pos, ball.size.x, paddle_rec) 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 return collided
} }
@@ -115,7 +119,7 @@ increment_score :: proc(score_side: ^int, max_score: int, player: bool, ball: ^E
increment_speed :: proc(ball: ^Entity) { increment_speed :: proc(ball: ^Entity) {
if ball.speed <= ball.max_speed { 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 ball.vel = rl.Vector2Normalize({ball.vel.x, ball.vel.y}) * ball.speed
} }
} }
@@ -148,7 +152,7 @@ resolve_collision :: proc(ball: ^Entity, paddle: Entity, is_player: bool) {
} }
update_ball_trail :: proc(ball: ^Entity, dt: f32) { update_ball_trail :: proc(ball: ^Entity, dt: f32) {
if ball.speed < 300 { if ball.speed < EFFECTS_SPEED {
ball.trail_count = 0 ball.trail_count = 0
return return
} }
+15 -4
View File
@@ -1,7 +1,8 @@
package game package game
Change_Resolution_Data :: struct { res_dir: int } 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 { Simple_Event :: enum {
Game_Start, Game_Start,
@@ -14,13 +15,13 @@ Simple_Event :: enum {
Game_Quit, Game_Quit,
UI_Select, UI_Select,
Score_Change, Score_Change,
Ball_Collision,
} }
Event :: union { Event :: union {
Simple_Event, Simple_Event,
Change_Resolution_Data, Change_Resolution_Data,
Game_Over_Data, Game_Over_Data,
Ball_Collision_Data,
} }
// global event storage // global event storage
@@ -56,6 +57,18 @@ process_events :: proc(game: ^Game) {
case Game_Over_Data: case Game_Over_Data:
push_scene(game, make_game_over_overlay(e.winner_is_player)) 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: case Simple_Event:
#partial switch e { #partial switch e {
case .Game_Start: case .Game_Start:
@@ -81,8 +94,6 @@ process_events :: proc(game: ^Game) {
case .UI_Select: case .UI_Select:
audio_play_sfx(&game.audio, .UI_Select) audio_play_sfx(&game.audio, .UI_Select)
case .Ball_Collision:
audio_play_sfx(&game.audio, .Ball_Collision)
} }
} }
} }
+68 -21
View File
@@ -1,10 +1,9 @@
package game package game
import "core:strings" import "core:strings"
import "core:math/rand"
import rl "vendor:raylib" import rl "vendor:raylib"
// VIEWPORT_W :: 1280
// VIEWPORT_H :: 720
VIEWPORT_W :: 640 VIEWPORT_W :: 640
VIEWPORT_H :: 360 VIEWPORT_H :: 360
WINDOW_TITLE :: "Pong" WINDOW_TITLE :: "Pong"
@@ -26,10 +25,16 @@ RESOLUTIONS := [?]Resolution{
Game :: struct { Game :: struct {
viewport: rl.RenderTexture2D, viewport: rl.RenderTexture2D,
bg_texture: rl.Texture2D, bg_texture: rl.Texture2D,
crt_shader: rl.Shader,
scene_stack: [dynamic]Scene, scene_stack: [dynamic]Scene,
should_close: bool, should_close: bool,
audio: Audio_Manager, audio: Audio_Manager,
// Camera state for screen effects / screen shake
camera: rl.Camera2D,
shake_intensity: f32,
shake_timer: f32,
// resolution state // resolution state
window_w: i32, window_w: i32,
window_h: i32, window_h: i32,
@@ -65,8 +70,8 @@ game_init :: proc(game: ^Game) {
game.scene_stack = make([dynamic]Scene, 0, 8) game.scene_stack = make([dynamic]Scene, 0, 8)
game.viewport = rl.LoadRenderTexture(VIEWPORT_W, VIEWPORT_H) game.viewport = rl.LoadRenderTexture(VIEWPORT_W, VIEWPORT_H)
// Force nearest neighbor point filtering on the viewport texture // Look into texture filters
rl.SetTextureFilter(game.viewport.texture, .POINT) rl.SetTextureFilter(game.viewport.texture, .BILINEAR)
// Generate soft CRT background radial texture once on startup // Generate soft CRT background radial texture once on startup
bg_center := rl.Color{22, 28, 36, 255} bg_center := rl.Color{22, 28, 36, 255}
@@ -74,7 +79,16 @@ game_init :: proc(game: ^Game) {
bg_img := rl.GenImageGradientRadial(VIEWPORT_W, VIEWPORT_H, 0.3, bg_center, bg_corner) bg_img := rl.GenImageGradientRadial(VIEWPORT_W, VIEWPORT_H, 0.3, bg_center, bg_corner)
game.bg_texture = rl.LoadTextureFromImage(bg_img) game.bg_texture = rl.LoadTextureFromImage(bg_img)
rl.UnloadImage(bg_img) // Free CPU memory rl.UnloadImage(bg_img) // Free CPU memory
// rl.SetTextureFilter(game.bg_texture, .BILINEAR)
game.crt_shader = rl.LoadShader(nil, "assets/shaders/crt.fs")
// Setup 2D camera matching the 640x360 viewport center
game.camera = rl.Camera2D{
target = rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0},
offset = rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0},
rotation = 0.0,
zoom = 1.0,
}
game.should_close = false game.should_close = false
set_root_scene(game, make_main_menu()) set_root_scene(game, make_main_menu())
@@ -84,6 +98,28 @@ game_update :: proc(game: ^Game) {
dt := rl.GetFrameTime() dt := rl.GetFrameTime()
input := poll_input() input := poll_input()
// -------------------------------------------------------------------------
// Update Camera Shake
// -------------------------------------------------------------------------
base_center := rl.Vector2{VIEWPORT_W / 2.0, VIEWPORT_H / 2.0}
if game.shake_timer > 0 {
game.shake_timer -= dt
// Generate random offset within current intensity bounds
offset_x := (f32(rand.float32()) * 2.0 - 1.0) * game.shake_intensity
offset_y := (f32(rand.float32()) * 2.0 - 1.0) * game.shake_intensity
game.camera.target = base_center + rl.Vector2{offset_x, offset_y}
if game.shake_timer <= 0 {
game.shake_intensity = 0.0
game.camera.target = base_center
}
} else {
game.camera.target = base_center
}
active_scene := get_active_scene(game) active_scene := get_active_scene(game)
if active_scene != nil { if active_scene != nil {
scene_update(active_scene, game, input, dt) scene_update(active_scene, game, input, dt)
@@ -103,12 +139,16 @@ game_render :: proc(game: ^Game) {
rl.BeginTextureMode(game.viewport) rl.BeginTextureMode(game.viewport)
rl.DrawTexture(game.bg_texture, 0, 0, rl.WHITE) rl.DrawTexture(game.bg_texture, 0, 0, rl.WHITE)
// @note: fine for now but probably not a good idea... // World/Scene objects rendered through 2D camera space
// Render all scenes in the stack from bottom to top rl.BeginMode2D(game.camera)
for i in 0 ..< len(game.scene_stack) {
scene_render(&game.scene_stack[i])
}
// @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])
}
rl.EndMode2D()
rl.EndTextureMode() rl.EndTextureMode()
render_game_to_window(game) render_game_to_window(game)
@@ -121,6 +161,7 @@ game_render :: proc(game: ^Game) {
game_quit :: proc(game: ^Game) { game_quit :: proc(game: ^Game) {
audio_manager_quit(&game.audio) audio_manager_quit(&game.audio)
rl.UnloadShader(game.crt_shader)
rl.UnloadTexture(game.bg_texture) rl.UnloadTexture(game.bg_texture)
rl.UnloadRenderTexture(game.viewport) rl.UnloadRenderTexture(game.viewport)
delete(GLOBAL_EVENTS) delete(GLOBAL_EVENTS)
@@ -133,6 +174,11 @@ game_should_run :: proc(game: Game) -> bool {
return !game.should_close && !rl.WindowShouldClose() 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 // Window Upscale Render
// ------------------------------------------------------------------------------ // ------------------------------------------------------------------------------
@@ -156,15 +202,16 @@ game_set_resolution :: proc(game: ^Game, res_index: int) {
render_game_to_window :: proc(game: ^Game) { render_game_to_window :: proc(game: ^Game) {
texture := game.viewport.texture texture := game.viewport.texture
rl.DrawTexturePro( rl.BeginShaderMode(game.crt_shader)
texture, rl.DrawTexturePro(
// src: inverted y coordinate flips opengl texture buffer correctly texture,
rl.Rectangle{0, 0, f32(texture.width), -f32(texture.height)}, // src: inverted y coordinate flips opengl texture buffer correctly
// dst: scales viewport canvas to fill window resolution rl.Rectangle{0, 0, f32(texture.width), -f32(texture.height)},
rl.Rectangle{0, 0, f32(game.window_w), f32(game.window_h)}, // dst: scales viewport canvas to fill window resolution
rl.Vector2{}, rl.Rectangle{0, 0, f32(game.window_w), f32(game.window_h)},
0, rl.Vector2{},
rl.WHITE, 0,
) rl.WHITE,
)
rl.EndShaderMode()
} }