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