love.keyboard.wasDown

Issue #1270 wontfix
airstruck
created an issue

Overview:

This is a feature request for love.keyboard.wasDown. It would work exactly like love.keyboard.isDown, except it returns true if one of the keys was pressed any time since the last update, instead of needing to be pressed at the exact moment the function is called.

Rationale:

Many times, it's more convenient to check lk.isDown from love.update instead of writing a love.keypressed handler. But this doesn't always work nicely. Suppose you can briefly tap a key to jump or fire; if you check for this with lk.isDown, it could easily miss... the player could press and release the key before the update fires, and in that case you'll never jump/fire.

If we introduce lk.wasDown, we could use that instead and the keypress would register. Additionally, calls to lk.wasDown could be paired with lk.isDown to determine whether a key was tapped and released, or was tapped and is still being held, all without love.keypressed being necessary.

This may sound trivial, but I've run into several cases now where "wasDown" would be preferable to "isDown." In some cases it's almost a necessity (e.g. non-intrusive library code without a huge API footprint), in others it's just a convenience. The same goes for love.mouse, really, but one thing at a time. :)

Comments (30)

  1. airstruck reporter

    Take a look at this example code:

    local function jump ()
        -- if we're on the ground, jump.
        if player.y == GROUND_HEIGHT then
            player.vy = player.highJumpVelocity
        end
    end
    
    function love.update (dt)
        -- 'w' jumps, 'a' and 'd' run left and right.
        local vx = 0
        if love.keyboard.isDown('w') then jump() end
        if love.keyboard.isDown('a') then vx = vx - player.runVelocity end
        if love.keyboard.isDown('d') then vx = vx + player.runVelocity end
        player.vx = vx
    end
    
    function love.keyreleased (key)
        -- if 'w' is released while we're ascending, cut jump short.
        if key == 'w' and player.vy < 0 then
            player.vy = math.max(player.vy, player.lowJumpVelocity)
        end
    end
    

    It has the problem mentioned earlier, you won't jump if the key was tapped and released between updates, when you'd really want a low jump. Replacing isDown with wasDown would fix that.

    It's also awkward that releasing a key needs to be handled with a callback while pressing it doesn't. If we could rely on wasDown('k') and not isDown('k') to indicate a key release event, we could have this instead:

    local function jump (released)
        -- if we're on the ground, jump.
        if player.y == GROUND_HEIGHT then
            player.vy = player.highJumpVelocity
        end
        -- if released while we're ascending, cut jump short.
        if released and player.vy < 0 then
            player.vy = math.max(player.vy, player.lowJumpVelocity)
        end
    end
    
    function love.update (dt)
        -- 'w' jumps, 'a' and 'd' run left and right
        local vx = 0
        if love.keyboard.wasDown('w') then jump(not love.keyboard.isDown('w')) end
        if love.keyboard.wasDown('a') then vx = vx - player.runVelocity end
        if love.keyboard.wasDown('d') then vx = vx + player.runVelocity end
        player.vx = vx
        -- do other stuff
    end
    
  2. Raidho

    I don't see much reason for the framework itself to keep track of release timing for every single key and button and every other input source, when it's simpler than trivial to keep track yourself.

    function love.keyreleased(key) wasDown[key] = true end

    Having that said, you should well be able to make a patch and pull request, considering you have done that before. Try adding second return value to existing functions, which would indicate the time elapsed since the key was pressed or released. Simply having a function that returns key state on the last frame is not very useful.

  3. Alex Szpakowski

    It seems like wasDown's purpose is a subset of love.keypressed (it is less useful and more prone to problems because it cannot detect multiple key presses within a frame).

  4. airstruck reporter

    @Alex Szpakowski, I tried to explain why in the description and first comment. Aside from that, I consider those callbacks to be essentially useless in certain cases; we've discussed that before, but the reasoning is the same as the reasoning behind the experimental event handling system that I was advocating for a while back, that existed for a short time in trunk between releases (before 0.10.0 release, I think). This solves the same problem, at least for keyboard input, but without the downside of "hidden behavior" that eventually caused that solution to be scrapped.

    With respect to being less useful, I would characterize it a little differently. In situations where detecting multiple presses between frames is necessary, this is not useful at all. In situations where you don't need to detect multiple presses between frames, it is not any less useful. At any rate, it could be argued that this more useful than love.keyboard.isDown, which might be something to consider before discarding this idea because it's "less useful than callbacks" (because by that logic, lk.isDown should not exist either).

    Other than that, it was my understanding that events between frames do not necessarily fire in the order they were received; I'm sure I read that on the forums somewhere. Is that correct? If that's the case, I think this would actually have more predictable behavior than event handlers.

    When you say it's "more prone to problems," what kind of problems do you anticipate?

  5. Alex Szpakowski

    This solves the same problem, at least for keyboard input, but without the downside of "hidden behavior" that eventually caused that solution to be scrapped.

    Except per-frame update logic cannot handle input events without losing information (and users will notice this).

    With respect to being less useful, I would characterize it a little differently. In situations where detecting multiple presses between frames is necessary, this is not useful at all.

    It is necessary in all cases except where you actually do want per-frame logic, which is rare for immediate actions (jumping is one case, but it's a minority).

    Other than that, it was my understanding that events between frames do not necessarily fire in the order they were received; I'm sure I read that on the forums somewhere. Is that correct? If that's the case, I think this would actually have more predictable behavior than event handlers.

    Input events are absolutely 100% guaranteed to be ordered correctly - love's input would suck if that wasn't the case.

    When you say it's "more prone to problems," what kind of problems do you anticipate?

    This is an "easy" API to use but the fact that it only has a couple niche legit use cases (all others are broken because it misses information) is not conveyed by the API, so new love users will misuse it, which is not something I want.

  6. Alex Szpakowski

    One of the strengths of love is that while it doesn't provide a lot of high level APIs, the fundamentals are very solid and reliable so building your own code on top of those is a pleasure. I think this feature would go against those ideals because it adds a simple function that will subtly break in many situations where people might use it.

  7. airstruck reporter

    @Alex Szpakowski, Thanks for your reply. Some examples would help, though. Per-frame update logic seems fine for running, jumping, and firing, and these are the examples I gave. You say those cases are a minority, but I wonder what kind of examples you'd consider to be the majority?

    Maybe I'm thinking about this wrongly, but there are only one or two cases I can think of where you'd need to know if the player was hitting a key very fast (more than 60 times per second). One would be double tapping a key, for example to dash, and the other would be a sort of mindless "how fast can you hit a key" game. I'm sure you don't consider that to be a "majority." What am I missing?

    About new users misusing things, wasn't that roughly the same explanation given for not including that non-destructive event handling system that lived in trunk for a short time, and did "handle all cases?"

    Is there any other hope for non-destructive event handling? It would be really nice to have in some form.

  8. airstruck reporter

    It seems like you think users would expect the function to do something more than what's proposed here, but I'm not sure why. It's very easy to describe the behavior: "wasDown returns true if one of the keys was pressed at any time since the last update." I'm not sure how someone could use that in a way where they'd expect something else to happen.

    This whole idea of not including things primarily because new users might screw something up seems like a pretty wobbly argument, frankly. Why do we have shaders in that case, or network sockets, or anything else new users are bound to screw up? Where do you draw the line?

  9. airstruck reporter

    the fundamentals are very solid and reliable so building your own code on top of those is a pleasure

    Building any kind of event-driven library code where you want raw (non-destructive) access to events is basically impossible. This is not a pleasure, it's just frustrating.

    The only semi-non-destructive solution that exists right now, as far as I can tell, is monkey-patching love.event.poll to shunt all the results off somewhere to be used later. That short-lived solution we had that I referred to earlier can be built on top of that, in Lua. This does work, but is an ugly hack. It would be really nice to see non-destructive event handling in some form.

  10. Raidho

    Event queue is a queue, of course draining it is destructive. And why would you want it the other way? If you keep events, it's because you'll manually process them elsewhere. That is redundant - you don't need any more than 1 handler for each event. And even if you do, the right way to do it is to envoke multiple handlers per event, not keeping it floating around until you finish your handling.

  11. airstruck reporter

    Event queue is a queue, of course draining it is destructive.

    So what? I don't care that draining the queue is destructive, I care that defining a handler is destructive.

    you don't need any more than 1 handler for each event

    I disagree. Can you name one other framework that only allows a single handler per event? I've never seen one. There's a reason for that.

    And even if you do, the right way to do it is to envoke multiple handlers per event, not keeping it floating around until you finish your handling.

    When I say "to be used later," I mean immediately after the call to the "normal" callback.

    I'm well aware of the right way to do it, and it is to invoke multiple handlers per event; this is the solution I originally advocated for, the same solution we had for a week or two before it was decided that there was too much danger of a new user screwing it up, and the same solution that the love.event.poll patch I have now uses.

    One handler per event is convenient for simple use cases, but an absolute nightmare when you have different parts of the code that are fully decoupled and need to consume events.

  12. Raidho

    The framework only keeps track of one handler for simplicity's sake. You can always implement it such way that it will handle multiple callbacks.

    function love.keypressed (...)
      for _, f in pairs (callbacks.keypressed) do f(...) end
    end
    
  13. airstruck reporter

    I'm sorry, but you're missing the point. Doing that is still destructive.

    That said, getting around Love's extremely limited event delegation system was only a secondary reason for this feature request. I look forward to @Alex Szpakowski's response regarding why he thinks per-frame event handling only addresses a minority of cases, and why he feels that the majority of the time we'd need to know if a user is hitting a key faster than 60 times per second instead of quietly debouncing it, and what he thinks users could possibly be expecting a function like this to do beyond what it says on the label.

  14. airstruck reporter
    • changed status to open

    Reopening pending further review. There are a lot of unanswered questions here and I feel that this has been prematurely closed without a complete discussion.

  15. Raidho

    Huh, I must be missing something. I assumed you were advocating for sensible things, but what are you saying, you advocating for something that's wrong by your own admission? Namely that event should not vanish as soon as you dispatch the handler?

    Also I want to bring your attention to the __call metamethod which makes tables callable like functions. Using them you can implement things like promises and waterfalls. And you can assign such tables as love callbacks.

  16. airstruck reporter

    You are either misunderstanding or misrepresenting what I said, and are still missing the point. Read through the comments again, I don't feel like litigating this with you endlessly.

    I will say this, though. Assigning any value to love callbacks is destructive. If you do it in one place, you cannot do it in another place without one stomping on the other. Therefore you can't have two parts of the code both handle events and be fully decoupled, because there will need to be a third part of the code that does something like what you suggest, forming an indirect coupling between those two parts. This is not feasible in certain cases, for example library code, so you end up with a situation where each handler introduces another point of coupling between library and client code, which is anything but a pleasure.

  17. Raidho

    Well that's just how Lua works, assignment operator cannot be overloaded. If you want multiple values stored in the same variable, you'll have to use something more sophisticated than =.

  18. airstruck reporter

    If you have a suggestion that isn't a bigger hack than monkey-patching poll, I'd love to hear it. I have thought about this a lot, and don't believe a better solution exists. For example a __newindex metamethod on love itself would do the same job as overriding =, but this is useless because then you can't remove handlers, and it really does amount to doing unexpected stuff behind the users back (which the short-lived solution was accused of doing, but I disagree with this completely; there is a dedicated API for it that's totally separate from the current callback system. This kind of thing is not generally considered to be a problem in any other framework with proper event delegation).

    Anyway, I still don't think we're on the same page. You can't author a library and then say "don't use love.foo=bar to assign handlers for events," because for better or worse this is just how love does it. This is why patching poll works, but assigning a functable to the handler does not. To achieve low coupling between library and client code, library needs to be able to register event handlers without interfering with current single-handler method.

    Which brings us back to this feature suggestion. I ran into another case where, once again, I want to have a library respond to some (keyboard) events, and as before, there is no good solution without resorting to weird hacks. Around the same time I noticed that wasPressed as described here would be much more useful to me in many cases than isPressed. So I believe the idea stands on its own merits, regardless of the problem of love's extremely limited event delegation, and should be given a fair two-way discussion (preferably without much more sidetracking with irrelevant stuff).

  19. Alex Szpakowski

    It's a bit rude to reopen a wontfix issue just because you don't agree with the decision.

    It's fine to disagree but that doesn't mean the decision hasn't been made - and if our opinion changes, we will reopen the issue ourselves.

  20. Alex Szpakowski

    Some examples would help, though. Per-frame update logic seems fine for running, jumping, and firing, and these are the examples I gave. You say those cases are a minority, but I wonder what kind of examples you'd consider to be the majority?

    If you have game logic which executes over time (such as running etc.) then isDown in love.update will work fine. wasDown might be useful for firing a weapon if pressing to fire once and pressing-and-holding to fire over time are the exact same code.

    wasDown is not appropriate for any pure actions, such as:

    • Switching weapons

    • Jumping if the game doesn't have hold-to-jump-higher (most games don't)

    • Toggling text/chat input

    • Opening a door

    • Pausing the game

    • Navigating menus

    etc etc. Essentially, almost anything which never has delta-time applied to it is not appropriate for wasDown. Almost anything that does have delta-time applied to it is not appropriate for wasDown.

    Maybe I'm thinking about this wrongly, but there are only one or two cases I can think of where you'd need to know if the player was hitting a key very fast (more than 60 times per second). One would be double tapping a key, for example to dash, and the other would be a sort of mindless "how fast can you hit a key" game. I'm sure you don't consider that to be a "majority." What am I missing?

    You're missing a couple things.

    • You cannot control framerates. Something may run at 144 FPS on your computer. It will run at 30 FPS on someone else's computer. It will run at 60 FPS most of the time on another person's computer, but aspects of the OS outside your control will cause the game to stutter occasionally. It will run at different framerates when you port your game to different types of devices (phones, consoles, etc). Certain actions in a game's code will cause certain frames to take longer, as well. You may not care if someone is able to press a key twice in 1/144th of a second, but frames last arbitrary amounts of time.

    • It's not about explicitly knowing that a user pressed a key very quickly, it's about not breaking the user's experience if they do. wasDown breaks if they do, for the majority of cases where actions are expected to be executed instantly when a user presses or releases a key.

    Is there any other hope for non-destructive event handling? It would be really nice to have in some form.

    Making input events nicer for library authors (without adding caveats) would be useful. All suggested methods so far have had caveats which impact usability for end-users or game authors.

    It seems like you think users would expect the function to do something more than what's proposed here, but I'm not sure why. It's very easy to describe the behavior: "wasDown returns true if one of the keys was pressed at any time since the last update." I'm not sure how someone could use that in a way where they'd expect something else to happen.

    The problem is that it can easily be used in a manner that causes immediate actions to be lost based on frame times (therefore it's not a good fit for most immediate actions). The fact that you, an established library author, didn't fully appreciate this bodes extremely poorly for newer game authors understanding this. So it will be misused if it's added.

  21. Raidho

    You can't author a library and then say "don't use love.foo=bar to assign handlers for events," because for better or worse this is just how love does it.

    You're missing one important detail: it's only the default love.run that does it. Users are free to change it as they please. If someone uses such a main loop function that doesn't calls any of the standard love.whatever callbacks, a would-be library wouldn't work altogether. And it would be even worse to tell these people not to use custom main loop function, or force them to adhere to an "unspoken standard" they deliberately chose to avoid.

    The right thing to do, is to expose according library state updater functions, and tell users to call those appropriately. This is what I did in my multitouch library. Granted, it also has auto-hookup function that sets it up to leech events off existing default handlers... and that nobody's using it, too, but that's a matter of poor presentation and small mobile dev base, not because it's bad or missing any features, so don't discard this opinion just because it's not popular.

  22. airstruck reporter

    @Alex Szpakowski, thanks for the detailed reply. I didn't mean to be rude by reopening the issue, just as I'm sure you didn't mean to be rude by closing it it without further discussion.

    Of course, I understand that very rapid multiple presses would "get lost," but I don't completely agree that it means wasDown is necessarily a bad fit for those. For example, when the user hits the "open/close door" key twice extremely fast, maybe the game author decides the intent is not to open the door and then immediately slam it shut again (effectively leaving it shut), but instead to simply open the door. In this case, the "lost" keypress amounts to something like debouncing, which is convenient. The same would apply to any toggles. There's no point in opening a console and closing it again in between frames, or pausing and unpausing, etc. so game author might choose to interpret those as simply opening the console, pausing the game, etc.

    Of course I also understand that you can't control frame rate, but we're still talking about tiny fractions of seconds 99% of the time.

    And of course having wasDown wouldn't preclude anyone from using event callbacks in situations where lost events are no good.

    It's very encouraging that you believe a less limiting (non destructive) event delegation system would be useful, though. If we had something like that, I wouldn't have bothered with this request. I still feel like you might be underestimating its usefulness, but a decent event delegation system would certainly make this less useful.

    @Raidho, don't get me started on love.run. Your "right thing to do" does not solve the problem of extremely tight coupling when many types of events need to be consumed.

  23. Alex Szpakowski

    closing it it without further discussion.

    Marking an issue as wontfix doesn't stop people from replying, FWIW.

    For example, when the user hits the "open/close door" key twice extremely fast, maybe the game author decides the intent is not to open the door and then immediately slam it shut again (effectively leaving it shut), but instead to simply open the door.

    If that's what the game author wants, this function would be a poor fit because it's framerate-dependent instead of time- or game event-dependent.

    tiny fractions of seconds 99% of the time.

    The issues probably won't be noticed much during development, which makes it even worse in some ways because end-users will notice the issues where the developer doesn't.

    And of course having wasDown wouldn't preclude anyone from using event callbacks in situations where lost events are no good.

    Isn't the main goal of your suggestion to replace input event callbacks with wasDown for library use? This function is not a suitable replacement for input event callbacks.

  24. airstruck reporter

    The main goal was outlined in the description and the example code in the first post. The secondary goal was for library use. I understand that it's not suitable for every case, but in some cases it is suitable; in particular it's fine for the case I have in mind (which isn't really relevant here).

    Being framerate dependent does make some amount of sense given that things like toggle switches make no sense to toggle on and off again in between frames, since that's effectively like not toggling it at all, because you can't see the result, where if it spans multiple frames you can see a result. Anyway, I get your point. One thing to consider is that if you did have a more detailed debouncing routine, lost events due to wasDown would almost certainly make no difference, since the debouncer would have eaten them anyway.

    By the way, you mentioned that it's not suitable for jumping without hold-to-jump-higher, but I'm not convinced that's true. One jump event should be enough, after that the player is in the air and more jump events will generally be ignored (special cases aside, like double jumping). Besides that, in my experience most 2d games do have hold-to-jump-higher; it's 3d games that mostly do not.

  25. airstruck reporter

    Input events are absolutely 100% guaranteed to be ordered correctly - love's input would suck if that wasn't the case.

    Finally found that post after a bit of searching: https://love2d.org/forums/viewtopic.php?f=3&t=83578&p=209972#p209972

    One other thing is that the event polling doesn't guarantee the order of events that were recorded since the last frame; this is usually not a big deal, since users usually aren't faster than the game loop, but just consider the fact that if they were, the order of a keypressed and keyreleased event will be arbitrary, which may screw things up, depending on your code.

    @Gabe Stilez, did you actually observe that behavior, or was it more of a guess? I'm curious about the circumstances where it occurred if you did observe it.

    I took that comment at face value; it was part of the motivation for this feature request. If it's incorrect, it's a little concerning that nobody bothered correcting it after 300 views.

  26. Gabe Stilez

    Okay so i did some more tests and i can thankfully say that i was wrong; i can't remember why i was led to believe that the events weren't ordered perfectly, but i just tested with both 0.10.2 and the newest 0.11 nightly, and they were. I'll also edit the post on the forums, for correctness' sake.

  27. airstruck reporter

    @Gabe Stilez, thanks for following up.

    @Alex Szpakowski, There are a few good examples of things where wasDown would not be appropriate, like rotating a tetris piece (need to be able to do this as fast as possible). These are very specific cases, though. The examples you gave are not convincing as examples of why this shouldn't be implemented, and if you think they are, you're incorrectly evaluating the usefulness of the proposed feature.

    Nobody cares if you can't toggle something on and off in the same frame, and it's ridiculous to suggest that the player needs to jump twice in the same frame. If you do your menus a certain way, it could be a problem, but there are a dozen other ways to do them where it won't matter. That covers all of your examples; in other words, they are weak examples.

    Essentially, almost anything which never has delta-time applied to it is not appropriate for wasDown.

    It's easy to make claims like this, I can just as well say it is appropriate for "almost anything."

    It's fine for anything you can't do more than once per frame. Many games won't let you do things as fast as you can hit a key (like jumping, or firing). Things like that are throttled one way or another. It's also fine for things where there's no reason to do it more than once per frame (like toggling a map on and off).

    Almost anything that does have delta-time applied to it is not appropriate for wasDown.

    It's fine for everything isDown works for. It's fine for "almost anything."

  28. Log in to comment