Download

← Examples

raycaster

A first-person walk through a hangar full of zombies. The engine draws every frame in Vader, pixel by pixel; raylib opens the window. Its output is that of the frames entry, which draws the game's views in characters.

examples/raycaster in the repository.

// examples/raycaster: a first-person walk through a hangar full of zombies, then
// a depot, in a window.
//
// The engine (`engine/`) draws each frame in Vader, pixel by pixel, into a
// buffer; raylib only opens the window, reads the keyboard and the mouse, plays
// the sounds, and shows the buffer, scaled up, as one texture. The level, the
// textures, the sprites and the sounds are files under `assets/`, read when the
// program is compiled. A level ends at its exit switch: thrown, the next level
// begins, the player keeping their health and bullets.
//
//   vader build --run --release examples/raycaster
//
// Moving goes by the keys' places: W A S D on a QWERTY keyboard, Z Q S D on an
// AZERTY one, or Up and Down; the mouse, Left and Right turn; the left button or
// Ctrl fires. A command letter goes by what its key types, whatever the layout: E
// (or Space) for the door or the switch ahead, M for the map, F (or F11) to fill
// the screen or leave it, P to save the window as `raycaster.png` in the working
// directory. Enter starts again once fallen, and goes on to the next level once
// one is done; Escape pauses. The game opens on its title: Enter plays, Escape
// leaves; from the pause, Escape plays on and Q leaves.
// `frames/` is the same engine without a window.

module "main"

import "engine"
import "raylib"
import "std/abort"
import "std/io"
import "std/math"

/// The size of the frame the engine draws; the window shows it as large as fits.
VIEW_WIDTH: i32: 960
VIEW_HEIGHT: i32: 540

/// The window's size when it opens, until the user resizes it or fills the screen.
/// The HUD's sizes and places are given in pixels of a window this size, and scale
/// with the view the window shows.
WINDOW_WIDTH: i32: 1280
WINDOW_HEIGHT: i32: 720

/// How loud the sounds play, as a share of how they were recorded.
VOLUME: f32: 0.5

/// Walking speed, in cells per second.
WALK_SPEED :: 3.2
/// Turning speed with the arrow keys, in radians per second.
TURN_SPEED :: 2.4
/// Radians turned per pixel the mouse moves.
MOUSE_TURN :: 0.0025
/// Each axis of a diagonal step is this share of a straight one, so that walking
/// diagonally is no faster: the square root of one half.
DIAGONAL :: 0.70710678

HELP :: "WASD / ZQSD: move   mouse: turn   click: fire   E: use   M: map   F: full screen   P: screenshot   Esc: pause"

/// The side of the HUD's icons, in HUD pixels.
ICON_SIZE: i32: 40

/// How far down the HUD the line of counts sits, in HUD pixels.
STATUS_TOP: i32: 646

/// What darkens the view under the title and the pause, so that their text reads.
SHADE :: Color {
    .r = 0,
    .g = 0,
    .b = 0,
    .a = 160,
}

/// Which screen the loop shows: what it draws over the view, and which keys it reads.
Phase :: enum {
    /// The title, over the level as it starts.
    Title,
    /// The game running.
    Playing,
    /// The game stopped, drawn as it stood.
    Paused,
    /// The player asked to leave.
    Leaving,
}

/// One game: the level as it stands, the player in it, and which of `LEVELS` it is.
Game :: struct {
    level: Level
    /// How `level` is lit: its `light_map`.
    lights: LightMap
    player: Player
    index: i64
}

/// How a game stands.
Progress :: enum {
    /// The player is in a level whose exit switch is not thrown yet.
    Playing,
    /// The player fell.
    Fallen,
    /// The player threw the exit switch of a level before the last.
    LevelDone,
    /// The player threw the exit switch of the last level.
    Won,
}

// The game at the start of `LEVELS[index]`, as the build read it.
game_at :: fn(index: i64) -> Game {
    level :: LEVELS[index]
    return Game {
        .level = level,
        .lights = light_map(level),
        .player = new_player(level),
        .index = index,
    }
}

/// A game from the start of the first level.
new_game :: fn() -> Game = game_at(0)

// The game in the level after `game`'s, the player keeping their health and
// their bullets.
next_level :: fn(game: Game) -> Game {
    next :: game_at(game.index + 1)
    next.player.health = game.player.health
    next.player.ammo = game.player.ammo
    return next
}

// How `game` stands.
progress :: fn(game: Game) -> Progress {
    if game.player.health <= 0 {
        return .Fallen
    }
    if !game.level.exited() {
        return .Playing
    }
    if game.index == LEVELS.len() - 1 {
        return .Won
    }
    return .LevelDone
}

// The game Enter starts once `game` is over: the next level after a level done,
// the first level again otherwise.
after :: fn(game: Game) -> Game {
    if game.progress() == .LevelDone {
        return game.next_level()
    }
    return new_game()
}

main :: fn() -> i32 {
    set_config_flags(FLAG_WINDOW_RESIZABLE)
    init_window(WINDOW_WIDTH, WINDOW_HEIGHT, "Vader raycaster")
    if !is_window_ready() {
        eprintln("raycaster: no window could be opened (raylib says why above)")
        return 1
    }
    defer close_window()
    // Escape pauses: only the window's close button and Q leave.
    set_exit_key(.Null)
    init_audio_device()
    defer close_audio_device()
    set_master_volume(VOLUME)
    set_target_fps(60)
    sounds :: load_sounds()
    defer unload_sounds(sounds)
    icons :: load_icons()
    defer unload_icons(icons)
    game := new_game()
    frame :: new_frame(VIEW_WIDTH, VIEW_HEIGHT)
    screen :: load_blank_texture(VIEW_WIDTH, VIEW_HEIGHT)
    defer unload_texture(screen)
    source :: Rectangle {
        .x = 0.0,
        .y = 0.0,
        .width = f32(VIEW_WIDTH),
        .height = f32(VIEW_HEIGHT),
    }
    show_map := true
    phase := Phase.Title
    for !window_should_close() {
        seconds :: f64(get_frame_time())
        level :: game.level
        player :: game.player
        noises: Noise[]: []
        typed :: typed_this_frame()
        next :: next_phase(phase, typed)
        if next == .Leaving {
            break
        }
        if next != phase {
            use_cursor_for(next)
        }
        phase = next
        if phase == .Playing {
            if game.progress() != .Playing && is_key_pressed(.Enter) {
                game = game.after()
                continue
            }
            // A fallen player's level plays on around them; a level done stops.
            if !level.exited() {
                play(level, player, seconds, typed, noises)
            }
        }
        if 'm' in typed {
            show_map = !show_map
        }
        if 'f' in typed || is_key_pressed(.F11) {
            toggle_borderless_windowed()
        }
        for noise in noises {
            play_sound(sound_of(sounds, noise))
        }
        render(frame, level, player, game.lights)
        if show_map {
            draw_minimap(frame, level, player, 4)
        }
        draw_crosshair(frame)
        update_texture(screen, frame.pixels)
        view :: fitted_view()
        begin_drawing()
        clear_background(BLACK)
        draw_texture_pro(screen, source, view, Vector2 { .x = 0.0, .y = 0.0 }, 0.0, WHITE)
        draw_hud(view, icons, phase, game)
        end_drawing()
        if 'p' in typed {
            take_screenshot("raycaster.png")
        }
    }
    return 0
}

// The characters typed this frame, letters in lower case. They count only on a
// frame where a key went down: a key held down types its character again and
// again, which would toggle the full screen or a door on every repeat.
typed_this_frame :: fn() -> char[] {
    pressed := false
    for get_key_pressed() != 0 {
        pressed = true
    }
    typed: char[]: []
    for {
        code :: get_char_pressed()
        if code == 0 {
            break
        }
        if pressed {
            typed.push(lower_case(code))
        }
    }
    return typed
}

// The character `code` stands for, an upper-case ASCII letter as its lower case.
lower_case :: fn(code: i32) -> char {
    if code >= 'A' && code <= 'Z' {
        return char(u32(code + 32))
    }
    return char(u32(code))
}

// The phase after this frame's keys and `typed` characters: from the title,
// Enter plays and Escape leaves; Escape pauses the game, and from the pause plays
// on, or Q leaves.
next_phase :: fn(phase: Phase, typed: char[]) -> Phase {
    match phase {
        .Title -> {
            if is_key_pressed(.Enter)  { return .Playing }
            if is_key_pressed(.Escape) { return .Leaving }
        }
        .Playing   -> {
            if is_key_pressed(.Escape) {
                return .Paused
            }
        }
        .Paused -> {
            if is_key_pressed(.Escape) { return .Playing }
            if 'q' in typed            { return .Leaving }
        }
        .Leaving -> {}
    }
    return phase
}

// Hide the cursor while the game runs, so that the mouse turns the view; show it
// on the other screens.
use_cursor_for :: fn(phase: Phase) -> void {
    if phase == .Playing {
        disable_cursor()
        return
    }
    enable_cursor()
}

// Run the game on by `seconds`: the player's keys and the `typed` characters,
// then the doors, the zombies and the player's own clocks.
//
// @param noises  what the player hears, appended to.
play :: fn(level: Level, player: Player, seconds: f64, typed: char[], noises: Noise[]) -> void {
    if player.health > 0 {
        steer(player, level, seconds)
        player.collect(level, noises)
        if is_mouse_button_down(.Left) || is_key_down(.LeftControl) {
            player.fire(level, noises)
        }
        if 'e' in typed || is_key_pressed(.Space) {
            player.use_ahead(level, noises)
        }
    }
    level.update_doors(player, seconds)
    level.update_enemies(player, seconds, noises)
    level.update_effects(seconds)
    player.update_player(seconds)
}

// Move and turn the player from the keys held and the mouse, for a frame of
// `seconds`.
steer :: fn(player: Player, level: Level, seconds: f64) -> void {
    forward := 0.0
    right := 0.0
    if is_key_down(.W) || is_key_down(.Up)   { forward += 1.0 }
    if is_key_down(.S) || is_key_down(.Down) { forward -= 1.0 }
    if is_key_down(.D)                       { right += 1.0 }
    if is_key_down(.A)                       { right -= 1.0 }
    if forward != 0.0 && right != 0.0 {
        forward *= DIAGONAL
        right *= DIAGONAL
    }
    stride :: WALK_SPEED * seconds
    player.walk(level, forward * stride, right * stride)
    angle := f64(get_mouse_delta().x) * MOUSE_TURN
    if is_key_down(.Right) { angle += TURN_SPEED * seconds }
    if is_key_down(.Left)  { angle -= TURN_SPEED * seconds }
    player.turn(angle)
}

// Where the window shows the frame: as large as fits, centred, black around it.
fitted_view :: fn() -> Rectangle {
    width :: f32(get_screen_width())
    height :: f32(get_screen_height())
    scale :: min(width / f32(VIEW_WIDTH), height / f32(VIEW_HEIGHT))
    return Rectangle {
        .x = (width - f32(VIEW_WIDTH) * scale) / 2.0,
        .y = (height - f32(VIEW_HEIGHT) * scale) / 2.0,
        .width = f32(VIEW_WIDTH) * scale,
        .height = f32(VIEW_HEIGHT) * scale,
    }
}

// What `phase` shows over `view`: the title, or the health, the bullets and the
// zombies left, the keys, the pause, and what became of the player.
draw_hud :: fn(view: Rectangle, icons: Icons, phase: Phase, game: Game) -> void {
    level :: game.level
    player :: game.player
    if phase != .Playing {
        draw_rectangle_rec(view, SHADE)
    }
    draw_text("${get_fps()} FPS", view.across(1180), view.down(10), view.scaled(20), RAYWHITE)
    if phase == .Title {
        view.draw_centred("VADER RAYCASTER", 260, 80)
        view.draw_centred("Enter: play   Esc: quit", 370, 30)
        return
    }
    view.draw_count(icons.health, 16, "${player.health}")
    view.draw_count(icons.ammo, 176, "${player.ammo}")
    view.draw_count(icons.zombies, 336, "${level.standing()} / ${level.enemies.len()}")
    if phase == .Paused {
        view.draw_centred("PAUSED", 260, 80)
        view.draw_centred("Esc: play on   Q: quit", 370, 30)
        return
    }
    draw_text(HELP, view.across(16), view.down(694), view.scaled(20), RAYWHITE)
    match game.progress() {
        .Fallen    -> view.draw_centred("YOU FELL. Enter: start again", 340, 40)
        .Won       -> {
            view.draw_centred("YOU WIN", 300, 80)
            view.draw_centred("Every level is done. Enter: play again", 390, 30)
        }
        .LevelDone -> {
            view.draw_centred("LEVEL COMPLETE", 300, 80)
            total :: level.enemies.len()
            down :: total - level.standing()
            view.draw_centred("Zombies down: ${down} / ${total}   Enter: next level", 390, 30)
        }
        .Playing   -> {
            if level.cleared() {
                view.draw_centred("Every zombie is down: find the exit", 60, 30)
            }
        }
    }
}

// Draw `icon` with `text` beside it on the line of counts, `x` HUD pixels from
// `view`'s left edge.
draw_count :: fn(view: Rectangle, icon: Texture2D, x: i32, text: string) -> void {
    place :: Rectangle {
        .x = f32(view.across(x)),
        .y = f32(view.down(STATUS_TOP)),
        .width = f32(view.scaled(ICON_SIZE)),
        .height = f32(view.scaled(ICON_SIZE)),
    }
    whole :: Rectangle {
        .x = 0.0,
        .y = 0.0,
        .width = f32(icon.width),
        .height = f32(icon.height),
    }
    draw_texture_pro(icon, whole, place, Vector2 { .x = 0.0, .y = 0.0 }, 0.0, WHITE)
    draw_text(
        text,
        view.across(x + ICON_SIZE + 8),
        view.down(STATUS_TOP + 6),
        view.scaled(30),
        RAYWHITE,
    )
}

// Draw `text` centred across `view`, its top `y` HUD pixels down.
draw_centred :: fn(view: Rectangle, text: string, y: i32, size: i32) -> void {
    pixels :: view.scaled(size)
    left :: i32(view.x + view.width / 2.0) - measure_text(text, pixels) / 2
    draw_text(text, left, view.down(y), pixels, RAYWHITE)
}

// `size` HUD pixels, in the window's pixels for `view`.
scaled :: fn(view: Rectangle, size: i32) -> i32 = i32(f32(size) * view.height / f32(WINDOW_HEIGHT))

// The window column `x` HUD pixels from `view`'s left edge.
across :: fn(view: Rectangle, x: i32) -> i32 = i32(view.x) + view.scaled(x)

// The window row `y` HUD pixels below `view`'s top edge.
down :: fn(view: Rectangle, y: i32) -> i32 = i32(view.y) + view.scaled(y)

// ---- icons ----------------------------------------------------------------

// The HUD's icons, as textures.
Icons :: struct {
    health: Texture2D
    ammo: Texture2D
    zombies: Texture2D
}

// Every icon, made into a texture: the window must be open.
load_icons :: fn() -> Icons = Icons {
    .health = load_icon(.Health),
    .ammo = load_icon(.Ammo),
    .zombies = load_icon(.Zombies),
}

// The texture of `icon`, one pixel per texel of its sprite.
load_icon :: fn(icon: Icon) -> Texture2D {
    picture :: icon_frame(icon)
    texture :: load_blank_texture(picture.width, picture.height)
    update_texture(texture, picture.pixels)
    return texture
}

unload_icons :: fn(icons: Icons) -> void {
    unload_texture(icons.health)
    unload_texture(icons.ammo)
    unload_texture(icons.zombies)
}

// ---- sounds ---------------------------------------------------------------

// The sounds the game plays, one per noise the engine makes. The zombies' shots
// have a sound of their own, from the same file as the player's, so that the
// two play over each other instead of cutting each other short.
Sounds :: struct {
    shot: Sound
    enemy_sees: Sound
    enemy_hurt: Sound
    enemy_dies: Sound
    enemy_fires: Sound
    player_hurt: Sound
    player_dies: Sound
    door_opens: Sound
    door_closes: Sound
    pick_up: Sound
    exit: Sound
}

// The sound files, from Freedoom, read by the build.
PISTOL_WAV: u8[]: @comptime read_sound("assets/sounds/dspistol.wav")
SIGHT_WAV: u8[]: @comptime read_sound("assets/sounds/dsposit1.wav")
PAIN_WAV: u8[]: @comptime read_sound("assets/sounds/dspopain.wav")
DEATH_WAV: u8[]: @comptime read_sound("assets/sounds/dspodth1.wav")
PLAYER_PAIN_WAV: u8[]: @comptime read_sound("assets/sounds/dsplpain.wav")
PLAYER_DEATH_WAV: u8[]: @comptime read_sound("assets/sounds/dspldeth.wav")
DOOR_OPENS_WAV: u8[]: @comptime read_sound("assets/sounds/dsdoropn.wav")
DOOR_CLOSES_WAV: u8[]: @comptime read_sound("assets/sounds/dsdorcls.wav")
PICK_UP_WAV: u8[]: @comptime read_sound("assets/sounds/dsitemup.wav")
EXIT_WAV: u8[]: @comptime read_sound("assets/sounds/dsswtchx.wav")

// The bytes of the sound file at `path`, relative to this source file: one that
// is missing stops the build.
read_sound :: fn(path: string) -> u8[] {
    data :: read_file_bytes(path)
    if data is IOError {
        panic("cannot read `${path}`: ${data.message()}")
    }
    return data
}

// Every sound, decoded for the audio device, which must be open.
load_sounds :: fn() -> Sounds = Sounds {
    .shot = decode_sound(PISTOL_WAV),
    .enemy_sees = decode_sound(SIGHT_WAV),
    .enemy_hurt = decode_sound(PAIN_WAV),
    .enemy_dies = decode_sound(DEATH_WAV),
    .enemy_fires = decode_sound(PISTOL_WAV),
    .player_hurt = decode_sound(PLAYER_PAIN_WAV),
    .player_dies = decode_sound(PLAYER_DEATH_WAV),
    .door_opens = decode_sound(DOOR_OPENS_WAV),
    .door_closes = decode_sound(DOOR_CLOSES_WAV),
    .pick_up = decode_sound(PICK_UP_WAV),
    .exit = decode_sound(EXIT_WAV),
}

// The sound in the WAV file `data`. The files are the build's, so one that does
// not decode is a broken build.
decode_sound :: fn(data: u8[]) -> Sound =
    load_sound_from_memory(".wav", data) ?? panic("a sound baked into the program does not decode")

unload_sounds :: fn(sounds: Sounds) -> void {
    unload_sound(sounds.shot)
    unload_sound(sounds.enemy_sees)
    unload_sound(sounds.enemy_hurt)
    unload_sound(sounds.enemy_dies)
    unload_sound(sounds.enemy_fires)
    unload_sound(sounds.player_hurt)
    unload_sound(sounds.player_dies)
    unload_sound(sounds.door_opens)
    unload_sound(sounds.door_closes)
    unload_sound(sounds.pick_up)
    unload_sound(sounds.exit)
}

// The sound that plays for `noise`.
sound_of :: fn(sounds: Sounds, noise: Noise) -> Sound = match noise {
    .Shot       -> sounds.shot
    .EnemySees  -> sounds.enemy_sees
    .EnemyHurt  -> sounds.enemy_hurt
    .EnemyDies  -> sounds.enemy_dies
    .EnemyFires -> sounds.enemy_fires
    .PlayerHurt -> sounds.player_hurt
    .PlayerDies -> sounds.player_dies
    .DoorOpens  -> sounds.door_opens
    .DoorCloses -> sounds.door_closes
    .PickUp     -> sounds.pick_up
    .Exit       -> sounds.exit
}