KREEP, input is king!

Published September 17, 2017
Advertisement

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.gif?w=6302016_05_09_gif_2.gif?w=6302016_05_09_gif_3.gif?w=630

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 :lol:.

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 :P).

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.gif?w=630

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_51.gif?w=6302016_05_09_gif_6.gif?w=630

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 :rolleyes:) + 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.gif?w=6302016_05_09_gif_7_2.gif?w=630

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 :wink:), 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 :D).

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 :wink:!
Best regards.

Next Entry KREEP, 1.1.
4 likes 2 comments

Comments

tnovelli

ROFL "I’m not going to go into a big yakking this time"

I'm trying to think of a better way but I just can't. It always comes down to "what if the player is holding a diagonal?" - there are so many edge cases. (1) They pressed down then left, they've been moving left for awhile, they hit a wall. Stop. (2) They pressed left then down, and there's a wall to the left. Move down. (3) They pressed down then left, and both directions are open. Move left. (4) They pressed down and left in the same cycle, so you don't know which came first... flip a coin?

I was also going to say event-driven input APIs help... but most libs only have a polling API, and dpads are a pain either way.

P.S. - I take the "250ms human reaction time" with a grain of salt. I think that's "pull the trigger when you see the jump scare". If your eye is already tracking an object, you can respond to changes in direction pretty quickly, maybe 50-100ms. And gamers are faster than average.

May 22, 2016 03:23 PM
Spidi

Hi tnovelli !

Sorry for the slow response. I'll try to answer your questions and set your doubts at rest.

ROFL "I’m not going to go into a big yakking this time"

Yeah, I can't help it, I always ends up like this when writing :D.

P.S. - I take the "250ms human reaction time" with a grain of salt. I think that's "pull the trigger when you see the jump scare". If your eye is already tracking an object, you can respond to changes in direction pretty quickly, maybe 50-100ms. And gamers are faster than average.

Indeed you are right, 250ms, as I've mentioned in my post is "the average reaction time for humans to a visual stimulus", so yeah, "jump scare" is a good description :). The 50-100ms for "response to changes" although is a tad bit low, since esports players are usually between 75-150ms on average for giving out and changing actions (tough this is from wikipedia so it "may" not be scientifically accurate :D). Based on this, for a good or a better than average gamer 200ms is indeed a joke, but for many (even players) it may be on the fence. I think my "concerns" were real, although no this is not a game-breaking issue, but this modifications still feels justifiable, at least for me.

+ little off-topic extra. Try this "game" for the 250ms stuff:
http://www.humanbenchmark.com/tests/reactiontime

I'm trying to think of a better way but I just can't. It always comes down to "what if the player is holding a diagonal?" - there are so many edge cases. (1) They pressed down then left, they've been moving left for awhile, they hit a wall. Stop. (2) They pressed left then down, and there's a wall to the left. Move down. (3) They pressed down then left, and both directions are open. Move left. (4) They pressed down and left in the same cycle, so you don't know which came first... flip a coin?

Well I think the logic I've presented has an "answer" for these points/cases, although I admit, my solution may be a little overcomplicated. So "what if the player is holding a diagonal?" has two situations:

1.: both directions were pressed at the same time (at least based on 60 frames per sec polling).
In this case, we can not guess which is the intended direction based on the key state, so we rely on the map, we move the character where it can go, since why the hell would any player hold down one direction but would like to face a wall? Possible direction wins. If you have both directions open, I guess this is your (4)th point, then you can flip a coin, but adding stochasticity to an algorithm which does not need it, it won't help :), so in my implementation a "fall-back" to the original way happens: "Up" beats "Down" beats "Left" beats "Right". I don't really think there is a better solution for this scenario, there is no trickery trick, that can predict what the player really wants...
Maybe you could increase input polling frequency or use an event-driven api as you mentioned, but that does not really/fully solve the "dilemma" :), especially since you only start to rely on millisecond differences, most probably unintentional ones.

2.: one direction were pressed a bit later.
All (1), (2) and (3) points are related to this situation. Here the later pressed key is the preferred one, but "movement" again is preferred over stopping and facing a wall, again basing it on the idea, that there are no logical situations in the game where facing a wall would help out a player any way :D. So when moving left for a while and hitting a wall, but down is still pressed and the way is clear, most probably the player still wants to move down at that point instead of facing the wall. Same way, following these same rules (2) and (3) is covered.

I have to emphasize this: most probably there is no perfect solution and you can not possibly predict the intention of the player in every single case, and indeed these "tricks" are extremely hacky/duct-tapey, but with these enhancements the game feels better to play, both on keyboard and gamepad and even on paper the whole thing sounds much better and logical than: "Up" beats "Down" beats "Left" beats "Right" :)!

Br.

June 02, 2016 07:08 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement