Log in to join the discussion
Back to list

Handling Keyboard Input in Lua Mods

General
OPApr 25, 05:20 AM170

TL;DRUnityEngine.Input.GetKey / GetMouseButton no longer works. Use InputAPI.RegisterKeyInput for keyboard hotkeys instead. It's event-driven, cross-platform, and integrates with the in-game key-binding UI.


Why the change?

Panzer War has migrated to Unity's new Input System package. Any Lua script that calls the legacy UnityEngine.Input API will throw:

InvalidOperationException: You are trying to read Input using the UnityEngine.Input class,
but you have switched active Input handling to Input System package in Player Settings.

Mods that still use Input.GetKey(...), Input.GetMouseButton(...), Input.GetAxis(...) etc. need to be updated.


The recommended approach: InputAPI.RegisterKeyInput

InputAPI is the official Lua-side input wrapper. It registers a callback pair (pressed / released) for a named action and a key code.

Signature

InputAPI.RegisterKeyInput(actionName, keyCode, onPressed, onReleased)
InputAPI.UnregisterKeyInput(actionName)
Parameter Type Description
actionName string Globally unique ID for this binding. Use a mod-specific prefix to avoid collisions, e.g. "MyMod_Reload".
keyCode string Input System keyboard name (lowercase). See the table below.
onPressed function(ctx) Called once when the key is pressed down.
onReleased function(ctx) Called once when the key is released.

Important: actionName must be unique across all active mods. Registering the same name twice prints an error and silently fails. Always prefix with your mod name.


Quick example — simplest form

-- Press F to print "fire", release F to print "stop"
InputAPI.RegisterKeyInput("MyMod_Fire", "f",
    function(ctx) print("fire")  end,
    function(ctx) print("stop")  end)

That's it. No Update() polling, no per-frame checks.


Recommended pattern — Behavior class with cleanup

For anything beyond a one-liner, wrap your binding in a Behavior class so it gets cleaned up when the mod is unloaded. Forgetting to unregister leaks the binding — it stays active until the game restarts.

Behavior()

Property = {
    keycode = {
        type  = "string",
        value = "space"   -- editable from the mod inspector
    }
}

local FireOnSpace = class("FireOnSpace")

function FireOnSpace:ctor()
    -- Prefix with mod name to avoid collisions with other mods
    self.actionName = "FireOnSpace_Trigger"
end

function FireOnSpace:OnStarted()
    -- Bind methods to self so we can unregister the exact same reference later
    self.onPressedCb  = function(ctx) self:OnPressed(ctx)  end
    self.onReleasedCb = function(ctx) self:OnReleased(ctx) end

    InputAPI.RegisterKeyInput(
        self.actionName,
        self.keycode,
        self.onPressedCb,
        self.onReleasedCb)
end

function FireOnSpace:OnDestroyed()
    -- Always unregister! This is the #1 source of "ghost binding" bugs.
    InputAPI.UnregisterKeyInput(self.actionName)
end

function FireOnSpace:OnPressed(ctx)
    print("Space pressed")
    -- your fire logic here
end

function FireOnSpace:OnReleased(ctx)
    print("Space released")
end

return FireOnSpace

Key points:

  1. Define keycode as a Property so the user can rebind it from the mod inspector without editing Lua.
  2. Store callbacks on self before registering. This keeps a stable reference for unregistering.
  3. Always unregister in OnDestroyed. The framework does not clean up bindings automatically.

Key code reference

keyCode is a string that maps to a Unity Input System keyboard binding. The API internally prefixes it with keyboard/, so you only pass the key name.

Legacy KeyCode New keyCode string
Space "space"
W / A / S / D "w" / "a" / "s" / "d"
F1F12 "f1""f12"
LeftShift "leftShift"
LeftCtrl "leftCtrl"
LeftAlt "leftAlt"
Return / Enter "enter"
Escape "escape"
Tab "tab"
19, 0 "1""9", "0"
Numpad 0–9 "numpad0""numpad9"
Arrows "upArrow" / "downArrow" / "leftArrow" / "rightArrow"
Backspace / Delete "backspace" / "delete"

For the full list see Unity's Input System keyboard documentation — any value from the Key enum works in lowercase first-letter form.


What InputAPI.RegisterKeyInput does not do

Need Use instead
Mouse buttons (left/right click) CompatibleInputManager.Instance.isWeaponFire / .isWeaponAim
Mouse delta / axis CompatibleInputManager.Instance.groundMouseDelta / .flightMouseDelta
Gamepad buttons / sticks CS.UnityEngine.InputSystem.Gamepad.current.<button>.isPressed
Polling raw Keyboard.current state CS.UnityEngine.InputSystem.Keyboard.current.<key>.isPressed

InputAPI.RegisterKeyInput is keyboard-only, by design — for game-action mouse input (fire, aim) you should use CompatibleInputManager so your mod stays compatible with mobile, controller, and Android mouse capture.


Common gotchas

1. Forgetting to unregister

The binding survives a mod reload. After enough hot-reloads you'll see:

Same action name MyMod_Fire has been registered

Always pair RegisterKeyInput with UnregisterKeyInput in your destruction lifecycle.

2. Action name collisions across mods

If two mods both register "Fire", the second one silently fails. Prefix every action name with your mod ID:

self.actionName = "AwesomeTankMod_Fire"

3. Capturing self correctly

This is wrong — self is nil inside the callback:

InputAPI.RegisterKeyInput("X", "f",
    function(ctx) self:OnPressed(ctx) end, ...)  -- self === nil here

Always store the callback on self first (see the class example above), or use an explicit closure:

local me = self
InputAPI.RegisterKeyInput("X", "f",
    function(ctx) me:OnPressed(ctx) end, ...)

4. The callback receives an InputAction.CallbackContext

You usually don't need it, but it carries timing and value info if you do:

function self:OnPressed(ctx)
    -- ctx.time, ctx.duration, ctx:ReadValue(...) etc.
end

Migration cheat sheet

Old code New code
if Input.GetKeyDown(KeyCode.F) then ... end Register onPressed callback
if Input.GetKeyUp(KeyCode.F) then ... end Register onReleased callback
if Input.GetKey(KeyCode.F) then ... end Set a flag in onPressed, clear it in onReleased, check the flag in your tick
if Input.GetMouseButton(0) then ... end CompatibleInputManager.Instance.isWeaponFire:Get()
if Input.GetMouseButton(1) then ... end CompatibleInputManager.Instance.isWeaponAim:Get()

Example: emulating GetKey (held-down state)

function MyMod:OnStarted()
    self.isHeld = false

    self.onPressedCb  = function() self.isHeld = true  end
    self.onReleasedCb = function() self.isHeld = false end

    InputAPI.RegisterKeyInput("MyMod_Hold", "f",
        self.onPressedCb, self.onReleasedCb)
end

function MyMod:Tick()
    if self.isHeld then
        -- runs every frame while F is down
    end
end

See also

  • frame/define/api.lua — Lua binding for InputAPI
  • frame/define/csharp.lua (lines 1514–1575) — full method signatures

If you have questions or run into issues, post in the modding community channel — please include your full Lua snippet and the actionName you're using.

0 replies
1 / 1
1/200/2000
Login To Reply