package org.jrenner.fps.entity; import com.badlogic.gdx.graphics.g3d.ModelInstance; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Quaternion; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.physics.bullet.collision.btCollisionObject; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.IntMap; import org.jrenner.fps.Direction; import org.jrenner.fps.Log; import org.jrenner.fps.Main; import org.jrenner.fps.Physics; import org.jrenner.fps.Player; import org.jrenner.fps.Tools; import org.jrenner.fps.effects.BlueExplosion; import org.jrenner.fps.graphics.EntityBillboard; import org.jrenner.fps.graphics.EntityModel; import org.jrenner.fps.move.FlyingMovement; import org.jrenner.fps.move.Movement; public abstract class Entity { public int id = -1; public static Array<Entity> list; public static Array<Integer> destroyQueue; public static IntMap<Entity> idMap; public static Array<Integer> usedIDs; protected static Quaternion q = new Quaternion(); protected static Matrix4 mtx = new Matrix4(); public EntityInterpolator interpolator = new EntityInterpolator(this); public float health = 100f; protected btCollisionObject body; protected Vector3 bodyOffset = new Vector3(); public Movement movement; public boolean onGround; public float distFromGround; /** height, width, depth dimensions of the entity */ protected Vector3 dimen = new Vector3(); protected Quaternion rotation = new Quaternion(); /** the Physics class collision callbacks will set collision position change, which is processed during update */ protected Vector3 collisionPositionChanges = new Vector3(); protected int collisionPositionChangeCount = 0; /** Entities have three choices for graphical representation: a 3d model, a billboard (decal), and none */ private EntityModel entityModel; /** null for non-player entity */ protected Player player; public static enum EntityGraphicsType { Decal, Model, None } public Entity(EntityGraphicsType graphicsType) { synchronized (Entity.list) { list.add(this); } switch(graphicsType) { case Decal: //entityDecal = new EntityDecal(this); entityModel = new EntityBillboard(this); break; case Model: entityModel = new EntityModel(this); break; case None: // do nothing! break; default: throw new GdxRuntimeException("unhandled enum type"); } } public static Entity getEntityById(int id) { Entity ent = idMap.get(id); if (ent == null) { if (!Main.isClient()) { throw new GdxRuntimeException("Fatal Error: Server could not find entity by id: " + id); //Log.error("Server could not find entity by id: " + id); } else { // this is where the client sees an entity it hasn't heard of before, and asks the server // for info about it in order to create it Log.debug("Client couldn't find entity by id, requesting entity info from server"); Main.getNetClient().requestEntityInfo(id); } } return ent; } public static boolean entityWithIdExists(int id) { synchronized (list) { for (Entity ent : list) { if (ent.id == id) { return true; } } } return false; } private static int nextEntityId; public static void assignEntityID(Entity ent) { if (usedIDs.contains(nextEntityId, false)) { nextEntityId++; } int id = nextEntityId; nextEntityId++; assignEntityID(ent, id); } public static void assignEntityID(Entity ent, int id) { for (Entity e : list) { if (e.id == id) { throw new GdxRuntimeException("Fatal Error: Cannot assign id to entity, other entity with id already exists: " + id); } } if (ent.id != -1) { throw new GdxRuntimeException("Fatal Error: Entity has already been assigned id: " + ent.id); } ent.id = id; usedIDs.add(id); idMap.put(id, ent); } public static void init() { list = new Array<>(); destroyQueue = new Array<>(); idMap = new IntMap<>(); usedIDs = new Array<>(); DynamicEntity.list = new Array<>(); EntityModel.list = new Array<>(); } public static void updateAll(float timeStep) { for (int i = 0; i < Entity.list.size; i++) { Entity ent = Entity.list.get(i); ent.update(timeStep); } processDestroyQueue(); } void update(float timeStep) { // the server sets entity data directly, no need to handle updates from server or interpolate if (!Main.isServer()) { interpolator.update(); if (player == null) { updateTransforms(); return; // don't simulate movement/physics of non-player entities for clients // they are simulated completely server-side } } handleCollisions(); updateDistFromGround(); int groundRayHits = Physics.lastDistanceFromGroundRayHitCount; float avgDist = Physics.lastDistanceFromGroundAvgDist; // when the ray went the full length and did not hit the ground, NaN is the return value onGround = false; float embedThreshold = 0f; if (!Float.isNaN(distFromGround)) { Vector3 vel = getVelocity(); if (distFromGround < 0.1f) { adjustPosition(tmp.set(0f, -distFromGround, 0f)); if (vel.y < 0f) vel.y = 0f; onGround = true; } if (distFromGround < embedThreshold && groundRayHits == 4 && avgDist <= 0f) { // penetrating into the ground if (player != null) { Log.debug("ground embed adjust"); } getPosition().y += -distFromGround; } else if (distFromGround > 0f) { Vector3 velocity = getVelocity(); // cap velocity to distance from ground if (velocity.y < 0 && distFromGround - velocity.y <= 0f) { velocity.y = -distFromGround; System.out.println("cap velocity: " + velocity.y); } } } movement.update(timeStep, onGround); updateTransforms(); } /** The way bullet works, there might be multiple collision points when two object collide with each other. * Therefore, we take the average of the position change caused by each collision and apply it as the final * position change. */ public void handleCollisions() { if (collisionPositionChangeCount > 0) { collisionPositionChanges.scl(1f / collisionPositionChangeCount); collisionPositionChanges.scl(-1f); // subtraction adjustPosition(collisionPositionChanges); collisionPositionChangeCount = 0; collisionPositionChanges.setZero(); } } // TODO check this works properly public void faceTowards(Vector3 targ) { float desired = Tools.getAngleFromAtoB(movement.getPosition(), targ, Vector3.Y); rotation.setEulerAngles(-desired, rotation.getPitch(), rotation.getRoll()); } public void updateTransforms() { // physics body transform q.setEulerAngles(rotation.getYaw(), 0f, 0f); // physics bodies only care about yaw mtx.set(q); mtx.setTranslation(tmp.set(getPosition()).add(bodyOffset)); body.setWorldTransform(mtx); } public void setDestination(Vector3 d) { movement.setDestination(d); } public void setRelativeDestination(Vector3 delta) { tmp.set(movement.getPosition()).add(delta); setDestination(tmp); } /** Stops ground units from moving up when looking up. * for example, if an entity is rotated to be facing almost straight up, * this method relativizes the destination to be "in front of" the entity * on the xz (ground) plane */ public void setRelativeDestinationByYaw(Vector3 delta) { // TODO what happens if one of the added vectors == Vector3.Y? i.e. straight up or straight down relativizeByYaw(delta); tmp.set(movement.getPosition()).add(delta); setDestination(tmp); } /** transform vector based on the current rotation, but set pitch to zero, useful for relative directions like "forward" and "back" */ public Vector3 relativizeByYaw(Vector3 v) { q.setEulerAngles(rotation.getYaw(), 0f, 0f); q.transform(v); return v; } /** transform vector by current rotation, makes vector relative to current facing */ public Vector3 relativize(Vector3 v) { rotation.transform(v); return v; } public void setPosition(Vector3 pos) { if (pos == null) throw new NullPointerException(); if (movement == null) throw new NullPointerException(); movement.setPosition(pos); } public void setPosition(float x, float y, float z) { setPosition(tmp.set(x, y, z)); } public void adjustPosition(Vector3 delta) { movement.getPosition().add(delta); } public void setVelocity(Vector3 vel) { movement.getVelocity().set(vel); } // TODO is there a more correct way to handle collision position changes? /** combines all collision position changes, which are averaged when processed during update */ public void addCollisionPositionChange(Vector3 posDelta) { collisionPositionChangeCount++; collisionPositionChanges.add(posDelta); } public Movement getMovement() { return movement; } static Vector3 tmp = new Vector3(); static Vector3 tmp2 = new Vector3(); static Vector3 tmp3 = new Vector3(); public Vector3 getPosition() { return movement.getPosition(); } public Vector3 getVelocity() { return movement.getVelocity(); } public void setRotation(Quaternion newRot) { rotation.set(newRot); } public void adjustRotation(Direction.Rotation rot) { System.out.println("adjust rotation: " + rot.vector); float rotSpeed = 4f; adjustYaw(-rot.vector.x * rotSpeed); adjustPitch(-rot.vector.y * rotSpeed); adjustRoll(-rot.vector.z * rotSpeed); } public Quaternion getRotation() { return rotation; } public void adjustVelocity(Vector3 delta) { movement.getVelocity().add(delta); } /** adjust velocity based on relative directions. i.e. Vector3.Z == forward, (0, 1, 1) == forward-up */ public void adjustVelocityRelativeByYaw(Vector3 delta) { adjustVelocity(relativizeByYaw(delta)); } public void setYawPitchRoll(float y, float p, float r) { getRotation().setEulerAngles(y, p, r); } public void setYawPitchRoll(Vector3 rot) { getRotation().setEulerAngles(rot.x, rot.y, rot.z); } public void lookAt(Vector3 pos) { tmp.set(pos).sub(getPosition()); q.setFromCross(Vector3.Z, tmp.nor()); setYawPitchRoll(q.getYaw(), getPitch(), getRoll()); } public float getYaw() { return rotation.getYaw(); } public void setYaw(float amt) { rotation.setEulerAngles(amt, rotation.getPitch(), rotation.getRoll()); } public void adjustYaw(float amt) { float yaw = getYaw(); yaw += amt; setYaw(yaw); } public float getPitch() { return rotation.getPitch(); } public void setPitch(float amt) { rotation.setEulerAngles(rotation.getYaw(), amt, rotation.getRoll()); } public void adjustPitch(float amt) { float pitch = getPitch(); // avoid gimbal lock // technically could use Quaternions for free rotation, but not necessary for FPS pitch = MathUtils.clamp(pitch + amt, -89f, 89f); setPitch(pitch); } public float getRoll() { return rotation.getRoll(); } public void setRoll(float amt) { rotation.setEulerAngles(rotation.getYaw(), rotation.getPitch(), amt); } public void adjustRoll(float amt) { float roll = getRoll(); roll += amt; setRoll(roll); } public Vector3 getDimensions() { return dimen; } public btCollisionObject getBody() { return body; } public boolean isFlyingEntity() { return movement instanceof FlyingMovement; } public Player getPlayer() { return player; } public void setPlayer(Player player) { if (player == null) throw new GdxRuntimeException("cannot set to null (yet!)"); this.player = player; } public static void destroy(int id) { Entity ent = getEntityById(id); ent.destroy(); } private boolean destroyed; public void destroy() { if (destroyed) return; destroyed = true; synchronized (destroyQueue) { Log.debug("destroy entity, id: " + id); destroyQueue.add(id); if (Main.isServer()) { Main.inst.server.queueDestroyedEntity(id); } if (Main.isClient()) { new BlueExplosion(getPosition()); } } } protected void removeFromGame() { synchronized (Entity.list) { list.removeValue(this, true); } if (entityModel != null) { EntityModel.list.removeValue(entityModel, true); } Physics.inst.removeBody(body); } public static void processDestroyQueue() { synchronized (destroyQueue) { for (int id : destroyQueue) { synchronized (Entity.list) { for (Entity ent : list) { if (ent.id == id) { ent.removeFromGame(); // id should be unique, unless something is broken break; } } } } destroyQueue.clear(); } } public void applyDamage(float dmg) { //Log.debug("Entity[" + id + "] took damage: " + dmg); health -= dmg; if (health <= 0f) { destroy(); } } public void updateDistFromGround() { if (Main.isMobile()) { distFromGround = Physics.inst.distanceFromGroundFast(movement.getPosition(), dimen); } else { distFromGround = Physics.inst.distanceFromGround(movement.getPosition(), dimen); } } public EntityModel getEntityModel() { return entityModel; } public float getHeight() { return dimen.y; } public float getWidth() { return dimen.x; } public float getDepth() { return dimen.z; } // TODO make this more accurate public float getRadius() { return Math.max(dimen.z, (Math.max(dimen.x, dimen.y))); } }