skip to Main Content

I’m attempting to write a bouncing ball game using flame in flutter. To detect collisions the onCollision and onCollisionStart methods are provided. What I had hoped is that onCollisionStart would give a precise location when two objects first hit each other. However, instead it gives a list of positions indicating where the two objects overlap after the first game-tick when this happens (i.e. onCollisionStart is called at the same time as onCollision, but is not called a second time if the same two objects are still colliding on the next tick).

An example of a collision issue

This is illustrated in the attached picture. The collision points are marked with red dots. If the ball were moving downwards, then the ball would have hit the top of the rectangle and so should bounce upwards. However, if the ball were moving horizontally, then its first point of contact would have been the top left corner of the box, and the ball would bounce upwards and to the left.

If I want to work out correct angle that the ball should fly off, then I would need to do some clever calculations to work out the point that the ball first started hitting the other object (those calculations would depend on the precise shape of the other object). Is there some way to work out the point at which the two objects first started colliding? Thanks

2

Answers


  1. Chosen as BEST ANSWER

    Following spydon's advice (well mostly) I've gone through coding the collision calculations between a circle and rectangle. This should be easily extended to any polygon, once I can find a way to get hold of the corner points of the other shape. At the moment I'm assuming that the rectangle is horizontal. I post the code here in case others find this useful, or want to offer improvements.

    @override
    void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
      super.onCollision(intersectionPoints, other);
    
      Vector2 topLeft = Vector2(other.absoluteTopLeftPosition.x, other.absoluteTopLeftPosition.y);
      Vector2 topRight = Vector2(other.absoluteTopLeftPosition.x + other.width, other.absoluteTopLeftPosition.y);
      Vector2 bottomLeft = Vector2(other.absoluteTopLeftPosition.x, other.absoluteTopLeftPosition.y + other.height);
      Vector2 bottomRight = Vector2(other.absoluteTopLeftPosition.x + other.width, other.absoluteTopLeftPosition.y + other.height);
    
      List<Vector2> corners = [topLeft, topRight, bottomRight, bottomLeft];
      List<String> cornerNames = ['topLeft', 'topRight', 'bottomRight', 'bottomLeft'];
    
      List<double> collisionTimes = [];
      List<String> collisionNames = [];
      List<List<Vector2>> collisionCorners = [];
      for (int i=0; i<corners.length; i++) {
        collisionTimes.add(collisionTimeLine(oldPosition!, position, corners[i], corners[(i+1) % corners.length], _radius));
        collisionNames.add('${cornerNames[i]} + ${cornerNames[(i+1) % corners.length]}');
        collisionCorners.add([corners[i], corners[(i+1) % corners.length]]);
        collisionTimes.add(collisionTimeCorner(startPosition, absoluteCenter, corners[i], _radius));
        collisionNames.add(cornerNames[i]);
        collisionCorners.add([corners[i]]);
      }
    
      double colTime = 100;
      int colIndex = -1;
      for (int i=0; i<collisionTimes.length; i++) {
        if (collisionTimes[i] < colTime) {
          colIndex = i;
          colTime = collisionTimes[i];
        }
        colTime = max(colTime, 0);
      }
    
      if (colIndex >= 0) {
        Vector2 location = collisionLocation(colTime, oldPosition!, absoluteCenter);
        Vector2 normal = Vector2(0,0);
        if (collisionCorners[colIndex].length == 2) {
          Vector2 diff = collisionCorners[colIndex][0] - collisionCorners[colIndex][1];
          normal = diff.scaleOrthogonalInto(1 / diff.length, normal);
        } else {
          normal = (location - collisionCorners[colIndex][0]).normalized();
        }
        _velocity.reflect(normal);
        position = location + _velocity.normalized() * (1-colTime) * (position.distanceTo(oldPosition!));
      }
    }
    
    double pointLineDistance(Vector2 point, Vector2 corner1, Vector2 corner2) {
      // Find the orthogonal distance between a point and a line
      double numerator = ((corner2.x - corner1.x) * (corner1.y - point.y) -
          (corner1.x - point.x) * (corner2.y - corner1.y)).abs();
      double denominator = sqrt(pow(corner2.x - corner1.x, 2) + pow(corner2.y - corner1.y, 2));
      if (denominator > 0) {
        return numerator / denominator;
      } else {
        return 1000;
      }
    }
    
    Vector2 pointProjection(Vector2 point, Vector2 corner1, Vector2 corner2) {
      // Gives the location of the orthogonal projection of point onto the line
      // defined by the two corners.
      Vector2 difference = (corner2 - corner1).normalized();
      Vector2 projection = difference * difference.dot(point - corner1) + corner1;
      return projection;
    }
    
    Vector2 collisionLocation(double collisionTime, Vector2 startPosition, Vector2 endPosition) {
      // Give the location of the collision point
      return startPosition * (1 - collisionTime) + endPosition * collisionTime;
    }
    
    double collisionTimeLine(Vector2 startPosition, Vector2 endPosition,
        Vector2 corner1, Vector2 corner2, double radius) {
      // Calculate the time of a collision between a line and the sprite
      double startDistance = pointLineDistance(startPosition, corner1, corner2);
      double endDistance = pointLineDistance(endPosition, corner1, corner2);
      double collisionTime = calcTime(startDistance, endDistance, radius);
      if (collisionTime < 10) {
        // If we have a valid collision, then calculate the collision location
        // and whether that is between the two corners, by first finding the
        // projection of the point onto the line.
        Vector2 location = collisionLocation(collisionTime, startPosition, endPosition);
        Vector2 pointOnLine = pointProjection(location, corner1, corner2);
        double cornerDistance = corner1.distanceTo(corner2);
        if (pointOnLine.distanceTo(corner1) < cornerDistance && pointOnLine.distanceTo(corner2) < cornerDistance) {
          return collisionTime;
        } else {
          return 1000;
        }
      } else {
        return 1000;
      }
    }
    
    double calcTime(double startDistance, double endDistance, double radius) {
      // Calculate the collision time, accounting for the case where the sprite
      // is intersecting the point/line at both times.
      if (startDistance < radius && endDistance < radius) {
        if (endDistance > startDistance) {
          // We're already heading away from the collision, so do nothing.
          return 1000;
        } else {
          return 0;
        }
      } else if (startDistance < radius || endDistance < radius) {
        // The normal case that we intersect the object only once.
        if (endDistance > startDistance) {
          // We're already heading away from the collision, so do nothing.
          return 1000;
        } else {
          return (radius - startDistance) / (endDistance - startDistance);
        }
      } else {
        // No collision detected
        return 1000;
      }
    }
    
    double collisionTimeCorner(Vector2 startPosition, Vector2 endPosition, Vector2 corner, double radius) {
      // Find the collision time between a corner and the sprite
      double startDistance = startPosition.distanceTo(corner);
      double endDistance = endPosition.distanceTo(corner);
      return calcTime(startDistance, endDistance, radius);
    }
    

  2. What you usually need for this is the normal of the collision, but unfortunately we don’t have that for the collision detection system yet.
    We do have it in the raytracing system though, so what you could do is send out a ray and see how it will bounce and then just bounce the ball in the same way.

    If you don’t want to use raytracing I suggest that you calculate the direction of the ball, which you might already have, but if you don’t you can just store the last position and subtract it from the current position.

    After that you need to find the normals of the edges where the intersection points are.

    Let’s say the ball direction vector is v, and the two normal vectors are n1 and n2.

    Calculate the dot product (this is build in to the vector_math library) of the ball direction vector and each of the normal vectors:

    dot1 = v.dot(n1)
    dot2 = v.dot(n2)
    

    Compare the results of the dot products:

    If dot1 > 0, n1 is facing the ball.
    If dot2 > 0, n2 is facing the ball.
    

    After that you can use v.reflect(nx) to get the direction where your ball should be going (where nx is the normal facing the ball).

    Hopefully we’ll have this built-in to Flame soon!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search