Platform games don't follow the laws of physics. Here's a few reasons.
1. We want the player to be able to move horizontally in the air. This lets the player tweak movement and land much more difficult jumps. This breaks the 1st law of Thermodynamics.
2. We want the jump (and the pull of gravity) to be limited by a max velocity. There are a few reasons for this. One, it makes the gameplay a bit more manageable to not be flying all over the place. Second, the algorithms of collision detection start to break down if the player (or any other object) moves faster than the height of a tile in a single update of the game loop.
3. We want the jump to have a constant upward motion, instead of a large initial velocity immediately starting to taper off by gravity. As long as a player presses the jump key (within limits), we want the player to jump upwards.
So let's look at the pieces needed to implement a jump:
For Platform Hack, I wanted jumps to be pretty versatile. Which means there will be double (and even triple) jumps, wall jumps and upgrades to jump power. So I needed a data type that could store and abstract away the details of the jump:
public struct JumpStruct
{
public int curNumJump;
public int totalNumJump;
public bool isJumping;
public bool wasJumping;
public float jumpTime;
public TimeSpan jumpTimer;
}
In the struct we have curNumJump and totalNumJump. This is used to keep track of double and triple jumps.
isJumping is a boolean thats mapped to the player jump key. wasJumping is a boolean that specifies whether the player was pressing jump *the previous update*. This is important for making sure the player presses the jump key for each jump instead of just holding it down.
jumpTime is the total amount of time the player can jump. Increasing this number allows the player to press down the jump key longer, and have larger jumps. jumpTimer is the actual timer that counts down as soon as the jump starts.
During each game loop, we must run the update on the player physics:
GravityVector.Y = MathHelper.Clamp(velocity.Y + GravityAcceleration * elapsed, -MaxFallSpeed, MaxFallSpeed);
PlayerVector.Y = DoJump(PlayerVector.Y, gameTime);
PlayerVector.X += MathHelper.Clamp(movement * MoveAcceleration * elapsed, -MaxMoveSpeed, MaxMoveSpeed);
velocity = GravityVector + PlayerVector + Force1Vector + Force2Vector;
velocity.X = MathHelper.Clamp(velocity.X, -MaxMoveSpeed, MaxMoveSpeed);
velocity.Y = MathHelper.Clamp(velocity.Y, -MaxMoveSpeed, MaxMoveSpeed);
Position += velocity * elapsed;
Position = new Vector2((float)Math.Round(Position.X), (float)Math.Round(Position.Y));
One thing I've done is kept the various velocities (GravityVector, PlayerVector) seperate, and then combined them before applying to the player movement. However, when I update gravity, it draws from the full velocity. This little tweak helps smooth out the upward curve of the player jump, or if they fall off a ledge. Gravity is also "clamped" at MaxFallSpeed, which acts as a sort of terminal velocity. Clamp is important for reason #2 above.
The Player.X velocity (horizontal movement) is updated by player control - the movement variable is set with the left and right buttons. This allows the player to move horizontally even if they are in the air.
The Player.X veloctiy is set with the DoJump function:
private float DoJump(float VelocityY, GameTime gametime)
{
VelocityY = 0;
if (JumpCheck(gametime, IsOnGround))
{
if ((IsOnGround) || playerJumpStruct.jumpTimer > TimeSpan.Zero)
{
if (playerJumpStruct.jumpTimer.TotalSeconds == playerJumpStruct.jumpTime)
{
level.thisPlatformerGame.gameAudio.playSound("PlayerJump");
}
}
// if we are in the ascent of the jump
if (TimeSpan.Zero < playerJumpStruct.jumpTimer && playerJumpStruct.jumpTimer <= TimeSpan.FromSeconds(playerJumpStruct.jumpTime))
{
GravityVector.Y = 0;
// fully override the vertical velocity with a power curve that gives players more control over the top of the jump
VelocityY = JumpLaunchVelocity * (1.0f - (float)Math.Pow((playerJumpStruct.jumpTime-playerJumpStruct.jumpTimer.TotalSeconds) / playerJumpStruct.jumpTime, JumpControlPower));
}
}
return VelocityY;
}
Essentially, this function first checks if we can jump. JumpCheck returns true if the player is on the ground, or the curNumJumps is less than totalNumJumps.
If we're on the ground, or the jump timer is less than the total jump time, then set the Player.Y velocity. We're using a power function to give the player a sort of exponential curve to upward velocity, starting with the most power and decreasing as we reach the top. We also are setting Gravity to 0 to give the jump a bit more "floatiness".
Once the DoJump returns, we have to calculate the player's new position. Velocity (and acceleration values - JumpAcceleration, GravityAcceleration) is stored in terms of seconds (500 pixels / second). Each update function only takes a fraction of a second, so we need to multiple the velocity by elapsed, which will give the appropriate amount to move the player.
Lastly, we round the player position to the nearest round number, so the sprite is positioned cleanly on the pixels. This is essential for player collision - otherwise the player can get stuck in floors and walls due to a fraction of a pixel overlapping.
The *feel* of the jump is essential to how the game plays. Stronger gravity and less precision of player movement can make the game much more difficult. Conversely, if gravity is too weak and the player is too floaty, the game can feel slow, frustrating or boring. Tweaking the GravityAcceleration, JumpAcceleration and Clamp values can drastically change the feel of the jump.
Next: I'll go into the details of the infamous grappling hook!
Note on Code samples: I'm more interested exploring algorithms and general concepts on this blog rather than complete code samples. The codebase I'm working with is a bit messy to upload for each example. If you want to follow along, start with the Platformer Starter Kit for XNA and go from there.
No comments:
Post a Comment