Files
pong_raylib/src/entity_ball.odin
T

261 lines
7.6 KiB
Odin

package game
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 = INITIAL_SPEED,
max_speed = MAX_SPEED,
vel = rl.Vector2{0,0},
shape = .Circle,
size = rl.Vector2{8, 0},
tween_duration = 0.65,
}
}
serve_ball :: proc(ball: ^Entity, serve_dir: u8) {
dir_x: f32 = 0.0
// Randomize initial direction: -1.0 or 1.0
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
}
reset_ball :: proc(ball: ^Entity) {
ball.pos = rl.Vector2{(VIEWPORT_W / 2), (VIEWPORT_H / 2)}
ball.vel = rl.Vector2{0,0}
ball.speed = INITIAL_SPEED
ball.trail_positions = {}
ball.trail_count = 0
}
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
if level.serve_timer <= 0 {
serve_ball(ball, level.serve_dir)
}
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)
if ball.speed >= EFFECTS_SPEED {
spawn_max_speed_sparks(&level.particles, level.player, ball.pos.y, 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)
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}
}
// bounds collision detection
if ball.pos.y >= (field_bounds.y + field_bounds.height) - ball.size.x { ball.vel.y = -ball.vel.y }
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, 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
}
check_ball_paddle_collision :: proc(ball: ^Entity, paddle: Entity) -> bool {
paddle_rec := rl.Rectangle{
x = paddle.pos.x,
y = paddle.pos.y,
width = paddle.size.x,
height = paddle.size.y,
}
collided := rl.CheckCollisionCircleRec(ball.pos, ball.size.x, paddle_rec)
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})
}
}
increment_speed :: proc(ball: ^Entity) {
if ball.speed <= ball.max_speed {
ball.speed += SPEED_INC
ball.vel = rl.Vector2Normalize({ball.vel.x, ball.vel.y}) * ball.speed
}
}
resolve_collision :: proc(ball: ^Entity, paddle: Entity, is_player: bool) {
ball_center_y := ball.pos.y
paddle_center_y := paddle.pos.y + (paddle.size.y / 2.0)
local_y := ball_center_y - paddle_center_y
half_height := paddle.size.y / 2.0
// check if we are in top or bottom half of paddle
hit_t := clamp(local_y / half_height, -1.0, 1.0)
// direction is dependant of if its the player
dir_x: f32 = is_player ? 1.0 : -1.0
// steer the angle by a value using its hit t value
dir_y := hit_t * 1.3
new_dir := rl.Vector2Normalize({dir_x, dir_y})
ball.vel = new_dir * ball.speed
increment_speed(ball)
// Resolve overlap
if is_player {
ball.pos.x = paddle.pos.x + paddle.size.x + ball.size.x
} else {
ball.pos.x = paddle.pos.x - ball.size.x
}
}
update_ball_trail :: proc(ball: ^Entity, dt: f32) {
if ball.speed < EFFECTS_SPEED {
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) {
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)
}