Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
RNavega
Party member
Posts: 251
Joined: Sun Aug 16, 2020 1:28 pm

Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by RNavega »

Some thoughts on the differences between these functions for reacting on key presses.
If you have anything useful to add, don't keep it to yourself =)

When you look in the source code of Löve, both love.keypressed() / keyreleased() as well as love.keyboard.isDown() are very fast functions, so the major difference between them isn't about performance, but rather how they force you to structure your program. This is because they represent different "questions" regarding keyboard input.
  • The love.keyboard.isDown() function is a test that asks "is (this key) pressed right now?", so it tests for a specific key. When you use it inside love.update() for example, your program is asking per frame "is it pressed? is it pressed? is it pressed?...".
  • The love.keypressed() / keyreleased() handlers ask (or rather, tell) that "(this key) has been pressed/released right now", and they run for all keys, differently than isDown() that tests for a specific key. These handlers are only executed when the press or the release happen, and not any other moment.
I wanted to investigate if one is better than the other for certain usecases.

love.keyboard.isDown()
It can only tell you if a key is pressed or not. It cannot tell you when that key started being pressed, or when it was released.
If you wanted to know when a key was pressed then you'd have to use a variable to keep track of the state of the key on the previous frame so you can know when there's a change:

Code: Select all

local wasSpacePressed = false

function love.update(dt)
    if love.keyboard.isDown('space') then
        if wasSpacePressed then
            -- Space was already pressed on the previous frame.
        else
            -- Space was *not* pressed on the previous frame, so do whatever you want to do
            -- when space *begins* being pressed, and mark it as being pressed from now on.
            -- (...)
            wasSpacePressed = true
        end
    else
        if wasSpacePressed then
            -- Space *stopped* being pressed. Do what you need to do when space stops being pressed.
        end
        wasSpacePressed = false
    end
end
If you want to use love.keyboard.isDown() and support customizable keys (like the user being able to go to a settings screen and remap some action to some other key, like changing the key for shoot, jump, interact etc.), then don't use the direct string name of the key (like love.keyboard.isDown('space')) but a table that stores the keys and handlers for each key:

Code: Select all

-- A table of 'keyData' tables.
-- Each 'keyData' table has the form {keyName, keyHandler}, where 'keyName' is the
-- string name of the key and 'keyHandler' is a function for reacting to that key.
local customizableKeys = {}

function love.update(dt)
    for index = 1, #customizableKeys do
        local keyData = customizableKeys[index]
        local keyName, keyHandler = keyData[1], keyData[2]
        if love.keyboard.isDown(keyName) then
            keyHandler(dt)
        end
    end
end
So each key has a handler function that gets called when that key is being held on that frame.
That 'customizableKeys' table can be built out of data stored in something like a save file that has the keyboard mappings that the user configured on your game settings screen.

love.keypressed() / keyreleased()
They can only tell you when the key was released or pressed, so they work great for things that happen immediately, like ending the program. From the Löve wiki:

Code: Select all

function love.keypressed(key)
    if key == 'esc' then
        love.event.quit()
    end
end
However, you won't know inside love.update() if that key remains pressed or released. You can find that out, since you can assume that if a key has a press event right now, then from this moment onward it will remain pressed until you receive the release event for that same key. After that, the key will remain released until you receive a new press event for that key, and so on. But to keep track of these things you'll be forced to write some code that imitates what isDown() already does.
Although you could use all of keypressed() / keyreleased() / isDown() together, I think using some sort of temporary handler system would work better: when the key is pressed, you "activate" a handler, and in love.update() you execute all active handlers, and when the key is released you "deactivate" its handler:

Code: Select all

local allKeyHandlers = {}
-- Fill the 'allKeyHandlers' table with keyHandler objects.
-- (...)

local activeKeyHandlers = {}


function love.keypressed(key)
    local keyHandler = allKeyHandlers[key]
    -- See if there's a key handler for the key being pressed.
    -- That is, if your game is listening to that key or not.
    if keyHandler then
        keyHandler:start()
        activeKeyHandlers[key] = keyHandler
    end
end


function love.update(dt)
    for key, keyHandler in pairs(activeKeyHandlers) do
        keyHandler:continue(dt)
    end
end


function love.keyreleased(key)
    local keyHandler = allKeyHandlers[key]
    if keyHandler then
        keyHandler:stop()
        activeKeyHandlers[key] = nil
    end
end
TL,DR:
If you just need to know if a key is being pressed or not for the current frame then love.keyboard.isDown() is the simplest and easiest method. It can also be used in a way that supports customizable keys (you store the key name, whatever it is, in a variable, and use it with isDown()).

However, if your game needs to react to the three possible states of a key (the start of the key press, the continuation of that key press, and the release of that key press), then you'll end up writing less code if you use keypressed() / keyreleased() with some sort of handler system like explained above. If your game has lots of relevant keys, your code will also be slightly more performant (but not in a noticeable way) than testing for all relevant keys, for every frame, like you would if you were only using isDown().
User avatar
zorg
Party member
Posts: 3444
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by zorg »

You can also smartly combine these to create a system where you can tell whether a key is in a specific state out of 4:
- pressed this frame,
- held this frame (press happened earlier),
- released this frame,
- not being held

Also, probably better to use the scancode parameters and love.keyboard.isScancodeDown instead of keyconstants considering the world isn't just america, and people can use keyboards with layouts other than US-EN QWERTY unless you want people to complain about y and z being mapped in the wrong place for QWERTZ users, or whatever AZERTY and DVORAK get asdf mapped to.
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
User avatar
slime
Solid Snayke
Posts: 3133
Joined: Mon Aug 23, 2010 6:45 am
Location: Nova Scotia, Canada
Contact:

Re: Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by slime »

keypressed is good for actions (like opening a menu, or consuming an item, or whatever). isDown is good for things that happen continuously over time (like movement).

If you try to use isDown for actions you'll run into edge cases where user input can be missed – for example if a key is pressed and released within one frame, which is more common than you might think. The worst part is you might never notice but your users probably will.
User avatar
Hugues Ross
Citizen
Posts: 85
Joined: Fri Oct 22, 2021 9:18 pm
Location: Quebec
Contact:

Re: Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by Hugues Ross »

slime wrote: Sun Dec 24, 2023 6:43 pm keypressed is good for actions (like opening a menu, or consuming an item, or whatever). isDown is good for things that happen continuously over time (like movement).

If you try to use isDown for actions you'll run into edge cases where user input can be missed – for example if a key is pressed and released within one frame, which is more common than you might think. The worst part is you might never notice but your users probably will.
Indeed! As a user who sometimes sends keystrokes to software though voice commands, this exact case has happened to me with a couple love projects before. It's not a huge deal, but it can certainly happen.
User avatar
Bobble68
Party member
Posts: 160
Joined: Wed Nov 30, 2022 9:16 pm
Contact:

Re: Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by Bobble68 »

I try to avoid using keypressed when I can, I much prefer being able to segment the controls into different modules (which you can probably do but I just find this makes more sense). I have a system set up which handles detection of the different things I might want, like thisFrame or upThisFrame or just isDown which looks like this

Code: Select all

function manageControls()
  controlController("interact",      love.keyboard.isDown("f"),              "F")
  controlController("flap",          love.keyboard.isScancodeDown("space") or  (joystick and joystick:isDown(1)),  "space")
  controlController("tuck",          love.keyboard.isScancodeDown("lshift") or love.keyboard.isScancodeDown("rshift"), "left shift")
  controlController("stasis",        love.keyboard.isDown("e"),      "E")
  controlController("timeBubble",    love.keyboard.isDown("1"),      "1")
  controlController("boost",         love.keyboard.isScancodeDown("lctrl"),  "left ctrl")
  controlController("lockDirection", love.keyboard.isScancodeDown("lalt"),   "left alt")
  
  controlController("menuLeft",      love.keyboard.isDown("q"),   "Q")
  controlController("menuRight",     love.keyboard.isDown("e"),   "E")
  controlController("menuUp",        love.keyboard.isDown("w") or love.keyboard.isDown("up"),   "W")
  controlController("menuDown",      love.keyboard.isDown("s") or love.keyboard.isDown("down"), "S")
  controlController("menuEnter",     love.keyboard.isDown("return"),  "Enter")
end
Idea is here is to make remapping the controls simple, so the first argument is how I refer to it in the code, the second is the boolean it uses to detect it (I like it because I can use boolean operators for more complex controls) and the last is how it displays in the UI in game.

Probably not the best system but I find it incredibly useful. Maybe with a little more effort I could get it set up to fix some of the edge cases.
Dragon
RNavega
Party member
Posts: 251
Joined: Sun Aug 16, 2020 1:28 pm

Re: Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by RNavega »

Thanks a lot guys, great comments.

I was also looking into the best ways of plugging the input component into the player update component. That is, connecting the two and letting things happen in the game because of input.

While you can of course have a bunch of tests in the player update code like "if space is pressed, try to jump" etc, which work great for simpler games, if you have a lot of actions, I think adding a level of separation similar to what Bobble68 showed above comes with more benefits: you can debug by sending "fake" events without having to actually press/release literal keys, you can log events to inspect them later, and it's easier to modify and extend the input component since it's completely separate.

As zorg pointed out, on a particular game frame there's 4 possible states that a (digital) button can have:
- Changing to pressed
- Continuing as pressed
- Changing to released
- Continuing as released

When one of these events happens, the input component would translate that event to the "game action" that it's mapped to, and somehow signal all objects that are interested in knowing about that game action.
Reading these articles has helped me with getting a clearer picture:
- The Observer / Subscriber pattern
- The Command pattern (a synonym for "action" I guess)

So I think there's two approaches here: each of those 4 button events can exist as commands, so an object that will listen to those commands should have like 3 or 4 new functions, like "onJumpActionStart", "onJumpActionContinue", "onJumpActionEnd". The other way is making a single "Jump" command that stores what kind of those 4 events that it's about, so any listeners would only need a single "onJumpAction" function for listening to commands of that type, and then inside those functions they would identify the kind of event that the Jump command represents.
There must be pros and cons to either approach.

For an analog input (like with Löve's joystick:getAxes() and joystick:getGamepadAxis()), you have things like a stick or pressure button that can have 2 states:
- Neutral
- Pressed
In the same way, you can go with one unique command for each of these states, or a single command that stores the state inside. In any case, when it has the "pressed" state then it must come with some extra data, like the strength of the pressure on however many axes the control has, like 2 axes for a stick and 1 axis for a pressure button.
RNavega
Party member
Posts: 251
Joined: Sun Aug 16, 2020 1:28 pm

Re: Best usecases for love.keypressed/keyreleased vs love.keyboard.isDown

Post by RNavega »

Hi. After trying to write something based only on keypressed/keyreleased to be used with a simple finite state machine, which was very educational, I noticed that my impressions were off!

Despite a key having 4 possible events (first press / continued press / first release / continued release), what ends up simplifying things is looking at it this way:
  • On a certain frame, a "keypressed" event for a key happens.
  • On some other frame, a "keyreleased" event for that same key will happen.
  • Between those two moments, zero or more calls to love.update() will happen.
So how do you make an entity react to those?
When implementing FSM states for an entity in your game, the entity itself will listen (AKA subscribe) to both "keypressed" and "keyreleased" events, and send those events to its current running state, whatever it is. Then its the state logic that will decide if the incoming event is relevant or not, if the state should be changed or not -- for instance, the Running state should definitely be interested in knowing if any of the running keys are released, such that if all keys are released, it must trigger a transition to the Idle state to stop running.

There is no need for emitting the "continued press" or "continued release" events: since these "continued" events would be emitted per-frame anyway, if your entity needs execute something per-frame, then it should just subscribe to some "update" event that you emit from inside love.update(), until the entity doesn't need that anymore and can unsubscribe from that "update" event.

Also, the only benefit between translating raw key events to game events, like "space" to "jump", "e" to "interact", "r" to "reload" and such, is that you can easily implement key remapping in this way: on a game settings screen or similar, the user can configure what key should trigger the "jump", "interact", "reload" etc events. But other than that, it makes no practical difference if an entity is listening to the game event or the raw key event directly.

The test code for this keypressed/keyreleased + FSM experiment that I was doing is this below, it's just the main.lua.

preview.jpg
preview.jpg (56.77 KiB) Viewed 12675 times

Code: Select all

-- ==============================================
-- Experiments with keypressed / keyreleased
-- handling, and a finite state machine.
--
-- By Rafael Navega (2023)
-- License: Public Domain
-- ==============================================

io.stdout:setvbuf('no')


-- Used for making linked lists of nodes.
-- For maximum speed, nodes are array-only tables in this format:
-- {value, previousNode, nextNode (, lastNode)}
-- With 'lastNode' only being present in the list node (the first node of the list).
-- With 'value' only being used in all nodes besides the list node.
-- The list node exists so that a list can be deemed "empty" if it only has that single node.
local LinkedNode = {}
LinkedNode.__index = LinkedNode

function LinkedNode.create()
    local listNode = setmetatable({}, LinkedNode)
    return listNode
end
setmetatable(LinkedNode, {__call = LinkedNode.create})

function LinkedNode:add(value)
    local lastNode = self[4] or self
    local newNode = {value, lastNode}
    -- Set the next node in the list to be the new one.
    lastNode[3] = newNode
    -- Mark that this node is the last one.
    self[4] = newNode
    return newNode
end

function LinkedNode:disconnect(node)
    if node == self[4] then
        self[4] = node[2]
    end
    -- Reconnect the neighbor nodes, if any.
    if node[3] then
        -- The node has a node ahead of it.
        if node[2] then
            -- The node has a node behind it as well.
            node[2][3] = node[3]
            node[3][2] = node[2]
        else
            node[3][2] = nil
        end
    elseif node[2] then
        node[2][3] = nil
    end
end

-- Go through all nodes and call their 'value' as a function.
-- An optional 'data' parameter is sent.
function LinkedNode:invokeValues(data)
    local node = self[3]
    while node do
        node[1](data)
        node = node[3]
    end
end

function LinkedNode:clear()
    local node = self[3]
    local oldNode
    while node do
        oldNode = node
        node = node[3]
        oldNode[2] = nil
        oldNode[3] = nil
    end
end


local ActionNotifiers = {pressScancodeMap={}, releaseScancodeMap={}, updateCallbacks=LinkedNode()}

function ActionNotifiers:onStart(scancode)
    local callbacks = self.pressScancodeMap[scancode]
    if callbacks then
        callbacks:invokeValues()
    end
end

function ActionNotifiers:onUpdate(dt)
    self.updateCallbacks:invokeValues(dt)
end

function ActionNotifiers:onStop(scancode)
    local callbacks = self.releaseScancodeMap[scancode]
    if callbacks then
        callbacks:invokeValues()
    end
end

function ActionNotifiers:subscribe(scancode, callback, isPress)
    if scancode == 'update' or scancode == 'onUpdate' then
        return self.updateCallbacks:add(callback)
    end
    local scancodeMap = (isPress and self.pressScancodeMap) or self.releaseScancodeMap
    local callbacks = scancodeMap[scancode]
    if not callbacks then
        -- Initialize a linked list of callbacks for the onStart and
        -- onStop events for this scancode.
        callbacks = LinkedNode()
        scancodeMap[scancode] = callbacks
    end
    local node = callbacks:add(callback)
    return node
end


local smallFont = love.graphics.getFont()
local bigFont = love.graphics.newFont(20)


local NO_OP = function() end
local ActorState = {}
ActorState.__index = function() return NO_OP end

function ActorState.prepare(preset)
    return setmetatable(preset, ActorState)
end


local myActor = {
    angle = 0.0,
    angleSpeed = 1.5,
    drawX = 300,
    drawY = 300,
    SPIN_RADIUS = 220,
    halfWidth = nil,
    halfHeight = nil,
    currentState = nil,
    alpha = 0.1,
    text = 'You Spin Me\nRight Round\nBaby',

    actorStateIdle = ActorState.prepare({name='Idle'}),
    actorStateFade = ActorState.prepare({name='Fade (In / Out)', speed=nil, target=nil}),
    actorStateSpinning = ActorState.prepare({name='Spinning'}),
}

function myActor.onSpaceStart()
    myActor.currentState:onSpaceStart()
end

function myActor.onUpdate(dt)
    myActor.currentState:onUpdate(dt)
end

function myActor.onSpaceStop()
    myActor.currentState:onSpaceStop()
end

function myActor:changeState(nextState, enterParams, exitParams)
    self.currentState:onExit(exitParams)
    nextState:onEnter(enterParams)
    self.currentState = nextState
end


-- The "idle" state.
function myActor.actorStateIdle:onSpaceStart()
    myActor:changeState(myActor.actorStateFade, myActor.fadeInParams)
end


-- The "fade in / out" state.
function myActor.actorStateFade:onEnter(enterParams)
    if enterParams then
        self.speed = enterParams.speed or 0.1
        self.target = enterParams.target or 1.0
        self.nextState = enterParams.nextState or error('actorStateFade:onEnter() -> nextState is nil')
    end
end

function myActor.actorStateFade:onUpdate(dt)
    myActor.alpha = myActor.alpha + self.speed * dt
    if (self.speed > 0.0 and myActor.alpha >= self.target) or
       (self.speed < 0.0 and myActor.alpha <= self.target) then
        myActor.alpha = self.target
        myActor:changeState(self.nextState)
    end
end

function myActor.actorStateFade:onSpaceStart()
    self:onEnter(myActor.fadeInParams)
end

function myActor.actorStateFade:onSpaceStop()
    self:onEnter(myActor.fadeOutParams)
end


-- The "spinning" state.
function myActor.actorStateSpinning:onUpdate(dt)
    myActor.angle = myActor.angle + (myActor.angleSpeed * dt)
end

function myActor.actorStateSpinning:onSpaceStop()
    myActor:changeState(myActor.actorStateFade, myActor.fadeOutParams)
end


function myActor:load()
    ActionNotifiers:subscribe('escape', love.event.quit)

    myActor.fadeInParams = {speed=1.0, target=1.0, nextState=myActor.actorStateSpinning}
    myActor.fadeOutParams = {speed=-1.0, target=0.1, nextState=myActor.actorStateIdle}

    self.currentState = myActor.actorStateIdle
    self.currentState:onEnter()
    ActionNotifiers:subscribe('space', self.onSpaceStart, true)
    ActionNotifiers:subscribe('space', self.onSpaceStop, false)
    ActionNotifiers:subscribe('update', self.onUpdate)
    self.halfWidth = bigFont:getWidth(self.text) / 2.0
    self.halfHeight = (bigFont:getHeight() * bigFont:getLineHeight() * 3) / 2.0
end


function love.load()
    love.window.setTitle('Keypressed / Keyreleased Tests')
    love.window.setMode(600, 600)
    myActor:load()
end

function love.keypressed(key, scancode)
    ActionNotifiers:onStart(scancode)
end

function love.keyreleased(key, scancode)
    ActionNotifiers:onStop(scancode)
end

function love.update(dt)
    -- Send "update" notifications to anyone interested in receiving them, like things that
    -- need per-frame updates.
    ActionNotifiers:onUpdate(dt)
end

function love.draw()
    love.graphics.setColor(1.0, 1.0, 1.0)
    love.graphics.setFont(smallFont)
    love.graphics.print('Press and hold Space to spin the text.\n\nPress Esc to quit.',
                        10, 10)

    love.graphics.setFont(bigFont)
    love.graphics.setColor(1.0, 1.0, 0.4)
    love.graphics.print('Current state:', 240, 260)
    love.graphics.setColor(0.6, 0.6, 0.6)
    love.graphics.print(myActor.currentState.name, 240, 260 + 25)

    love.graphics.setColor(1.0, 1.0, 0.4)
    love.graphics.print('Space key:', 240, 260 + 75)
    love.graphics.setColor(0.6, 0.6, 0.6)
    love.graphics.print(love.keyboard.isScancodeDown('space') and 'Pressed' or 'Released', 240, 260 + 75 + 25)

    local offsetX = math.cos(myActor.angle) * myActor.SPIN_RADIUS
    local offsetY = math.sin(myActor.angle) * myActor.SPIN_RADIUS
    love.graphics.setColor(1.0, 1.0, 1.0, myActor.alpha)
    love.graphics.print(myActor.text,
                        myActor.drawX + offsetX - myActor.halfWidth,
                        myActor.drawY + offsetY - myActor.halfHeight)
end
Post Reply

Who is online

Users browsing this forum: No registered users and 58 guests