paint-brush
Using Collision Detection to Make Your Game Character Jumpby@eugene-kleshnin
1,474 reads
1,474 reads

Using Collision Detection to Make Your Game Character Jump

by Eugene KleshninMarch 12th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

This is Part 3 of the series, where I learn to create a simple platformer game using Flame Engine. In this part, we’re gonna add vertical movement with the help of collision detection. In the last parts, we created a game level with a parallax background, added a player character, and taught him to run.

People Mentioned

Mention Thumbnail
featured image - Using Collision Detection to Make Your Game Character Jump
Eugene Kleshnin HackerNoon profile picture

This is Part 3 of the series, where I learn to create a simple platformer game using

Flame Engine.


In the last parts (1, 2), we created a game level with a parallax background, added a player character, The Boy, and taught him to run. Of course, any platformer would be boring without jumping, so in this part, we’re gonna add vertical movement with the help of collision detection.

Adding gravity

The first thing we need to do is to add the force that constantly pulls the player to the ground.

I need to say, that if your game requires complex physics, it might be worth exploring Forge2D - the physics engine module for Flame. It deserves a series of its own, so to keep it simple in this series we’ll replicate gravity with just the means of Flame.


Add several new constants to the top of the TheBoy component:

final double _gravity = 15; // How fast The Boy gets pull down
final double _jumpSpeed = 500; // How high The Boy jumps
final double _maxGravitySpeed = 300; // Max speed The Boy can have when falling


Now, let’s apply it to the player’s y velocity by adding these two lines to the update method, right after setting velocity.x:

_velocity.y += _gravity;
_velocity.y = _velocity.y.clamp(-_jumpSpeed, _maxGravitySpeed);


The first line increments the vertical velocity of The Boy each game cycle by the _gravity amount. The second line limits the velocity to be between the _jumpSpeed and _maxGravitySpeed in order to avoid unlimited acceleration.


If we run the game now, we’ll see that The Boy falls through the ground. At this point, it’s the expected behavior, because the game doesn’t know yet, that the ground should be solid. For the game engine, our level is just a set of sprites, and we need to tell it, which tiles are the platforms. Luckily, it could be done, with the Tiled editor, which we used to create the level in Part 2.


Adding platforms

Return to the Tiled. As you may have noticed, a Tiled map has different layers, similar to Adobe Photoshop. We already have a Tile layer, that contains tiles for our level. Now, we’ll add a new Object layer, which will represent the boundaries of our platforms. Name it “Platforms”


New Object Layer


Now, select this layer, and using the Rectangle tool, draw rectangles where you placed your platforms and ground tiles. Once you’re finished, if you hide your Tile layer, it should be like this:


The objects layer with platforms positions


What we did, is we added the “invisible” layer, that holds the information on the position of the platforms. Next, we’ll use the game engine to translate those into actual components, that we can later use for collision detection. Save the level file and return to the IDE.


Let’s add the component that represents the platform to the objects folder:

class Platform extends PositionComponent {

  Platform(Vector2 position, Vector2 size) : super(position: position, size: size);

  @override
  Future<void> onLoad() async {
    return super.onLoad();
  }
}


Now, go to the game.dart and add a new method:

void spawnObjects(RenderableTiledMap tileMap) {
    final platforms = tileMap.getLayer<ObjectGroup>("Platforms");

    for (final platform in platforms!.objects) {
      add(Platform(Vector2(platform.x, platform.y), Vector2(platform.width, platform.height)));
    }
  }


And call it from the onLoad after we added our level component:

spawnObjects(level.tileMap);


Let’s go through what happened here. We took our tilemap object and fetched the layer we just created by its name. Be aware that the key here should exactly match the layer name in Tiled: tileMap.getLayer<ObjectGroup>("Platforms").


Then, we iterate through all the objects in this layer, and for each of them create a Platform component with the same position and size.

Now our game is populated with Platforms, that correspond to the platform tiles the player sees. However, they do nothing for now, and for them to work we need to add collision detection.


Detect collisions

Collision detection is a very common problem in game dev. Essentially, it means that when two game objects intersect, we want to trigger some events. For example, a bullet hits the target, two cars crash into each other, or The Boy lands on a platform.


The Flame engine provides a useful API to handle collision detection. First, let’s do some prep work. Open game.dart and add HasCollisionDetection mixin to tell the engine that we want to track collisions:

class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection


Then, go to the Platform class and add a hitbox, the component’s area we want to use for collision detection:

@override
  Future<void> onLoad() async {
    add(RectangleHitbox()..collisionType = CollisionType.passive);
    return super.onLoad();
  }


There’re three types of collisions:

  • Active - can collide with other hitboxes of Active and Passive type
  • Passive - can collide only with Active hitboxes
  • Inactive - collision detection is disabled


Since platforms will only collide with The Boy, their type is passive.

Let’s go to theboy.dart and add a hitbox for him as well to the end of onLoad:

add(CircleHitbox());


Here we’re using a Circle hitbox because it fits the shape of the character better, but also it’d be easier to detect the collision for this shape. But more about it later.


Add the CollisionCallbacks mixin to TheBoy:

class TheBoyPlayer extends SpriteAnimationComponent
    with KeyboardHandler, CollisionCallbacks, HasGameRef<PlatformerGame>


And override the method onCollision

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other)


This method will be triggered every time TheBoy component collides with another component.

The first param is the points where two components intersect, and the second param is the component TheBoy collides with.


The collision handling method I will use is the one described in the DevKage’s tutorial (link at the end of the story) with slight improvements. I should mention, that there are a bunch of ways a collision could be resolved. I’m gonna use a rather simple one that does a decent job, but of course, it could be improved further.


The collision of the player’s circle hitbox and the platforms’ rectangular hitbox at the moment onCollision is called, could be schematized like this:

Collision schema


The problem here is that hitboxes intersect and in each game loop, the player will continue to sink into the platform. To avoid that, we need to calculate how deep the circle hitbox sank into the rectangular and move it up the same amount of pixels. That will create the illusion of the player standing on the platform.

Calculation of the penetration depth


We know the circle radius, and intersection points, so we can calculate the penetration depth:

@override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    if (other is Platform) {
      if (intersectionPoints.length == 2) {
        final mid = (intersectionPoints.elementAt(0) +
            intersectionPoints.elementAt(1)) / 2;

        final collisionVector = absoluteCenter - mid;
        double penetrationDepth = (size.x / 2) - collisionVector.length;

        collisionVector.normalize();
        position += collisionVector.scaled(penetrationDepth);
      }
    }

    super.onCollision(intersectionPoints, other);
  }


First, we calculate the middle point between the points of intersection (mid). Then, using the circle center and mid we calculate the collision vector - the direction The Boy moves towards the platform. Next, using the radius (size.x / 2) and length of collisionVector we calculate how deep the circle hitbox is penetrating the rectangular platform. Finally, we normalize the vector to have just the direction of the collision and update the player’s position by penetrationDepth multiplied by the normalized vector to keep the direction.


If we run the game, The Boy doesn’t fall through the platform anymore. And if he moves forward, the wall stops him.


The Boy no longer falls through the ground


Jumping

Let’s add jumping. First, we need to detect if the arrow key was pressed. Add a new variable to the TheBoy class:

bool _hasJumped = false;


Modify the onKeyEvent method and add the following to the bottom:

_hasJumped = keysPressed.contains(LogicalKeyboardKey.keyW) || keysPressed.contains(LogicalKeyboardKey.arrowUp);


Next, edit the update method and add this right after applying gravity:

if (_hasJumped) {
      _velocity.y = -_jumpSpeed;
      _hasJumped = false;
}


Here we check if the arrow key was pressed, and update The Boy’s y velocity by the _jumpSpeed

If you run the game now, you’ll see that The Boy learned how to jump! The only problem is that if you press the arrow key mid-jump, he will jump again. We need to fix that.


We’re gonna return to the onCollision method and save the component The Boy is standing on. Then, when the collision ends, we’re gonna clear this reference. Lastly, we’re gonna check if the reference is not empty before applying our jump logic. This will allow The Boy to jump only when there’s an active collision with the ground.


Add these variables to the TheBoy class:

Component? _standingOn; // The component The Boy is currently standing on
final Vector2 up = Vector2(0, -1); // Up direction vector we're gonna use to determine if The Boy is on the ground
final Vector2 down = Vector2(0, 1); // Down direction vector we're gonna use to determine if The Boy hit the platform above


Go to onCollision method and add the following right after collisionVector normalization:

if (up.dot(collisionVector) > 0.9) {
  _standingOn = other;
}


Here we check if The Boy is colliding with the platform below him and save the reference to that component.


Next, add onCollisionEnd method implementation, which will be triggered every time The Boy stops colliding with a platform:

@override
  void onCollisionEnd(PositionComponent other) {
    if (other == _standingOn) {
      _standingOn = null;
    }
    super.onCollisionEnd(other);
  }


Finally, modify the jumping logic:

if (_hasJumped) {
  if (_standingOn != null) {
    _velocity.y = -_jumpSpeed;
  }
  _hasJumped = false;
}


Cool, The Boy should be allowed to jump only once now. But there’s one more improvement we can add. If you jump below a platform, you’ll notice that The Boy hangs for a second. To avoid that, we’re gonna check if The Boy is colliding with a platform on the top, similarly as we did with the ground. Then we’re gonna bounce The Boy by modifying his vertical velocity.


Add else branch to the condition we added earlier:

if (up.dot(collisionVector) > 0.9) {
  _standingOn = other;
} else if (down.dot(collisionVector) > 0.9) {
  _velocity.y += _gravity;
}

Well done! Now The Boy can jump and fall.


Update jump animation

One little improvement we can add, that greatly improves the look of the game is using different animations for jumps. Let’s do that.


Add _jumpAnimation and _fallAnimation to onLoad method, similar to what we’ve done for idle and run animations:

_jumpAnimation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.range(
        start: 4,
        end: 4,
        amount: 6,
        textureSize: Vector2.all(20),
        stepTimes: [0.12],
      ),
    );

    _fallAnimation = SpriteAnimation.fromFrameData(
      game.images.fromCache(Assets.THE_BOY),
      SpriteAnimationData.range(
        start: 5,
        end: 5,
        amount: 6,
        textureSize: Vector2.all(20),
        stepTimes: [0.12],
      ),
    );


Then, let’s modify updateAnimation method to include new animations:

void updateAnimation() {
    if (_standingOn != null) {
      if (_horizontalDirection == 0) {
        animation = _idleAnimation;
      } else {
        animation = _runAnimation;
      }
    } else {
      if (_velocity.y > 0) {
        animation = _fallAnimation;
      } else {
        animation = _jumpAnimation;
      }
    }
  }


Here, we check if The Boy is standing on the ground (_standingOn != null), then we use the run or the idle animation. Otherwise, we check if The Boy jumps or falls by checking the sign of the vertical velocity and then we apply the appropriate animation.


Different animations on jump and fall

Awesome! The animations look much better.

Summary

That’s it for Part 3. We learned how to add simple physics and collision detection. The Boy now can jump and the game is starting to take its final shape. In the last part, we’ll add Coins - another type of object The Boy can interact with. And also we’ll add a HUD to display the collected Coins.


Other stories of the series:


The complete code for this part you can find on my github

Resources

At the end of each part, I’ll be adding a list of awesome creators and resources I learned from.