flutterflameforge2d

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


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);
  }
}



Solution

  • 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

    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);
    }