Project Zomboid

Project Zomboid

BindAid - Input Handler
 This topic has been pinned, so it's probably important
dhert  [developer] 10 Mar, 2023 @ 2:31pm
BindAid's Keyboard Input Optimization - What It Be, and How It Do
There are 4 general events that are triggered for input handling, and each event will call multiple functions for each key press.
Event
Trigger
# of Functions
OnKeyStartPressed
When a key is pressed
10
OnKeyKeepPressed
When a key is held, every frame
9
OnKeyPressed
When a key is released
26
OnCustomUIKey
When a key is released
1

This occurs for ALL key presses, whether they have actions bound or not. Multiple key presses multiply the number of functions called per frame.

Most of the time, this has minimal impact on performance for keyboard input in Project Zomboid as most of the functions called will immediately check for their key first and end. However, there are some that do not and may also perform additional checks; this is redundant when there is no action to perform on a key press and unnecessary overhead. On low-end machines, or when the game/device is undergoing stress, this can can cause be brief input delays.

Instead, the key binds, events, and functions that are being called have been organized into an array with the key and event as its index. All vanilla events are then removed from the event stack, and a single event is added for each to trigger the appropriate key and function per event given. This reduces the overhead for each of the Events to only call ONE function, and if the key is not found in the array then no further action is done.

How it works

The key bind/event cache is built last during the `OnGameStart` event, so any other mods should have already made their changes at this point. I also have not experienced any mods that change key binds after `OnGameStart`, and am not sure why you would do this anyway. The cache is rebuilt when settings are updated.

The cache is built in the following manner:
cache[key] = { ["OnKeyStartPressed"] = { func1, func2 }, ["OnKeyKeepPressed"] = { func1 } }

The `key` is the numerical value of all key binds. When a key is pressed it is used to access the list of events, if any, bound to the key. The event then contains an indexed array of functions to run on this key press.

Why do this?

Most games engines I have found handles key presses in this way: where operations are indexed or there is some sort of additional control to prevent all functions from being run like this. While this may provide limited improvements, it is an improvement nonetheless.

Benchmark

NOTE: This requires a change to the Lua in the base game by adding a new file, and may produce a lot of noise in your log. I don't recommend doing this unless you're curious, and if you do, you should be sure to delete this file or verify your game's files with Steam.

Disclaimer

To be completely honest, we're not looking at substantial improvements for most players. The Event functions are already optimized, in that most simply end when the key doesn't match. In my testing, the current key bind input event stack takes on average 1 Millisecond (Ms) to complete; any delays come from the action being performed by the key and not from the number of functions being run. When there was a lot onscreen, and in a dedicated server, I could receive occasional delays of up to about 10Ms.

With this mod enabled, I consistently receive no more than 1Ms for each Event loop; unless an action is performed on the key, such as opening the crafting window.

Let's take the following code as our benchmark. In the base game folder, create the following file: `media/lua/shared/!a_KeyTimer.lua`
-- control for time. --- this is reset on each call of the keystack for each key local time = 0 -- event start functions, store the times local onKeyDownStart = function(key) time = getTimestampMs() end local onKeyHoldStart = function(key) time = getTimestampMs() end local onKeyUpStart = function(key) time = getTimestampMs() end -- Add our Events to the stack. --- Due to how Project Zomboid loads files, this will be first on the Lua-side Events.OnKeyStartPressed.Add(onKeyDownStart) Events.OnKeyKeepPressed.Add(onKeyHoldStart) Events.OnKeyPressed.Add(onKeyUpStart) -- event end functions. subtract the current from the start, and print if greater than 1 local onKeyDownEnd = function(key) time = getTimestampMs() - time if time > 1 then print("onKeyDownEnd: " .. tostring(key) .. " | Time: " .. tostring(time)) end end local onKeyHoldEnd = function(key) time = getTimestampMs() - time if time > 1 then print("onKeyHoldEnd: " .. tostring(key) .. " | Time: " .. tostring(time)) end end local onKeyUpEnd = function(key) time = getTimestampMs() - time if time > 1 then print("onKeyUpEnd: " .. tostring(key) .. " | Time: " .. tostring(time)) end end -- Add our key events when creating the player; most key events should be assigned by now. local postStart = function() Events.OnKeyStartPressed.Add(onKeyDownEnd) Events.OnKeyKeepPressed.Add(onKeyHoldEnd) Events.OnKeyPressed.Add(onKeyUpEnd) end Events.OnCreatePlayer.Add(postStart)

This will save the current Millisecond timestamp in the `time` variable at the start of each event. The `OnCreatePlayer` function adds our other functions to the end of the Event stacks, and so at the end of the Event the current Millisecond timestamp is subtracted from the start's to give us our delta time. If this takes over 1Ms, then it will print the key and the delta time.

This most commonly will begin to give results after there are a bunch of zombies on screen, and mostly during `onKeyUpEnd()` due to the number of functions called.

As mentioned previously, I always get good performance on my machine. I would love to hear any reports (positive or negative) if you tried this yourself!
Last edited by dhert; 10 Mar, 2023 @ 2:42pm