Skip to content

Input

Games in KKE never ask "is W down?". They ask about actions: "was jump pressed?", "how much move?". Each action has default keys and buttons, and players can rebind all of them in the settings, controllers and flight sticks included. The whole system is described in Input; these recipes are the parts you use every day.

Your own action: hold to charge, release to throw

input.define(id, label, key) makes an action with a default key. Hold T to charge, let go to throw a ball where the camera looks: harder (and redder) the longer you held it.

throw.lua
-- throw.lua: your own controls. Hold T to charge, let go to throw; the
-- longer you hold, the harder it flies. Players can rebind T in the
-- settings like any other action. (docs/cookbook/input.md)

-- define(id, label, default key): once per action. The label is what
-- the rebinding screen shows.
input.define("throw", "Throw a ball (hold to charge)", "T")

local charging = false
local charge = 0              -- seconds held
local MAX_CHARGE = 1.5

hook.Add("Think", "throw.update", function(dt)
  if input.held("throw") then
    -- Held: build up charge while the key is down.
    charging = true
    charge = math.min(charge + dt, MAX_CHARGE)
  elseif charging then
    -- Not held any more, but it was last frame: it was just released.
    charging = false
    local power = 4 + 16 * (charge / MAX_CHARGE)  -- 4 to 20 m/s
    local from = camera.position() + camera.forward() * 1.5
    physics.sphere {
      pos = from,
      radius = 0.18,
      density = 800,
      velocity = camera.forward() * power + Vec(0, 2, 0),
      color = Vec(1, 1 - charge / MAX_CHARGE, 0.2),  -- yellow to red with charge
    }
    print(string.format("Thrown at %.0f m/s", power))
    charge = 0
  end
end)

Download throw.lua

  • input.pressed(id) is true for one frame when the action starts, input.held(id) for as long as it's on, and input.value(id) gives an analog value (a trigger pulled half way is 0.5).
  • "Released" is "held last frame, not now": keep a variable (charging) that remembers last frame.
  • Key names are SDL's: "T", "Space", "Left Shift", "Right Ctrl", "Up", "F5", "Keypad 1".

Toggles, and the standard actions

Every game made from the starter template already has the character actions: move, look, jump, sprint, walk, crouch, fire, aim, interact and camera.toggle, on keyboard and mouse and on a controller. Scripts read them like their own. Here L switches a lamp on and off, and holding sprint makes it glow brighter.

A lamp beside the stairs

toggle.lua
-- toggle.lua: one key, two ways. L switches a lamp on and off (pressed:
-- once per press), and holding the engine's own "sprint" action makes it
-- glow brighter while held. (docs/cookbook/input.md)

input.define("lamp", "Lamp on/off", "L")

local lamp = nil        -- the lamp's body id while it's on
local bright = false

local function setLamp(on, glow)
  if lamp then physics.remove(lamp); lamp = nil end
  if on then
    local c = glow and Vec(1, 1, 0.7) or Vec(0.9, 0.7, 0.3)
    lamp = physics.sphere { pos = Vec(-2, 1.2, 3), radius = glow and 0.4 or 0.3, color = c, static = true }
  end
end

setLamp(true, false)

hook.Add("Think", "toggle.keys", function()
  if input.pressed("lamp") then
    setLamp(lamp == nil, bright)           -- on if it was off, off if on
  end
  -- The standard character actions (move, jump, sprint, ...) are there
  -- too: read them the same way.
  local sprinting = input.held("sprint")
  if lamp and sprinting ~= bright then
    bright = sprinting
    setLamp(true, bright)
  end
end)

Download toggle.lua

Binding in C++: pads, holds, chords and axes

In C++ you get everything the bindings screen can do. The cookbook game adds its own controls in games/cookbook/Bindings.h: a button on both keyboard and pad, a hold and a release on one key, a Ctrl chord, and an axis driven by two keys and both triggers.

games/cookbook/Bindings.h
inline void addCookbookBindings(kke::InputMap& in) {
    using kke::InputModule;
    using kke::Trigger;

    // A button action: Tab on the keyboard, the View/Back button on a pad.
    in.defineAction({ "camera.next", "Next camera", "Camera" });
    in.addBinding(InputModule::bind("camera.next", InputModule::key(SDL_SCANCODE_TAB)));
    in.addBinding(InputModule::bind("camera.next", InputModule::pad(SDL_GAMEPAD_BUTTON_BACK)));

    // Hold to charge, release to throw: two actions on one key.
    in.defineAction({ "throw.charge", "Charge a throw", "Actions" });
    in.defineAction({ "throw", "Throw", "Actions" });
    in.addBinding(InputModule::bind("throw.charge", InputModule::key(SDL_SCANCODE_Q), Trigger::Hold));
    in.addBinding(InputModule::bind("throw", InputModule::key(SDL_SCANCODE_Q), Trigger::Release));

    // A chord: Ctrl+R resets the level; plain R stays free for something else.
    in.defineAction({ "level.reset", "Reset the level", "Game" });
    kke::Binding reset = InputModule::bind("level.reset", InputModule::key(SDL_SCANCODE_R));
    reset.modifiers.push_back(InputModule::key(SDL_SCANCODE_LCTRL));
    in.addBinding(reset);

    // An axis from two keys and a trigger: zoom in with X or the right
    // trigger (analog: half pulled = half speed), out with the left one.
    in.defineAction({ "zoom", "Zoom", "Camera", "game", kke::ActionType::Axis1D });
    kke::Binding in1 = InputModule::bind("zoom", InputModule::key(SDL_SCANCODE_X), Trigger::Continuous);
    in.addBinding(in1);
    kke::Binding out1 = InputModule::bind("zoom", InputModule::key(SDL_SCANCODE_Z), Trigger::Continuous);
    out1.scale = -1.0f;
    in.addBinding(out1);
    kke::Binding padIn = InputModule::bind("zoom", InputModule::padAxis(SDL_GAMEPAD_AXIS_RIGHT_TRIGGER), Trigger::Continuous);
    padIn.deadzone = 0.1f;
    in.addBinding(padIn);
    kke::Binding padOut = InputModule::bind("zoom", InputModule::padAxis(SDL_GAMEPAD_AXIS_LEFT_TRIGGER), Trigger::Continuous);
    padOut.deadzone = 0.1f;
    padOut.scale = -1.0f;
    in.addBinding(padOut);
}

Call it from your module's init, after the standard actions, then save them as the defaults the settings screen resets to:

kke::InputMap& in = m_input->map(0); // player 0; split screen has more
kke::InputModule::defineCharacterActions(in);
cookbook::addCookbookBindings(in);
m_input->commitDefaults();

and read them in update the same way Lua does:

if (in.pressed("camera.next")) nextCamera();
if (in.held("throw.charge")) charge += dt;
if (in.pressed("throw")) throwBall(charge);
zoom -= in.axis("zoom") * 4.0f * dt;              // -1 .. 1
const glm::vec2 move = in.axis2("move");           // stick or WASD, length up to 1

The unit test Cookbook.BindingsDoWhatTheInputPageSays (tests/test_cookbook.cpp) presses these keys and buttons on a fake keyboard and pad and checks each action does what this page says.

Triggers at a glance

Trigger Fires Good for
Press on the way down, held while down jump, interact
Release on the way up throw on release
Hold after holdTime (0.35 s) held charge, hold to lean
Tap released within tapTime quick actions sharing a key with a hold
DoubleTap second press within 0.3 s dodge, quick reload
Continuous the whole time, with its analog value movement, zoom, throttle
Toggle each press flips it crouch, walk

What players change is saved in input.json next to the game; the defaults stay in your code. Left-handed layouts, twin flight sticks and rebinding screens are in Input.

Next: move things around.