Created
September 25, 2024 14:16
-
-
Save bones-ai/f3fb5a6a37ac53ba97b73cf80f7ae109 to your computer and use it in GitHub Desktop.
A small fps written in Odin and raylib
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "core:fmt" | |
import "core:math" | |
import "core:math/linalg" | |
import "core:math/rand" | |
import "core:slice" | |
import rl "vendor:raylib" | |
// MARK: Consts | |
// Window | |
WINDOW_W :: 1920 | |
WINDOW_H :: 1080 | |
// Player | |
MOUSE_SENS :: 0.1 | |
PLAYER_SPEED :: 0.1 | |
JUMP_FORCE :: 0.5 | |
GRAVITY :: 0.02 | |
ARENA_SIZE :: 50 | |
// Enemy | |
ENEMY_COUNT :: 50 | |
ENEMY_SPEED :: 0.05 | |
ENEMY_HEALTH :: 5 | |
SPAWN_INTERVAL :: 1.0 | |
SPAWN_COUNT :: 1 | |
// Gun | |
BULLET_SPEED :: 0.5 | |
BULLET_LIFETIME :: 2.0 | |
FIRE_RATE :: 0.1 | |
MAX_BULLETS :: 100 | |
BULLET_DAMAGE :: 10 | |
// MARK: Structs | |
GameState :: struct { | |
cam3d: rl.Camera3D, | |
player: Player, | |
solids: []Solid, | |
enemies: [dynamic]Enemy, | |
bullets: [MAX_BULLETS]Bullet, | |
last_enemy_spawn_ts: f32, | |
} | |
Player :: struct { | |
position: rl.Vector3, | |
velocity: rl.Vector3, | |
yaw: f32, | |
pitch: f32, | |
is_grounded: bool, | |
last_bullet_fire_ts: f32, | |
} | |
Solid :: struct { | |
position: rl.Vector3, | |
size: rl.Vector3, | |
color: rl.Color, | |
} | |
Enemy :: struct { | |
position: rl.Vector3, | |
size: rl.Vector3, | |
color: rl.Color, | |
health: int, | |
} | |
Bullet :: struct { | |
position: rl.Vector3, | |
direction: rl.Vector3, | |
lifetime: f32, | |
active: bool, | |
} | |
// MARK: !!2D!! | |
draw_2d :: proc(state: ^GameState) { | |
rl.DrawFPS(10, 10) | |
// Crosshair | |
ww, wh := rl.GetRenderWidth(), rl.GetRenderHeight() | |
rl.DrawRectangle(ww/2 - 10, wh/2 - 1, 20, 3, rl.BLACK) | |
rl.DrawRectangle(ww/2 - 1, wh/2 - 10, 3, 20, rl.BLACK) | |
} | |
// MARK: !!3D!! | |
draw_3d :: proc(state: ^GameState) { | |
rl.BeginMode3D(state.cam3d) | |
defer rl.EndMode3D() | |
rl.DrawGrid(ARENA_SIZE, 1) | |
for s in state.solids { | |
rl.DrawCubeV(s.position, s.size, s.color) | |
} | |
for enemy in state.enemies { | |
if enemy.health > 0 { | |
rl.DrawCubeV(enemy.position, enemy.size, enemy.color) | |
} | |
} | |
for bullet in state.bullets { | |
if bullet.active { | |
rl.DrawSphere(bullet.position, 0.05, rl.WHITE) | |
} | |
} | |
} | |
// MARK: Update | |
update :: proc(state: ^GameState) { | |
update_camera(state) | |
update_player(state) | |
update_enemies(state) | |
update_bullets(state) | |
handle_shooting(state) | |
handle_enemy_spawning(state) | |
} | |
handle_enemy_spawning :: proc(state: ^GameState) { | |
current_time := f32(rl.GetTime()) | |
if current_time - state.last_enemy_spawn_ts >= SPAWN_INTERVAL { | |
state.last_enemy_spawn_ts = current_time | |
for i in 0..<SPAWN_COUNT { | |
r := u8(rand.int31_max(255)) | |
g := u8(rand.int31_max(255)) | |
b := u8(rand.int31_max(255)) | |
new_enemy := Enemy{ | |
position = { | |
rand.float32_range(-ARENA_SIZE/2, ARENA_SIZE/2), | |
1.0, | |
rand.float32_range(-ARENA_SIZE/2, ARENA_SIZE/2), | |
}, | |
size = {1.0, 2.0, 1.0}, | |
color = {r, g, b, 255}, | |
health = ENEMY_HEALTH, | |
} | |
append_elem(&state.enemies, new_enemy) | |
} | |
} | |
} | |
update_bullets :: proc(state: ^GameState) { | |
for &bullet in &state.bullets { | |
if !bullet.active { | |
continue | |
} | |
bullet.lifetime -= rl.GetFrameTime() | |
if bullet.lifetime <= 0 { | |
bullet.active = false | |
continue | |
} | |
movement := bullet.direction * BULLET_SPEED | |
bullet.position = bullet.position + movement | |
// Collision with enemies | |
for &enemy in &state.enemies { | |
if enemy.health <= 0 { | |
continue | |
} | |
if rl.CheckCollisionSpheres(bullet.position, 0.1, enemy.position, 1.0) { | |
enemy.health -= BULLET_DAMAGE | |
bullet.active = false | |
break | |
} | |
} | |
} | |
} | |
handle_shooting :: proc(state: ^GameState) { | |
current_time := f32(rl.GetTime()) | |
if rl.IsMouseButtonDown(.LEFT) && (current_time - state.player.last_bullet_fire_ts) >= FIRE_RATE { | |
state.player.last_bullet_fire_ts = current_time | |
// Find an inactive bullet | |
// TODO make this better | |
for &bullet in &state.bullets { | |
if !bullet.active { | |
bullet.active = true | |
bullet.position = state.cam3d.position | |
bullet.direction = rl.Vector3Normalize(state.cam3d.target - state.cam3d.position) | |
bullet.lifetime = BULLET_LIFETIME | |
break | |
} | |
} | |
} | |
} | |
update_enemies :: proc(state: ^GameState) { | |
for &enemy in state.enemies { | |
if enemy.health <= 0 { | |
continue | |
} | |
direction := state.player.position - enemy.position | |
direction = rl.Vector3Normalize(direction) | |
movement := direction * ENEMY_SPEED | |
enemy_pos := enemy.position + movement | |
if (is_position_valid(state.solids, enemy_pos)) { | |
enemy_pos.y = 1 | |
enemy.position = enemy_pos | |
} | |
} | |
} | |
update_player :: proc(state: ^GameState) { | |
// Look left-right | |
// Yaw - horizontal, Pitch - vertical | |
yaw := state.player.yaw - (MOUSE_SENS * rl.GetMouseDelta().x) | |
pitch := state.player.pitch - (MOUSE_SENS * rl.GetMouseDelta().y) | |
state.player.yaw = math.mod(yaw, 360) | |
state.player.pitch = clamp(pitch, -89, 89) | |
state.player.is_grounded = is_on_solid_surface(state.solids, state.player.position) | |
if state.player.is_grounded && rl.IsKeyPressed(.SPACE) { | |
state.player.velocity.y = JUMP_FORCE | |
state.player.is_grounded = false | |
} | |
// Gravity | |
if !state.player.is_grounded { | |
state.player.velocity.y -= GRAVITY | |
} else { | |
state.player.velocity.y = 0 | |
} | |
movement := rl.Vector3{} | |
speed := PLAYER_SPEED | |
// Slow movement in air | |
if !state.player.is_grounded do speed *= 0.7 | |
// Sprint | |
if rl.IsKeyDown(.LEFT_SHIFT) do speed *= 1.5 | |
forward := rl.Vector3{ | |
math.sin_f32(state.player.yaw * rl.DEG2RAD), | |
0, | |
math.cos_f32(state.player.yaw * rl.DEG2RAD), | |
} | |
right := rl.Vector3{forward.z, 0, -forward.x} | |
if rl.IsKeyDown(.W) do movement += forward | |
if rl.IsKeyDown(.S) do movement -= forward | |
if rl.IsKeyDown(.D) do movement -= right | |
if rl.IsKeyDown(.A) do movement += right | |
if rl.Vector3Length(movement) > 0 { | |
movement = rl.Vector3Normalize(movement) | |
movement = movement * f32(speed) | |
} | |
new_position := state.player.position | |
new_position.x += movement.x | |
new_position.z += movement.z | |
// Try moving separately along each axis | |
if !is_position_valid(state.solids, new_position) { | |
new_position.z = state.player.position.z | |
if !is_position_valid(state.solids, new_position) { | |
new_position.x = state.player.position.x | |
new_position.z = state.player.position.z + movement.z | |
if !is_position_valid(state.solids, new_position) { | |
// Invalid new pos, reset | |
new_position.x = state.player.position.x | |
new_position.z = state.player.position.z | |
} | |
} | |
} | |
// Vertical movement | |
new_position.y += state.player.velocity.y | |
if !is_position_valid(state.solids, new_position) { | |
if state.player.velocity.y < 0 { | |
state.player.is_grounded = true | |
state.player.velocity.y = 0 | |
for y_offset := 0.0; y_offset <= 1.0; y_offset += 0.1 { | |
test_position := new_position | |
test_position.y = math.floor(new_position.y) + f32(y_offset) | |
if is_position_valid(state.solids, test_position) { | |
new_position.y = test_position.y | |
break | |
} | |
} | |
} else { | |
new_position.y = state.player.position.y | |
state.player.velocity.y = 0 | |
} | |
} else { | |
state.player.is_grounded = false | |
} | |
state.player.position = new_position | |
state.player.is_grounded = is_on_solid_surface(state.solids, state.player.position) | |
} | |
update_camera :: proc(state: ^GameState) { | |
// Interpolate cam to player | |
lerp_factor :: 0.5 | |
state.cam3d.position = linalg.lerp(state.cam3d.position, state.player.position, lerp_factor) | |
// Update camera target based on player's look direction | |
yaw_sin := math.sin_f32(state.player.yaw * rl.DEG2RAD) | |
yaw_cos := math.cos_f32(state.player.yaw * rl.DEG2RAD) | |
pitch_cos := math.cos_f32(state.player.pitch * rl.DEG2RAD) | |
pitch_sin := math.sin_f32(state.player.pitch * rl.DEG2RAD) | |
look_dir := rl.Vector3 { yaw_sin * pitch_cos, pitch_sin, yaw_cos * pitch_cos } | |
state.cam3d.target = state.cam3d.position + look_dir | |
} | |
// MARK: Utils | |
is_position_valid :: proc(solids: []Solid, pos: rl.Vector3) -> bool { | |
// block size hard coded for now | |
block_size := rl.Vector3{1, 2, 1} | |
bmin, bmax := pos - block_size * 0.5, pos + block_size * 0.5 | |
for solid in solids { | |
solid_min := solid.position - solid.size * 0.5 | |
solid_max := solid.position + solid.size * 0.5 | |
if bmin.x <= solid_max.x && bmax.x >= solid_min.x && | |
bmin.y <= solid_max.y && bmax.y >= solid_min.y && | |
bmin.z <= solid_max.z && bmax.z >= solid_min.z { | |
return false | |
} | |
} | |
return true | |
} | |
// TODO this is mostly the same as collision check proc | |
is_on_solid_surface :: proc(solids: []Solid, target: rl.Vector3) -> bool { | |
pos := target | |
pos.y -= 0.2 | |
block_size := rl.Vector3{1, 2, 1} | |
bmin, bmax := pos - block_size * 0.5, pos + block_size * 0.5 | |
for solid in solids { | |
solid_min := solid.position - solid.size * 0.5 | |
solid_max := solid.position + solid.size * 0.5 | |
if bmin.x <= solid_max.x && bmax.x >= solid_min.x && | |
bmin.z <= solid_max.z && bmax.z >= solid_min.z && | |
math.abs(bmin.y - solid_max.y) < 0.3 { | |
return true | |
} | |
} | |
return false | |
} | |
// MARK: Main | |
main :: proc() { | |
rl.InitWindow(WINDOW_W, WINDOW_H, "small fps") | |
defer rl.CloseWindow() | |
rl.SetWindowState(rl.ConfigFlags{.WINDOW_RESIZABLE, .FULLSCREEN_MODE}) | |
rl.DisableCursor() | |
rl.SetTargetFPS(120) | |
// Game State | |
cam3d := rl.Camera3D { | |
position = {0.0, 2.0, 4.0}, | |
target = {0.0, 2.0, 0.0}, | |
up = {0.0, 1.0, 0.0}, | |
fovy = 60.0, | |
projection = .PERSPECTIVE, | |
} | |
solids: []Solid = { | |
// Ground | |
{ {0, -0.5, 0}, {ARENA_SIZE, 1, ARENA_SIZE}, rl.DARKGRAY }, | |
// Arena walls | |
{ {-ARENA_SIZE/2, 0, 0}, {1, 15, ARENA_SIZE}, rl.VIOLET }, | |
{ {ARENA_SIZE/2, 0, 0}, {1, 15, ARENA_SIZE}, rl.VIOLET }, | |
{ {0, 0, ARENA_SIZE/2}, {ARENA_SIZE, 15, 1}, rl.DARKBROWN }, | |
{ {0, 0, -ARENA_SIZE/2}, {ARENA_SIZE, 15, 1}, rl.DARKBROWN }, | |
// Top large platform | |
{ {ARENA_SIZE/2, 7, 0}, {15, 1, ARENA_SIZE}, rl.RED }, | |
// Steps to large platform | |
{ {0, 0, -ARENA_SIZE/2}, {2, 1, 20}, rl.PINK }, | |
{ {2, 1, -ARENA_SIZE/2}, {2, 1, 20}, rl.WHITE }, | |
{ {4, 2, -ARENA_SIZE/2}, {2, 1, 20}, rl.PINK }, | |
{ {6, 3, -ARENA_SIZE/2}, {2, 1, 20}, rl.WHITE }, | |
{ {8, 4, -ARENA_SIZE/2}, {2, 1, 20}, rl.PINK }, | |
{ {10, 5, -ARENA_SIZE/2}, {2, 1, 20}, rl.WHITE }, | |
{ {12, 6, -ARENA_SIZE/2}, {2, 1, 20}, rl.PINK }, | |
{ {15, 7, -ARENA_SIZE/2}, {4, 1, 20}, rl.WHITE }, | |
// Random platforms | |
{ {4, 2, 12}, {3, 1, 3}, rl.SKYBLUE }, | |
} | |
player := Player { | |
position = {0.0, 2.0, 4.0}, | |
velocity = {0, 0, 0}, | |
is_grounded = true, | |
} | |
enemies := make([dynamic]Enemy) | |
defer delete(enemies) | |
for i in 0..<ENEMY_COUNT { | |
r := u8(rand.int31_max(255)) | |
g := u8(rand.int31_max(255)) | |
b := u8(rand.int31_max(255)) | |
random_color := rl.Color{r, g, b, 255} | |
enemy := Enemy{ | |
position = { | |
rand.float32_range(-ARENA_SIZE/2, ARENA_SIZE/2), | |
1.0, | |
rand.float32_range(-ARENA_SIZE/2, ARENA_SIZE/2), | |
}, | |
size = {1.0, 2.0, 1.0}, | |
color = random_color, | |
health = 1 | |
} | |
append(&enemies, enemy) | |
} | |
game_state := GameState{ | |
cam3d = cam3d, | |
player = player, | |
enemies = enemies, | |
solids = solids | |
} | |
for !rl.WindowShouldClose() { | |
update(&game_state) | |
rl.BeginDrawing() | |
defer rl.EndDrawing() | |
rl.ClearBackground(rl.BEIGE) | |
draw_3d(&game_state) | |
draw_2d(&game_state) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment