KREEP, missed tap.

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.

Operation KREEP lane change maneuver 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.

Operation KREEP lane change maneuver input handling GIF

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

Operation KREEP missed lane change maneuver input handling 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.

Input buffering technique frame-by-frame depiction

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!

KREEP, input is king!

Hi there!

I’m not going to go into a big yakking this time about the obvious again. Summarizing: still not advancing as planned and my online presence is still far from adequate, but the update I’ve been working on is “finished”. Finished in the sense, that I’ve added all the features, fixes and fine-tunings I really wanted to add, but it is not yet released, so a final test and a last big “marketing” push is ahead of me…

This time I would like to talk about the last feature I’ve implemented, and as the title suggests, it is input handling related. I feel like it was bit of a daring act, but in the final stage of the development I’ve decided to rewrite most of the input handling logic of KREEP as the finishing step. Yep, it was kind of a bald move, and took some serious effort, both design and implementation wise, at least compared to other features I’ve been working on lately, but it made such a huge difference, that I’m really glad I made it!

A while ago I had a rather lengthy test session with some friends and colleagues. They told me they had a blast, but I could squeeze out some constructive ( negative πŸ™‚ ) criticism too. It was targeting the input handling, notedly the movement of the player characters. While I was observing my peers playing, I noticed this sentence come up a couple of times: “it’s not moving in the direction I want it to move”. It wasn’t angry/bad, but heard it enough to start thinking about it, but when I asked around, no one could actually pinpoint the problem, or describe it in more detail, only there was “something annoying” about the feel of the player control.

Some other developer friends, actually praised the controls before, stating, that it is really tight, and feels like older Pac-Man or Bomberman games, so it took me some time to figure out the problem, but approximately two weeks ago I had an “a-ha” moment while playing and realized what was bugging my buddies. The game indeed feels like old Pac-Man or Bomberman games, but I discovered some problems with this scheme (at least with my implementation). The movement is discrete as in the mentioned games, so by pressing a direction key, your character will not stop until it reaches the next tile and the game is pretty fast. It takes 0.2 seconds, so 12 frames (with 60 fps fixed loop), for a player character to move a full-tile distance. When trying to do tight “maneuvers”, so turning around a corner, or entering a door, or simply changing your direction at the right moment, you have to be spot on, otherwise you can miss the corner/door! Based on what I’ve found, this 0.2 seconds is already lower than the average reaction time for humans to a visual stimulus (which is 0.25 seconds by the way). This is pretty common in games, so reducing game speed was not something I planned to change though, especially because it would modify the design and game-feel a lot. I went further down the rabbit hole and found, that not only you have to be spot on in KREEP, but the logic I’ve implemented for deciding which direction to “prefer”, when multiple keys/buttons are pressed in a given frame, does not “aid” the player. It is pretty much stupid (simplistic) and fails utterly in a sense, because in the before mentioned situations (maneuvering), you usually have two buttons pressed…

Here it is what I’m talking about, what the user intends to do is on the first GIF, and the second and third GIF shows what happens from time to time:

2016_05_09_GIF_1Β  2016_05_09_GIF_2Β  2016_05_09_GIF_3

In the first “failure” case, the player is holding down “Right” and “Down” together for a split second and releases “Right” too late, and in the second case “Down” is pressed too late. The latter problem is really hard to battle, but can be done to some degree (still experimenting with that, more on it a little later), but the first one is actually not “fair” (at least the players feel that way: “it’s not moving in the direction I want it to move”) and it can be fixed using a simple idea + a full rewrite of my previous input handling logic πŸ˜€ πŸ˜› .

So previously I used a pretty simple input handling logic for controlling the player character movement (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");
    }
}

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();
    }
}

Input handling in “Moving” character state.

There is a huge problem in both parts. One is that a direction “preference” is hard-coded, so “Up” beats “Down” beats “Left” beats “Right” and the other is that while “Moving” the current direction is again “preferred” over other directions, for no real obvious reasons ( except for it is easy to code πŸ˜€ ).

Both problems and the previously mentioned “multiple buttons pressed” issue can be eliminated easily by adding time-stamps to button presses! Instead of simply checking one button after the other, we always check each direction and the later a button was pressed the more “preferred” it is, due to a simple logic which is: the last button pressed by the player is most probably is the “intended” new direction. This logic off-course can be further enhanced with another trick. It is most probably isn’t the “intention” of a player to face a wall when multiple direction buttons are pressed and some of them would mean simply trying to move into concrete, so besides time-stamps, possible directions are checked also.

Here it is, the further enhanced “smart” input handling algorithm (warning, warning incoming pseudo code again):

bool canMoveTo;
direction target;
time pressed;

void handleIdle() {
    canMoveTo = false;
    target = null;
    pressed = time.min;

    detectTarget();

    if (canMoveTo) {
        startMoving(target);
    } else if (target != null) {
        changeDirection(target);
    }
}

void detectTarget() {
    foreach (direction) {
        if (input.isPressed(direction)) {
            if (canMove(direction)) {
                // prefer movement over hitting a wall
                // if no walkable target is detected yet use this one!
                if (pressed < input.pressedTime(direction) or not canMoveTo) {
                    targetDetected(direction);
                }
                canMoveTo = true;
            } else (not canMoveTo) {
                if (pressed < input.pressedTime(direction)) {
                    targetDetected(direction);
                }
            }
        }
    }
}

void targetDetected(t) {
    target = t;
    pressed = input.pressedTime(t);
}

New input handling in “Idle” character state.

bool canMoveTo;
direction target;
time pressed;

void handleMoving() {
    canMoveTo = false;
    target = null;
    pressed = time.min;

    detectTarget();

    if (canMoveTo and target == currentDirection) {
        continueMovement();
    } else {
        if (canMoveTo) {
            changeDirection(target);
        } else if (target != null) {
            changeDirection(target);
            stopMovement();
        } else {
            stopMovement();
        }
    }
}

New input handling in “Moving” character state.

And here is the representation of the inner workings of the new algorithm in action:

2016_05_09_GIF_4

The direction arrows represent the pressed direction buttons by the player and the lighter color means the most recent button press. Both possible directions hold an orange question mark until the decision about the direction is made (this is not actually checked or saved anywhere until the respective frame). The frame in which the decision happens is “frozen” for a tad bit in the GIF so the choice is clearly visible.

It worked wondrously πŸ™‚ !!! The movement become a bit easier using the keyboard, the multi-press problem disappeared, but the gamepad + thumbstick based control feel got a real “level up” due to this modification! It is really cool. After completing and trying it, I felt that all the updates I’ve added to the game (new maps, new mutators and achievements) are simple gimmicks compared to this modification. It really makes a difference and I’m really happy I made it.

After a lot of testing, I’ve found a situation where the new logic was kind of detrimental, and I felt like it may not actually follow the players intention. When a corridor gets blocked by a dynamic entity (a player or the KREEP), the new logic actually “tries” to move the player in a different direction, like in the following situation:

2016_05_09_GIF_5Β  2016_05_09_GIF_6

Here the player presses “Down” than a bit later “Left” in both cases, but in the second case another player blocks the corridor. Since “Down” is still pressed, due to the new logic, the player starts to move downwards as there is nothing in the way. I felt like in most cases this could be counter intuitive, since the player usually tries to move towards these “dynamic blockers” (due to the game rules this is the most logical goal), so I introduced some extra code, which separates dynamic and static blockers (collidable map tiles) and handles dynamically blocked tiles just as much “preferred” as walkable tiles, so that only the button-press time-stamp makes the difference in these cases. Again this worked like a charm, but all-in-all it is pretty ugly and “duct-taped” ( so no pseudo code this time πŸ˜› ) + the whole thing took a long time to experiment, implement and test thoroughly.

What I’m still fiddling with, but is not going to be in the upcoming Steam release, is the second issue from the original “perceived” control problems: pressing the intended direction too late. This is much trickier and it is much more a player fault than the first one, but can be helped a little with an “input window”. For one or two frames, you buffer specific situations where different input state would have ended in a different direction. Than later you reposition the player, if still possible / makes sense, and it is much more likely, that the given direction is only a late press (e.g.: in the new position it would be blocked by a wall and no other directions are pressed at the current “late” frame). Most probably in these situations a one or two frame continuation in the same direction will not be noticeable by players, but will extinguish almost all late-press annoyances.Β Here it is, a little animation showing the inner workings of the “input window” algorithm in action:

2016_05_09_GIF_7_1Β  2016_05_09_GIF_7_2

In the GIF there is a one frame “window” represented. This frame in which the decision and reposition happens is “frozen” for a tad bit so the choice is clearly visible. The second GIF shows the animation sped up to the same level as the characters move in the game. Even on this GIF with rectangles and lines, the one frame “window” and repositioning is barely visible so I have high hopes, but the implementation is tricky, so it’s going to take some time + I’m already in a “I really want to release this game on Steam” mood πŸ™‚ !

Overall working on this problem was a wonderful experience, because it taught me how much difference good input handling makes (input IS king πŸ˜‰ !), and that it is worth putting energy into seemingly miniscule issues/ideas too, since they may end up awarding huge benefits ( + I F’ING LOVE GAME DEVELOPMENT πŸ˜€ ).

I’m planning to release the update in two separate turns. First giving it out to those who already bought the game on itch.io and IndieGameStand within a week or two, than releasing the game on Steam a week or two afterwards.

Sorry for the long write again, stay tuned for more πŸ˜‰ !
Best regards.