Skip to content

C++

Lua is for the game; C++ is for the engine and for anything that needs to be fast or to talk to the engine directly. A KKE game in C++ is a list of modules, and this page shows how to write one, give Lua a way to use it, and test it.

Your own module

A module is a class with a name, the modules it depends on, and the lifecycle methods it needs. This is the whole of the cookbook's CookbookPlayer header, minus its private members:

class CookbookPlayer : public kke::Module {
public:
    const char* name() const override { return "CookbookPlayer"; }
    std::vector<kke::ModuleDependency> dependencies() const override;
    void init(kke::Application& app) override;             // once, in dependency order
    void update(const kke::UpdateContext& ctx) override;   // every frame (ctx.dt)
    void render(const kke::RenderContext& ctx) override;   // draw
    void renderShadow(const kke::ShadowRenderContext& ctx) override;
    void onEvent(const SDL_Event& event) override;         // raw input, window events
};

Dependencies are how a module finds the others, and they decide the order of init:

std::vector<kke::ModuleDependency> CookbookPlayer::dependencies() const {
    return { { std::type_index(typeid(kke::RigidBodyModule)), true, "the character controller and collision" },
             { std::type_index(typeid(kke::InputModule)), true, "move, look and camera actions" },
             { std::type_index(typeid(kke::ScriptModule)), false, "the view.* and player.* Lua bindings" } };
}

true is required (the game refuses to start without it, with that reason in the log); false is optional, and app.getModule<T>() returns null when it's missing. Then main.cpp lists the modules:

kke::Application app("KKE Cookbook", 1280, 720);
app.addModule<kke::SettingsModule>("settings.json");
app.addModule<kke::InputModule>("input.json");
app.addModule<kke::RigidBodyModule>();
app.addModule<kke::ModelModule>();
app.addModule<kke::UiModule>();
app.addModule<kke::ScriptModule>("scripts");
app.addModule<cookbook::CookbookPlayer>();
app.run();

Other lifecycle methods, when you need them: fixedUpdate (60 Hz, for physics-rate logic), frameStart/frameEnd, compute (GPU work before rendering), renderUi (a Dear ImGui panel in developer builds), shutdown. A module that throws in any of them is disabled and logged, and the rest of the game goes on. engine/include/kke/Module.h has them all; games/template/ is the smallest complete game, and tools/new_game NAME copies it for you (Make your own game).

Your own Lua bindings

A binding is a table name, a function name and a C++ function. It reads arguments from the Lua stack, pushes results, and returns how many. The cookbook's view table, which the cameras page uses from Lua, is all of these:

games/cookbook/CookbookPlayer.cpp
// Lua bindings: a table name, a function name and a C++ lambda. Arguments
// come off the Lua stack; push the results and return how many.
void CookbookPlayer::registerLua() {
    auto* scripts = m_app->getModule<kke::ScriptModule>();
    if (!scripts) return;
    kke::ScriptVM& vm = scripts->vm();
    // view.mode("topdown") switches; view.mode() says which one is on.
    vm.registerFunction("view", "mode", [this](lua_State* L) {
        if (lua_isstring(L, 1)) {
            View v = View::Third;
            if (!viewFromName(lua_tostring(L, 1), v))
                return luaL_error(L, "view.mode: no camera '%s' (first, third, orbit, topdown, iso, side, fixed, cinematic)", lua_tostring(L, 1));
            setView(v);
        }
        lua_pushstring(L, viewName(m_view));
        return 1;
    });
    vm.registerFunction("view", "shake", [this](lua_State* L) {
        shake(static_cast<float>(luaL_optnumber(L, 1, 0.5)));
        return 0;
    });
    // view.path({ {pos = Vec(..), target = Vec(..), time = 0}, ... }, loop)
    vm.registerFunction("view", "path", [this](lua_State* L) {
        luaL_checktype(L, 1, LUA_TTABLE);
        std::vector<kke::CameraRig::Keyframe> keys;
        const lua_Integer n = luaL_len(L, 1);
        for (lua_Integer i = 1; i <= n; ++i) {
            lua_geti(L, 1, i);
            const int k = lua_gettop(L);
            keys.push_back({ kke::ScriptVM::fieldVec3(L, k, "pos", glm::vec3(0.0f)), kke::ScriptVM::fieldVec3(L, k, "target", glm::vec3(0.0f)),
                             kke::ScriptVM::fieldNumber(L, k, "time", static_cast<float>(i - 1)) });
            lua_pop(L, 1);
        }
        if (keys.size() < 2) return luaL_error(L, "view.path: needs at least two keyframes");
        m_rig.setCinematic(std::move(keys), lua_toboolean(L, 2) != 0);
        setView(View::Cinematic);
        return 0;
    });
    // The same player.* as the starter template, so recipes run in both.
    vm.registerFunction("player", "position", [this](lua_State* L) {
        kke::ScriptVM::pushVec3(L, m_rigid->world().characterPosition(m_player));
        return 1;
    });
    vm.registerFunction("player", "teleport", [this](lua_State* L) {
        m_loco->teleport(kke::ScriptVM::toVec3(L, 1, spawn));
        return 0;
    });
    vm.registerFunction("player", "facing", [this](lua_State* L) {
        kke::ScriptVM::pushVec3(L, m_loco->facing());
        return 1;
    });
}
  • luaL_checkstring(L, 1), luaL_optnumber(L, 1, 0.5), lua_toboolean: Lua's own API reads arguments by position. kke::ScriptVM::toVec3(L, i) reads a Vec, fieldVec3(L, table, "pos", fallback) a named field of a table argument, and pushVec3 returns one.
  • luaL_error(L, ...) reports a mistake with the script's file and line, and stops that call, not the game.
  • Anything registered with a kke::ApiFunction (label, doc, typed parameters) instead of a bare name also becomes a block in the node graph and a row in the Lua API reference.

kke::ScriptVM runs scripts on their own coroutines with an instruction budget and a memory cap, so a script can't hang or exhaust the game; Scripting in Lua has the limits.

Testing your code

The engine's tests are GoogleTest, in tests/, and run as kke_tests. Pure logic (which is most game logic once it's separated from drawing) tests in a few lines; the cookbook's tests are in tests/test_cookbook.cpp:

TEST(Cookbook, TurnTowardsStopsAtTheLimit) {
    const glm::vec3 ahead(0, 0, 1), side(1, 0, 0);
    const glm::quat limited = cookbook::turnTowards(ahead, side, 30.0f);
    EXPECT_NEAR(glm::degrees(std::acos(glm::dot(limited * ahead, ahead))), 30.0f, 0.01f);
}

Input, physics and Lua all test without a window: kke::InputMap reads from any kke::InputState (a fake keyboard in the test), a kke::RigidWorld steps by itself, and a kke::ScriptVM runs Lua with whatever bindings you give it. The input and physics pages show one of each.

Run them with ./build/bin/kke_tests, or one suite: ./build/bin/kke_tests --gtest_filter='Cookbook*'.

Going further

  • Drawing: kke::DynamicMeshRenderer draws a mesh you build from vertices (the cookbook's player body); kke::ModelModule loads FBX and OBJ and draws instances with skinning.
  • Warnings are errors: the engine builds with -DKKE_WARNINGS_AS_ERRORS=ON in CI, and a Release build with any warning at all fails. Fix them as they come.
  • Reading the engine: every engine/include/kke/*.h starts with a comment saying what it's for and why it's built that way, and AI_GUIDE.md is the map of the codebase.
  • Where the design lives: the guides are one page per system; the roadmap says what's solid and what isn't yet.