Advertisement
  1. Game Development
  2. Platformer

Basic 2D Platformer Physics, Part 1

Scroll to top
Read Time: 20 min
This post is part of a series called Basic 2D Platformer Physics .
Basic 2D Platformer Physics, Part 2

This series will cover how to create a simple and robust physics system for a platformer game. In this part, we'll look at character collision data.

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 AABBExample of an AABBExample 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.

1
public struct AABB
2
{
3
}

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.

1
public struct AABB
2
{
3
    public Vector2 center;
4
	public Vector2 halfSize;
5
}

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

1
public AABB(Vector2 center, Vector2 halfSize)
2
{
3
    this.center = center;
4
    this.halfSize = halfSize;
5
}

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.

1
public bool Overlaps(AABB other)
2
{
3
	if ( Mathf.Abs(center.x - other.center.x) > halfSize.x + other.halfSize.x ) return false;
4
	if ( Mathf.Abs(center.y - other.center.y) > halfSize.y + other.halfSize.y ) return false;
5
	return true;
6
}

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-AxisDemonstrating a check on the X-AxisDemonstrating 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.

1
public class MovingObject
2
{
3
}

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.

1
public class MovingObject
2
{
3
    public Vector2 mOldPosition;
4
	public Vector2 mPosition;
5
    
6
    public Vector2 mOldSpeed;
7
    public Vector2 mSpeed;
8
    
9
    public Vector2 mScale;
10
}

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.

1
public AABB mAABB;
2
public Vector2 mAABBOffset;

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.

1
public bool mPushedRightWall;
2
public bool mPushesRightWall;
3
4
public bool mPushedLeftWall;
5
public bool mPushesLeftWall;
6
7
public bool mWasOnGround;
8
public bool mOnGround;
9
10
public bool mWasAtCeiling;
11
public bool mAtCeiling;

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.

1
public void UpdatePhysics()
2
{	
3
}

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

1
public void UpdatePhysics()
2
{
3
    mOldPosition = mPosition;
4
    mOldSpeed = mSpeed;
5
    
6
    mWasOnGround = mOnGround;
7
	mPushedRightWall = mPushesRightWall;
8
	mPushedLeftWall = mPushesLeftWall;
9
	mWasAtCeiling = mAtCeiling;
10
}

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

1
mPosition += mSpeed*Time.deltaTime;

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.

1
if (mPosition.y < 0.0f)
2
{
3
    mPosition.y = 0.0f;
4
    mOnGround = true;
5
}
6
else
7
    mOnGround = false;

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

1
mAABB.center = mPosition + mAABBOffset;

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.

1
mTransform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y),-1.0f);
2
mTransform.localScale = new Vector3(mScale.x, mScale.y, 1.0f);

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.

1
public class Character : MovingObject
2
{
3
}

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. 

1
public enum KeyInput
2
{
3
    GoLeft = 0,
4
	GoRight,
5
	GoDown,
6
	Jump,
7
	Count
8
}

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.

1
protected bool[] mInputs;
2
protected bool[] mPrevInputs;

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.

1
protected bool Released(KeyInput key)
2
{
3
    return (!mInputs[(int)key] && mPrevInputs[(int)key]);
4
}
5
6
protected bool KeyState(KeyInput key)
7
{
8
    return (mInputs[(int)key]);
9
}
10
11
protected bool Pressed(KeyInput key)
12
{
13
    return (mInputs[(int)key] && !mPrevInputs[(int)key]);
14
}

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.

1
public enum CharacterState
2
{
3
    Stand,
4
    Walk,
5
    Jump,
6
    GrabLedge,
7
};

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.

1
public CharacterState mCurrentState = CharacterState.Stand;
2
public float mJumpSpeed;
3
public float mWalkSpeed;

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.

1
public void CharacterUpdate()
2
{
3
    switch (mCurrentState)
4
    {
5
        case CharacterState.Stand:
6
            break;
7
        case CharacterState.Walk:
8
            break;
9
        case CharacterState.Jump:
10
            break;
11
        case CharacterState.GrabLedge:
12
            break;
13
    }
14
}

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.

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    break;

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

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    mAnimator.Play("Stand");
4
    break;

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

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    mAnimator.Play("Stand");
4
    
5
    if (!mOnGround)
6
    {
7
        mCurrentState = CharacterState.Jump;
8
        break;
9
    }
10
    break;

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

1
case CharacterState.Stand:
2
    mSpeed = Vector2.zero;
3
    mAnimator.Play("Stand");
4
    
5
    if (!mOnGround)
6
    {
7
        mCurrentState = CharacterState.Jump;
8
        break;
9
    }
10
    
11
    if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft))
12
    {
13
        mCurrentState = CharacterState.Walk;
14
        break
15
    }
16
    break;

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

1
if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft))
2
{
3
    mCurrentState = CharacterState.Walk;
4
    break;
5
}
6
else if (KeyState(KeyInput.Jump))
7
{
8
    mSpeed.y = mJumpSpeed;
9
    mCurrentState = CharacterState.Jump;
10
    break;
11
}

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.

1
case CharacterState.Walk:
2
    mAnimator.Play("Walk");
3
    break;

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.

1
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
2
{
3
    mCurrentState = CharacterState.Stand;
4
    mSpeed = Vector2.zero;
5
    break;
6
}

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.

1
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
2
{
3
    mCurrentState = CharacterState.Stand;
4
    mSpeed = Vector2.zero;
5
    break;
6
}
7
else if (KeyState(KeyInput.GoRight))
8
{
9
    if (mPushesRightWall)
10
        mSpeed.x = 0.0f;
11
    else
12
        mSpeed.x = mWalkSpeed;
13
        
14
    mScale.x = Mathf.Abs(mScale.x);
15
}
16
else if (KeyState(KeyInput.GoLeft))
17
{
18
    if (mPushesLeftWall)
19
        mSpeed.x = 0.0f;
20
    else
21
        mSpeed.x = -mWalkSpeed;
22
23
    mScale.x = -Mathf.Abs(mScale.x);
24
}

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.

1
if (KeyState(KeyInput.Jump))
2
{
3
    mSpeed.y = mJumpSpeed;
4
    mAudioSource.PlayOneShot(mJumpSfx, 1.0f);
5
    mCurrentState = CharacterState.Jump;
6
    break;
7
}

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.

1
if (KeyState(KeyInput.Jump))
2
{
3
    mSpeed.y = mJumpSpeed;
4
    mAudioSource.PlayOneShot(mJumpSfx, 1.0f);
5
    mCurrentState = CharacterState.Jump;
6
    break;
7
}
8
else if (!mOnGround)
9
{
10
    mCurrentState = CharacterState.Jump;
11
    break;
12
}

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.

1
mAnimator.Play("Jump");

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

1
mSpeed.y += Constants.cGravity * Time.deltaTime;

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

1
mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed);

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.

1
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
2
{
3
    mSpeed.x = 0.0f;
4
}
5
else if (KeyState(KeyInput.GoRight))
6
{
7
    if (mPushesRightWall)
8
        mSpeed.x = 0.0f;
9
    else
10
        mSpeed.x = mWalkSpeed;
11
    mScale.x = Mathf.Abs(mScale.x);
12
}
13
else if (KeyState(KeyInput.GoLeft))
14
{
15
    if (mPushesLeftWall)
16
        mSpeed.x = 0.0f;
17
    else
18
        mSpeed.x = -mWalkSpeed;
19
    mScale.x = -Mathf.Abs(mScale.x);
20
}

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. 

1
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f)
2
    mSpeed.y = Mathf.Min(mSpeed.y, Constants.cMinJumpSpeed);

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.

1
public void UpdatePrevInputs()
2
{
3
    var count = (byte)KeyInput.Count;
4
5
    for (byte i = 0; i < count; ++i)
6
        mPrevInputs[i] = mInputs[i];
7
}

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

1
UpdatePhysics();

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.

1
if (mOnGround && !mWasOnGround)
2
    mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);

Finally, let's update the previous inputs.

1
UpdatePrevInputs();

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.

1
public void CharacterUpdate()
2
{
3
    switch (mCurrentState)
4
    {
5
        case CharacterState.Stand:
6
7
            mWalkSfxTimer = cWalkSfxTime;
8
            mAnimator.Play("Stand");
9
10
            mSpeed = Vector2.zero;
11
12
            if (!mOnGround)
13
            {
14
                mCurrentState = CharacterState.Jump;
15
                break;
16
            }
17
18
            //if left or right key is pressed, but not both

19
            if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft))
20
            {
21
                mCurrentState = CharacterState.Walk;
22
                break;
23
            }
24
            else if (KeyState(KeyInput.Jump))
25
            {
26
                mSpeed.y = mJumpSpeed;
27
                mAudioSource.PlayOneShot(mJumpSfx);
28
                mCurrentState = CharacterState.Jump;
29
                break;
30
            }
31
32
            break;
33
        case CharacterState.Walk:
34
            mAnimator.Play("Walk");
35
36
            mWalkSfxTimer += Time.deltaTime;
37
38
            if (mWalkSfxTimer > cWalkSfxTime)
39
            {
40
                mWalkSfxTimer = 0.0f;
41
                mAudioSource.PlayOneShot(mWalkSfx);
42
            }
43
44
            //if both or neither left nor right keys are pressed then stop walking and stand

45
46
            if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
47
            {
48
                mCurrentState = CharacterState.Stand;
49
                mSpeed = Vector2.zero;
50
                break;
51
            }
52
            else if (KeyState(KeyInput.GoRight))
53
            {
54
                if (mPushesRightWall)
55
                    mSpeed.x = 0.0f;
56
                else
57
                    mSpeed.x = mWalkSpeed;
58
                mScale.x = -Mathf.Abs(mScale.x);
59
            }
60
            else if (KeyState(KeyInput.GoLeft))
61
            {
62
                if (mPushesLeftWall)
63
                    mSpeed.x = 0.0f;
64
                else
65
                    mSpeed.x = -mWalkSpeed;
66
                mScale.x = Mathf.Abs(mScale.x);
67
            }
68
69
            //if there's no tile to walk on, fall

70
            if (KeyState(KeyInput.Jump))
71
            {
72
                mSpeed.y = mJumpSpeed;
73
                mAudioSource.PlayOneShot(mJumpSfx, 1.0f);
74
                mCurrentState = CharacterState.Jump;
75
                break;
76
            }
77
            else if (!mOnGround)
78
            {
79
                mCurrentState = CharacterState.Jump;
80
                break;
81
            }
82
83
            break;
84
        case CharacterState.Jump:
85
86
            mWalkSfxTimer = cWalkSfxTime;
87
88
            mAnimator.Play("Jump");
89
90
            mSpeed.y += Constants.cGravity * Time.deltaTime;
91
92
            mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed);
93
94
            if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f)
95
            {
96
                mSpeed.y = Mathf.Min(mSpeed.y, 200.0f);
97
            }
98
99
            if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft))
100
            {
101
                mSpeed.x = 0.0f;
102
            }
103
            else if (KeyState(KeyInput.GoRight))
104
            {
105
                if (mPushesRightWall)
106
                    mSpeed.x = 0.0f;
107
                else
108
                    mSpeed.x = mWalkSpeed;
109
                mScale.x = -Mathf.Abs(mScale.x);
110
            }
111
            else if (KeyState(KeyInput.GoLeft))
112
            {
113
                if (mPushesLeftWall)
114
                    mSpeed.x = 0.0f;
115
                else
116
                    mSpeed.x = -mWalkSpeed;
117
                mScale.x = Mathf.Abs(mScale.x);
118
            }
119
120
            //if we hit the ground

121
            if (mOnGround)
122
            {
123
                //if there's no movement change state to standing

124
                if (mInputs[(int)KeyInput.GoRight] == mInputs[(int)KeyInput.GoLeft])
125
                {
126
                    mCurrentState = CharacterState.Stand;
127
                    mSpeed = Vector2.zero;
128
                    mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);
129
                }
130
                else    //either go right or go left are pressed so we change the state to walk

131
                {
132
                    mCurrentState = CharacterState.Walk;
133
                    mSpeed.y = 0.0f;
134
                    mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);
135
                }
136
            }
137
            break;
138
139
        case CharacterState.GrabLedge:
140
            break;
141
    }
142
143
    UpdatePhysics();
144
145
    if ((!mWasOnGround && mOnGround)
146
        || (!mWasAtCeiling && mAtCeiling)
147
        || (!mPushedLeftWall && mPushesLeftWall)
148
        || (!mPushedRightWall && mPushesRightWall))
149
        mAudioSource.PlayOneShot(mHitWallSfx, 0.5f);
150
151
    UpdatePrevInputs();
152
}

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
1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
}

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

1
public const float cWalkSpeed = 160.0f;
2
public const float cJumpSpeed = 410.0f;
3
public const float cMinJumpSpeed = 200.0f;
4
public const float cHalfSizeY = 20.0f;
5
public const float cHalfSizeX = 6.0f;

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

1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
    mPosition = transform.position;
4
}

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.

1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
    mPosition = transform.position;
4
    mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY);
5
    mAABBOffset.y = mAABB.halfSize.y;
6
}

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

1
public void CharacterInit(bool[] inputs, bool[] prevInputs)
2
{
3
    mPosition = transform.position;
4
    mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY);
5
    mAABBOffset.y = mAABB.halfSize.y;
6
    
7
    mInputs = inputs;
8
    mPrevInputs = prevInputs;
9
    
10
    mJumpSpeed = Constants.cJumpSpeed;
11
    mWalkSpeed = Constants.cWalkSpeed;
12
    
13
    mScale = Vector2.one;
14
}

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.

1
public class Game
2
{
3
    public Character mPlayer;
4
    bool[] mInputs;
5
    bool[] mPrevInputs;
6
    
7
    void Start ()
8
    {
9
        inputs = new bool[(int)KeyInput.Count];
10
        prevInputs = new bool[(int)KeyInput.Count];
11
12
        player.CharacterInit(inputs, prevInputs);
13
    }
14
}

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

1
void Update()
2
{
3
    inputs[(int)KeyInput.GoRight] = Input.GetKey(goRightKey);
4
    inputs[(int)KeyInput.GoLeft] = Input.GetKey(goLeftKey);
5
    inputs[(int)KeyInput.GoDown] = Input.GetKey(goDownKey);
6
    inputs[(int)KeyInput.Jump] = Input.GetKey(goJumpKey);
7
}
8
9
void FixedUpdate()
10
{
11
    player.CharacterUpdate();
12
}

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 controllerAn animation of the character controllerAn 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.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.