Flutter-Flame: How to calculate the relect of a raycast?

251

There are couple of things wrong in your code. But your guess is correct, the math for reflecting the raycast about normal is wrong. To be more precise, the vector you are using as the first raycast seems wrong.

Instead of,

var i = info.eventPosition.game;

it should be something like this,

var i = callback.point - (screenToWorld(camera.viewport.effectiveSize) / 2);

Basically, it should be a relative vector starting from your original start point and pointing towards the hit point. Hope this solves your problem.

Here are some more thing that seem off in your code

  • Multiple callback classes: You don't need to define multiple callback classes for capturing multiple raycasts. You can use multiple objects of the same class. And if you really want to push it, you can even use same object again and again depending on your use case.

  • Return from reportFixture(): I see that you are returning 0 from this method. In your simple case this will probably work fine, but returning 0 implies that you want the raycast to stop at the very first fixture that it encounters. Unfortunately, world.raycast does not report fixtures in any fixed order. So if there are multiple fixtures along the ray, reportFixture might get called for the farthest fixture. If you truly want to get the nearest fixture, you should return the fraction parameters as the return.

I am posting an example code here which can report you multiple hits depending upon the nBounces parameter.

void newRaycast(Vector2 start, Vector2 end, int nBounces) {
  if (nBounces > 0) {
    final callback = NearestRayCastCallback();
    world.raycast(callback, start, end);
    
    // Make sure that we got a hit.
    if (callback.nearestPoint != null && callback.normalAtInter != null) {
      // Store the hit location for rendering later on.
      points.add(worldToScreen(callback.nearestPoint!.clone()));

      // Figure out the current ray direction and then reflect it
      // about the collision normal.
      Vector2 originalDirection =
          (callback.nearestPoint! - start).normalized();
      final dotProduct = callback.normalAtInter!.dot(originalDirection);
      final newDirection =
          originalDirection - callback.normalAtInter!.scaled(2 * dotProduct);

      // Call newRayCast with new start and end points. New end is just a point 500
      // units along the new direction.
      newRaycast(
          callback.nearestPoint!.clone(), newDirection.scaled(500), --nBounces);
    }
  }
}

Here is the definition for my callback class.

class NearestRayCastCallback extends RayCastCallback {
    Vector2? nearestPoint;
    Vector2? normalAtInter;

    @override
    double reportFixture(
        Fixture fixture, Vector2 point, Vector2 normal, double fraction) {
    nearestPoint = point;
    normalAtInter = normal;
    return fraction;
    }
}

In the above code, points is final points = List<Vector2>.empty(growable: true); and this is how I am rendering lines from those points:

for (int i = 0; i < points.length - 1; ++i) {
  canvas.drawLine(points[i].toOffset(), points[i + 1].toOffset(),
      Paint()..color = Colors.black..strokeWidth = 2);
}
Share:
251
HarrisonQi
Author by

HarrisonQi

Updated on January 02, 2023

Comments

  • HarrisonQi
    HarrisonQi over 1 year

    Thank you for your reading. My English is not good, I'll explain this as well as I can. I need to generate the reflection line in canvas, but it not work well.

    enter image description here

    I think my problem is the "The formula for calculating the reflected vector".

    The mathematics I learned before is not the same as canvas, so I am very confused

    This is my code (wrong), you can run this (the first line is right, but the reflection is wrong):

    import 'package:flame/game.dart';
    import 'package:flame/input.dart';
    import 'package:flame_forge2d/body_component.dart';
    import 'package:flame_forge2d/flame_forge2d.dart';
    import 'package:flame_forge2d/forge2d_game.dart';
    import 'package:flutter/material.dart';
    
    import 'component/fixture/fixture_data.dart';
    
    void main() {
      runApp(
        GameWidget(
          game: TestGame(),
        ),
      );
    }
    
    class TestGame extends Forge2DGame with MultiTouchDragDetector {
      List<Offset> points = [Offset.zero, Offset.zero, Offset.zero];
      late var boundaries;
    
      @override
      Future<void> onLoad() async {
        await super.onLoad();
    
        boundaries = createBoundaries(this);
        boundaries.forEach(add);
    
        points[0] = (camera.canvasSize / 2).toOffset();
      }
    
      @override
      void update(double dt) {
        super.update(dt);
      }
    
      @override
      void render(Canvas canvas) {
        super.render(canvas);
    
        canvas.drawLine(points[0], points[1], Paint()..color = Colors.white);
    
        canvas.drawLine(points[1], points[2], Paint()..color = Colors.white);
    
        canvas.drawCircle(points[2], 30, Paint()..color = Colors.white);
      }
    
      /// @param [pointStart] start point of line
      /// @param [point2] second point of line
      /// @param [x] the position x of point need to calculate
      /// @return Offset of the point on line
      Offset calculateOffsetByX(Offset pointStart, Offset point2, double x) {
        //y =  ax + b
        final a = (pointStart.dy - point2.dy) / (pointStart.dx - point2.dx);
        final b = pointStart.dy - a * pointStart.dx;
        return Offset(x, a * x + b);
      }
    
      /// @param [pointStart] start point of line
      /// @param [point2] second point of line
      /// @param [y] the position y of point need to calculate
      /// @return Offset of the point on line
      Offset calculateOffsetByY(Offset pointStart, Offset point2, double y) {
        //y =  ax + b
        final a = (pointStart.dy - point2.dy) / (pointStart.dx - point2.dx);
        final b = pointStart.dy - a * pointStart.dx;
        return Offset((y - b) / a, y);
      }
    
      @override
      void onDragUpdate(int pointerId, DragUpdateInfo info) {
        var callback = MyRayCastCallBack(this);
    
        var finalPos = getFinalPos(screenToWorld(camera.viewport.effectiveSize) / 2,
            info.eventPosition.game);
    
        world.raycast(
            callback, screenToWorld(camera.viewport.effectiveSize) / 2, finalPos);
    
        var n = callback.normal;
        var i = info.eventPosition.game;
        var r = i + (n * (2 * i.dot(n)));
    
        var callback2 = MyRayCastCallBack2(this);
        world.raycast(callback2, callback.point, r);
      }
    
      Vector2 getFinalPos(Vector2 startPos, Vector2 touchPos) {
        return Vector2(
            calculateOffsetByY(startPos.toOffset(), touchPos.toOffset(), 0).dx,
            calculateOffsetByY(startPos.toOffset(), touchPos.toOffset(), 0).dy);
      }
    }
    
    class MyRayCastCallBack extends RayCastCallback {
      TestGame game;
      late Vector2 normal;
      late Vector2 point;
    
      MyRayCastCallBack(this.game);
    
      @override
      double reportFixture(
          Fixture fixture, Vector2 point, Vector2 normal, double fraction) {
        game.points[1] = game.worldToScreen(point).toOffset();
        this.normal = normal;
        this.point = point;
        return 0;
      }
    }
    
    class MyRayCastCallBack2 extends RayCastCallback {
      TestGame game;
      late Vector2 normal;
      late Vector2 point;
    
      MyRayCastCallBack2(this.game);
    
      @override
      double reportFixture(
          Fixture fixture, Vector2 point, Vector2 normal, double fraction) {
        game.points[2] = game.worldToScreen(point).toOffset();
        this.normal = normal;
        this.point = point;
        return 0;
      }
    }
    
    List<Wall> createBoundaries(Forge2DGame game) {
      /*final topLeft = Vector2.zero();
      final bottomRight = game.screenToWorld(game.camera.viewport.effectiveSize);
      final topRight = Vector2(bottomRight.x, topLeft.y);
      final bottomLeft = Vector2(topLeft.x, bottomRight.y);*/
      final bottomRight =
          game.screenToWorld(game.camera.viewport.effectiveSize) / 8 * 7;
    
      final topLeft = game.screenToWorld(game.camera.viewport.effectiveSize) / 8;
      final topRight = Vector2(bottomRight.x, topLeft.y);
      final bottomLeft = Vector2(topLeft.x, bottomRight.y);
    
      return [
        Wall(topLeft, topRight, FixtureKey.wallTop),
        Wall(topRight, bottomRight, FixtureKey.wallRight),
        Wall(bottomRight, bottomLeft, FixtureKey.wallBottom),
        Wall(bottomLeft, topLeft, FixtureKey.wallLeft),
      ];
    }
    
    class Wall extends BodyComponent {
      final Vector2 start;
      final Vector2 end;
      final FixtureKey fixtureKey;
    
      Wall(this.start, this.end, this.fixtureKey);
    
      @override
      Body createBody() {
        final shape = EdgeShape()..set(start, end);
    
        final fixtureDef = FixtureDef(shape)
          ..restitution = 0
          ..density = 1.0
          ..friction = 0
          ..userData = FixtureData(type: FixtureType.wall, key: fixtureKey);
        ;
    
        final bodyDef = BodyDef()
          ..userData = this // To be able to determine object in collision
          ..position = Vector2.zero()
          ..type = BodyType.static;
    
        return world.createBody(bodyDef)..createFixture(fixtureDef);
      }
    }