/*******************************************************************************
* 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.objects;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ai.steer.Steerable;
import com.badlogic.gdx.ai.steer.SteeringAcceleration;
import com.badlogic.gdx.ai.utils.Location;
import com.badlogic.gdx.graphics.g3d.Model;
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.btCollisionShape;
import com.mygdx.game.GameScreen;
import com.mygdx.game.pathfinding.Triangle;
import com.mygdx.game.scene.GameScene;
import com.mygdx.game.utilities.BulletLocation;
import com.mygdx.game.utilities.BulletSteeringUtils;
import com.mygdx.game.utilities.Constants;
import com.mygdx.game.utilities.Steerer;
/**
* A {@code SteerableBody} has the ability to exploit steering behaviors through its active {@link #steerer}.
*
* @author jsjolund
*/
public class SteerableBody extends GameModelBody implements Steerable<Vector3> {
public interface SteerSettings {
float getTimeToTarget();
float getArrivalTolerance();
float getDecelerationRadius();
float getPredictionTime();
float getPathOffset();
float getZeroLinearSpeedThreshold();
float getIdleFriction();
}
public final SteerSettings steerSettings;
private Triangle currentTriangle;
private long currentTriangleFrameId = Gdx.graphics.getFrameId() + 12345L;
/**
* Outputs the linear steering of the steering behaviour.
* Angular steering is currently not used.
*/
final SteeringAcceleration<Vector3> steeringOutput = new SteeringAcceleration<Vector3>(new Vector3());
/**
* Holds the active steerer
*/
public Steerer steerer;
private boolean isSteering;
/**
* Used to adjust model orientation when following a path.
*/
protected final Quaternion targetOrientation = new Quaternion();
protected final Quaternion currentOrientation = new Quaternion();
protected final Vector3 targetOrientationVector = new Vector3(Vector3.Z);
/**
* Holds steering data. Use getters and setters.
*/
private Vector3 position = new Vector3();
private float boundingRadius;
private final Vector3 linearVelocity = new Vector3();
private final Vector3 angularVelocity = new Vector3();
private float zeroLinearSpeedThreshold;
private float maxLinearSpeed;
private float maxLinearAcceleration;
private boolean tagged;
private float maxAngularSpeed;
private float maxAngularAcceleration;
/**
* Various temporary objects used in calculation
*/
private boolean wasSteering = false;
private final Matrix4 tmpMatrix = new Matrix4();
private final Vector3 tmpVec = new Vector3();
private final Quaternion tmpQuat = new Quaternion();
/**
* @param model Model to instantiate
* @param name Name of model
* @param location World position at which to place the model instance
* @param rotation The rotation of the model instance in degrees
* @param scale Scale of the model instance
* @param shape Collision shape with which to construct a rigid body
* @param mass Mass of the body
* @param belongsToFlag Flag for which collision layers this body belongs to
* @param collidesWithFlag Flag for which collision layers this body collides with
* @param callback If this body should trigger collision contact callbacks.
* @param noDeactivate If this body should never 'sleep'
* @param steerSettings Steerable settings
*/
public SteerableBody(Model model, String name,
Vector3 location, Vector3 rotation, Vector3 scale,
btCollisionShape shape, float mass,
short belongsToFlag, short collidesWithFlag,
boolean callback, boolean noDeactivate,
SteerSettings steerSettings) {
super(model, name,
location, rotation, scale,
shape, mass,
belongsToFlag, collidesWithFlag,
callback, noDeactivate);
// Set the bounding radius used by steering behaviors like collision avoidance,
// raycast collision avoidance and some others. Note that calculation only takes
// into account dimensions on the horizontal plane since we are steering in 2.5D
this.boundingRadius = (boundingBox.getWidth() + boundingBox.getDepth()) / 4;
this.steerSettings = steerSettings;
setZeroLinearSpeedThreshold(steerSettings.getZeroLinearSpeedThreshold());
// Don't allow physics engine to turn character around any axis.
// This prevents it from gaining any angular velocity as a result of collisions, for instance.
// Usually, you use angular factor Vector3.Y, which allows the engine to turn it only around
// the up axis, but here we can use Vector3.Zero since we directly set linear and angular
// velocity in applySteering() instead of using force and torque.
// This gives us (almost?) total control over character's motion.
// Of course, subclasses can specify different angular factor, if needed.
body.setAngularFactor(Vector3.Zero);
}
@Override
public void update(float deltaTime) {
super.update(deltaTime);
if (steerer == null) {
return;
}
// Calculate steering acceleration
isSteering = steerer.calculateSteering(steeringOutput);
if (isSteering) {
if (!wasSteering) {
// Start steering since this character was not already steering
startSteering();
}
// Apply steering acceleration since this character is steering
applySteering(steeringOutput, deltaTime);
} else { //if (wasSteering) {
// Stop steering since this character is not steering now but was steering before
stopSteering(true);
}
}
/**
* Starts steering; this clears friction since this character is now controlled by the steerer.
*/
protected void startSteering() {
wasSteering = true;
body.setFriction(0);
modelTransform.getRotation(currentOrientation, true);
if (steerer != null) {
steerer.startSteering();
}
}
/**
* Stops steering; this restores normal friction so it cannot slide down most slopes.
* Removes any angular velocity the body accumulated.
* Sets the body to the orientation of the model.
* @param clearLinearVelocity whether linear velocity should be cleared or not
*/
protected void stopSteering(boolean clearLinearVelocity) {
wasSteering = false;
body.setFriction(steerSettings.getIdleFriction());
body.setAngularVelocity(Vector3.Zero);
// Since we were only rotating the model when steering, set body to
// model rotation when finished moving.
position = getPosition();
modelTransform.setFromEulerAngles(
currentOrientation.getYaw(),
currentOrientation.getPitch(),
currentOrientation.getRoll()).setTranslation(position);
body.setWorldTransform(modelTransform);
if (steerer != null) {
clearLinearVelocity = steerer.stopSteering();
}
steerer = null;
steeringOutput.setZero();
if (clearLinearVelocity) {
body.setLinearVelocity(Vector3.Zero);
}
}
/**
* Finds the current triangle and sets the model to be visible on the same layer as mesh part index of current triangle
* @param scene the game scene
*/
public void updateSteerableData(GameScene scene) {
visibleOnLayers.clear();
visibleOnLayers.set(getCurrentTriangle(scene).meshPartIndex);
}
/**
* Applies the linear component of the steering behaviour. As for the angular component,
* the orientation of the model and the body is set to follow the direction of motion (non independent facing).
*
* @param steering the steering acceleration to apply
* @param deltaTime the time between this frame and the previous one
*/
protected void applySteering(SteeringAcceleration<Vector3> steering, float deltaTime) {
// Update linear velocity trimming it to maximum speed
linearVelocity.set(body.getLinearVelocity().mulAdd(steering.linear, deltaTime).limit(getMaxLinearSpeed()));
body.setLinearVelocity(linearVelocity);
// Failed attempt to clear angular velocity possibly due to collision
// Actually, this issue has been fixed by setting the angular factor
// to Vector3.Zero in SteerableBody constructor
//body.setAngularVelocity(Vector3.Zero);
// Maybe we should do this even if applySteering is not invoked
// since the entity might move because of other bodies that are pushing it
updateSteerableData(GameScreen.screen.engine.getScene());
// Calculate the target orientation of the model based on the direction of motion
// Note that the entity might twitch or jitter slightly when it finds itself in a situation with
// conflicting responses from different behaviors. If you need to mitigate this scenario you can decouple
// the heading from the velocity vector and average its value over the last few frames, for instance 5.
// This smoothed heading vector will be used to work out model's orientation.
if (!linearVelocity.isZero(getZeroLinearSpeedThreshold())) {
position = getPosition();
targetOrientationVector.set(linearVelocity.x, 0, -linearVelocity.z).nor();
modelTransform.setToLookAt(targetOrientationVector, Constants.V3_UP).setTranslation(position);
body.setWorldTransform(modelTransform);
targetOrientation.setFromMatrix(true, tmpMatrix.setToLookAt(targetOrientationVector, Constants.V3_UP));
// Set current orientation of model, setting orientation of body causes problems when applying force.
currentOrientation.slerp(targetOrientation, 10 * deltaTime);
modelTransform.setFromEulerAngles(
currentOrientation.getYaw(),
currentOrientation.getPitch(),
currentOrientation.getRoll()).setTranslation(position);
}
body.activate();
}
/**
* @return True if linear velocity of body is not within threshold of zero
*/
public boolean isMoving() {
return !body.getLinearVelocity().isZero(getZeroLinearSpeedThreshold());
}
/**
* @return True if linear steering output is not within threshold of zero
*/
public boolean isSteering() {
return steerer != null && isSteering;
}
@Override
public Vector3 getLinearVelocity() {
return linearVelocity.set(body.getLinearVelocity());
}
@Override
public float getAngularVelocity() {
return angularVelocity.set(body.getAngularVelocity()).len();
}
@Override
public float getBoundingRadius() {
return boundingRadius;
}
@Override
public boolean isTagged() {
return tagged;
}
@Override
public void setTagged(boolean tagged) {
this.tagged = tagged;
}
@Override
public float getZeroLinearSpeedThreshold() {
return zeroLinearSpeedThreshold;
}
@Override
public void setZeroLinearSpeedThreshold(float value) {
zeroLinearSpeedThreshold = value;
}
@Override
public float getMaxLinearSpeed() {
return maxLinearSpeed;
}
@Override
public void setMaxLinearSpeed(float maxLinearSpeed) {
this.maxLinearSpeed = maxLinearSpeed;
}
@Override
public float getMaxLinearAcceleration() {
return maxLinearAcceleration;
}
@Override
public void setMaxLinearAcceleration(float maxLinearAcceleration) {
this.maxLinearAcceleration = maxLinearAcceleration;
}
@Override
public float getMaxAngularSpeed() {
return maxAngularSpeed;
}
@Override
public void setMaxAngularSpeed(float maxAngularSpeed) {
this.maxAngularSpeed = maxAngularSpeed;
}
@Override
public float getMaxAngularAcceleration() {
return maxAngularAcceleration;
}
@Override
public void setMaxAngularAcceleration(float maxAngularAcceleration) {
this.maxAngularAcceleration = maxAngularAcceleration;
}
@Override
public Vector3 getPosition() {
return body.getWorldTransform().getTranslation(position);
}
/**
* Get the rotation of the model for this Steerable, around the Y-axis.
* This might not be equal to the rotation of the collision body while steering is active.
* <p>
* When orientation is 0, character faces positive X axis.
* Rotating [0:PI] makes the character turn to the left from its perspective.
* Rotating [0:-PI] turns it right.
*
* @return
*/
@Override
public float getOrientation() {
return BulletSteeringUtils.vectorToAngle(getDirection(tmpVec));
}
/**
* Set the rotation of the model and collision body for this Steerable, around the Y-axis.
* <p>
* When orientation is 0, character faces positive X axis.
* Rotating [0:PI] makes the character turn to the left from its perspective.
* Rotating [0:-PI] turns it right.
*
* @param orientation
*/
@Override
public void setOrientation(float orientation) {
position = getPosition();
BulletSteeringUtils.angleToVector(tmpVec, -orientation);
modelTransform.setToLookAt(tmpVec, Constants.V3_UP).setTranslation(position);
body.setWorldTransform(modelTransform);
currentOrientation.setFromMatrix(modelTransform);
}
@Override
public float vectorToAngle(Vector3 vector) {
return BulletSteeringUtils.vectorToAngle(vector);
}
@Override
public Vector3 angleToVector(Vector3 outVector, float angle) {
return BulletSteeringUtils.angleToVector(outVector, angle);
}
@Override
public Location<Vector3> newLocation() {
return new BulletLocation();
}
/**
* Returns the world position of the lowest point of the body.
*
* @param out Output vector
* @return The output vector for chaining
*/
public Vector3 getGroundPosition(Vector3 out) {
body.getWorldTransform().getTranslation(out);
out.y -= boundingBox.getHeight() / 2;
return out;
}
/**
* Sets the vector to point in the direction the model is facing
*
* @param out Output vector
* @return The output vector for chaining
*/
public Vector3 getDirection(Vector3 out) {
return modelTransform.getRotation(tmpQuat, true).transform(out.set(Vector3.Z));
}
/**
* Returns the triangle which the steerable is standing on
*/
public Triangle getCurrentTriangle() {
return getCurrentTriangle(GameScreen.screen.engine.getScene());
}
public Triangle getCurrentTriangle(GameScene scene) {
long frameId = Gdx.graphics.getFrameId();
// Find the triangle at most once per frame
if (currentTriangleFrameId != frameId) {
currentTriangleFrameId = frameId;
final Vector3 pos = getPosition();
// This test is O(1) and, according to the coherence assumption, it should succeed most of the times
// since the entity is usually not far from where it was in the previous frame
currentTriangle = scene.navMesh.groundRayTest(pos, halfExtents.y + .2f, null);
if (currentTriangle == null) {
//Gdx.app.log(tag, "Frame " + frameId + ": Finding closest navigation mesh position for " + this);
// This test is O(n) where n is the number of meshes.
currentTriangle = scene.navMesh.getClosestTriangle(pos, tmpVec, null);
}
else {
//Gdx.app.log(tag, "Frame " + frameId + ": Vertical test has found navigation mesh for " + this);
}
}
return currentTriangle;
}
}