Skip to content

Game Development Guide

🎮 Entity System Deep Dive

The Vibe API's entity system is the foundation of your games. Understanding it thoroughly will help you build better, more efficient games.

Entity Lifecycle

Every entity follows a predictable lifecycle:

lua
-- 1. Spawn
local player_id = vibe.spawn("sprite", {
    texture = "res://assets/player.png",
    x = 100, y = 100,
    tag = "player"
})

-- 2. Use (get/set properties, find by tag)
local x = vibe.get(player_id, "x")
vibe.set(player_id, "x", x + 50)
local players = vibe.find("player")

-- 3. Destroy
vibe.destroy(player_id)
-- Entity is gone, ID is no longer valid

Entity Types

Sprites

Perfect for visual game elements - players, enemies, items, backgrounds.

lua
-- Basic sprite
local basic = vibe.spawn("sprite", {
    texture = "res://assets/player.png",
    x = 100, y = 100,
    tag = "player"
})

-- Scaled sprite
local large = vibe.spawn("sprite", {
    texture = "res://assets/enemy.png",
    x = 200, y = 100,
    scale = 2.0,  -- 2x larger
    tag = "enemy"
})

-- Auto-fitted sprite (maintains aspect ratio)
local fitted = vibe.spawn("sprite", {
    texture = "res://assets/coin.png",
    x = 300, y = 100,
    cell_size = 24,  -- Fits in 24x24 cell
    tag = "collectible"
})

-- Exit entities (always on top)
local exit = vibe.spawn_exit({
    texture = "res://assets/door.png",
    x = 500, y = 200,
    cell_size = 48,
    tag = "exit"
})

Labels

Perfect for UI, debugging, and text-based game elements.

lua
-- UI text
local score = vibe.spawn("label", {
    text = "Score: 0",
    x = 10, y = 10,
    tag = "ui"
})

-- Dynamic text that updates
function update_score_display(new_score)
    vibe.set(score, "text", "Score: " .. new_score)
end

-- Positioned labels for in-world text
local sign = vibe.spawn("label", {
    text = "Welcome to the dungeon!",
    x = 250, y = 150,
    tag = "world_text"
})

Efficient Entity Management

Batch Operations with Tags

lua
-- Destroy all bullets at once
function clear_bullets()
    local bullets = vibe.find("bullet")
    for i = 1, #bullets do
        vibe.destroy(bullets[i])
    end
end

-- Update all enemies
function update_enemies(dt)
    local enemies = vibe.find("enemy")
    for i = 1, #enemies do
        local enemy = enemies[i]
        -- Update each enemy
        move_enemy(enemy, dt)
    end
end

Smart Entity Tracking

lua
-- Keep track of important entities
local game_entities = {
    player = nil,
    score_label = nil,
    lives_label = nil
}

function spawn_player()
    game_entities.player = vibe.spawn("sprite", {
        texture = "res://assets/player.png",
        x = 100, y = 200,
        tag = "player"
    })
end

function update_player(dt)
    if game_entities.player then
        -- Direct access, no searching needed
        local x = vibe.get(game_entities.player, "x")
        -- ... update logic
    end
end

Managing Entity States

lua
-- Use tables to store entity data
local entity_data = {}

function spawn_enemy(x, y)
    local id = vibe.spawn("sprite", {
        texture = "res://assets/enemy.png",
        x = x, y = y, tag = "enemy"
    })
    
    -- Store additional data
    entity_data[id] = {
        health = 3,
        speed = 50,
        last_direction_change = vibe.time_ms()
    }
    
    return id
end

function update_enemy(enemy_id, dt)
    local data = entity_data[enemy_id]
    if not data then return end
    
    -- Use stored data for behavior
    if vibe.time_ms() - data.last_direction_change > 2000 then
        data.speed = -data.speed  -- Change direction
        data.last_direction_change = vibe.time_ms()
    end
    
    local x = vibe.get(enemy_id, "x")
    vibe.set(enemy_id, "x", x + data.speed * dt)
end

function destroy_enemy(enemy_id)
    vibe.destroy(enemy_id)
    entity_data[enemy_id] = nil  -- Clean up data
end

⌨️ Input Handling Patterns

Movement Patterns

Smooth Movement

lua
function update_player_smooth(dt)
    local speed = 200  -- pixels per second
    local h = vibe.axis("horizontal")
    local v = vibe.axis("vertical")
    
    if h ~= 0 or v ~= 0 then
        local players = vibe.find("player")
        if #players > 0 then
            local player = players[1]
            local x = vibe.get(player, "x")
            local y = vibe.get(player, "y")
            
            vibe.set(player, "x", x + h * speed * dt)
            vibe.set(player, "y", y + v * speed * dt)
        end
    end
end

Grid-Based Movement

lua
local move_timer = 0
local move_delay = 0.2  -- seconds between moves
local grid_size = 32

function update_player_grid(dt)
    move_timer = move_timer + dt
    
    if move_timer >= move_delay then
        local moved = false
        
        if vibe.key("up") then
            move_player_grid(0, -grid_size)
            moved = true
        elseif vibe.key("down") then
            move_player_grid(0, grid_size)
            moved = true
        elseif vibe.key("left") then
            move_player_grid(-grid_size, 0)
            moved = true
        elseif vibe.key("right") then
            move_player_grid(grid_size, 0)
            moved = true
        end
        
        if moved then
            move_timer = 0
        end
    end
end

function move_player_grid(dx, dy)
    local players = vibe.find("player")
    if #players > 0 then
        local player = players[1]
        local x = vibe.get(player, "x") + dx
        local y = vibe.get(player, "y") + dy
        vibe.set(player, "x", x)
        vibe.set(player, "y", y)
    end
end

Action Input Patterns

Button Press Detection

lua
-- Detect single press (not hold)
local button_states = {
    space = false,
    up = false,
    down = false,
    left = false,
    right = false
}

function on_tick(dt)
    -- Check for button presses
    check_button_press("space", function()
        -- Jump or shoot
        player_action()
    end)
    
    check_button_press("up", function()
        -- Interact with objects above
        interact_up()
    end)
    
    -- Update button states
    for key, _ in pairs(button_states) do
        button_states[key] = vibe.key(key)
    end
end

function check_button_press(key, callback)
    local pressed_now = vibe.key(key)
    local pressed_before = button_states[key]
    
    if pressed_now and not pressed_before then
        callback()
    end
end

Input Buffering

lua
-- Buffer inputs for responsive controls
local input_buffer = {}
local buffer_time = 0.1  -- 100ms buffer

function add_to_buffer(action)
    table.insert(input_buffer, {
        action = action,
        time = vibe.time_ms()
    })
end

function process_input_buffer()
    local now = vibe.time_ms()
    
    -- Remove old inputs
    for i = #input_buffer, 1, -1 do
        if now - input_buffer[i].time > buffer_time * 1000 then
            table.remove(input_buffer, i)
        end
    end
    
    -- Process buffered inputs
    if #input_buffer > 0 then
        local action = input_buffer[1].action
        table.remove(input_buffer, 1)
        execute_action(action)
    end
end

function on_tick(dt)
    -- Collect inputs
    if vibe.key("space") then add_to_buffer("jump") end
    if vibe.key("up") then add_to_buffer("attack") end
    
    -- Process when ready
    if player_can_act() then
        process_input_buffer()
    end
end

Combo System

lua
local combo_sequence = {}
local combo_timeout = 1000  -- 1 second
local last_input_time = 0

function add_combo_input(input)
    local now = vibe.time_ms()
    
    -- Reset if too much time passed
    if now - last_input_time > combo_timeout then
        combo_sequence = {}
    end
    
    table.insert(combo_sequence, input)
    last_input_time = now
    
    -- Check for complete combos
    check_combos()
end

function check_combos()
    local sequence = table.concat(combo_sequence, "")
    
    if sequence == "upup" then
        execute_combo("double_jump")
        combo_sequence = {}
    elseif sequence == "leftright" then
        execute_combo("dash")
        combo_sequence = {}
    end
    
    -- Limit sequence length
    if #combo_sequence > 4 then
        table.remove(combo_sequence, 1)
    end
end

🔊 Audio Implementation

Basic Sound Effects

lua
-- Organize sounds for easy management
local sounds = {
    player = {
        jump = "res://assets/sounds/jump.wav",
        hurt = "res://assets/sounds/hurt.wav",
        die = "res://assets/sounds/die.wav"
    },
    enemy = {
        hit = "res://assets/sounds/enemy_hit.wav",
        die = "res://assets/sounds/enemy_die.wav"
    },
    pickup = {
        coin = "res://assets/sounds/coin.wav",
        powerup = "res://assets/sounds/powerup.wav"
    },
    ui = {
        select = "res://assets/sounds/select.wav",
        confirm = "res://assets/sounds/confirm.wav"
    }
}

function play_sound(category, sound_name)
    if sounds[category] and sounds[category][sound_name] then
        vibe.play_sfx(sounds[category][sound_name])
    end
end

-- Usage
play_sound("player", "jump")
play_sound("pickup", "coin")

Audio Cooldowns

lua
-- Prevent audio spam
local audio_cooldowns = {}

function play_sound_with_cooldown(sound_path, cooldown_ms)
    local now = vibe.time_ms()
    cooldown_ms = cooldown_ms or 100  -- Default 100ms
    
    if not audio_cooldowns[sound_path] or 
       now - audio_cooldowns[sound_path] >= cooldown_ms then
        vibe.play_sfx(sound_path)
        audio_cooldowns[sound_path] = now
        return true
    end
    
    return false  -- Sound was blocked by cooldown
end

-- Usage
function on_collision()
    play_sound_with_cooldown("res://assets/hit.wav", 250)  -- Max once per 250ms
end

Dynamic Audio

lua
-- Randomized sound variations
local footstep_sounds = {
    "res://assets/sounds/step1.wav",
    "res://assets/sounds/step2.wav",
    "res://assets/sounds/step3.wav"
}

function play_footstep()
    local index = math.random(1, #footstep_sounds)
    vibe.play_sfx(footstep_sounds[index])
end

-- Context-aware audio
function play_context_sound(base_name, context)
    local sound_path = "res://assets/sounds/" .. base_name .. "_" .. context .. ".wav"
    vibe.play_sfx(sound_path)
end

-- Usage
play_context_sound("enemy_die", "fire")    -- enemy_die_fire.wav
play_context_sound("player_hurt", "ice")   -- player_hurt_ice.wav

🎯 Common Game Patterns

Collision Detection

lua
-- Basic distance-based collision
function check_collision(id1, id2, radius)
    local x1 = vibe.get(id1, "x")
    local y1 = vibe.get(id1, "y")
    local x2 = vibe.get(id2, "x")
    local y2 = vibe.get(id2, "y")
    
    local dx = x1 - x2
    local dy = y1 - y2
    local distance = math.sqrt(dx * dx + dy * dy)
    
    return distance < radius
end

-- Rectangle collision for grid-based games
function check_rect_collision(id1, id2, size)
    size = size or 32
    local x1 = vibe.get(id1, "x")
    local y1 = vibe.get(id1, "y")
    local x2 = vibe.get(id2, "x")
    local y2 = vibe.get(id2, "y")
    
    return math.abs(x1 - x2) < size and math.abs(y1 - y2) < size
end

-- Batch collision checking
function check_all_collisions()
    local bullets = vibe.find("bullet")
    local enemies = vibe.find("enemy")
    
    for b = 1, #bullets do
        for e = 1, #enemies do
            if check_collision(bullets[b], enemies[e], 24) then
                -- Handle collision
                vibe.destroy(bullets[b])
                damage_enemy(enemies[e])
            end
        end
    end
end

Spawn Management

lua
-- Wave spawning
local wave_config = {
    {enemies = 3, delay = 1.0, type = "basic"},
    {enemies = 5, delay = 0.8, type = "basic"},
    {enemies = 2, delay = 2.0, type = "strong"},
    {enemies = 8, delay = 0.5, type = "mixed"}
}

local current_wave = 1
local enemies_spawned = 0
local spawn_timer = 0

function update_wave_spawning(dt)
    if current_wave > #wave_config then
        return  -- All waves complete
    end
    
    local wave = wave_config[current_wave]
    spawn_timer = spawn_timer + dt
    
    if spawn_timer >= wave.delay and enemies_spawned < wave.enemies then
        spawn_enemy_by_type(wave.type)
        enemies_spawned = enemies_spawned + 1
        spawn_timer = 0
    end
    
    -- Check if wave complete
    if enemies_spawned >= wave.enemies and #vibe.find("enemy") == 0 then
        current_wave = current_wave + 1
        enemies_spawned = 0
        spawn_timer = 0
    end
end

Game State Management

lua
-- Simple game state system
local game_state = "menu"  -- menu, playing, paused, game_over

function on_tick(dt)
    if game_state == "menu" then
        handle_menu_input()
    elseif game_state == "playing" then
        update_game_logic(dt)
    elseif game_state == "paused" then
        handle_pause_input()
    elseif game_state == "game_over" then
        handle_game_over_input()
    end
end

function change_state(new_state)
    -- Exit current state
    if game_state == "playing" then
        -- Pause music, save progress, etc.
    end
    
    game_state = new_state
    
    -- Enter new state
    if game_state == "playing" then
        initialize_level()
    elseif game_state == "game_over" then
        show_final_score()
    end
end

Resource Management

lua
-- Simple object pooling
local bullet_pool = {}
local max_pool_size = 50

function get_bullet()
    if #bullet_pool > 0 then
        local bullet = table.remove(bullet_pool)
        return bullet
    else
        return vibe.spawn("sprite", {
            texture = "res://assets/bullet.png",
            tag = "bullet"
        })
    end
end

function return_bullet(bullet_id)
    if #bullet_pool < max_pool_size then
        -- Hide bullet off-screen
        vibe.set(bullet_id, "x", -100)
        vibe.set(bullet_id, "y", -100)
        table.insert(bullet_pool, bullet_id)
    else
        vibe.destroy(bullet_id)
    end
end

This guide covers the core patterns you'll use in most games. For complete working examples, check out Examples and Tutorials. For optimization techniques, see Advanced Topics.

Built with VitePress for the Vibe API Documentation