我正在编写一个2d物理引擎以获得乐趣。
我为不同的形状编写了不同的类BoxObject
是一个矩形,BallObject
是一个圆,LineObject
是一条线。所有这些都实现了PhysicsObject
接口。
正在模拟的所有对象都驻留在列表中。
public static List<PhysicsObject> objects = new ArrayList<PhysicsObject>();
需要检查所有这些对象是否相互碰撞。
Collision
类包含名为penetrationData
的方法,以获取所有可能类型对的渗透数据,例如penetrationData(BoxObject a, BallObject b)
和penetrationData(BallObject a, LineObject b)
等等。这就是它的样子。
package com.optimus.game.physics;
public class Collision {
// Default - no collision
public static float[] penetrationData(PhysicsObject a, PhysicsObject b) {
return null;
}
// Line vs Circle
public static float[] penetrationData(LineObject line, BallObject ball) {
float distance = line.distance(ball.x, ball.y);
if (Math.abs(distance) > ball.radius) {
return null;
}
float penetration = ball.radius - Math.abs(distance);
float normalX = line.perpendicularX();
float normalY = line.perpendicularY();
return new float[] {normalX, normalY, penetration};
}
// Circle vs Line
public static float[] penetrationData(BallObject ball, LineObject line) {
float[] data = penetrationData(line, ball);
// reverse the normal
if (data != null) {
data[0] = -data[0];
data[1] = -data[1];
}
return data;
}
// Circle vs Circle
public static float[] penetrationData(BallObject a, BallObject b) {
// Vector from A to B
float normalX = b.x - a.x;
float normalY = b.y - a.y;
float penetration = 0;
// calculate the penetration and normal direction
// ...
// ... (code skipped)
return new float[] {normalX, normalY, penetration};
}
// Rect vs Rect
public static float[] penetrationData(BoxObject a, BoxObject b) {
// Vector from A to B
float normalX = b.x - a.x;
float normalY = b.y - a.y;
float penetration = 0;
// calculate the penetration and normal direction
// ...
// ... (code skipped)
return new float[] {normalX, normalY, penetration};
}
// Rect vs Circle
public static float[] penetrationData(BoxObject box, BallObject ball) {
// Vector from A to B
float normalX = ball.x - (box.x + box.width / 2f);
float normalY = ball.y - (box.y + box.height / 2f);
float penetration = 0;
// calculate the penetration and normal direction
// ...
// ... (code skipped)
return new float[] {normalX, normalY, penetration};
}
// Circle vs Rect
public static float[] penetrationData(BallObject ball, BoxObject box) {
float[] data = penetrationData(box, ball);
// reverse the normal
if (data != null) {
data[0] = -data[0];
data[1] = -data[1];
}
return data;
}
}
我检查了碰撞......
public static void checkAndResolveCollisions(List<PhysicsObject> objects) {
// check collision between all objects in a list
// list of objects can contain BallObject, LineObject and
// BoxObject all of which implement PhysicsObject
// loop over all possible pairs
for (int i = 0; i < objects.size(); i++) {
for (int j = i + 1; j < objects.size(); j++) {
// here I want that apt function is called based on type
// but objects.get(int i) always returns a PhysicsObject
float[] data = Collision.penetrationData(objects.get(i), objects.get(j));
// data = null implies no overlap
if (data == null) {
continue;
}
// Calculate relative velocity in terms of the normal direction
float normalX = data[0];
float normalY = data[1];
float penetration = data[2];
// Resolve the collision along the normal
// ...
// ... (code skipped)
}
}
}
但它不起作用,每次penetrationData
都会返回null
......
如果我获得这样的渗透数据,那么工作是什么,但是这段代码很糟糕,我只能通过观察它来判断......
// here is what works, but I know is bad java code
float[] data = getAptPenetrationData(objects.get(i), objects.get(j));
其中getAptPenetrationData
是一个通用函数,用于编译并调用apt函数
// generic function that calls others
public static float[] getAptPenetrationData(PhysicsObject a, PhysicsObject b) {
// bad java code...
boolean aIsBall = a instanceof BallObject;
boolean aIsBox = a instanceof BoxObject;
boolean aIsLine = a instanceof LineObject;
boolean bIsBall = b instanceof BallObject;
boolean bIsBox = b instanceof BoxObject;
boolean bIsLine = b instanceof LineObject;
float[] data = null;
// circle vs circle
if (aIsBall && bIsBall) {
data = Collision.penetrationData((BallObject)a, (BallObject)b);
// box vs box
} else if (aIsBox && bIsBox) {
data = Collision.penetrationData((BoxObject)a, (BoxObject)b);
// box vs circle
} else if (aIsBox && bIsBall) {
data = Collision.penetrationData((BoxObject)a, (BallObject)b);
// circle vs box
} else if (aIsBall && bIsBox) {
data = Collision.penetrationData((BallObject)a, (BoxObject)b);
// circle vs line
} else if (aIsBall && bIsLine) {
data = Collision.penetrationData((BallObject)a, (LineObject)b);
// line vs circle
} else if (aIsLine && bIsBall) {
data = Collision.penetrationData((LineObject)a, (BallObject)b);
}
return data;
}
我必须遗漏一些东西,必须有更好的方法来做我想做的事情。任何帮助表示赞赏。感谢。
修改
根据Zim-Zam的建议,碰撞现在已经解决了......
public static void checkAndResolveCollisions(List<PhysicsObject> objects) {
// check collision between all objects in a list
// list of objects can contain BallObject, LineObject and
// BoxObject all of which implement PhysicsObject
// loop over all possible pairs
for (int i = 0; i < objects.size(); i++) {
for (int j = i + 1; j < objects.size(); j++) {
// does not work, goes into infinite recursion
// still calls PhysicsObject.penetrationData(PhysicsObject)
float[] data = objects.get(i).penetrationData(objects.get(j));
// data = null implies no overlap
if (data == null) {
continue;
}
// Calculate relative velocity in terms of the normal direction
float normalX = data[0];
float normalY = data[1];
float penetration = data[2];
// Resolve the collision along the normal
// ...
// ... (code skipped)
}
}
}
但只有在我将以下方法签名添加到PhysicsObject
界面
public PenetrationData penetrationData(PhysicsObject other);
public PenetrationData penetrationData(BallObject other);
public PenetrationData penetrationData(BoxObject other);
public PenetrationData penetrationData(LineObject other);
这仍然不理想,因为每次实施新的PhysicsObject
我都必须更改界面。
PhysicsObject实现(仅penetrationData
个函数) -
BallObject.java
public class BallObject implements PhysicsObject {
@Override
public float[] penetrationData(PhysicsObject other) {
return other.penetrationData(this);
}
@Override
public float[] penetrationData(LineObject line) {
return Collision.penetrationData(this, line);
}
@Override
public float[] penetrationData(BoxObject box) {
return Collision.penetrationData(this, box);
}
@Override
public float[] penetrationData(BallObject ball) {
return Collision.penetrationData(this, ball);
}
// ... rest of the code
}
BoxObject.java
public class BoxObject implements PhysicsObject {
@Override
public float[] penetrationData(PhysicsObject other) {
return other.penetrationData(this);
}
@Override
public float[] penetrationData(LineObject line) {
return null; // not implemented
}
@Override
public float[] penetrationData(BoxObject box) {
return Collision.penetrationData(this, box);
}
@Override
public float[] penetrationData(BallObject ball) {
return Collision.penetrationData(this, ball);
}
// ... rest of the code
}
LineObject.java
public class LineObject implements PhysicsObject {
@Override
public float[] penetrationData(PhysicsObject other) {
return other.penetrationData(this);
}
@Override
public float[] penetrationData(LineObject line) {
return null; // not implemented
}
@Override
public float[] penetrationData(BoxObject box) {
return null; // not implemented
}
@Override
public float[] penetrationData(BallObject ball) {
return Collision.penetrationData(this, ball);
}
// ... rest of the code
}
答案 0 :(得分:3)
如前所述,您需要双重调度。但是,我不建议使用重量级和样板文件的访客模式。您应该用Java实现自己的调度系统。例如,构建从对象对到渗透数据计算器的映射。然后,您将拥有大量instanceof
,而不是很多map.put()
。您甚至可以通过使用 varargs 方法避免重复put
,该方法可用于伪造地图文字:
static Map<ObjPair, CollisionCalc> map(Object... kvs) {
final Map<ObjPair, CollisionCalc> ret = new HashMap<>();
for (int i = 0;;i < kvs.length)
ret.put((ObjPair)kvs[i++], (CollisionCalc)kvs[i++]);
return ret;
}
在客户端,这应如下所示:
public static float[] penetrationData(PhyObject o1, PhyObject o2) {
return map.get(new ObjPair(o1, o2)).penetrationData(o1, o2);
}
答案 1 :(得分:2)
一种选择是使用multimethod扩展名,例如Java MultiMethod Framework,它在运行时自动执行参数向下转换。例如,Collision.penetrationData(physicaObject obj1, physicsObject obj2)
将自动解析为Collision.penetrationData(ballObject obj1, squareObject obj2)
或其他任何内容。
另一种选择是在每个physicsObject1.resolvePenetrationData(physicsObject obj1)
子类中放置一个physicsObject
方法 - 这将自动确定physicsObject1
的实际类型。然后在resolvePenetrationData
中,您可以确定physicsObject obj1
参数的实际类型,并调用相应的静态方法。您仍将手动确定其中一个对象的类型,但会自动解析另一个对象的类型。或者,您可以通过重载resolvePenetrationData
以接受PhysicsObject
参数和BallObject
参数等来自动解析这两种对象类型。按每个参数调用resolvePenetrationData
两次,
class BallObject {
float[] resolvePenetrationData(PhysicsObject obj1) {
return obj1.resolvePenetrationData(this);
}
float[] void resolvePenetrationData(BallObject obj1) {
return Collision.penetrationData(this, obj1);
}
float[] resolvePenetrationData(SquareObject obj1) {
return Collision.penetrationData(this, obj1);
}
//etc
}
答案 2 :(得分:1)
不幸的是,实际上没有。 Java的虚拟方法提供单一调度,但您需要的是双重调度。
使用巨型交换机/ if else的明显方法的一种替代方法是做类似访问者的模式。但是,根据您的需要,这可能会或可能不会更好。