From 2fc3bd07a25c98931eea459faf185ba8ea7e7788 Mon Sep 17 00:00:00 2001 From: Jason Hilder Date: Mon, 28 Sep 2026 08:05:54 +0200 Subject: [PATCH] Big additions. Updated speeds etc for smaller resolution. Added a squish animation on collision of paddles. Updated the paddles from here to indicate a hit. Make use of simple particle system for on hit effects. --- src/entity_ball.odin | 142 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 11 deletions(-) diff --git a/src/entity_ball.odin b/src/entity_ball.odin index 73bd040..441df3c 100644 --- a/src/entity_ball.odin +++ b/src/entity_ball.odin @@ -1,5 +1,6 @@ package game +import "core:math" import "core:math/rand" import rl "vendor:raylib" @@ -8,12 +9,13 @@ init_ball :: proc() -> Entity { type = .Ball, active = true, pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)}, - speed = 500, - max_speed = 800, + speed = 100, + max_speed = 550, vel = rl.Vector2{0,0}, shape = .Circle, - size = rl.Vector2{15, 15} - } + size = rl.Vector2{8, 0}, + tween_duration = 0.65, + } } serve_ball :: proc(ball: ^Entity) { @@ -26,10 +28,15 @@ 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 = 500 + ball.speed = 300 } update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) { + // squish animation check + if ball.tween_elapsed < ball.tween_duration { + ball.tween_elapsed += dt + } + // first check the level timer are we waiting to serve? if level.serve_timer > 0 { level.serve_timer -= dt @@ -41,14 +48,30 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) { return } + // Update trail history buffer + update_ball_trail(ball, dt) + radius := ball.size.x field_bounds := level.field_bounds // paddle collisions detection (player then enemy) if check_ball_paddle_collision(ball, level.player) { + 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 >= 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) + } else { + level.opponent.color = rl.Color{255,255,255,0} + level.player.color = rl.Color{255,255,255,0} } // bounds collision detection @@ -56,13 +79,13 @@ update_ball :: proc(ball: ^Entity, level: ^Level, dt: f32) { if ball.pos.y <= field_bounds.y + ball.size.x { ball.vel.y = -ball.vel.y } if ball.pos.x >= (field_bounds.x + field_bounds.width) - ball.size.x { - increment_score(&level.score_player, level.score_win, true) + increment_score(&level.score_player, level.score_win, true, ball) reset_ball(ball) level.serve_timer = level.wait_time } if ball.pos.x <= field_bounds.x + ball.size.x { - increment_score(&level.score_enemy, level.score_win, false) + increment_score(&level.score_enemy, level.score_win, false, ball) reset_ball(ball) level.serve_timer = level.wait_time } @@ -82,14 +105,17 @@ check_ball_paddle_collision :: proc(ball: ^Entity, paddle: Entity) -> bool { return collided } -increment_score :: proc(score_side: ^int, max_score: int, player: bool) { +increment_score :: proc(score_side: ^int, max_score: int, player: bool, ball: ^Entity) { score_side^ += 1 - if score_side^ == max_score do emit_event(Game_Over_Data{player}) + 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 += 100 + ball.speed += 45 ball.vel = rl.Vector2Normalize({ball.vel.x, ball.vel.y}) * ball.speed } } @@ -121,6 +147,100 @@ resolve_collision :: proc(ball: ^Entity, paddle: Entity, is_player: bool) { } } +update_ball_trail :: proc(ball: ^Entity, dt: f32) { + if ball.speed < 300 { + ball.trail_count = 0 + return + } + + ball.trail_timer += dt + // get pos every 20ms + sample_rate := f32(0.02) + + if ball.trail_timer >= sample_rate { + ball.trail_timer = 0.0 + + // shift old pos back + for i := MAX_TRAILS - 1; i > 0; i -= 1 { + ball.trail_positions[i] = ball.trail_positions[i - 1] + } + + // store new pos + ball.trail_positions[0] = ball.pos + if ball.trail_count < MAX_TRAILS { + ball.trail_count += 1 + } + } +} + +spawn_max_speed_sparks :: proc(particles: ^[dynamic]Particle, paddle: Entity, ball_y: f32, is_player: bool) { + count := 12 + + impact_x := is_player ? (paddle.pos.x + paddle.size.x) : paddle.pos.x + impact_y := clamp(ball_y, paddle.pos.y, paddle.pos.y + paddle.size.y) + impact_pos := rl.Vector2{impact_x, impact_y} + + for _ in 0 ..< count { + // Direct sparks outward from the paddle face + base_angle := is_player ? f32(0.0) : rl.PI // 0 rad = right, PI rad = left + spread := (f32(rand.float32()) - 0.5) * 1.2 + angle := base_angle + spread + + speed := f32(rand.float32() * 180.0 + 100.0) + + append(particles, Particle{ + pos = impact_pos, + vel = {math.cos(angle) * speed, math.sin(angle) * speed}, + color = rl.SKYBLUE, + life = 0.25, + max_life = 0.25, + size = f32(rand.float32() * 2.0 + 1.5), + }) + } +} + +render_ball_trail :: proc(ball: Entity) { + if ball.trail_count == 0 do return + + base_radius := ball.size.x + + for i in 0 ..< ball.trail_count { + pos := ball.trail_positions[i] + + // Farther back in history = smaller and fainter + progress := f32(i + 1) / f32(ball.trail_count) // 0.0 -> 1.0 + + radius := base_radius * (1.0 - (progress * 0.6)) // Shrinks slightly + alpha := u8(180.0 * (1.0 - progress)) // Fades out + + trail_color := rl.Color{rl.MAROON.r, rl.MAROON.g, rl.MAROON.b, alpha} + + rl.DrawCircleV(pos, radius, trail_color) + } +} + render_ball :: proc(ball: Entity, field_bounds: rl.Rectangle) { - rl.DrawCircleV(ball.pos, ball.size.x, rl.MAROON) + render_ball_trail(ball) + + radius_x := ball.size.x + radius_y := ball.size.x + + // if animation active + if ball.tween_elapsed < ball.tween_duration { + radius_x = rl.EaseBackOut( + ball.tween_elapsed, + 5.0, + ball.size.x - 5.0, + ball.tween_duration + ) + + radius_y = rl.EaseBackOut( + ball.tween_elapsed, + 12.0, + ball.size.x - 12.0, + ball.tween_duration + ) + } + + rl.DrawEllipse(i32(ball.pos.x), i32(ball.pos.y), radius_x, radius_y, rl.MAROON) }