Skip to content

Instantly share code, notes, and snippets.

@bones-ai
Created September 25, 2024 14:16
Show Gist options
  • Save bones-ai/f3fb5a6a37ac53ba97b73cf80f7ae109 to your computer and use it in GitHub Desktop.
Save bones-ai/f3fb5a6a37ac53ba97b73cf80f7ae109 to your computer and use it in GitHub Desktop.
A small fps written in Odin and raylib
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