Quantcast
Channel: Envato Tuts+ Game Development
Viewing all articles
Browse latest Browse all 728

Basic 2D Platformer Physics, Part 1

$
0
0

Character Collisions

Alright, so the premise looks like this: we want to make a 2D platformer with simple, robust, responsive, accurate and predictable physics. We don't want to use a big 2D physics engine in this case, and there are a few reasons for this:

  • unpredictable collision responses
  • hard to set up accurate and robust character movement
  • much more complicated to work with
  • takes a lot more processing power than simple physics

Of course, there are also many pros to using an off-the-shelf physics engine, such as being able to set up complex physics interactions quite easily, but that's not what we need for our game.

A custom physics engine helps the game to have a custom feel to it, and that's really important! Even if you're going to start with a relatively basic setup, the way in which things will move and interact with each other will always be influenced by your own rules only, rather than someone else's. Let's get to it!

Character Bounds

Let's start by defining what kind of shapes we'll be using in our physics. One of the most basic shapes we can use to represent a physical object in a game is an Axis Aligned Bounding Box (AABB). AABB is basically an unrotated rectangle.

Example of an AABB

In a lot of platformer games, AABBs are enough to approximate the body of every object in the game. They are extremely effective, because it is very easy to calculate an overlap between AABBs and requires very little data—to describe an AABB, it's enough to know its center and size.

Without further ado, let's create a struct for our AABB.

As mentioned earlier, all we need here as far as data is concerned are two vectors; the first one will be the AABB's center, and the second one the half size. Why half size? Most of the time for calculations we'll need the half size anyway, so instead of calculating it every time we'll simply memorize it instead of the full size.

Let's start by adding a constructor, so it's possible to create the struct with custom parameters.

With this we can create the collision-checking functions. First, let's do a simple check whether two AABBs collide with each other. This is very simple—we just need to see whether the distance between the centers on each axis is less than the sum of half sizes.

Here's a picture demonstrating this check on the x axis; the y axis is checked in the same manner.

Demonstrating a check on the X-Axis

As you can see, if the sum of half sizes were to be smaller than the distance between the centers, no overlap would be possible. Notice that in the code above, we can escape the collision check early if we find that the objects do not overlap on the first axis. The overlap must exist on both axes, if the AABBs are to collide in 2D space.

Moving Object

Let's start by creating a class for an object that is influenced by the game's physics. Later on, we'll use this as a base for an actual player object. Let's call this class MovingObject.

Now let's fill this class with the data. We'll need quite a lot of information for this object:

  • Position and previous frame's Position
  • speed and previous frame's speed
  • scale
  • AABB and an offset for it (so we can align it with a sprite)
  • is object on the ground and whether it was on the ground last frame
  • is object next to the wall on the left and whether it was next to it last frame
  • is object next to the wall on the right and whether it was next to it last frame
  • is object at the ceiling and whether it was at the ceiling last frame

Position, speed and scale are 2D vectors.

Now let's add the AABB and the offset. The offset is needed so we can freely match the AABB to the object's sprite.

And finally, let's declare the variables which indicate the position state of the object, whether it is on the ground, next to a wall or at the ceiling. These are very important because they will let us know whether we can jump or, for example, need to play a sound after bumping into a wall.

These are the basics. Now, let's create a function that will update the object. For now we won't be setting everything up, but just enough so we can start creating basic character controls.

The first thing we'll want to do here is to save the previous frame's data to the appropriate variables.

Now let's update the position using the current speed.

And just for now, let's make it so that if the vertical position is less than zero, we assume the character's on the ground. This is just for now, so we can set up the character's controls. Later on, we'll do a collision with a tilemap.

After this, we need to also update AABB's center, so it actually matches the new position.

For the demo project, I'm using Unity, and to update the position of the object it needs to be applied to the transform component, so let's do that as well. The same needs to be done for the scale.

As you can see, the rendered position is rounded up. This is to make sure the rendered character is always snapped to a pixel.

Character Controls

Data

Now that we have our basic MovingObject class done, we can start by playing with the character movement. It's a very important part of the game, after all, and can be done pretty much right away—no need to delve too deep into the game systems just yet, and it'll be ready when we'll need to test our character-map collisions.

First, let's create a Character class and derive it from the MovingObject class.

We'll need to handle a few things here. First of all, the inputs—let's make an enum which will cover all of the controls for the character. Let's create it in another file and call it KeyInput. 

As you can see, our character can move left, right, down and jump up. Moving down will work only on one-way platforms, when we want to fall through them.

Now let's declare two arrays in the Character class, one for the current frame's inputs and another for the previous frame's. Depending on a game, this setup may make more or less sense. Usually, instead of saving the key state to an array, it is checked on demand using an engine's or framework's specific functions. However, having an array which is not strictly bound to real input may be beneficial, if for example we want to simulate key presses.

These arrays will be indexed by the KeyInput enum. To easily use those arrays, let's create a few functions that will help us check for a specific key.

Nothing special here—we want to be able to see whether a key was just pressed, just released, or if it's on or off.

Now let's create another enum which will hold all of the character's possible states.

As you can see, our character can either stand still, walk, jump, or grab a ledge. Now that this is done, we need to add variables such as jump speed, walk speed, and current state.

Of course there's some more data needed here such as character sprite, but how this is going to look depends a lot on what kind of engine you're going to use. Since I'm using Unity, I'll be using a reference to an Animator to make sure the sprite plays animation for an appropriate state.

Update Loop

Alright, now we can start the work on the update loop. What we'll be doing here will depend on the current state of the character.

Stand State

Let's start by filling up what should be done when the character is not moving—in the stand state. First of all, the speed should be set to zero.

We also want to show the appropriate sprite for the state.

Now, if the character is not on the ground, it can no longer stand, so we need to change the state to jump.

If the GoLeft or GoRight key is pressed, then we'll need to change our state to walk.

In case the Jump key is pressed, we want to set the vertical speed to the jump speed and change the state to jump.

That's going to be it for this state, at least for now. 

Walk State

Now let's create a logic for moving on the ground, and right away start playing the walking animation.

Here, if we don't press the left or right button or both of these are pressed, we want to go back to the standing still state.

If the GoRight key is pressed, we need to set the horizontal speed to mWalkSpeed and make sure that the sprite is scaled appropriately—the horizontal scale needs to be changed if we want to flip the sprite horizontally. 

We also should move only if there is actually no obstacle ahead, so if mPushesRightWall is set to true, then the horizontal speed should be set to zero if we're moving right.

We also need to handle the left side in the same way.

As we did for the standing state, we need to see if a jump button is pressed, and set the vertical speed if that is so.

Otherwise, if the character is not on the ground then it needs to change the state to jump as well, but without an addition of vertical speed, so it simply falls down.

That's it for the walking. Let's move to the jump state.

Jump State

Let's start by setting an appropriate animation for the sprite.

In the Jump state, we need to add gravity to the character's speed, so it goes faster and faster towards the ground.

But it would be sensible to add a limit, so the character cannot fall too fast.

In many games, when the character is in the air, the maneuverability decreases, but we'll go for some very simple and accurate controls which allow for full flexibility when in the air. So if we press the GoLeft or GoRight key, the character moves in the direction while jumping as fast as it would if it were on the ground. In this case we can simply copy the movement logic from the walking state.

Finally, we're going to make the jump higher if the jump button is pressed longer. To do this, what we'll actually do is make the jump lower if the jump button is not pressed. 

As you can see, if the jump key is not pressed and the vertical speed is positive, then we clamp the speed to the max value of cMinJumpSpeed (200 pixels per second). This means that if we were to just tap the jump button, the speed of the jump, instead of being equal to mJumpSpeed (410 by default), will get lowered to 200, and therefore the jump will be shorter.

Since we don't have any level geometry yet, we should skip the GrabLedge implementation for now.

Update the Previous Inputs

Once the frame is all finished, we can update the previous inputs. Let's create a new function for this. All we'll need to do here is move the key state values from the mInputs array to the mPrevInputs array.

At the very end of the CharacterUpdate function, we still need to do a couple of things. The first is to update the physics.

Now that the physics is updated, we can see if we should play any sound. We want to play a sound when the character bumps any surface, but right now it can only hit the ground because the collision with tilemap is not implemented yet. 

Let's check if the character has just fallen onto the ground. It's very easy to do so with the current setup—we just need to look up if the character is on the ground right now, but wasn't in the previous frame.

Finally, let's update the previous inputs.

All in all, this is how the CharacterUpdate function should look now, with minor differences depending on the kind of engine or framework you're using.

Init the Character

Let's write an Init function for the character. This function will take the input arrays as the parameters. We will supply these from the manager class later on. Other than this, we need to do things like:

  • assign the scale
  • assign the jump speed
  • assign the walk speed
  • set the initial position
  • set the AABB

We'll be using a few of the defined constants here.

In the case of the demo, we can set the initial position to the position in the editor.

For the AABB, we need to set the offset and the half size. The offset in the case of the demo's sprite needs to be just the half size.

Now we can take care of the rest of the variables.

We need to call this function from the game manager. The manager can be set up in many ways, all depending on the tools you're using, but in general the idea is the same. In the manager's init, we need to create the input arrays, create a player, and init it.

Additionally, in the manager's update, we need to update the player and player's inputs.

Note that we update the character's physics in the fixed update. This will make sure that the jumps will always be the same height, no matter what frame rate our game works with. There's an excellent article by Glenn Fiedler on how to fix the timestep, in case you're not using Unity.

Test the Character Controller

At this point we can test the character's movement and see how it feels. If we don't like it, we can always change the parameters or the way the speed is changed upon key presses.

An animation of the character controller

Summary

The character controls may seem very weightless and not as pleasant as a momentum-based movement for some, but this is all a matter of what kind of controls would suit your game best. Fortunately, just changing the way the character moves is fairly easy; it's enough to modify how the speed value changes in the walk and jump states.

That's it for the first part of the series. We've ended up with a simple character movement scheme, but not much more. The most important thing is that we laid out the way for the next part, in which we'll make the character interact with a tilemap.


Viewing all articles
Browse latest Browse all 728

Trending Articles