Search code examples
fluttergame-developmentflame

How to handle Object collision for platformer game?


I watched a few tutorials and tried to put together the code for the onCollision method in my Player class. The tutorial persons' explanation was good, but I still don't understand how exactly it works. The Vector2 mid represents the middle, like the picture below

enter image description here

Intuitively, I can understand how you could get the length of the overlapping part (x in the picture) and add it to the Vector2 representing the player's position so that the player cannot walk/fall/jump through a Platform. However, some Vector2 math is beyond me. I've read through this description of linear algebra for game dev which has concepts like normalization but having trouble applying it to understand these lines of code specifically:

    Vector2 collisionNormal = absoluteCenter - mid;
    final seperationDist = (size.x / 2) - collisionNormal.length;
    collisionNormal.normalize();
    if (Vector2(0, -1).dot(collisionNormal) > 0.9) {
      _isOnGround = true;
    }
    position += collisionNormal.scaled(seperationDist);

Can someone help me understand this, or generally how this problem would be solved for platformers in game development?

My Player class (onCollision method being the relevant part):

class Player extends SpriteAnimationGroupComponent with HasGameRef<Gain>, KeyboardHandler, CollisionCallbacks {
  String character;
  Player({pos, this.character = "Ninja Frog"}) : super(position: pos);

  final double stepTime = 0.05;

  ...
  @override
  void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    if (other is Platform) {
      if (intersectionPoints.length == 2) {
        Vector2 mid = (intersectionPoints.elementAt(0) + intersectionPoints.elementAt(1)) / 2;
        Vector2 collisionNormal = absoluteCenter - mid;
        final seperationDist = (size.x / 2) - collisionNormal.length;
        collisionNormal.normalize();
        if (Vector2(0, -1).dot(collisionNormal) > 0.9) {
          _isOnGround = true;
        }
        position += collisionNormal.scaled(seperationDist);
      }
    }
    super.onCollision(intersectionPoints, other);
  }
}

My Platform class that represents walls, ground, ceiling of game:

class Platform extends PositionComponent with CollisionCallbacks {
  bool canJumpThrough;
  Platform({required Vector2 position, required Vector2 size, this.canJumpThrough = false}) : super(position: position, size: size);

  @override
  FutureOr<void> onLoad() {
    add(RectangleHitbox(collisionType: CollisionType.passive));
    return super.onLoad();
  }
}

Solution

  • This is the vector pointing from the center of your ball to the point in the middle of the two intersection points:

    Vector2 collisionNormal = absoluteCenter - mid;
    

    This gets the distance from the middle intersection point to the bottom of the ball, i.e. how much the ball overlaps the platform:

    final seperationDist = (size.x / 2) - collisionNormal.length;
    

    This makes the collisionNormal vector of a length 1, but still pointing in the same direction:

    collisionNormal.normalize();
    

    This part they are probably doing to be able to later in the tutorial decide which direction the collision came from. The collisionNormal here (if the platform is always underneath us and we don't overshoot the center of the ball before a collision) would always be Vector2(0, -1) (if we weren't in the screen coordinate system that has a flipped y-axis it would be Vector2(0, 1)).

    The dot product here is used to make sure that the collision came from below, if the collisionNormal instead would be Vector2(0, 1), then the ball would have hit something from above, and the dot product would instead be -1, instead of 1 which it will be now.

    if (Vector2(0, -1).dot(collisionNormal) > 0.9) {
      _isOnGround = true;
    }
    

    Here they adjust the position so that the ball is again on top of the platform, instead of inside of it. They adjust the position by combining it with the collisionNormal which is multiplied (scaled) by the length to the middle of the collision points (separationDist).

    position += collisionNormal.scaled(seperationDist);
    

    So if the ball center was at (1,1), the middle of the intersection points (1,2) and the balls radius 2 (size.x / 2), then the collision normal would be (0,-1) and the separation distance 1, and this calculation would be:

    (1,1) += (0,-1)*1
    (1,1) += (0,-1)
    (1,0)
    

    So since the ball's anchor is center, its center position would now be at Vector2(1,0) and its bottom just touching the ground without overlapping.