我将 Android Studio 与 Java、LibGDX 和 Box2D 结合使用。
我需要可视化球运动的预测,至少直到接下来的两次碰撞为止。
现在代码可以工作,但投影不精确,正如您在图像中看到的,当球到达第一个碰撞点时它们移动的位置。
那么,如何使它们尽可能准确?
谢谢
public static void debugBallNextCollisions(MyGame game, World world, Viewport viewport, Body ball, float maxPredictionDistance, int qtyRecursions) {
Array<Vector2[]> raysVector2 = getNextCollisionsRay(world, ball, maxPredictionDistance, qtyRecursions);
game.shapeRenderer.end();
game.shapeRenderer.setProjectionMatrix(viewport.getCamera().combined);
Gdx.gl.glEnable(GL20.GL_BLEND);
game.shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
for(Vector2[] vector2s : new Array.ArrayIterator<>(raysVector2)) {
game.shapeRenderer.line(vector2s[0].x, vector2s[0].y, vector2s[1].x, vector2s[1].y);
}
game.shapeRenderer.end();
}
public static Array<Vector2[]> getNextCollisionsRay(World world, Body ball, float maxPredictionDistance, int qtyRecursions) {
Array<Vector2[]> collisionRays = new Array<>();
Vector2 velocity = ball.getLinearVelocity();
if (!velocity.isZero()) {
Vector2 vector2start = new Vector2(ball.getPosition());
Vector2 vector2End = new Vector2(vector2start).add(velocity.nor().scl(maxPredictionDistance));
getNextCollisionsRayRecursive(world, vector2start, vector2End, velocity, maxPredictionDistance, String.valueOf(ball.getUserData()), collisionRays, qtyRecursions);
}
return collisionRays;
}
private static void getNextCollisionsRayRecursive(World world, Vector2 startPos, Vector2 endPos, Vector2 velocity, float maxPredictionDistance, String bodyId, Array<Vector2[]> collisionRays, int qtyRecursions) {
DetailedRayCastCallback callback2 = new DetailedRayCastCallback(startPos, endPos, bodyId);
Vector2 vector2start2 = new Vector2(startPos);
Vector2 vector2End2 = new Vector2(vector2start2).add(velocity.nor().scl(maxPredictionDistance));
world.rayCast(callback2, vector2start2, vector2End2);
if (callback2.closestHit != null) {
CollisionInfo hit2 = callback2.closestHit;
collisionRays.add(new Vector2[]{new Vector2(vector2start2.x, vector2start2.y), new Vector2(hit2.collisionPoint.x, hit2.collisionPoint.y)});
if(--qtyRecursions > 0) {
Vector2 endPos2 = new Vector2(hit2.reflectedVector).scl(maxPredictionDistance).add(hit2.collisionPoint);
getNextCollisionsRayRecursive(world, hit2.collisionPoint, endPos2, hit2.reflectedVector, maxPredictionDistance, bodyId, collisionRays, qtyRecursions);
}
}
else {
Vector2 velocityEnd2 = new Vector2(vector2start2).add(velocity);
collisionRays.add(new Vector2[]{new Vector2(vector2start2.x, vector2start2.y), new Vector2(velocityEnd2.x, velocityEnd2.y)});
}
}
private static class DetailedRayCastCallback implements RayCastCallback {
public CollisionInfo closestHit;
private Vector2 position;
private Vector2 velocity;
private String bodyId;
public DetailedRayCastCallback(Vector2 position, Vector2 velocity, String bodyId) {
this.position = new Vector2(position);
this.velocity = new Vector2(velocity);
this.bodyId = bodyId;
}
@Override
public float reportRayFixture(Fixture fixture, Vector2 point, Vector2 normal, float fraction) {
if (fixture.getBody().getUserData() == bodyId) return -1;
Vector2 reflectedVelocity = calculateReflection(velocity, normal);
closestHit = new CollisionInfo(position, point, normal, reflectedVelocity, fixture);
return fraction;
}
}
private static class CollisionInfo {
public Vector2 initialPoint;
public Vector2 collisionPoint;
public Vector2 surfaceNormal;
public Vector2 reflectedVector;
public CollisionInfo(Vector2 initial, Vector2 collision, Vector2 normal, Vector2 reflected, Fixture hitFixture) {
this.initialPoint = new Vector2(initial);
this.collisionPoint = new Vector2(collision);
this.surfaceNormal = new Vector2(normal);
this.reflectedVector = new Vector2(reflected);
}
}
private static Vector2 calculateReflection(Vector2 incomingVector, Vector2 surfaceNormal) {
float dotProduct = incomingVector.dot(surfaceNormal);
Vector2 reflection = new Vector2(incomingVector).sub(surfaceNormal.scl(2 * dotProduct));
return reflection;
}
debugBallNextCollisions(game, world, viewport, ball.body, WORLD_HEIGHT, 5);
使用光线投射是有问题的,因为即使使用多条光线,预测也可能是错误的。
考虑这个图,
棕褐色圆圈是球,它从左向右移动。蓝色框是第 n 个障碍物,三个黑色箭头是为寻找下一次碰撞而投射的光线。在这种情况下,会得到错误的结果,因为两条光线完全错过了障碍物,而相交的一条光线显示与左面而不是角部发生碰撞。这意味着反射矢量不会围绕面的法线反射(通常情况如此),而是沿着从角到圆中心的矢量反射。
此外,Box2D光线投射报告的碰撞将位于光线和障碍物之间的交叉点,但由于您正在尝试预测球的路径,因此您还需要计算圆沿路径向后移动多远实际上与墙壁碰撞,距离至少是圆的半径。
这只是需要处理的一种边缘情况。
另一种可能对您有用的方法(取决于您的场景的复杂程度)可能是通过简单地模拟以小增量向前移动球并检查圆线交叉点来暴力破解它,
在上面的模拟中,我沿着球的方向前进
0.1f
步,如果其中一步是碰撞,我会从中反射并再次迭代,最大深度为 4 次碰撞。
这种方法显然计算量较大,但如果需要,可以与光线投射结合起来进行优化。
上面动画的完整代码,完全没有优化是:
package com.bornander.sb3d;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.*;
import com.badlogic.gdx.physics.box2d.*;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ScreenUtils;
public class SandboxGame extends ApplicationAdapter {
private static class Segment
{
public Vector2 a = new Vector2();
public Vector2 b = new Vector2();
public Vector2 normal = new Vector2();
public Fixture fixture;
public Segment(Vector2 a, Vector2 b, Fixture fixture) {
this.a.set(a);
this.b.set(b);
this.fixture = fixture;
normal.set(b).sub(a).nor();
normal.set(-normal.y, normal.x);
}
}
private static class Prediction
{
private Array<Segment> segments = new Array<>();
public Circle collisionPoint = new Circle();
private Array<Circle> tests = new Array<>();
public Array<Vector2> collisions = new Array<>();
public void clear() {
segments.clear();
tests.clear();
collisions.clear();
}
public void add(Circle test) {
tests.add(new Circle(test));
}
public void render(ShapeRenderer shapeRenderer) {
shapeRenderer.setColor(Color.GREEN);
for(Segment segment : segments)
shapeRenderer.line(segment.a, segment.b);
shapeRenderer.setColor(Color.DARK_GRAY);
//for(Circle test : tests)
// shapeRenderer.circle(test.x, test.y, test.radius);
shapeRenderer.setColor(Color.MAGENTA);
for(int i = 0; i < collisions.size - 1; ++i) {
shapeRenderer.line(collisions.get(i), collisions.get(i+1));
}
shapeRenderer.setColor(Color.PURPLE);
shapeRenderer.circle(collisionPoint.x, collisionPoint.y, collisionPoint.radius);
}
}
private OrthographicCamera camera;
private ShapeRenderer shapeRenderer;
private World world;
private Box2DDebugRenderer debugRenderer;
private Body block;
private float blockSpeed = 20;
private float ballRadius = 2.0f;
private Body ball;
private Prediction prediction = new Prediction();
@Override
public void create() {
float aspectRatio = Gdx.graphics.getHeight() / (float)Gdx.graphics.getWidth();
float w = 100.0f;
camera = new OrthographicCamera(w, w * aspectRatio);
camera.position.set(Vector3.Zero);
shapeRenderer = new ShapeRenderer();
world = new World(Vector2.Zero, false);
debugRenderer = new Box2DDebugRenderer();
makeWall(-40, 0, 0);
makeWall(40, 0, 0);
makeWall(0, -30, 90 * MathUtils.degreesToRadians);
makeWall(0, 30, 90 * MathUtils.degreesToRadians);
block = makeBlock(0, -24, 90 * MathUtils.degreesToRadians);
ball = makeBall(0, 0, 10, 5);
}
private void makeWall(float x, float y, float angle) {
float w = 1.0f;
float h = 35.0f;
PolygonShape wallShape = new PolygonShape();
wallShape.setAsBox(1, 35);
FixtureDef fd = new FixtureDef();
fd.shape = wallShape;
fd.friction = 0.5f;
fd.restitution = 1.0f;
// Associate some Polygons with the fixtures, as they are easy to use
Polygon polygon = new Polygon(new float[8]);
polygon.setVertex(0, -w, -h);
polygon.setVertex(1, w, -h);
polygon.setVertex(2, w, h);
polygon.setVertex(3, -w, h);
BodyDef bd = new BodyDef();
bd.type = BodyDef.BodyType.StaticBody;
Body b = world.createBody(bd);
Fixture f = b.createFixture(fd);
f.setUserData(polygon);
b.setTransform(x, y, angle);
wallShape.dispose();
}
private Body makeBlock(float x, float y, float angle) {
float w = 1.0f;
float h = 5.0f;
PolygonShape blockShape = new PolygonShape();
blockShape.setAsBox(w, h);
FixtureDef fd = new FixtureDef();
fd.shape = blockShape;
fd.friction = 0.5f;
fd.restitution = 1.0f;
// Associate some Polygons with the fixtures, as they are easy to use
Polygon polygon = new Polygon(new float[8]);
polygon.setVertex(0, -w, -h);
polygon.setVertex(1, w, -h);
polygon.setVertex(2, w, h);
polygon.setVertex(3, -w, h);
BodyDef bd = new BodyDef();
bd.type = BodyDef.BodyType.KinematicBody;
Body b = world.createBody(bd);
Fixture f = b.createFixture(fd);
f.setUserData(polygon);
b.setTransform(x, y, angle);
b.setLinearVelocity(blockSpeed, 0);
blockShape.dispose();
return b;
}
private Body makeBall(float x, float y, float vx, float vy) {
CircleShape circleShape = new CircleShape();
circleShape.setRadius(ballRadius);
FixtureDef fd = new FixtureDef();
fd.shape = circleShape;
fd.friction = 0.5f;
fd.restitution = 1.0f;
fd.density = 2.0f;
BodyDef bd = new BodyDef();
bd.type = BodyDef.BodyType.DynamicBody;
Body b = world.createBody(bd);
b.createFixture(fd);
b.setTransform(x, y, 0);
b.setLinearVelocity(vx, vy);
circleShape.dispose();
return b;
}
@Override
public void render() {
world.step(Gdx.graphics.getDeltaTime(), 8, 8);
Array<Polygon> polygons = new Array<>();
Array<Fixture> fixtures = new Array<>();
world.getFixtures(fixtures);
for(Fixture fixture : fixtures) {
if (fixture.getUserData() instanceof Polygon) {
Polygon polygon = (Polygon)fixture.getUserData();
polygon.setPosition(fixture.getBody().getPosition().x, fixture.getBody().getPosition().y);
polygon.setRotation(fixture.getBody().getAngle() * MathUtils.radiansToDegrees);
polygons.add(polygon);
}
}
prediction.clear();
predict(ball.getWorldCenter(), ball.getLinearVelocity());
if (block != null) {
if (block.getLinearVelocity().x > 0 && block.getWorldCenter().x > 40)
block.setLinearVelocity(-blockSpeed, 0);
if (block.getLinearVelocity().x < 0 && block.getWorldCenter().x < -40)
block.setLinearVelocity(blockSpeed, 0);
}
camera.update();
ScreenUtils.clear(Color.BLACK);
shapeRenderer.setProjectionMatrix(camera.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
renderGrid();
debugRenderer.render(world, camera.combined);
renderBallPredictedPath();
shapeRenderer.setColor(Color.YELLOW);
for(Polygon polygon : polygons) {
shapeRenderer.polygon(polygon.getTransformedVertices());
}
prediction.render(shapeRenderer);
shapeRenderer.end();
}
private void renderGrid() {
shapeRenderer.setColor(Color.DARK_GRAY);
for(int x = -50; x <= 50; x += 5) {
shapeRenderer.setColor(x == 0 ? Color.LIGHT_GRAY : Color.DARK_GRAY);
shapeRenderer.line(x, -50, x, 50);
}
for(int y = -50; y <= 50; y += 5) {
shapeRenderer.setColor(y == 0 ? Color.LIGHT_GRAY : Color.DARK_GRAY);
shapeRenderer.line(-50, y, 50, y);
}
}
private void predict(Vector2 position, Vector2 direction) {
float stepSize = 0.1f;
Vector2 a = new Vector2(position);
Vector2 b = new Vector2(direction);
b.nor().scl(stepSize);
prediction.collisions.add(new Vector2(a.x, a.y));
Array<Fixture> fixtures = new Array<>();
Array<Segment> segments = new Array<>();
world.getFixtures(fixtures);
for(Fixture fixture : fixtures) {
if (fixture.getUserData() instanceof Polygon) {
Polygon polygon = (Polygon)fixture.getUserData();
polygon.setPosition(fixture.getBody().getPosition().x, fixture.getBody().getPosition().y);
polygon.setRotation(fixture.getBody().getAngle() * MathUtils.radiansToDegrees);
float[] vertices = polygon.getTransformedVertices();
int vc = vertices.length;
for (int j = 0; j < vertices.length; j += 2) {
int i0 = (j + 0) % vc;
int i1 = (j + 1) % vc;
int i2 = (j + 2) % vc;
int i3 = (j + 3) % vc;
Vector2 sa = new Vector2(vertices[i0], vertices[i1]);
Vector2 sb = new Vector2(vertices[i2], vertices[i3]);
segments.add(new Segment(sa, sb, fixture));
}
}
}
Intersector.MinimumTranslationVector mtv = new Intersector.MinimumTranslationVector();
Fixture previousFixture = null;
for(int iteration = 0; iteration < 5; ++iteration) {
Circle circle = new Circle(0, 0, ballRadius);
boolean found = false;
for (float i = 0; i < 1000.0f; i += stepSize) {
circle.setPosition(a.x + b.x * i, a.y + b.y * i);
prediction.add(circle);
for (Segment segment : segments) {
if (b.dot(segment.normal) < MathUtils.PI)
{
if (segment.fixture != previousFixture && Intersector.intersectSegmentCircle(segment.a, segment.b, circle, mtv)) {
prediction.collisionPoint.set(circle);
prediction.collisions.add(new Vector2(circle.x, circle.y));
found = true;
a.set(circle.x, circle.y);
Vector2 reflection = new Vector2();
Vector2 n = new Vector2(segment.normal);
float dn2 = b.dot(segment.normal) * 2.0f;
reflection.set(b).sub(n.scl(dn2)).nor();
b.set(reflection);
previousFixture = segment.fixture;
break;
}
}
}
if (found)
break;
}
}
}
private void renderBallPredictedPath() {
Vector2 position = new Vector2();
Vector2 velocity = new Vector2();
Vector2 a = new Vector2();
Vector2 b = new Vector2();
position.set(ball.getWorldCenter());
velocity.set(ball.getLinearVelocity());
a.set(position);
b.set(a).add(velocity);
shapeRenderer.setColor(Color.RED);
shapeRenderer.line(a, b);
}
}