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

Show which button to press

Never write "press E": on a controller there is no E. A prompt is a picture of the button, on whatever the player is holding, and it changes the moment they pick up something else. Here are the same six prompts on a keyboard, an Xbox pad, a DualSense, a Switch pad, a Steam Deck and a touch screen (the pictures are Xelu's free prompts):

The same prompts on each device

prompts.lua
-- prompts.lua: show the player which button to press, as a picture of
-- that button on the device they are using right now: a key on the
-- keyboard, A on an Xbox pad, Cross on a PlayStation pad, a tap on a
-- touch screen. Pick up another device and every prompt changes by
-- itself. (docs/cookbook/input.md)

-- Your own action: G on the keyboard, the right bumper on a controller.
input.define("greet", "Wave hello", "G", "rb")

-- <prompt> is an RmlUi element the engine adds: give it an action (or a
-- raw button like "touch:hold") and a label.
local panel = ui.open([[
<rml>
<head>
  <style>
    body { font-family: Noto Sans; font-size: 22dp; color: #f2f4f8; }
    div { display: block; }
    #panel { position: absolute; left: 40dp; top: 40dp; width: 520dp; padding: 16dp 24dp;
             background-color: #10141ecc; border-radius: 12dp; }
    #panel div { margin: 6dp 0; }
    #style { font-size: 16dp; color: #9aa3b8; }
  </style>
</head>
<body>
  <div id="panel">
    <div id="style"></div>
    <div><prompt action="move" label="Move"/></div>
    <div><prompt action="jump" label="Jump"/></div>
    <div><prompt action="sprint" label="Sprint"/></div>
    <div><prompt action="fire" label="Shoot"/></div>
    <div><prompt action="greet" label="Wave hello"/></div>
    <div><prompt action="ui.accept" label="Accept"/>  <prompt action="ui.back" label="Back"/></div>
    <div id="said"></div>
  </div>
</body>
</rml>]])

local function showStyle()
  ui.text(panel, "style", "Prompts for: " .. input.style())
end
showStyle()

-- InputStyle runs when the player switches devices. <prompt> elements
-- redo themselves; text you built with input.promptText needs redoing.
hook.Add("InputStyle", "prompts.style", function(style)
  print("now showing prompts for " .. style)
  showStyle()
end)

hook.Add("Think", "prompts.greet", function()
  if input.pressed("greet") then
    -- {action} in a string becomes that button's picture.
    ui.rml(panel, "said", input.promptText("Hello! Press {greet} again to wave twice."))
  end
end)

-- The picture's file, to use in your own <img> or HUD.
print("the greet button's picture: " .. tostring(input.glyph("greet")))

Download prompts.lua

  • <prompt action="jump" label="Jump"/> in a document needs no code at all; input.promptText("{jump} jump") makes the same markup for ui.rml, and input.prompt("jump", "Jump") makes one.
  • {a}, {rb}, {key:Space}, {mouse:left}, {touch:tap} name a button that isn't an action ("press A to join").
  • Give your actions a controller button (input.define's fourth argument) or pads show nothing for them.
  • Try it without a controller: KKE_PROMPT_STYLE=playstation starts the game showing PlayStation prompts.

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, d-pad right on a pad (View/Back
    // and Start belong to the pause menu every game shares).
    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_DPAD_RIGHT)));

    // 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.