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