Thursday, March 10, 2016

Nintendo tricks for 2D platformers: The game feel

Nintendo is often praised for the excellent gameplay in their titles. One critical aspect of this is how well the controls usually feel and how responsive the characters in the screen are to your inputs. During development of Mario 64, Shigeru Myamoto used a "secret garden", a place where there was no fancy terrain or hazards and you could just walk around with Mario. The idea was to use this place to adjust Mario´s movement until it felt right. Like Myamoto said, it should be fun to just walk around. To achieve this, Nintendo uses some "tricks".

Update 25/08/2016: Included Automatic horizontal momentum cancellation and Double jump hold up. Check http://www.juicybeast.com for a more detailed explanation.


Index

  • Size matters: Block based world
  • Collisions
  • Collision threshold
  • Runnign over gaps
  • Minimum walking speed
  • Skidding and turning around
  • Jump height
  • Preservation of momentum
  • Jump vertical anatomy
  • Horizontal movement in the air
  • Safety jump extension
  • Sprinting
  • Conclusion
  • Bonus 1: Automatic horizontal momentum cancellation
  • Bonus 2: Double jump hold up

Size matters: Block based world

All the units in the Mario games are relative to blocks. Maximum jump distance and height, distance to reach the maximum horizontal speed, distance to skid...Even Mario´s, the enemy´s, obstacles´ and objects´ sizes can all be expressed in blocks: * Mario is 1 block wide and 1 block high, while Super Mario is 1 block wide and 2 blocks high. * A Goomba, Mushroom, Boo, Muncher, spike, Spiketop, Spring...are all 1 block wide and 1 block high.
This makes it really easy to design challenges as well as is an invaluable tool to communicate the game´s metrics to the player. The players will be able to read the game´s distances without any previous information and have the answers to the challenges the level will present beforehand (Can you reach this platform? Can you jump over this pit?).

Collisions

Even though Mario´s sprite can be expressed in block units, his collison bounding box is in fact smaller. If Mario walks against a wall he can enter it for 3 or 4 pixels (just his moustache, a part of the cap and the hand on the back). Conversely, when he jumps up he can enter a block for 3 or 4 frames before triggering a collison. Also, when running off a block, Mario will start falling before all his sprite leaves the block (again, 3 or 4 pixels earlier).
Having a smaller collision mask like this helps movement feel better as well as allows for some behaviours that make movement feel "right": * It´s easier to jump up between 2 blocks. * It´s easier to fall between 2 blocks. * It allows falling between 2 blocks while walking. * Collisions feel better as it seems that things are colliding with Mario´s body, instead of with the tip of his moustache. In general, it´s always a good idea to have a collision mask smaller than the player sprite. This allows for close savings and is extensively used in shoot´em ups for example.

Collision threshold

When jumping up against a block, Mario games will check what distance into the block the player is. If the player is right below the block, Mario will jump straight up and hit it. However, if Mario is near the edge of the block, the game allows for some threshold: if Mario is inside this threshold (3-4 pixels from the edge) he will instead be pushed out to the side of the block and allowed to jump up without hitting it. This will only happen when there´s a free space to be moved to.

Running over gaps

A cool little trick in all Mario games is that you can run over one block gaps without falling. I have no idea how Mario games do this, but I´ve implemented a method that seems to work perfectly: * When the player runs off a block, get the x-position of the block he fell from. * Check if there´s another block at the same height and at a distance 2*Block from the block Mario fell off. * If so, don´t allow the player to fall.
In my game, the id of the block where the MC is standing is always stored. In this case, I store the id of the original block and update it to the block at the other side of the gap once the MC has passed the medium point between both.

Minimum walking speed

The character should feel like it has weight. In action games, like the Megaman series, you pushed a direction and Megaman started walking in that direction at full speed. In Mario games however, Mario will start moving immediately but take some time to reach the maximum speed. This acceleration provides a feeling of weight to the character. But there is more: instead of applying a constant acceleration from 0, what Mario does is that he instantly achieves a minimum walk speed as soon as you press a direction (we´ll call this spWalkMin). Then, in the next frame he will start accelerating until he reaches the "normal" walking speed (spWalkMax).
The same happens when you are moving and let go of the direction: Mario will gradually come to a stop. In fact what happens is that the speed will deccelerate from spWalkMax to spWalkMin and once it reaches spWalkMin, instantly become 0.
These modifications allow for the character to resemble a realistic movement, while at the same time gives it a boost in responsiveness so it feels better to control. If we got rid of spWalkMin and worked from 0 all accelerations and stops would take more time and make the character feel slower and less responsive.

Skidding and turning around

If we make Mario run and suddenly press the opposite direction he will start skidding, loosing speed over distance until he starts running in the newly pressed direction. If the game followed a realistic physics engine, Mario would skid until his speed passed 0 and then start accelerating in the opposite direction. However what really happens is that the speed will only drop to a set value (spSkidTurn) and then instantly start running in the opposite direction with a reversed speed (-spSkidTurn). Again, this way the games mimics real life movement to a point, while keeping the character responsive.
I may need to check this, but I think that in Mario games usually spSkidTurn = spWalkMin, which helps in keeping a consistent behaviour.

Jump height

In Mario games, the horizontal speed is treated in ranges:
  • sp <= spWalkMin (Stop). This will only apply when we´re totally stopped or in the very first frame after we started walking.
  • spWalkMin < sp < spWalkMax (Slow Walk)
  • spWalkMax < sp < spRun (Normal Walk)
  • spRun < sp < spRunMax (Run)
  • sp = spRunMax (Sprint)
This ties directly with jump height: the faster Mario goes, the higher he´ll be able to jump. In SMW, this assignment is also done in ranges:
  • Stop => jumpHeight = 4 blocks
  • Walk => jumpHeight = 5 blocks
  • Run => jumpHeight = 5 blocks
  • Sprint => jumpHeight = 6 blocks
Note that the jumpHeight for Walk and Run is the same, probably because Nintendo wanted to make all levels beatable without running. The sprint´s jumpHeight is really never required in SMW, although it may be needed for some secret.
This relation of horizontal and jumpHeight has an immediate effect on gameplay: the faster Mario goes, the higher he´ll be able to jump (and consecuentially, the longer horizontal distance the jump will cover). It effects how you take every jump in the game, always making sure to run a few steps before achieving any jump, like you would do in real life. Again, the standarization in ranges makes the movement more cohesive and easier to understand by the player (horizontal jumping distance is also defined in blocks).

Preservation of momentum

When jumping Mario will preserve his horizontal speed if the player releases the direction button. This means that, in order to stop, the player must push in the opposite direction. This behaviour is also applied when Mario walks or runs off a block: his horizontal speed will be maintained, which will make him fall a distance off the block that he ran off.
I haven´t checked this, but I suspect that when running and then crouching, Mario will deccelerate at a slower pace than if the player just let the direction key go. This allows for a technique where you can run, crouch and slide under enemies or hazards, without affecting the normal movement.

Jump vertical anatomy

The jump in super Mario games is really something. It describes a nice arc, is responsive and fast and feels great. Instead of applying an initial vertical speed and letting gravity do the work of bringing Mario down, Nintendo used a different approach:
  1. Pressing the button gives the player an initial vertical speed (iniJumpSpeed) and starts 2 counters (we´ll can them cntA and cntB). As long as the button is pressed, this speed is mantained and cntA increases. cntB will increase regardless.
  2. Once cntA reaches a certain amount or if the player releases the jump button, the player will no longer have control of the jump´s height. cntA will stop counting, but cntB will continue. During this time, Mario will continue rising with the initial speed.
  3. One cntB reaches a set value, the second phase of the jump will start. Gravity will kick in, slowly making the speed turn around.
  4. Once the speed reaches -iniJumpSpeed, it will be capped at this value and Mario will continue falling until he reaches the ground.
Playing with the values of iniJumpSpeed, cntA, cntB and gravity will give out different types of jumps. For example, iniJumpSpeed depends on the horizontal speed before jumping, so it can be set to a higher value if Mario is runing instead of walking, allowing him to jump higher. If the target values of cntA and cntB are set to be equal, the second phase of the jump will start as soon as the player releases the jump button, giving a more angular jump (like in Super Meat Boy). This control is more responsive, but is more demanding of the player and makes it much harder to do things like jumping on top of small moving targets (like a Goomba). Setting cntB to a higher value than cntA will give a higher jump with a nicer arc. The value of gravity also affects the shape of the arc: a smaller value will make the jump floatier and the peak of the jump will be smoother, but if you set it too small the jump will become too floaty.

Horizontal movement in the air

When moving in the air two neat tricks are used, depending on whether you are pressing the same direction you´re facing or if you press the opposite:
  • Same direction: Mario will accelerate towards the maximum speed of his current speed range. That is, if he already started the jump with sp = spWalkMax he won´t accelerate at all. However, if he started the jump before reaching the maximum speed (spWalkMin < sp < spWalkMax) he will be able to accelerate to spWalkMax, but not beyond. Accelerating mid-air is not a realistic behaviour, but it makes the character more responsive to player input, is integral to platform games and is a way to keep the speeds in the designed limit values, providing a more cohesive movement.
  • Opposite direction: Mario will deccelerate with a higher than usual acceleration (this allows for nice air movement). Note that in this case the Skid Turn trick is not used, so we can have more control in the air to, for example, kill all horizontal speed and land at the center of a platform.

Safety jump extension

When Mario falls from a platform there are a small number of frames where a press of the Jump button will still trigger a jump. This way there will be no frustration over missing simple jumps. A special note is necessary here: to avoid pixel-perfect jumps in a level you should design the jump distance of any obstacle without having the Safety jump extension in mind.
This is a really intelligent trick Nintendo designed and is surely one of the things that made the original Super Mario Bros. thrive in arcades everywhere.

Sprinting

Sprinting in SMW works like a hidden P-Meter of SMB3: Mario must run for a fixed distance at max speed (spRun) to be able to enter the last and fastest speed range. Once he´s started running at spRun the P-Meter will start to fill and every frame that he´s not running it will quickly decrease (even in the air). This finally comes out as something like "Mario must run for at least 10 blocks to sprint". This way it´s really easy to take this into consideration when designing your levels, having direct control over when the player will be able to sprint or not.

Conclusion

All these tricks are really intelligent modifications that can be applied to any platformer game and that will only make it better, so there´s really no reason why you shouldn´t implement them in your game to achieve a great feeling.


Bonus 1: Automatic horizontal momentum cancellation

When moving horizontally and releasing the directional button, or pressing in the opposite direction, the player will progressively come to a halt. However this is hard to account for and can easily make the character feel uncontrollable, with the player constantly having to make small corrections to every movement.
We should check that the player isn´t going to run on an enemy or obstacle and if so, kill momentum immediately. Also, it may be a good idea to kill momentum if the player is about to fall through an edge in this same situation.

This should help a lot with precise movements, like when trying to climb a series f blocks stacked on top of each other.


Bonus 2: Double jump hold up

Sometimes when the player is falling down he may press the jump button just before touching the ground, which will trigger the double jump if available. The player may not have wanted this, so if he was counting on using the double jump after this one he´s screwed.

A nice way to solve this is by checking the distance the player has to the floor before triggering a double jump. If this distance is very small and the floor is safe (ie: no spikes or lava or whatever) we´ll hold the double jump until after we land (1-5 frames, needs testing). If the player was going to fall on an enemy, we can hold the jump until it touches the enemy and trigger a bounce as if the player was helding down the jump button to bounce high. If the floor is indeed hazardous, we´ll trigger the double jump immediately, as that must clearly be the player´s intention.

No comments:

Post a Comment