Skip to content

Advanced Topics

🚀 Performance Optimization

Entity Management Best Practices

Minimize Entity Lookups

lua
-- ❌ Bad: Repeated lookups
function on_tick(dt)
    local players = vibe.find("player")  -- Lookup 1
    if #players > 0 then
        local bullets = vibe.find("bullet")  -- Lookup 2
        for i = 1, #bullets do
            local enemies = vibe.find("enemy")  -- Lookup 3 (in loop!)
            -- ... collision checking
        end
    end
end

-- ✅ Good: Cache lookups
function on_tick(dt)
    local players = vibe.find("player")
    local bullets = vibe.find("bullet")
    local enemies = vibe.find("enemy")
    
    if #players > 0 then
        for b = 1, #bullets do
            for e = 1, #enemies do
                -- ... collision checking
            end
        end
    end
end

Efficient Collision Detection

lua
-- Spatial partitioning for many entities
local GRID_SIZE = 64
local collision_grid = {}

function clear_collision_grid()
    collision_grid = {}
end

function add_to_grid(entity_id, x, y, tag)
    local grid_x = math.floor(x / GRID_SIZE)
    local grid_y = math.floor(y / GRID_SIZE)
    local key = grid_x .. "," .. grid_y
    
    if not collision_grid[key] then
        collision_grid[key] = {}
    end
    
    table.insert(collision_grid[key], {
        id = entity_id,
        tag = tag,
        x = x,
        y = y
    })
end

function get_nearby_entities(x, y, radius, target_tag)
    local results = {}
    local grid_radius = math.ceil(radius / GRID_SIZE)
    local center_gx = math.floor(x / GRID_SIZE)
    local center_gy = math.floor(y / GRID_SIZE)
    
    for gx = center_gx - grid_radius, center_gx + grid_radius do
        for gy = center_gy - grid_radius, center_gy + grid_radius do
            local key = gx .. "," .. gy
            if collision_grid[key] then
                for i = 1, #collision_grid[key] do
                    local entity = collision_grid[key][i]
                    if entity.tag == target_tag then
                        local dx = x - entity.x
                        local dy = y - entity.y
                        local distance = math.sqrt(dx * dx + dy * dy)
                        if distance <= radius then
                            table.insert(results, entity.id)
                        end
                    end
                end
            end
        end
    end
    
    return results
end

-- Usage
function on_tick(dt)
    clear_collision_grid()
    
    -- Populate grid
    local bullets = vibe.find("bullet")
    for i = 1, #bullets do
        local bullet = bullets[i]
        local x, y = vibe.get(bullet, "x"), vibe.get(bullet, "y")
        add_to_grid(bullet, x, y, "bullet")
    end
    
    local enemies = vibe.find("enemy")
    for i = 1, #enemies do
        local enemy = enemies[i]
        local x, y = vibe.get(enemy, "x"), vibe.get(enemy, "y")
        add_to_grid(enemy, x, y, "enemy")
        
        -- Check for nearby bullets
        local nearby_bullets = get_nearby_entities(x, y, 32, "bullet")
        for j = 1, #nearby_bullets do
            handle_collision(enemy, nearby_bullets[j])
        end
    end
end

Memory Management

Clean Up Data References

lua
local entity_data = {}
local entity_timers = {}

function spawn_enemy()
    local id = vibe.spawn("sprite", {
        texture = "res://assets/enemy.png",
        x = 100, y = 100, tag = "enemy"
    })
    
    entity_data[id] = {
        health = 3,
        speed = 100,
        state = "patrol"
    }
    entity_timers[id] = 0
    
    return id
end

function destroy_enemy(id)
    vibe.destroy(id)
    
    -- ✅ Clean up all references
    entity_data[id] = nil
    entity_timers[id] = nil
end

Object Pooling

lua
-- Pool for frequently created/destroyed objects
local bullet_pool = {}
local MAX_POOL_SIZE = 100

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

function return_to_pool(bullet_id)
    if #bullet_pool < MAX_POOL_SIZE then
        -- Hide 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

function shoot_bullet(x, y, dx, dy)
    local bullet = get_pooled_bullet()
    vibe.set(bullet, "x", x)
    vibe.set(bullet, "y", y)
    
    -- Store velocity data
    bullet_velocities[bullet] = {dx = dx, dy = dy}
    
    return bullet
end

Frame Budget Management

Performance Monitoring

lua
local performance_stats = {
    frame_count = 0,
    budget_warnings = 0,
    max_frame_time = 0,
    total_frame_time = 0
}

function on_tick(dt)
    local frame_start = vibe.time_ms()
    performance_stats.frame_count = performance_stats.frame_count + 1
    
    -- Your game logic here
    update_game_logic(dt)
    
    local frame_time = vibe.time_ms() - frame_start
    performance_stats.total_frame_time = performance_stats.total_frame_time + frame_time
    performance_stats.max_frame_time = math.max(performance_stats.max_frame_time, frame_time)
    
    if frame_time > 4 then  -- Budget warning threshold
        performance_stats.budget_warnings = performance_stats.budget_warnings + 1
        print("Frame time warning:", frame_time .. "ms")
    end
    
    -- Performance report every 5 seconds
    if performance_stats.frame_count % 300 == 0 then
        report_performance()
    end
end

function report_performance()
    local avg_frame_time = performance_stats.total_frame_time / performance_stats.frame_count
    print("Performance Report:")
    print("  Average frame time:", string.format("%.2f", avg_frame_time) .. "ms")
    print("  Max frame time:", performance_stats.max_frame_time .. "ms")
    print("  Budget warnings:", performance_stats.budget_warnings)
    print("  Entities:", count_all_entities())
end

function count_all_entities()
    local total = 0
    local tags = {"player", "enemy", "bullet", "coin", "ui"}
    for i = 1, #tags do
        total = total + #vibe.find(tags[i])
    end
    return total
end

Workload Distribution

lua
-- Spread expensive work across multiple frames
local update_queue = {}
local MAX_UPDATES_PER_FRAME = 10

function add_to_update_queue(entity_id, update_type)
    table.insert(update_queue, {
        id = entity_id,
        type = update_type,
        added_time = vibe.time_ms()
    })
end

function process_update_queue()
    local processed = 0
    
    while #update_queue > 0 and processed < MAX_UPDATES_PER_FRAME do
        local item = table.remove(update_queue, 1)
        
        -- Skip if entity no longer exists or item is too old
        if vibe.get(item.id, "x") ~= nil and 
           vibe.time_ms() - item.added_time < 5000 then
            
            if item.type == "ai_update" then
                update_enemy_ai(item.id)
            elseif item.type == "pathfinding" then
                calculate_path(item.id)
            end
            
            processed = processed + 1
        end
    end
end

function on_tick(dt)
    -- Quick updates first
    update_movement(dt)
    check_collisions()
    
    -- Then process expensive updates
    process_update_queue()
end

🎨 Advanced Design Patterns

Component-Entity System

lua
-- Simple ECS implementation
local components = {
    position = {},
    velocity = {},
    health = {},
    ai = {},
    sprite = {}
}

function add_component(entity_id, component_type, data)
    components[component_type][entity_id] = data
end

function get_component(entity_id, component_type)
    return components[component_type][entity_id]
end

function has_component(entity_id, component_type)
    return components[component_type][entity_id] ~= nil
end

function remove_component(entity_id, component_type)
    components[component_type][entity_id] = nil
end

-- System functions operate on components
function movement_system(dt)
    for entity_id, velocity in pairs(components.velocity) do
        local pos = components.position[entity_id]
        if pos then
            pos.x = pos.x + velocity.x * dt
            pos.y = pos.y + velocity.y * dt
            
            -- Update actual entity position
            vibe.set(entity_id, "x", pos.x)
            vibe.set(entity_id, "y", pos.y)
        end
    end
end

function health_system()
    for entity_id, health in pairs(components.health) do
        if health.current <= 0 then
            destroy_entity(entity_id)
        end
    end
end

function destroy_entity(entity_id)
    vibe.destroy(entity_id)
    
    -- Clean up all components
    for component_type, _ in pairs(components) do
        components[component_type][entity_id] = nil
    end
end

-- Usage
function spawn_enemy()
    local id = vibe.spawn("sprite", {
        texture = "res://assets/enemy.png",
        x = 100, y = 100, tag = "enemy"
    })
    
    add_component(id, "position", {x = 100, y = 100})
    add_component(id, "velocity", {x = 50, y = 0})
    add_component(id, "health", {current = 3, max = 3})
    add_component(id, "ai", {state = "patrol", timer = 0})
    
    return id
end

State Machine System

lua
local state_machines = {}

function create_state_machine(entity_id, initial_state, states)
    state_machines[entity_id] = {
        current_state = initial_state,
        states = states,
        state_timer = 0,
        data = {}  -- Persistent data across states
    }
end

function update_state_machine(entity_id, dt)
    local sm = state_machines[entity_id]
    if not sm then return end
    
    sm.state_timer = sm.state_timer + dt
    local current_state = sm.states[sm.current_state]
    
    if current_state and current_state.update then
        local new_state = current_state.update(entity_id, dt, sm.state_timer, sm.data)
        
        if new_state and new_state ~= sm.current_state then
            -- State transition
            if current_state.exit then
                current_state.exit(entity_id, sm.data)
            end
            
            sm.current_state = new_state
            sm.state_timer = 0
            
            local next_state = sm.states[new_state]
            if next_state and next_state.enter then
                next_state.enter(entity_id, sm.data)
            end
        end
    end
end

-- Example: Enemy AI state machine
function create_enemy_ai(enemy_id)
    create_state_machine(enemy_id, "patrol", {
        patrol = {
            enter = function(id, data)
                data.direction = math.random() > 0.5 and 1 or -1
                data.speed = 50
            end,
            update = function(id, dt, timer, data)
                -- Move back and forth
                local x = vibe.get(id, "x")
                vibe.set(id, "x", x + data.direction * data.speed * dt)
                
                -- Change direction every 3 seconds
                if timer > 3 then
                    data.direction = -data.direction
                    return "patrol"  -- Reset timer
                end
                
                -- Check if player is nearby
                local players = vibe.find("player")
                if #players > 0 then
                    local px = vibe.get(players[1], "x")
                    if math.abs(x - px) < 100 then
                        return "chase"
                    end
                end
            end
        },
        
        chase = {
            enter = function(id, data)
                data.speed = 100  -- Faster when chasing
            end,
            update = function(id, dt, timer, data)
                local players = vibe.find("player")
                if #players > 0 then
                    local x = vibe.get(id, "x")
                    local px = vibe.get(players[1], "x")
                    
                    -- Chase player
                    if px > x then
                        vibe.set(id, "x", x + data.speed * dt)
                    else
                        vibe.set(id, "x", x - data.speed * dt)
                    end
                    
                    -- Stop chasing if too far
                    if math.abs(x - px) > 200 then
                        return "patrol"
                    end
                else
                    return "patrol"
                end
            end
        }
    })
end

Event System

lua
local event_listeners = {}

function add_event_listener(event_name, callback)
    if not event_listeners[event_name] then
        event_listeners[event_name] = {}
    end
    table.insert(event_listeners[event_name], callback)
end

function remove_event_listener(event_name, callback)
    if event_listeners[event_name] then
        for i = #event_listeners[event_name], 1, -1 do
            if event_listeners[event_name][i] == callback then
                table.remove(event_listeners[event_name], i)
                break
            end
        end
    end
end

function emit_event(event_name, ...)
    if event_listeners[event_name] then
        for i = 1, #event_listeners[event_name] do
            event_listeners[event_name][i](...)
        end
    end
end

-- Usage
add_event_listener("enemy_death", function(enemy_id, killer_id)
    -- Award points
    score = score + 100
    
    -- Spawn explosion
    local ex = vibe.get(enemy_id, "x")
    local ey = vibe.get(enemy_id, "y")
    spawn_explosion(ex, ey)
    
    -- Play sound
    vibe.play_sfx("res://assets/explosion.wav")
    
    -- Check win condition
    if #vibe.find("enemy") <= 1 then  -- -1 because this enemy isn't destroyed yet
        emit_event("level_complete")
    end
end)

add_event_listener("level_complete", function()
    spawn_exit_portal()
    vibe.play_sfx("res://assets/victory.wav")
end)

function kill_enemy(enemy_id, killer_id)
    emit_event("enemy_death", enemy_id, killer_id)
    vibe.destroy(enemy_id)
end

🧠 Lua Environment Deep Dive

Available Standard Library

Safe Math Functions

lua
-- All math functions available
local angle = math.atan2(dy, dx)
local distance = math.sqrt(dx * dx + dy * dy)
local random_float = math.random()
local random_int = math.random(1, 10)
local rounded = math.floor(value + 0.5)
local sine_wave = math.sin(vibe.time_ms() * 0.001)

-- Seed random number generator for consistent randomness
math.randomseed(12345)

String Manipulation

lua
-- String operations
local formatted = string.format("Score: %d, Time: %.2f", score, elapsed_time)
local parts = {}
for word in string.gmatch("hello world lua", "%w+") do
    table.insert(parts, word)
end

local upper = string.upper("hello")
local length = string.len("hello")
local substring = string.sub("hello", 2, 4)  -- "ell"

Table Operations

lua
-- Array operations
local enemies = {1, 2, 3, 4, 5}
table.insert(enemies, 6)         -- Add to end
table.insert(enemies, 1, 0)      -- Insert at position 1
local removed = table.remove(enemies, 2)  -- Remove from position 2
table.sort(enemies)              -- Sort in place

-- Custom sorting
local entities = {{id = 3, health = 5}, {id = 1, health = 3}}
table.sort(entities, function(a, b)
    return a.health > b.health  -- Sort by health descending
end)

Memory-Efficient Patterns

Table Reuse

lua
-- ❌ Creates garbage every frame
function update_bullets(dt)
    for i = 1, #bullets do
        local velocity = {x = 100, y = 0}  -- New table every iteration!
        -- ... use velocity
    end
end

-- ✅ Reuse tables
local temp_velocity = {x = 0, y = 0}  -- Reusable table

function update_bullets(dt)
    for i = 1, #bullets do
        temp_velocity.x = 100
        temp_velocity.y = 0
        -- ... use temp_velocity
    end
end

Efficient Data Structures

lua
-- For large collections of simple data, use arrays instead of hash tables
-- ❌ Hash table (slower iteration)
local entity_positions = {
    [123] = {x = 100, y = 200},
    [456] = {x = 150, y = 250}
}

-- ✅ Parallel arrays (faster iteration)
local entity_ids = {123, 456}
local entity_x = {100, 150}
local entity_y = {200, 250}

for i = 1, #entity_ids do
    local id = entity_ids[i]
    local x = entity_x[i]
    local y = entity_y[i]
    -- Process entity
end

Advanced Lua Techniques

Coroutines for Sequences

lua
-- Create timed sequences without blocking
function create_enemy_wave()
    return coroutine.create(function()
        -- Spawn 3 enemies with 1 second delay between each
        for i = 1, 3 do
            spawn_enemy()
            
            -- Wait for 1 second
            local start_time = vibe.time_ms()
            repeat
                coroutine.yield()
            until vibe.time_ms() - start_time >= 1000
        end
        
        print("Wave complete!")
    end)
end

local wave_coroutine = nil

function on_tick(dt)
    -- Start wave if needed
    if not wave_coroutine and should_start_wave() then
        wave_coroutine = create_enemy_wave()
    end
    
    -- Continue wave sequence
    if wave_coroutine and coroutine.status(wave_coroutine) ~= "dead" then
        coroutine.resume(wave_coroutine)
    end
end

Closures for Behavior

lua
-- Create specialized behavior functions
function create_sine_mover(amplitude, frequency)
    local start_time = vibe.time_ms()
    local start_y = nil
    
    return function(entity_id, dt)
        if not start_y then
            start_y = vibe.get(entity_id, "y")
        end
        
        local elapsed = (vibe.time_ms() - start_time) * 0.001
        local offset = math.sin(elapsed * frequency) * amplitude
        vibe.set(entity_id, "y", start_y + offset)
    end
end

-- Usage
local enemy_behaviors = {}

function spawn_floating_enemy()
    local id = vibe.spawn("sprite", {
        texture = "res://assets/enemy.png",
        x = 100, y = 100, tag = "enemy"
    })
    
    enemy_behaviors[id] = create_sine_mover(50, 2.0)  -- 50px amplitude, 2 Hz
    return id
end

function update_enemies(dt)
    local enemies = vibe.find("enemy")
    for i = 1, #enemies do
        local enemy = enemies[i]
        if enemy_behaviors[enemy] then
            enemy_behaviors[enemy](enemy, dt)
        end
    end
end

🔧 Configuration and Customization

Resolution Independence

lua
local CONFIG = {
    reference_width = 800,
    reference_height = 600,
    scale_mode = "fit"  -- "fit", "fill", or "stretch"
}

function get_scale_factors()
    local w, h = vibe.width(), vibe.height()
    local scale_x = w / CONFIG.reference_width
    local scale_y = h / CONFIG.reference_height
    
    if CONFIG.scale_mode == "fit" then
        local scale = math.min(scale_x, scale_y)
        return scale, scale
    elseif CONFIG.scale_mode == "fill" then
        local scale = math.max(scale_x, scale_y)
        return scale, scale
    else  -- stretch
        return scale_x, scale_y
    end
end

function scale_position(ref_x, ref_y)
    local scale_x, scale_y = get_scale_factors()
    return ref_x * scale_x, ref_y * scale_y
end

function scale_size(ref_size)
    local scale_x, scale_y = get_scale_factors()
    return ref_size * math.min(scale_x, scale_y)
end

-- Usage
function spawn_ui_element()
    local x, y = scale_position(100, 50)
    local size = scale_size(32)
    
    vibe.spawn("sprite", {
        texture = "res://assets/button.png",
        x = x, y = y,
        cell_size = size,
        tag = "ui"
    })
end

Game Configuration System

lua
local config = {
    gameplay = {
        player_speed = 200,
        enemy_speed = 100,
        bullet_speed = 300,
        spawn_rate = 1.0
    },
    audio = {
        sfx_volume = 1.0,
        music_volume = 0.8
    },
    graphics = {
        particle_count = 100,
        screen_shake = true
    }
}

function get_config(section, key, default)
    if config[section] and config[section][key] ~= nil then
        return config[section][key]
    end
    return default
end

function set_config(section, key, value)
    if not config[section] then
        config[section] = {}
    end
    config[section][key] = value
end

-- Usage throughout your game
function update_player(dt)
    local speed = get_config("gameplay", "player_speed", 200)
    -- ... movement logic
end

function spawn_enemy()
    local speed = get_config("gameplay", "enemy_speed", 100)
    -- ... spawning logic
end

🐛 Advanced Debugging

Debug Overlay System

lua
local debug_info = {
    enabled = false,
    show_fps = true,
    show_entity_count = true,
    show_positions = false
}

function toggle_debug()
    debug_info.enabled = not debug_info.enabled
    update_debug_display()
end

function update_debug_display()
    -- Remove old debug UI
    local debug_labels = vibe.find("debug_ui")
    for i = 1, #debug_labels do
        vibe.destroy(debug_labels[i])
    end
    
    if not debug_info.enabled then return end
    
    local y_offset = 10
    
    if debug_info.show_fps then
        local fps = calculate_fps()
        vibe.spawn("label", {
            text = "FPS: " .. fps,
            x = 10, y = y_offset,
            tag = "debug_ui"
        })
        y_offset = y_offset + 20
    end
    
    if debug_info.show_entity_count then
        local count = count_all_entities()
        vibe.spawn("label", {
            text = "Entities: " .. count,
            x = 10, y = y_offset,
            tag = "debug_ui"
        })
        y_offset = y_offset + 20
    end
end

function on_tick(dt)
    -- Your game logic
    update_game(dt)
    
    -- Debug input
    if vibe.key("space") and vibe.key("up") then  -- Space + Up
        toggle_debug()
    end
    
    -- Update debug display
    if debug_info.enabled then
        update_debug_display()
    end
end

Performance Profiler

lua
local profiler = {
    enabled = false,
    timers = {},
    results = {}
}

function profile_start(name)
    if profiler.enabled then
        profiler.timers[name] = vibe.time_ms()
    end
end

function profile_end(name)
    if profiler.enabled and profiler.timers[name] then
        local duration = vibe.time_ms() - profiler.timers[name]
        
        if not profiler.results[name] then
            profiler.results[name] = {total = 0, count = 0, max = 0}
        end
        
        local result = profiler.results[name]
        result.total = result.total + duration
        result.count = result.count + 1
        result.max = math.max(result.max, duration)
        
        profiler.timers[name] = nil
    end
end

function print_profile_results()
    print("=== PROFILER RESULTS ===")
    for name, result in pairs(profiler.results) do
        local avg = result.total / result.count
        print(string.format("%s: avg=%.2fms, max=%.2fms, calls=%d", 
              name, avg, result.max, result.count))
    end
    print("========================")
end

-- Usage
function on_tick(dt)
    profile_start("player_update")
    update_player(dt)
    profile_end("player_update")
    
    profile_start("enemy_update")
    update_enemies(dt)
    profile_end("enemy_update")
    
    profile_start("collision_check")
    check_collisions()
    profile_end("collision_check")
    
    -- Print results every 5 seconds
    if profiler.enabled and vibe.time_ms() % 5000 < 16 then
        print_profile_results()
        profiler.results = {}  -- Reset
    end
end

This advanced guide covers optimization techniques, design patterns, and professional development practices. Combined with the other documentation sections, you now have comprehensive knowledge of the Vibe API and how to build efficient, well-structured games!

For specific implementation help, refer back to the Complete API Reference and Examples and Tutorials.

Built with VitePress for the Vibe API Documentation