03-17-2025, 05:56 AM
Part 6 - Refining the Controls
The feel of player movement and the responsiveness of the controls are two of the most important aspects of a platformer. Before we get too much further building out the levels of our game we want to dial in our player controls and movement as much as possible. Otherwise, if we spend a bunch of time now building out our level and then find later that the jump is too low or too high or too "floaty" or we want to change the walk/run speed of our character this could cause us a lot of re-work to make the levels work with the new mechanics.
That being said, depending on the type of game we are making, the player movement that we have at the end of Part 5 might be totally serviceable, it might even be preferred. In this particular example, however, we want to aim for movement that is more like Mario's from Super Mario Bros on the NES.
The first thing we want to do to improve the feel of the player movement is to incorporate some acceleration. In our current logic, when the user presses left or right, the player character is immediately at full velocity. When the key is released, the movement immediately stops. This can leave the movement feeling a bit flat.
To incorporate acceleration, we can change our current implementation so that instead of full velocity being applied immediately when the direction key is applied, we instead increase the velocity a bit more each frame that the button is pressed and decrease the velocity a bit each frame after the key is released.
Let's start by adding some new constants for acceleration and max velocity:
Next, we need to make the same changes to our left movement logic:
Now we can add the logic to decelerate when the player is no longer pressing the left or right arrow keys.
Finally, after the end of our If..ElseIf..Else block, we can add a call to set the horizontal velocity of our player based on our updated vx variable:
If you try out the program now you will see that our player character takes a little bit to get up to full speed and when you release the arrow keys he'll continue moving for a step or two before he comes to a stop. You can experiment with the acceleration constant to get just the right feel. For example, if you set it lower to a value of 3, it will feel more like we're trying to run on a slipper surface like ice. (You can see an example of this type of movement in Sleighless which has Santa running around on the ice and snow.)
Alright, the horizontal movement is looking better, now let's look at adjusting a few more things to improve the feel of the player movement:
We can address the "floaty-ness" of the jump by increasing the speed of the jump and adding some new logic to juice up the effect of gravity a bit:
Finally, let's add some logic to strike a jump pose when our player is in the air and return to an idle pose when he has stopped moving:
If we run the program now, we will see that the controls and movement feel much more responsive. However, there is one issue that still remains. If we walk just a little past the edge of the center platform and stop, we seem to be levitating.
The reason for this has to do with the way collision detection works in GX. By default, it uses the entity's height and width to determine the bounding box when checking for collisions. Our player character has a default collision boundary that looks like this:
We can adjust this default bounding box with a call to the GXEntityCollisionOffset method which allows us to tell GX how many pixels in from the left, top, right and bottom sides of the entity we should offset the collision bounding box
Here now are all of the changes we covered in Part 6:
The feel of player movement and the responsiveness of the controls are two of the most important aspects of a platformer. Before we get too much further building out the levels of our game we want to dial in our player controls and movement as much as possible. Otherwise, if we spend a bunch of time now building out our level and then find later that the jump is too low or too high or too "floaty" or we want to change the walk/run speed of our character this could cause us a lot of re-work to make the levels work with the new mechanics.
That being said, depending on the type of game we are making, the player movement that we have at the end of Part 5 might be totally serviceable, it might even be preferred. In this particular example, however, we want to aim for movement that is more like Mario's from Super Mario Bros on the NES.
The first thing we want to do to improve the feel of the player movement is to incorporate some acceleration. In our current logic, when the user presses left or right, the player character is immediately at full velocity. When the key is released, the movement immediately stops. This can leave the movement feeling a bit flat.
To incorporate acceleration, we can change our current implementation so that instead of full velocity being applied immediately when the direction key is applied, we instead increase the velocity a bit more each frame that the button is pressed and decrease the velocity a bit each frame after the key is released.
Let's start by adding some new constants for acceleration and max velocity:
Code: (Select All)
...
'$Include: 'lib/gx.bi'
Const MOVE_RIGHT = 1
Const MOVE_LEFT = 2
Const ACCEL = 7
Const MAX_VELOCITY = 120
...
Let's also add a new global variable to track the current player direction:Code: (Select All)
...
Dim Shared direction As Integer
direction = MOVE_RIGHT
...
Next, we need to make a number of changes to our logic which handles player movement. Let's take the opportunity to move this logic from the OnUpdate method into a dedicated method named HandlePlayerEvents:Code: (Select All)
...
Sub OnUpdate (e As GXEvent)
HandlePlayerMovement e
End Sub
...
Sub HandlePlayerMovement (e As GXEvent)
If GXKeyDown(GXKEY_LEFT) Then
GXEntityVX player, -60
GXEntityAnimate player, MOVE_LEFT, 10
ElseIf GXKeyDown(GXKEY_RIGHT) Then
GXEntityVX player, 60
GXEntityAnimate player, MOVE_RIGHT, 10
Else
GXEntityVX player, 0
GXEntityAnimateStop player
End If
If GXEntityVY(player) = 0 And GXKeyDown(GXKEY_SPACEBAR) Then
GXEntityVY player, -120
End If
End Sub
...
Now, let's add some variables to use for our velocity adjustments and initialize them to the current player velocity:Code: (Select All)
...
Sub HandlePlayerMovement (e As GXEvent)
Dim As Integer vx, vy
vx = GXEntityVX(player)
vy = GXEntityVY(player)
...
Then, we'll incorporate the velocity changes, starting with the right movement. We'll replace our original simple algorithm...Code: (Select All)
...
ElseIf GXKeyDown(GXKEY_RIGHT) Then
GXEntityVX player, 60
...
...with the following:Code: (Select All)
...
ElseIf GXKeyDown(GXKEY_RIGHT) Then
vx = vx + ACCEL
If vx > MAX_VELOCITY Then vx = MAX_VELOCITY
direction = MOVE_RIGHT
...
With this change, we will increase the player's horizontal velocity by our acceleration constant (ACCEL). We'll add a condition to make sure that the velocity doesn't exceed our MAX_VELOCITY constant. Then we'll update our variable that is tracking the direction our player is facing.Next, we need to make the same changes to our left movement logic:
Code: (Select All)
...
If GXKeyDown(GXKEY_LEFT) Then
vx = vx - ACCEL
If vx < MAX_VELOCITY * -1 Then vx = MAX_VELOCITY * -1
direction = MOVE_LEFT
...
Now we can add the logic to decelerate when the player is no longer pressing the left or right arrow keys.
Code: (Select All)
...
' slow down when a direction key is not pressed
ElseIf vx > 0 Then
vx = vx - ACCEL
If vx < 0 Then vx = 0
ElseIf vx < 0 Then
vx = vx + ACCEL
If vx > 0 Then vx = 0
Else
...
Finally, after the end of our If..ElseIf..Else block, we can add a call to set the horizontal velocity of our player based on our updated vx variable:
Code: (Select All)
...
End If
GXEntityVX player, vx
...
If you try out the program now you will see that our player character takes a little bit to get up to full speed and when you release the arrow keys he'll continue moving for a step or two before he comes to a stop. You can experiment with the acceleration constant to get just the right feel. For example, if you set it lower to a value of 3, it will feel more like we're trying to run on a slipper surface like ice. (You can see an example of this type of movement in Sleighless which has Santa running around on the ice and snow.)
Alright, the horizontal movement is looking better, now let's look at adjusting a few more things to improve the feel of the player movement:
- The jump feels a little too "floaty" for what we are wanting in this game.
- The player seems to keep walking in the air instead of striking a jumping pose.
- The player isn't returning to an idle pose when he has stopped moving.
We can address the "floaty-ness" of the jump by increasing the speed of the jump and adding some new logic to juice up the effect of gravity a bit:
Code: (Select All)
...
If GXEntityVY(player) = 0 And GXKeyDown(GXKEY_SPACEBAR) Then
GXEntityVY player, -260
End If
' fall faster for a less "floaty" jump
If vy > 0 Then
GXEntityVY player, vy + 25
ElseIf vy < 0 Then
GXEntityVY player, vy + 10
End If
...
Finally, let's add some logic to strike a jump pose when our player is in the air and return to an idle pose when he has stopped moving:
Code: (Select All)
...
' show jump frame
If vy <> 0 Then
GXEntityAnimateStop player
GXEntityFrameSet player, direction, 2
End If
' idle
If vy = 0 And vx = 0 Then
GXEntityAnimateStop player
GXEntityFrameSet player, direction, 1
End If
...
If we run the program now, we will see that the controls and movement feel much more responsive. However, there is one issue that still remains. If we walk just a little past the edge of the center platform and stop, we seem to be levitating.
The reason for this has to do with the way collision detection works in GX. By default, it uses the entity's height and width to determine the bounding box when checking for collisions. Our player character has a default collision boundary that looks like this:
We can adjust this default bounding box with a call to the GXEntityCollisionOffset method which allows us to tell GX how many pixels in from the left, top, right and bottom sides of the entity we should offset the collision bounding box
Code: (Select All)
...
GXEntityApplyGravity player, GX_TRUE
GXEntityCollisionOffset player, 3, 5, 3, 0
GXSceneStart
...
Here now are all of the changes we covered in Part 6: