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