KREEP, missed tap.

Published September 17, 2017
Advertisement

Hello everyone!

In my last post about Operation KREEP, I mentioned that for the 1.2 update of the game I made some improvements to the input handling logic and hinted a near future deep-dive into this topic. Quite a while ago, right before releasing the Steam version, I wrote a similar post describing the input handling enhancements I made back than. Although it is a bit lengthy, if you are interested in the technical details of high level input handling logic I highly recommend it. Not a requirement though, since I'm continuing this post with its summary to level up your knowledge for easier digesting of the upcoming technical details.

Short recap

The game plays on a grid and all entities move complete tiles (no standing in between two tiles). Each "move" action by a player will actually take multiple frames to complete (precisely 12 which is 200 milliseconds under 60 fps). The players usually do not feel this (it does not feel laggy/bugging), since it is a quite fast and action packed game + 200 ms is not much and the overall rules/design of the game is deeply intertwined with grid based movement.

The initial movement handling logic was utterly simplistic. If a direction button is pressed the player moves towards that direction, with a silly hard coded priority for handling cases when multiple direction buttons are down: "Up" beats "Down" beats "Left" beats "Right". When a player is already moving and the corresponding direction button is held down it will be handled with highest priority, so continuing movement forward is considered "important/intentional".

Warning, warning incoming pseudo code:


void handleIdle() {
    if input.isPressed("up") {
        startMovement("up");
    } else if input.isPressed("down") {
        startMovement("down");
    } else if input.isPressed("left") {
        startMovement("left");
    } else if input.isPressed("right") {
        startMovement("right");
    }
}

First pass of input handling in "Idle" character state.


void handleMoving() {
    if (input.isPressed(currentDirection)) {
        continueMovement();
    } else if (input.nonePressed) {
        stopMovement();
    } else {
        // this will handle direction change
        // the same way as in "Idle" state
        handleIdle();
    }
}

First pass of input handling in "Moving" character state.

That is it. This simple control mechanism was really easy to code certainly but it wasn't intuitive nor responsive, and clearly intentional actions were missed out from time to time. It took me some time to realize that it was bugging many players and it could be improved a lot.

Around the 1.1 (Steam) release, I made significant changes to this system, by introducing some smart checks to figure out the intentions of a player as best as possible. These rules included:

  • Checking the surroundings of the player character.
  • Taking non-walkable target tiles into consideration (making them a less preferred choice).
  • Taking dynamic blockers like other players, props or the KREEP, into consideration (just as important targets as walkable tiles).
  • Saving the elapsed time since the last press of each direction button to use it for prioritization (presses closer to the direction change in time considered more important/intentional).

These modification made a huge difference back than. At least the "testing committee" (a.k.a. friends) had an immediate positive reaction, although I still had some ideas for improvement I was thrilled by the results. For more details about these enhancements, please check the old post. I'm jumping onto new stuff now!

The missed tap

One thing that was still bugging me related to these movement controls and the overall responsiveness of the game is the "missed tap". Due to one move action taking 12 frames, the direction change evaluation logic runs "rarely" and it is easy to miss it by a frame or two. An occasional maneuver is trying to change "lanes", by moving one tile perpendicular to our current direction, but continuing in the original direction right afterwards.


2016_12_12_gif_1_1.gif

Some players (including me), try to achieve "lane changing" by holding down the main direction button and tapping the perpendicular direction button. The perpendicular direction gets bigger priority, due to the press occurring closer to direction evaluation in time, so it would be selected as the new direction for the player.

2016_12_12_gif_1_2.gif

But being a short tap the button state may be released one or two frames early and usually the following happens:

2016_12_12_gif_2_1.gif

Based on my guesswork, trying to achieve "lane changing" with a tap fails 3 out of 4 times (may be even worse). This is not hard to detect and sort-of can be made sure to be not mixed up with different intentions, so here comes my solution.

Implementation details

Instead of saving only one elapsed time since the press of a direction button, two timers are saved for the last two states (regardless whether it is pressed or released currently). This way we can buffer the most recent changes and the preceding actions of the players related to movement (buffering input events and their timings).


struct BufferedInput
{
    bool pressed;
    float currentElapsed;
    float previousElapsed;
 
    void update(bool state, float dt)
    {
        if (pressed == state)
        {
            currentElapsed += dt;
        }
        else
        {
            previousElapsed = currentElapsed;
            currentElapsed = dt;
            pressed = state; // pressed changed, timers swapped, current restarted...
        }
    }
}

That is the most crucial part of the solution. From now on we can detect the "missed taps" when evaluating the player movement, since we have all the required data. I think each game needs a little fine-tuning / trial and error regarding this part as timings and speed wildly varies between them, but my logic and my numbers may be useful:


const float FrameTime = 1f / 60f; // frame time in case of 60 fps
const float MovementTime = 12 * FrameTime;
 
bool detectBufferedTap(BufferedInput input)
{
    if (!input.pressed)
    {
        var tapTime = input.currentElapsed + input.previousElapsed;
        if (tapTime <= (MovementTime - 2 * FrameTime))
        {
            if (input.currentElapsed &amp;lt;= input.previousElapsed)
            {
                return true
            }
        }
    }
    return false;
}

This means that the game considers a situation a missed tap, when a direction button is released during evaluation, a press occurred at least 2 frames after leaving the last tile (last direction evaluation) and the button was in a pressed state for at least as much time as it was released during these x <= 10 frames.


2016_12_12_taps.png

Taking these "missed taps" into account with just as much priority as a pressed input button, while the player is moving and a direction evaluation occurs, reverses the 3 out of 4 failures, so approximately 3 out of 4 times (maybe even better) a short tap is enough for a tile lane change. Tried tweaking this logic and the numbers, but could not really improve the consistency further. I'm happy with these results though. And again, after this update, controlling the game felt much better than before!

Probably there won't be updates for (nor posts about) Operation KREEP for a long while, since despite my efforts the game could only reach a miniscule audience + I'm getting fully occupied by my upcoming game Unified Theory, but who knows what the future holds...

Take care!

1 likes 5 comments

Comments

SaurabhTorne

Why not have a little custom stack to store the last taps.

December 14, 2016 12:53 PM
Spidi

Hi Pyrocute,

Well technically you absolutely could! I don't really see the benefit though, it is just an implementation detail...
I guess you would store the elapsed times in the stack and you would push a new value when the button state changes, otherwise you would update the topmost value. That is almost the same result as with my approach, but you could look back for all the press/tap times.
Why I don't think it adds anything useful is the usability of this data:
Of course it is cool to have all these state changes and times since the last evaluation, the important one for the direction decision is the last tap/press closest in time to the evaluation. Even if two taps occur in the same direction while moving, which thinking about it is actually pretty rare, since an ultra-short tap takes at least 2 to 3 frames, the first one is not really useful, as it does not hold too much meaningful information related to the decision making about the direction to select.

From an implementation point of view I think it is just as much valid as the one I presented. Takes about the same amount of code to handle/update and the same amount of memory and processing in most cases.

As for the "Why not", I don't think there is one :D . This is the first structure, that came to my mind, as I needed to preserve the last tap until the evaluation and this served the purpose. A stack just as much would have worked...

December 15, 2016 09:40 AM
Navyman

Thank you for sharing your inventive solution!

December 15, 2016 03:04 PM
SaurabhTorne

Hi Spidi,

Your method is definitely fast for input handling. Stack would make it complicated. Thank you for writing such a useful article.

Pyrocute

December 16, 2016 10:32 AM
Spidi

"Entry posted by Spidi · 9 hours ago"

Hi, don't know if this is a bug or intended?! I did edit the entry 9 hours ago, due to it's old "code"  blocks being jumbled, but it is close to a year old :) .

I don't understand how it got featuring again, especially since I do have an entry which I posted 9 hours ago :D ...

 

September 18, 2017 05:26 AM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement