/*******************************************************************************
* Copyright 2015 See AUTHORS file.
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
******************************************************************************/
package com.mygdx.game;
import com.badlogic.gdx.ai.GdxAI;
import com.badlogic.gdx.ai.msg.MessageManager;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.g3d.ModelCache;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.physics.bullet.DebugDrawer;
import com.badlogic.gdx.physics.bullet.collision.*;
import com.badlogic.gdx.physics.bullet.dynamics.*;
import com.badlogic.gdx.physics.bullet.linearmath.btIDebugDraw;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Bits;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.LongMap;
import com.mygdx.game.objects.*;
import com.mygdx.game.pathfinding.Triangle;
import com.mygdx.game.scene.GameScene;
import com.mygdx.game.settings.GameSettings;
import com.mygdx.game.utilities.Engine;
import com.mygdx.game.utilities.Entity;
import com.mygdx.game.utilities.Observer;
/**
* Class which keeps track of game objects, performs physics simulation and collision detection,
* as well as decides which models to render.
*
* @author jsjolund
*/
public class GameEngine extends Engine implements Disposable, Observer {
/**
* A ClosestRayResultCallback which takes object layers into account (e.g. house floors)
*/
private class LayeredClosestRayResultCallback extends ClosestRayResultCallback {
private final Vector3 rayFrom = new Vector3();
private final Vector3 rayTo = new Vector3();
private final Vector3 tmp = new Vector3();
private final Ray ray = new Ray();
private Bits layers;
private float hitFraction = 1;
private float rayDistance = 0;
public LayeredClosestRayResultCallback(Vector3 rayFromWorld, Vector3 rayToWorld) {
super(rayFromWorld, rayToWorld);
}
@Override
public void setClosestHitFraction(float value) {
super.setClosestHitFraction(value);
this.hitFraction = value;
}
public void setRay(Ray ray, float rayDistance) {
this.ray.set(ray);
this.rayDistance = rayDistance;
rayFrom.set(ray.origin);
rayTo.set(ray.direction).scl(rayDistance).add(rayFrom);
setRayFromWorld(rayFrom);
setRayToWorld(rayTo);
}
public void setLayers(Bits layers) {
this.layers = layers;
}
@Override
public float addSingleResult(LocalRayResult rayResult, boolean normalInWorldSpace) {
float hitFraction = rayResult.getHitFraction();
btCollisionObject hitObj = rayResult.getCollisionObject();
Entity entity = getEntity(hitObj.getUserPointer());
if (entity instanceof GameModel) {
GameModel model = (GameModel) entity;
if (hitFraction < this.hitFraction && (layers == null || model.visibleOnLayers.intersects(layers))) {
this.hitFraction = hitFraction;
super.addSingleResult(rayResult, normalInWorldSpace);
return hitFraction;
}
} else if (entity.getId() == scene.navmeshBody.getId()) {
Triangle triangle = scene.navMesh.rayTest(ray, rayDistance, layers);
if (triangle == null) {
// Triangle is not on allowed layer
return 1;
}
Intersector.intersectRayTriangle(ray, triangle.a, triangle.b, triangle.c, tmp);
hitFraction = rayFrom.dst(tmp) / rayFrom.dst(rayTo);
if (hitFraction < this.hitFraction) {
this.hitFraction = hitFraction;
rayResult.setHitFraction(hitFraction);
super.addSingleResult(rayResult, normalInWorldSpace);
return hitFraction;
}
}
return 1;
}
}
// Collision flags
public final static short NONE_FLAG = 0;
public final static short NAVMESH_FLAG = 1 << 6;
public final static short PC_FLAG = 1 << 10;
public final static short GROUND_FLAG = 1 << 8;
public final static short OBJECT_FLAG = 1 << 9;
public final static short ALL_FLAG = -1;
// Bullet classes
private final btDynamicsWorld dynamicsWorld;
private final btDispatcher dispatcher;
private final btConstraintSolver constraintSolver;
private final btDbvtBroadphase broadphase;
private final DebugDrawer debugDrawer;
private final LayeredClosestRayResultCallback callback = new LayeredClosestRayResultCallback(Vector3.Zero, Vector3.Z);
private final btCollisionConfiguration collisionConfig;
private final CollisionContactListener contactListener;
private final Vector3 rayFrom = new Vector3();
private final Vector3 rayTo = new Vector3();
private final LongMap<GameObject> objectsById = new LongMap<GameObject>();
private final LongMap<GameModel> modelsById = new LongMap<GameModel>();
private GameScene scene;
// Models
private boolean modelCacheDirty = true;
private final ModelCache modelCache = new ModelCache(new ModelCache.Sorter(), new ModelCache.TightMeshPool());
private final Array<GameModel> dynamicModels = new Array<GameModel>();
private Bits visibleLayers = new Bits();
public Array<SteerableBody> characters = new Array<SteerableBody>();
public GameEngine() {
collisionConfig = new btDefaultCollisionConfiguration();
dispatcher = new btCollisionDispatcher(collisionConfig);
broadphase = new btDbvtBroadphase();
constraintSolver = new btSequentialImpulseConstraintSolver();
dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase,
constraintSolver, collisionConfig);
dynamicsWorld.setGravity(GameSettings.GRAVITY);
debugDrawer = new DebugDrawer();
dynamicsWorld.setDebugDrawer(debugDrawer);
debugDrawer.setDebugMode(btIDebugDraw.DebugDrawModes.DBG_DrawWireframe);
contactListener = new CollisionContactListener();
}
public Entity rayTest(Ray ray, Vector3 hitPointWorld, short belongsToFlag, short collidesWithFlag,
float rayDistance, Bits layers) {
rayFrom.set(ray.origin);
rayTo.set(ray.direction).scl(rayDistance).add(rayFrom);
callback.setCollisionObject(null);
callback.setClosestHitFraction(1f);
callback.setRay(ray, rayDistance);
callback.setLayers(layers);
callback.setCollisionFilterMask(belongsToFlag);
callback.setCollisionFilterGroup(collidesWithFlag);
dynamicsWorld.rayTest(rayFrom, rayTo, callback);
if (callback.hasHit()) {
if (hitPointWorld != null) {
callback.getHitPointWorld(hitPointWorld);
}
long entityId = callback.getCollisionObject().getUserPointer();
return getEntity(entityId);
}
return null;
}
public GameScene getScene() {
return scene;
}
public Bits getVisibleLayers() {
return visibleLayers;
}
public void setScene(GameScene scene) {
// TODO: Remove any previous scene
this.scene = scene;
addEntity(scene.navmeshBody);
Array<GameObject> objs = new Array<GameObject>();
scene.getGameObjects(objs);
for (GameObject obj : objs) {
addEntity(obj);
// TODO: handle this in a better way
// Ideally the engine should not know the name of the entities in the scene
if (obj.name.equals("human") || obj.name.equals("dog")) {
characters.add((SteerableBody) obj);
}
}
}
@Override
public void dispose() {
collisionConfig.dispose();
dispatcher.dispose();
dynamicsWorld.dispose();
broadphase.dispose();
constraintSolver.dispose();
contactListener.dispose();
debugDrawer.dispose();
callback.dispose();
}
public ModelCache getModelCache() {
if (modelCacheDirty) {
updateModelCache(visibleLayers);
}
return modelCache;
}
public Array<GameModel> getDynamicModels() {
if (modelCacheDirty) {
updateModelCache(visibleLayers);
}
return dynamicModels;
}
private void updateModelCache(Bits visibleLayers) {
dynamicModels.clear();
modelCache.begin();
for (GameObject obj : objectsById.values()) {
if (obj instanceof GameModelBody) {
GameModelBody model = (GameModelBody) obj;
if (model.mass == 0) {
// All bodies with mass of zero are static so cache them if visible
if (model.visibleOnLayers.intersects(visibleLayers)) {
modelCache.add(model.modelInstance);
}
} else {
// Dynamic bodies are checked for visibility in render method
dynamicModels.add(model);
}
} else if (obj instanceof Billboard) {
// TODO: If more billboards than selection marker are ever used, handle them here
dynamicModels.add((Billboard) obj);
} else if (obj instanceof GameModel) {
// TODO: If non-static models without bodies are ever used, handle them here
GameModel model = (GameModel) obj;
if (model.visibleOnLayers.intersects(visibleLayers)) {
modelCache.add(model.modelInstance);
}
}
}
modelCache.end();
modelCacheDirty = false;
}
@Override
public void addEntity(Entity entity) {
super.addEntity(entity);
boolean isStaticBody = true;
if (entity instanceof Ragdoll) {
Ragdoll gameObj = (Ragdoll) entity;
for (btRigidBody bodyPart : gameObj.bodyPartMap.keys()) {
bodyPart.setUserPointer(entity.getId());
dynamicsWorld.addRigidBody(bodyPart, gameObj.belongsToFlag, gameObj.collidesWithFlag);
}
}
if (entity instanceof GameModelBody) {
GameModelBody gameObj = (GameModelBody) entity;
gameObj.body.setUserPointer(entity.getId());
dynamicsWorld.addRigidBody(gameObj.body, gameObj.belongsToFlag, gameObj.collidesWithFlag);
for (btTypedConstraint constraint : gameObj.constraints) {
dynamicsWorld.addConstraint(constraint, true);
}
if (gameObj.mass > 0) {
isStaticBody = false;
dynamicModels.add(gameObj);
}
} else if (entity instanceof InvisibleBody) {
InvisibleBody gameObj = (InvisibleBody) entity;
gameObj.body.setUserPointer(entity.getId());
dynamicsWorld.addRigidBody(gameObj.body, gameObj.belongsToFlag, gameObj.collidesWithFlag);
}
if (entity instanceof GameModel) {
GameModel gameObj = (GameModel) entity;
modelsById.put(entity.getId(), gameObj);
}
if (entity instanceof GameObject) {
GameObject gameObj = (GameObject) entity;
objectsById.put(entity.getId(), gameObj);
}
modelCacheDirty = isStaticBody;
}
@Override
public void removeEntity(Entity entity) {
modelCacheDirty = true;
if (entity instanceof Ragdoll) {
Ragdoll gameObj = (Ragdoll) entity;
for (btRigidBody bodyPart : gameObj.bodyPartMap.keys()) {
dynamicsWorld.removeCollisionObject(bodyPart);
}
}
if (entity instanceof GameModelBody) {
GameModelBody gameObj = (GameModelBody) entity;
dynamicsWorld.removeCollisionObject(gameObj.body);
for (btTypedConstraint constraint : gameObj.constraints) {
dynamicsWorld.removeConstraint(constraint);
}
} else if (entity instanceof InvisibleBody) {
InvisibleBody gameObj = (InvisibleBody) entity;
dynamicsWorld.removeCollisionObject(gameObj.body);
}
if (entity instanceof GameModel) {
modelsById.remove(entity.getId());
GameModel gameObj = (GameModel) entity;
if (dynamicModels.contains(gameObj, true)) {
modelCacheDirty = false;
}
dynamicModels.removeValue(gameObj, true);
}
if (entity instanceof GameObject) {
objectsById.remove(entity.getId());
}
super.removeEntity(entity);
}
public void debugDrawWorld(Camera camera) {
debugDrawer.begin(camera);
dynamicsWorld.debugDrawWorld();
debugDrawer.end();
}
public void setDebugMode(int mode) {
debugDrawer.setDebugMode(mode);
}
public void update(float deltaTime) {
// Update AI time
GdxAI.getTimepiece().update(deltaTime);
// Dispatch delayed messages
MessageManager.getInstance().update();
// Update Bullet simulation
// On default fixedTimeStep = 1/60, small objects (the stick) will fall through
// the ground (the ground has relatively big triangles).
dynamicsWorld.stepSimulation(deltaTime, 10, 1f / 240f);
for (GameObject object : objectsById.values()) {
if (object != null) {
object.update(deltaTime);
}
}
}
@Override
public void notifyEntitySelected(GameCharacter entity) {
}
@Override
public void notifyLayerChanged(Bits layer) {
visibleLayers.clear();
visibleLayers.or(layer);
updateModelCache(visibleLayers);
}
@Override
public void notifyCursorWorldPosition(float x, float y, float z) {
}
public class CollisionContactListener extends ContactListener {
public boolean onContactAdded(btCollisionObject colObj0, int partId0, int index0, btCollisionObject colObj1, int partId1, int index1) {
Entity entity0 = getEntity(colObj0.getUserPointer());
Entity entity1 = getEntity(colObj1.getUserPointer());
Stick stick = null;
if (entity0 instanceof Stick) {
stick = (Stick) entity0;
} else if (entity1 instanceof Stick) {
stick = (Stick) entity1;
}
if (stick != null && !stick.hasLanded && stick.body.getLinearVelocity().isZero(0.1f)) {
stick.owner.onStickLanded();
}
return true;
}
}
}