Platforming Nudging Assistance

Platforming Nudging Assistance
Making a solid feeling platformer isn't as simple as setting up horizontal movement, jumping, and gravity. There are a lot of tricks and behind the scenes magic involved in making something feel good to the player. In the past I've talked about ledge forgiveness, and input buffering, and now I'm going to add nudging assistance to the list!

Ledge forgiveness, input buffering are two tricks that let the player execute their intended action even if they screw it up slightly. Nudging assistance also falls into that category.

The Problem


My latest small game Starforger II is all about roaming around procedural levels. A lot of times the levels have one tile openings in walls that the player wants to navigate into. Take a look at what happens with just a typical platforming physics system when a player tries to navigate into a small tunnel like this.

Image


Even though the player really wants to slide into that hole as they fly by they simply cant because there is no frame in which the player is able to actually navigate into that tile. If their vertical speed is too great they will miss the hole every time even though they are holding down input that would make it seem like they should be able to squeeze into there.

Image


Image


The Fix


The fix for this isn't exactly easy, but it is possible. The idea is that if the player is going by a possible opening while holding the directional input toward the opening they should be nudged into the opening. Here's what it looks like in action.

Image


Awesome! But how is this result obtained? My method involves doing some sweeping collision tests and checks on the platformer character to see if they're able to squeeze into a space, and if they are I manually bump them over by one pixel which is enough to let them land in the space. Let's dig into the details!

The Details


Here's all the code I use for boosting the character into the space when flying by. I'll go through the entire block of code step by step after. It's important to note that I'm using Otter and the PlatformingMovement component for the base of the character's movement system.
if (!movement.OnGround) {
if (!boosted) {
var originalY = Y;
var ypixels = (int)Math.Ceiling(Math.Abs(movement.SumSpeedY) / movement.SpeedScale);
for (var i = 0; i < ypixels; i++) {
Y = originalY + i * Math.Sign(movement.SumSpeedY);
gridX = (int)Util.SnapToGrid(X, 16) + 8;
gridY = (int)Util.SnapToGrid(Y, 16) + 8;

var checkGridX = gridX + 16 * facingDirection;
if (!Overlap(checkGridX, gridY, CollisionTag.Ground, CollisionTag.Breakable)) {
if (Overlap(checkGridX, gridY + 16, CollisionTag.Ground, CollisionTag.Breakable)) {
if (Overlap(checkGridX, gridY - 16, CollisionTag.Ground, CollisionTag.Breakable)) {

if (movement.Axis.X > 0.5f) {

if (!Overlap(X + 1, Y, CollisionTag.Ground, CollisionTag.Breakable)) {
X += 1;
boosted = true;
break;
}
}
if (movement.Axis.X < -0.5f) {
if (!Overlap(X - 1, Y, CollisionTag.Ground, CollisionTag.Breakable)) {
X -= 1;
boosted = true;
break;
}
}
}
}
}
}

if (!boosted) {
Y = originalY;
}
}

}
if (movement.OnGround || movement.AgainstWallLeft || movement.AgainstWallRight) {
boosted = false;
}

The very first thing I check is if the character is on the ground. If they're on the ground then there's no need to check for any of this stuff. If the character is not on the ground, I check to see if the character has already been "boosted." Basically I only want to boost the character into the space once per time they're trying to navigate into it. Without this check the character will be boosted multiple times and end up awkwardly and noticeably sliding into the space. The whole trick is to have the player not notice we're doing this! You'll notice that later I reset the boosted flag if the player is on the ground, or if they're against a wall.

If the player is off the ground, and not yet boosted, then the fun begins. First I store the original Y position of the character, and then figure out how many pixels are going to be checked. For example, if the character is moving at a speed of 10 pixels per frame, that means I need to store the original Y of the character and then test the next 10 pixels of the character's potential movement. Keep in mind that this happening before the character actually moves those 10 pixels this frame.

Image


Now I go ahead and check every pixel the character is going to potentially move to. The gridX and gridY values correspond to the character's position on the tile grid. In the case of this game it was a 16 x 16 grid. I also add 8 so that the character is centered in the grid space. I'm going to use this to test for occupied or open tiles to see if there is an opening that the player is trying to squeeze into.

Image


Now I'm interested in checking the grid tiles that are in front of the character, so checkGridX is the horizontal position that I'm going to be checking. I get this value by taking the current grid x position of the character and adding plus or minus 16 to it. The facingDirection variable is set earlier by the character. If the character is facing left it's -1, and if the character is facing right it's 1. So if I multiply that by 16 then add that to the character's current grid x position I end up getting the next grid space over.

With that I can do a bunch of overlap checks against the ground or breakable blocks. I'm looking to see if the grid position next to the character is open, and the grid spaces above and below that space are occupied. This means that there's a one tile opening that the character is trying to squeeze into.

Image


Image


If all of those overlap checks end up being true then at this point I check to see if the input is being pushed in the direction of the opening. If that checks out then I do one final collision check to make sure I'm not about to nudge the character into a wall. If that checks out then the character is moved by one pixel and the boosted flag is set to true, and the loop is broken since there's no need to check anymore during this frame.

Image


One of the final things to take care of is the case where the player was not boosted at all this update. If that is the case then at the end of the pixel sweeping tests the Y value of the character needs to be restored to the original Y value that it started at.

Lastly if the character lands on the ground again then set the boosted flag back to false so that the next time they're airborne they can check for gaps again.

There is one final fix that I had to do with this algorithm and that's where the AgainstWallLeft an AgainstWallRight checks happen at the very end. What I noticed happening sometimes is that the character would be boosted toward a space when they were more than one pixel away from the hole. That means that the boosted flag would be set to true, but they weren't actually put into the space, and they wouldn't get another chance until they hit the ground.

Image


Adding the checks to see if they are against a wall is a hacky way to solve this, but it works. Basically if the character is sliding up or down directly against the wall trying to squeeze into a space then I keep allowing them to recheck for a hole every update. Actually the whole algorithm could probably be improved to only check for holes when the character is against a wall, and holding the appropriate input first, but this is just a first working iteration for now.

Image


Platformer Magic Secrets


Having platforming characters be able to squeeze into tight spaces easily is just the beginning of what you can do with platforming nudging assistance. Go and play one of the old Mario games and see what happens when you barely collide with a block by just one or two pixels. The game will totally nudge Mario in the proper direction to make sure he has a smooth path. The designers and programmers in that case imagined that it would be frustrating for Mario's hitbox to collide with something by just one or two pixels when jumping upwards, so instead he gets pushed around things to ensure a smooth ascent.

Image


Offspring Fling also has "push into a hole" assistance as the player is one tile tall and jumping into tunnels in walls is important for navigating the levels. More recently you can also check out all the tricks Matt Thorson has applied with nudging in the TowerFall platforming engine. It's as smooth as butter!

If you play a really solid feeling platformer where you feel like you have no issue executing your intended actions then chances are there's a lot of magical tricks being applied in secret to help you accomplish your goals. Understanding what these tricks are and how to apply them is pivotal in creating awesome feeling platformer!

Comments

TodesBrot
TodesBrot
Interesting article! I just so happen to be making a platforming engine right now :D
Have you also found a way to solve this problem without having a tiled environment?
Posted September 25th 2014 3:05 PM
Evan
Evan
Awesome stuff! I love your blog posts about detailed design. I incorporated ledge forgiveness into my game. All these little details make a big difference!
Posted September 30th 2014 10:26 AM
Kyle
Kyle
Thanks!

TodesBrot: I'm not sure, but it should be pretty straight forward to do this without tiles. It would just involve figuring out a slightly different way to detect open space that the character wants to be in, but it would most likely involve just testing for overlaps in different areas using the character's hitbox.
Posted September 30th 2014 5:53 PM
Bo
Bo
TodesBrot:
Hope this reaches you. I have an idea for doing this without tiles. Suppose you have a character falling at a speed of 10 pixels per frame. Instead of simply adding 10 to his y coordinate at the start of each frame and then checking collisions, you might want to add only 1 to his y coordinate and then check for collisions /ten times/. Thus, you'd get a far more accurate register and probably notice any openings you could slide in to. However, this is WAY more taxing on CPU because you're doing collision checks ten times as often as you would normally (or rather, x times as many where x is your speed).
Posted October 13th 2014 10:02 PM
new comment!

Post your comment!

Name
Email
Comment