📜 Scripting
Fase: 6 — O Olimpo
Namespace: Caffeine::Script
Status: 📅 Planejado (TBD)
RFs: RF6.6 (scripting hot-reload), RF4.7 (bindings iniciais)
Visão Geral
O módulo de Scripting expõe a Caffeine Engine a uma linguagem de script para que designers e scribes possam programar comportamentos de jogo sem escrever C++. O sistema é opt-in — jogos puramente em C++ não têm overhead de scripting.
Estado: A linguagem de script é TBD (To Be Decided). As duas candidatas são:
| Critério |
Lua 5.4 |
AngelScript 2.x |
| Familiaridade na indústria |
✅ Alta (LÖVE, Roblox, WoW) |
🟡 Média |
| Performance |
✅ Muito boa (LuaJIT disponível) |
✅ Boa (compilado) |
| Tipagem |
❌ Dinâmica (erros em runtime) |
✅ Estática (erros na compilação) |
| Integração C++ |
🟡 Via sol2/toLua |
✅ Nativa, API C++ direta |
| Tamanho do runtime |
✅ ~300KB |
🟡 ~600KB |
| Hot-reload |
✅ reload do chunk |
🟡 recompilação necessária |
| Recomendação |
✅ Preferida |
Alternativa |
Decisão preliminar: Lua + sol2 para binding automático C++→Lua. A decisão final será tomada na Fase 4 (RF4.7).
API Planejada
namespace Caffeine::Script {
// ============================================================================
// @brief Componente ECS que associa um script Lua a uma entidade.
// ============================================================================
struct ScriptComponent {
Assets::AssetHandle<ScriptAsset> script; // handle para o ficheiro .lua
// Funções padrão do ciclo de vida (chamadas pelo ScriptSystem):
// - onCreate(entity) chamado uma vez após criação
// - onUpdate(entity, dt) chamado todo o frame
// - onDestroy(entity) chamado antes de destruição
// - onCollision(entity, other) chamado ao colidir (requer Physics2D)
};
// ============================================================================
// @brief Asset que representa um script Lua compilado.
// ============================================================================
struct ScriptAsset {
std::string sourcePath; // caminho do .lua no disco
std::string source; // código-fonte carregado
// Bytecode Lua compilado (se LuaJIT for usado):
std::vector<u8> bytecode;
};
// ============================================================================
// @brief VM Lua principal. Um único estado Lua por aplicação.
//
// Expõe ao Lua:
// - Entity: criação, destruição, get/set de componentes
// - Transform: posição, rotação, escala
// - Input: isKeyDown, getAxis
// - Events: on(), emit()
// - Debug: log(), draw()
//
// Threading: a VM Lua é single-threaded. Scripts rodam no thread principal.
// ============================================================================
class LuaVM {
public:
bool init();
void shutdown();
// Executa um ficheiro .lua e regista as suas funções globais
bool loadScript(std::string_view path);
// Hot-reload: recarrega um ficheiro .lua em disco sem reiniciar a VM
bool reloadScript(std::string_view path);
// Chama uma função Lua por nome com argumentos arbitrários
template<typename... Args>
bool call(std::string_view function, Args&&... args);
// Acesso ao estado sol2 (para registar bindings customizados)
sol::state& state() { return m_lua; }
private:
sol::state m_lua;
void registerCaffeineBindings(); // regista toda a API da engine
void registerEntityBindings();
void registerTransformBindings();
void registerInputBindings();
void registerEventBindings();
void registerDebugBindings();
void registerMathBindings();
};
// ============================================================================
// @brief Sistema ECS que executa scripts em entidades com ScriptComponent.
//
// Por frame:
// 1. Itera todas as entidades com ScriptComponent
// 2. Para cada entidade, chama script.onUpdate(entity, dt)
// 3. Detecta erros Lua e regista via LogSystem (sem crash)
// ============================================================================
class ScriptSystem : public ECS::ISystem {
public:
explicit ScriptSystem(LuaVM* vm);
void onInit(ECS::World& world) override;
void onUpdate(ECS::World& world, f32 dt) override;
void onShutdown(ECS::World& world) override;
// Chama onCreate() em todas as entidades com ScriptComponent recém-criadas
void initNewScripts(ECS::World& world);
private:
LuaVM* m_vm;
void callLifecycle(std::string_view fn, ECS::Entity entity, f32 dt = 0.0f);
void handleLuaError(std::string_view fn, ECS::Entity entity,
const sol::error& err);
};
// ============================================================================
// @brief Watcher de ficheiros de script para hot-reload.
//
// Em modo editor (CF_EDITOR_MODE), monitoriza a pasta /scripts/.
// Quando um .lua é modificado, recarrega via LuaVM::reloadScript().
// ============================================================================
class ScriptWatcher {
public:
void init(LuaVM* vm, std::string_view scriptsDir);
void shutdown();
// Chame no loop principal para verificar mudanças
void poll();
private:
LuaVM* m_vm = nullptr;
std::string m_dir;
HashMap<std::string, std::filesystem::file_time_type> m_mtimes;
};
} // namespace Caffeine::Script
Bindings Lua — API Exposta
Entity & Components
-- Criar e destruir entidades:
local entity = caffeine.world.create()
caffeine.world.destroy(entity)
-- Adicionar/remover componentes:
caffeine.world.addTransform(entity, { x=100, y=200, rotation=0, scaleX=1, scaleY=1 })
caffeine.world.addSprite(entity, { texture="hero.caf", r=1, g=1, b=1, a=1 })
-- Obter/modificar componentes:
local t = caffeine.world.getTransform(entity)
t.x = t.x + 10
caffeine.world.setTransform(entity, t)
Input
-- Teclado:
if caffeine.input.isKeyDown("Space") then
jump()
end
-- Eixos analógicos (gamepad ou teclado mapeado):
local horizontal = caffeine.input.getAxis("Horizontal") -- -1 a 1
local vertical = caffeine.input.getAxis("Vertical")
-- Mouse:
local mx, my = caffeine.input.mousePosition()
if caffeine.input.isMouseButtonDown(1) then -- botão esquerdo
shoot(mx, my)
end
Eventos
-- Subscrever evento:
local handle = caffeine.events.on("OnCollision2D", function(entityA, entityB)
if entityA == hero then
takeDamage(10)
end
end)
-- Emitir evento customizado:
caffeine.events.emit("OnPlayerDied", { score = 1500 })
-- Cancelar subscrição:
caffeine.events.off(handle)
Debug
-- Log:
caffeine.debug.log("Hero position: " .. t.x .. ", " .. t.y)
caffeine.debug.warn("Low HP!")
caffeine.debug.error("Critical failure!")
-- Debug draw:
caffeine.debug.drawCircle(t.x, t.y, 32, 1, 0, 0, 1) -- x, y, radius, r, g, b, a
caffeine.debug.drawRect(0, 0, 100, 100, 0, 1, 0, 1)
Math
local v1 = caffeine.math.vec2(10, 20)
local v2 = caffeine.math.vec2(5, 15)
local sum = caffeine.math.add(v1, v2) -- { x=15, y=35 }
local len = caffeine.math.length(v1) -- 22.36...
local dir = caffeine.math.normalize(v1)
local dot = caffeine.math.dot(v1, v2)
Exemplo Completo: Hero Script
-- scripts/hero.lua
-- Script de comportamento do herói
local SPEED = 200.0
local JUMP_V = -400.0
local isGrounded = false
function onCreate(entity)
caffeine.debug.log("Hero created: " .. entity)
end
function onUpdate(entity, dt)
local t = caffeine.world.getTransform(entity)
local rb = caffeine.world.getRigidBody2D(entity)
-- Movimento horizontal
local h = caffeine.input.getAxis("Horizontal")
rb.velocityX = h * SPEED
-- Pulo
if caffeine.input.isKeyDown("Space") and isGrounded then
rb.velocityY = JUMP_V
isGrounded = false
end
-- Flip do sprite
local sprite = caffeine.world.getSprite(entity)
if h < 0 then sprite.flipX = true
elseif h > 0 then sprite.flipX = false
end
caffeine.world.setSprite(entity, sprite)
caffeine.world.setRigidBody2D(entity, rb)
end
function onCollision(entity, other)
-- Assume que o chão tem o componente "Ground"
if caffeine.world.hasComponent(other, "Ground") then
isGrounded = true
end
end
function onDestroy(entity)
caffeine.debug.log("Hero destroyed")
end
Hot-Reload de Scripts
[Editor Mode ativo: CF_EDITOR_MODE=1]
│
▼
ScriptWatcher::poll() (chamado todo frame)
│
Detecta mudança em scripts/hero.lua
│
▼
LuaVM::reloadScript("scripts/hero.lua")
│
Recarrega o chunk Lua
(globals são substituídos na VM)
│
▼
Da próxima invocação de onUpdate():
usa o código novo — sem restart
Segurança e Sandboxing
Por defeito, scripts Lua têm acesso restrito:
| Permitido |
Bloqueado |
Todas as APIs caffeine.* |
os.* (sistema de ficheiros/processos) |
math.*, string.*, table.* |
io.* (I/O directo) |
require (apenas módulos do jogo) |
load, loadstring (execução dinâmica) |
pcall, xpcall (error handling) |
debug.* (acesso a internals da VM) |
Em modo editor (CF_EDITOR_MODE), as restrições de io.* podem ser relaxadas para permitir salvar dados de save state.
Critério de Aceitação
Dependências
- Upstream:
- Downstream:
- — (módulo de utilizador final)
Referências
📜 Scripting
Visão Geral
O módulo de Scripting expõe a Caffeine Engine a uma linguagem de script para que designers e scribes possam programar comportamentos de jogo sem escrever C++. O sistema é opt-in — jogos puramente em C++ não têm overhead de scripting.
Estado: A linguagem de script é TBD (To Be Decided). As duas candidatas são:
API Planejada
Bindings Lua — API Exposta
Entity & Components
Input
Eventos
Debug
Math
Exemplo Completo: Hero Script
Hot-Reload de Scripts
Segurança e Sandboxing
Por defeito, scripts Lua têm acesso restrito:
caffeine.*os.*(sistema de ficheiros/processos)math.*,string.*,table.*io.*(I/O directo)require(apenas módulos do jogo)load,loadstring(execução dinâmica)pcall,xpcall(error handling)debug.*(acesso a internals da VM)Em modo editor (
CF_EDITOR_MODE), as restrições deio.*podem ser relaxadas para permitir salvar dados de save state.Critério de Aceitação
.luasimplesonUpdate(entity, dt)é chamado todo frame para todas as entidades com ScriptComponent.luaem disco recarrega o script sem reiniciar a aplicaçãoos.execute()retorna erro em scripts de jogoDependências
World,Entity,ISystem,ComponentIDInputManagerEventBusLogSystem,DebugDraw.luaReferências
docs/fase4/ecs.md— ECS World e sistemasdocs/fase4/events.md— EventBusdocs/fase2/input.md— InputManagerdocs/MASTER.md— Documentação unificada