package org.jrenner.fps;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.physics.bullet.Bullet;
import com.badlogic.gdx.physics.bullet.DebugDrawer;
import com.badlogic.gdx.physics.bullet.collision.ClosestRayResultCallback;
import com.badlogic.gdx.physics.bullet.collision.ContactListener;
import com.badlogic.gdx.physics.bullet.collision.RayResultCallback;
import com.badlogic.gdx.physics.bullet.collision.btBoxShape;
import com.badlogic.gdx.physics.bullet.collision.btBroadphaseInterface;
import com.badlogic.gdx.physics.bullet.collision.btCapsuleShape;
import com.badlogic.gdx.physics.bullet.collision.btCollisionConfiguration;
import com.badlogic.gdx.physics.bullet.collision.btCollisionDispatcher;
import com.badlogic.gdx.physics.bullet.collision.btCollisionObject;
import com.badlogic.gdx.physics.bullet.collision.btCollisionShape;
import com.badlogic.gdx.physics.bullet.collision.btCollisionWorld;
import com.badlogic.gdx.physics.bullet.collision.btDbvtBroadphase;
import com.badlogic.gdx.physics.bullet.collision.btDefaultCollisionConfiguration;
import com.badlogic.gdx.physics.bullet.collision.btDispatcher;
import com.badlogic.gdx.physics.bullet.collision.btManifoldPoint;
import com.badlogic.gdx.physics.bullet.linearmath.btIDebugDraw;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Disposable;
import org.jrenner.fps.entity.DynamicEntity;
import org.jrenner.fps.entity.Entity;
import static com.badlogic.gdx.physics.bullet.collision.btCollisionObject.CollisionFlags;
// TODO make sure all bullet objects are properly disposed, specifically shapes
public class Physics implements Disposable {
public static Physics inst;
btCollisionConfiguration collisionConfig;
btDispatcher dispatcher;
MyContactListener contactListener;
btBroadphaseInterface broadphaseInterface;
private btCollisionWorld world;
public Array<btCollisionShape> shapes;
public Array<btCollisionObject> objects;
private Array<Disposable> disposables;
DebugDrawer debugDrawer;
public Physics() {
Bullet.init();
shapes = new Array<>();
objects = new Array<>();
disposables = new Array<>();
if (Main.isClient()) {
debugDrawer = new DebugDrawer();
debugDrawer.setDebugMode(btIDebugDraw.DebugDrawModes.DBG_MAX_DEBUG_DRAW_MODE);
}
inst = this;
collisionConfig = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(collisionConfig);
broadphaseInterface = new btDbvtBroadphase();
// TODO is it worth comparing performance with other broadphase types?
//broadphaseInterface = new btSimpleBroadphase();
//broadphaseInterface = new btAxisSweep3(tmp.set(0f, -10, 0f), tmp2.set(GameWorld.WORLD_WIDTH, 200f, GameWorld.WORLD_DEPTH));
world = new btCollisionWorld(dispatcher, broadphaseInterface, collisionConfig);
if (Main.isClient()) {
world.setDebugDrawer(debugDrawer);
}
contactListener = new MyContactListener();
disposables.add(world);
if (debugDrawer != null) {
disposables.add(debugDrawer);
}
disposables.add(collisionConfig);
disposables.add(dispatcher);
disposables.add(broadphaseInterface);
disposables.add(contactListener);
}
/** Static geometry, being static, will never collide with other static geometry, greatly increasing collision detection performance */
public void addStaticGeometryToWorld(btCollisionObject obj) {
short colGroup = STATIC_GEOMETRY;
short colMask = DYNAMIC_ENTITIES;
world.addCollisionObject(obj, colGroup, colMask);
}
/** Suitable for moving entites that need to interact with each other and static geometry */
public void addDynamicEntityToWorld(btCollisionObject obj) {
short colGroup = DYNAMIC_ENTITIES;
short colMask = DYNAMIC_ENTITIES | STATIC_GEOMETRY;
world.addCollisionObject(obj, colGroup, colMask);
}
// Collision filters
public static final short STATIC_GEOMETRY = 0x01;
public static final short DYNAMIC_ENTITIES = 0x02;
public static final short ALL_OBJECTS = 0xFF;
public static void applyStaticGeometryCollisionFlags(btCollisionObject obj) {
obj.setCollisionFlags(CollisionFlags.CF_STATIC_OBJECT);
}
public static void applyDynamicEntityCollisionFlags(btCollisionObject obj) {
obj.setCollisionFlags(CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK); // allows ContactListeners to be called for this object
}
/** This is where collisions are actually handled */
class MyContactListener extends ContactListener {
@Override
public boolean onContactAdded(btManifoldPoint cp, btCollisionObject colObj0, int partId0, int index0, btCollisionObject colObj1, int partId1, int index1) {
Entity a = getEntity(colObj0);
Entity b = getEntity(colObj1);
if (a != null || b != null) {
// the distance between the point on body A that collided and the point on body B that collided
float dist = cp.getDistance();
// the normal vector of the collision
cp.getNormalWorldOnB(norm);
cp.getPositionWorldOnA(worldA);
cp.getPositionWorldOnA(worldB);
if (a != null) {
entityCollision(a, norm, worldA, worldB, dist, b);
}
// for object b, we must reverse the normal direction
if (b != null) {
entityCollision(b, norm.scl(-1f), worldB, worldA, dist, a);
}
}
return true;
}
private Entity getEntity(btCollisionObject obj) {
if (obj.userData instanceof Entity) return (Entity) obj.userData;
return null;
}
private Vector3 norm = new Vector3();
private Vector3 worldA = new Vector3();
private Vector3 worldB = new Vector3();
/** this is where the magic happens! */
private void entityCollision(Entity ent, Vector3 normal, Vector3 myPoint, Vector3 otherPoint, float dist, Entity otherEntity) {
if (Math.abs(dist) < 0.01f) return; // ignoring tiny collisions seems to help stability?
// Change Position
tmp.set(myPoint).sub(otherPoint).nor().add(normal); // direction of vel + normal
tmp.nor();
Vector3 posDelta = tmp3.set(tmp).scl(dist);
ent.addCollisionPositionChange(posDelta);
// Change Velocity
// slide along the faces of geometry, but bounce off other entities
Vector3 vel = ent.getVelocity();
boolean bounce = otherEntity != null;
if (!bounce) {
// slide along the wall
// TODO don't slide "up" vertical inclines of a certain steepness
projectVectorOntoPlane(vel, normal);
} else {
// bounce off of other entities
/*float speed = vel.len();
tmp4.set(vel).nor(); // entity unit velocity
float dotScalar = tmp2.set(tmp4).dot(norm);
tmp3.set(norm).scl(-2f * dotScalar);
tmp2.set(tmp4).add(tmp3).nor();
tmp2.scl(speed);
// don't bounce up or down
tmp2.y = 0f;
ent.adjustVelocity(tmp2);
otherEntity.adjustVelocity(tmp2.scl(-1f));*/
}
}
}
private static void projectVectorOntoPlane(Vector3 vec, Vector3 planeNorm) {
// Formula: vector v1_projected = v1 - Dot(v1, n) * n;
tmp2.set(planeNorm).scl(tmp.set(vec).dot(planeNorm));
vec.sub(tmp2);
}
public static float TIME_STEP = 1 / 60f;
public static float TIME_SCALE = 1f; // should probably always be 1f
public void run() {
world.performDiscreteCollisionDetection();
}
private static Matrix4 mtx = new Matrix4();
public btCollisionObject createBoxObject(Vector3 boxSize) {
btCollisionShape shape = new btBoxShape(boxSize);
shapes.add(shape);
btCollisionObject obj = new btCollisionObject();
obj.setCollisionShape(shape);
obj.setWorldTransform(mtx.idt());
obj.setUserValue(objects.size);
objects.add(obj);
return obj;
}
public static btCollisionShape playerShape;
public btCollisionObject createCapsuleObject(float radius, float height) {
if (playerShape == null) {
playerShape = new btCapsuleShape(radius, height);
}
shapes.add(playerShape);
btCollisionObject obj = new btCollisionObject();
obj.setCollisionShape(playerShape);
obj.setWorldTransform(mtx.idt());
obj.setUserValue(objects.size);
objects.add(obj);
return obj;
}
public void debugDraw() {
debugDrawer.begin(View.inst.getCamera());
world.debugDrawWorld();
System.gc();
/*for (DynamicEntity ent : DynamicEntity.list) {
debugDrawDynamicEntity(ent);
}*/
debugDrawer.end();
}
public void debugDrawDynamicEntity(DynamicEntity ent) {
world.debugDrawObject(ent.getBody().getWorldTransform(), playerShape, tmp.set(1f, 1f, 1f));
}
private static Vector3 tmp = new Vector3();
private static Vector3 tmp2 = new Vector3();
private static Vector3 tmp3 = new Vector3();
private static Vector3 tmp4 = new Vector3();
private static Vector3 tmp5 = new Vector3();
private static Vector3[] rectVectors = new Vector3[]{new Vector3(), new Vector3(), new Vector3(), new Vector3()};
public static int lastDistanceFromGroundRayHitCount;
/** if a four-corners raycast was used, more than one ray might have hit, and the avg dist is stored here
* otherwise it is equal to the dist of the single ray hit */
public static float lastDistanceFromGroundAvgDist;
/** finds the distance from the bottom of the of the passed dimensions to the ground
* This method only uses the center point
* This is simpler than checking for each of the four corners
* but less accurate
* @return distance to the ground, or Float.NaN when raycast did not hit the ground */
public float distanceFromGroundFast(Vector3 position, Vector3 dimen) {
float hitDist = Float.NaN;
float downStep = 2f + dimen.y; // length of ray
float rayOriginHeight = 0f;
// test single point
rectVectors[0].set(position);
Vector3 point = rectVectors[0];
castRayStaticOnly(point, tmp.set(point).sub(0f, downStep, 0f));
if (raycastReport.hit) {
hitDist = raycastReport.hitDistance;
}
if (!Float.isNaN(hitDist)) {
// if embedded in ground, a negative value will be returned
hitDist -= (rayOriginHeight + dimen.y/2f);
}
lastDistanceFromGroundAvgDist = hitDist;
return hitDist;
}
/** finds the distance from the bottom of the passed dimensions to the ground.
* This method uses the four corners of the dimensions
* this is more expensive than checking a single point, but also more accurate
* @return distance to the ground, or Float.NaN when raycast did not hit ground */
public float distanceFromGround(Vector3 position, Vector3 dimen) {
lastDistanceFromGroundRayHitCount = 0;
lastDistanceFromGroundAvgDist = 0f;
float lowest = Float.NaN;
float downStep = 2f + dimen.y; // length of ray
// test four corners
float x = dimen.x / 4f;
float z = dimen.z / 4f;
float rayOriginHeight = 0f;
// by starting the ray from the center of the dimensions
// we can catch cases where the dimensions are already partially
// embedded underneath the ground.
// in this case, a negative distance will be returned
rectVectors[0].set(position).add(-x, rayOriginHeight, -z);
rectVectors[1].set(position).add(-x, rayOriginHeight, z);
rectVectors[2].set(position).add(x, rayOriginHeight, z);
rectVectors[3].set(position).add(x, rayOriginHeight, -z);
for (int i = 0; i < rectVectors.length; i++) {
Vector3 point = rectVectors[i];
castRayStaticOnly(point, tmp.set(point).sub(0f, downStep, 0f));
if (raycastReport.hit) {
lastDistanceFromGroundAvgDist += raycastReport.hitDistance;
lastDistanceFromGroundRayHitCount++;
if (Float.isNaN(lowest) || raycastReport.hitDistance < lowest) {
lowest = raycastReport.hitDistance;
}
}
}
// we started the ray from the center, but we want the distance to ground from the bottom
if (!Float.isNaN(lowest)) {
// if embedded in ground, a negative value will be returned
lowest -= (rayOriginHeight + dimen.y/2f);
}
lastDistanceFromGroundAvgDist /= lastDistanceFromGroundRayHitCount;
return lowest;
}
public float getShadowHeightAboveGround(Vector3 position) {
castRay(position, tmp.set(position).sub(0f, Shadow.MAX_SHADOW_HEIGHT, 0f));
return raycastReport.hitDistance;
}
/** the normal of the surface below a position through raycasting */
public Vector3 getFloorNormal(Vector3 position) {
float rayDist = 100f;
castRay(position, tmp.set(position).sub(0f, rayDist, 0f));
tmp.setZero();
if (raycastReport.hit) {
tmp.set(raycastReport.hitNormal);
}
return tmp;
}
private ClosestRayResultCallback staticRayCallback;
//private ClosestNotMeRayResultCallback notMeRayCallback
private ClosestRayResultCallback rayCallback;
private static void setCallbackRayPositions(ClosestRayResultCallback cb, Vector3 from, Vector3 to) {
cb.setRayFromWorld(from);
cb.setRayToWorld(to);
}
public void castRayStaticOnly(Vector3 position, Vector3 end) {
if (staticRayCallback == null) {
staticRayCallback = new ClosestRayResultCallback(position, end);
staticRayCallback.setCollisionFilterGroup(DYNAMIC_ENTITIES);
staticRayCallback.setCollisionFilterMask(STATIC_GEOMETRY);
}
staticRayCallback.setCollisionObject(null);
staticRayCallback.setClosestHitFraction(1f);
setCallbackRayPositions(staticRayCallback, position, end);
executeRayCast(position, end, staticRayCallback);
}
public void castRay(Vector3 position, Vector3 end) {
if (rayCallback == null) {
rayCallback = new ClosestRayResultCallback(position, end);
rayCallback.setCollisionFilterGroup(ALL_OBJECTS);
rayCallback.setCollisionFilterMask(ALL_OBJECTS);
}
rayCallback.setCollisionObject(null);
rayCallback.setClosestHitFraction(1f);
setCallbackRayPositions(rayCallback, position, end);
executeRayCast(position, end, rayCallback);
}
/*public void castRayNotMe(Vector3 position, Vector3 end, btCollisionObject self) {
if (notMeRayCallback == null) {
notMeRayCallback = new ClosestNotMeRayResultCallback(self);
notMeRayCallback.setCollisionFilterGroup(ALL_OBJECTS);
notMeRayCallback.setCollisionFilterMask(ALL_OBJECTS);
}
notMeRayCallback.setCollisionObject(nu
rayCallback.getRayFromWorld().setValue(position.x, position.y, position.z);
rayCallback.getRayToWorld().setValue(end.x, end.y, end.z);ll);
notMeRayCallback.setClosestHitFraction(1f);
notMeRayCallback.getRayFromWorld().setValue(position.x, position.y, position.z);
notMeRayCallback.getRayToWorld().setValue(end.x, end.y, end.z);
notMeRayCallback.
executeRayCast(position, end, notMeRayCallback);
}*/
private void executeRayCast(Vector3 position, Vector3 end, RayResultCallback callback) {
raycastReport.reset();
world.rayTest(position, end, callback);
raycastReport.hit = callback.hasHit();
if (raycastReport.hit) {
float length = position.dst(end);
raycastReport.hitDistance = length * callback.getClosestHitFraction();
if (callback instanceof ClosestRayResultCallback) {
ClosestRayResultCallback cb = (ClosestRayResultCallback) callback;
Vector3 normal = tmp;
cb.getHitNormalWorld(tmp);
raycastReport.hitNormal.set(normal.x, normal.y, normal.z);
}
}
}
public static class RaycastReport {
public boolean hit;
public Vector3 hitNormal = new Vector3();
public float hitDistance;
public void reset() {
hit = false;
hitNormal.setZero();
hitDistance = -1f;
}
@Override
public String toString() {
return String.format("Hit: %s, Distance: %s, Normal: %s", hit, Tools.fmt(hitDistance), Tools.fmt(hitNormal));
}
}
public RaycastReport raycastReport = new RaycastReport();
public void removeBody(btCollisionObject body) {
world.removeCollisionObject(body);
body.dispose();
}
@Override
public void dispose() {
for (Disposable disp : disposables) {
/*if (disp instanceof BulletBase) {
System.out.println("ownership: " + ((BulletBase) disp).hasOwnership());
}*/
Tools.dispose(disp);
}
for (btCollisionShape shape : shapes) {
shape.dispose();
}
for (btCollisionObject object : objects) {
object.dispose();
}
disposables.clear();
shapes.clear();
objects.clear();
}
}