/*******************************************************************************
* 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.steerers;
import com.badlogic.gdx.ai.GdxAI;
import com.badlogic.gdx.ai.steer.SteeringAcceleration;
import com.badlogic.gdx.ai.steer.behaviors.FollowPath;
import com.badlogic.gdx.ai.steer.utils.paths.LinePath;
import com.badlogic.gdx.ai.steer.utils.paths.LinePath.LinePathParam;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Bits;
import com.mygdx.game.GameEngine;
import com.mygdx.game.GameRenderer;
import com.mygdx.game.GameScreen;
import com.mygdx.game.objects.GameObject;
import com.mygdx.game.objects.SteerableBody;
import com.mygdx.game.pathfinding.NavMeshGraphPath;
import com.mygdx.game.pathfinding.NavMeshPointPath;
import com.mygdx.game.pathfinding.Triangle;
import com.mygdx.game.settings.GameSettings;
import com.mygdx.game.utilities.Entity;
import com.mygdx.game.utilities.MyShapeRenderer;
/**
* A steerer to follow a path while avoiding collisions.
*
* @author jsjolund
* @author davebaol
*/
public class FollowPathSteerer extends CollisionAvoidanceSteererBase {
/**
* Path of triangles on the navigation mesh. Used to construct path points.
*/
public final NavMeshGraphPath navMeshGraphPath = new NavMeshGraphPath();
/**
* Path points on the navigation mesh, which the steerable will follow.
*/
public final NavMeshPointPath navMeshPointPath = new NavMeshPointPath();
/**
* Path which is rendered on screen
*/
public final Array<Vector3> pathToRender = new Array<Vector3>();
/**
* Steering behaviour for path following
*/
public final FollowPath<Vector3, LinePath.LinePathParam> followPathSB;
/**
* Holds the path segments for steering behaviour
*/
protected final LinePath<Vector3> linePath;
/**
* Path segment index the steerable is currently following.
*/
private int currentSegmentIndex = -1;
/**
* Points from which to construct the path segments the steerable should follow
*/
private final Array<Vector3> centerOfMassPath = new Array<Vector3>();
private Vector3 tmpVec1 = new Vector3();
private Ray stationarityRayLow = new Ray();
private Ray stationarityRayHigh = new Ray();
private float stationarityRayLength;
private Color stationarityRayColor;
public FollowPathSteerer(final SteerableBody steerableBody) {
super(steerableBody);
// At least two points are needed to construct a line path
Array<Vector3> waypoints = new Array<Vector3>(new Vector3[]{new Vector3(), new Vector3(1, 0, 1)});
this.linePath = new LinePath<Vector3>(waypoints, true);
this.followPathSB = new FollowPath<Vector3, LinePath.LinePathParam>(steerableBody, linePath, 1);
this.prioritySteering.add(followPathSB);
}
public boolean calculateNewPath(Ray ray, Bits visibleLayers) {
if (GameScreen.screen.engine.getScene().navMesh.getPath(
steerableBody.getCurrentTriangle(),
steerableBody.getGroundPosition(tmpVec1),
ray, visibleLayers,
GameSettings.CAMERA_PICK_RAY_DST,
navMeshGraphPath)) {
calculateNewPath0();
return true;
}
return false;
}
public boolean calculateNewPath(Triangle targetTriangle, Vector3 targetPoint) {
if (GameScreen.screen.engine.getScene().navMesh.getPath(
steerableBody.getCurrentTriangle(),
steerableBody.getGroundPosition(tmpVec1),
targetTriangle,
targetPoint,
navMeshGraphPath)) {
calculateNewPath0();
return true;
}
return false;
}
/**
* Calculate the navigation mesh point path, then assign this steering provider to the owner
*/
private void calculateNewPath0() {
navMeshPointPath.calculateForGraphPath(navMeshGraphPath);
pathToRender.clear();
pathToRender.addAll(navMeshPointPath.getVectors());
centerOfMassPath.clear();
// Since the navmesh path is on the ground, we need to translate
// it to align with body origin
for (Vector3 v : navMeshPointPath) {
centerOfMassPath.add(new Vector3(v).add(0, steerableBody.halfExtents.y, 0));
}
linePath.createPath(centerOfMassPath);
followPathSB.setTimeToTarget(steerableBody.steerSettings.getTimeToTarget())
.setArrivalTolerance(steerableBody.steerSettings.getArrivalTolerance())
.setDecelerationRadius(steerableBody.steerSettings.getDecelerationRadius())
.setPredictionTime(steerableBody.steerSettings.getPredictionTime())
.setPathOffset(steerableBody.steerSettings.getPathOffset());
steerableBody.setZeroLinearSpeedThreshold(steerableBody.steerSettings.getZeroLinearSpeedThreshold());
currentSegmentIndex = -1;
collisionAvoidanceSB.setEnabled(true);
deadlockDetection = false;
// Make this steerer active
steerableBody.steerer = this;
}
/**
* Path segment index the steerable is currently following.
*/
public int getCurrentSegmentIndex() {
return currentSegmentIndex;
}
@Override
public void startSteering() {
}
@Override
public boolean stopSteering() {
// Clear path
pathToRender.clear();
navMeshPointPath.clear();
navMeshGraphPath.clear();
return false;
}
boolean deadlockDetection;
float deadlockDetectionStartTime;
float collisionDuration;
private static final float DEADLOCK_TIME = .5f;
private static final float MAX_NO_COLLISION_TIME = DEADLOCK_TIME + .5f;
@Override
public boolean processSteering(SteeringAcceleration<Vector3> steering) {
// Check if steering target path segment changed.
LinePathParam pathParam = followPathSB.getPathParam();
int traversedSegment = pathParam.getSegmentIndex();
if (traversedSegment > currentSegmentIndex) {
currentSegmentIndex = traversedSegment;
}
if (prioritySteering.getSelectedBehaviorIndex() == 0) {
/*
* Collision avoidance management
*/
float pr = proximity.getRadius() * 1.5f;
if (linePath.getEndPoint().dst2(steerableBody.getPosition()) <= pr * pr) {
// Disable collision avoidance near the end of the path since the obstacle
// will likely prevent the entity from reaching the target.
collisionAvoidanceSB.setEnabled(false);
deadlockDetectionStartTime = Float.POSITIVE_INFINITY;
} else if (deadlockDetection) {
// Accumulate collision time during deadlock detection
collisionDuration += GdxAI.getTimepiece().getDeltaTime();
if (GdxAI.getTimepiece().getTime() - deadlockDetectionStartTime > DEADLOCK_TIME && collisionDuration > DEADLOCK_TIME * .6f) {
// Disable collision avoidance since most of the deadlock detection period has been spent on collision avoidance
collisionAvoidanceSB.setEnabled(false);
}
} else {
// Start deadlock detection
deadlockDetectionStartTime = GdxAI.getTimepiece().getTime();
collisionDuration = 0;
deadlockDetection = true;
}
return true;
}
/*
* Path following management
*/
float dst2FromPathEnd = steerableBody.getPosition().dst2(linePath.getEndPoint());
// Check to see if the entity has reached the end of the path
if (steering.isZero() && dst2FromPathEnd < followPathSB.getArrivalTolerance() * followPathSB.getArrivalTolerance()) {
return false;
}
// Check if collision avoidance must be re-enabled
if (deadlockDetection && !collisionAvoidanceSB.isEnabled() && GdxAI.getTimepiece().getTime() - deadlockDetectionStartTime > MAX_NO_COLLISION_TIME) {
collisionAvoidanceSB.setEnabled(true);
deadlockDetection = false;
}
// If linear speed is very low and the entity is colliding something at his feet, like a step of the stairs
// for instance, we have to increase the acceleration to make him go upstairs.
float minVel = .2f;
if (steerableBody.getLinearVelocity().len2() > minVel * minVel) {
stationarityRayColor = null;
} else {
steerableBody.getGroundPosition(stationarityRayLow.origin).add(0, 0.05f, 0);
steerableBody.getDirection(stationarityRayLow.direction).scl(1f, 0f, 1f).nor();
stationarityRayLength = steerableBody.getBoundingRadius() + 0.4f;
Entity hitEntityLow = GameScreen.screen.engine.rayTest(stationarityRayLow, null, GameEngine.ALL_FLAG, GameEngine.PC_FLAG, stationarityRayLength, null);
if (hitEntityLow instanceof GameObject) {
stationarityRayColor = Color.RED;
stationarityRayHigh.set(stationarityRayLow);
stationarityRayHigh.origin.add(0, .8f, 0);
Entity hitEntityHigh = GameScreen.screen.engine.rayTest(stationarityRayHigh, null, GameEngine.ALL_FLAG, GameEngine.PC_FLAG, stationarityRayLength, null);
if (hitEntityHigh == null) {
// The entity is touching a small obstacle with his feet like a step of the stairs.
// Increase the acceleration to make him go upstairs.
steering.linear.scl(8);
}
else if (hitEntityHigh instanceof GameObject) {
// The entity is touching a higher obstacle like a tree, a column or something.
// Here we should invent something to circumvent this kind of obstacles :)
//steering.linear.rotateRad(Constants.V3_UP, Constants.PI0_25);
}
} else {
stationarityRayColor = Color.BLUE;
}
}
return true;
}
@Override
public void draw(GameRenderer gameRenderer) {
super.draw(gameRenderer);
if (pathToRender.size > 0 && currentSegmentIndex >= 0) {
MyShapeRenderer shapeRenderer = gameRenderer.shapeRenderer;
shapeRenderer.setProjectionMatrix(gameRenderer.viewport.getCamera().combined);
// Draw path target position
Vector3 t = gameRenderer.vTmpDraw1.set(followPathSB.getInternalTargetPosition());
t.y -= steerableBody.halfExtents.y;
float size = .05f;
float offset = size / 2;
shapeRenderer.begin(MyShapeRenderer.ShapeType.Filled);
shapeRenderer.setColor(Color.CORAL);
shapeRenderer.box(t.x - offset, t.y - offset, t.z + offset, size, size, size);
// Draw path
shapeRenderer.set(MyShapeRenderer.ShapeType.Line);
Vector3 p = t;
int i = getCurrentSegmentIndex() + 1;
if (i + 1 < pathToRender.size && linePath.calculatePointSegmentSquareDistance(gameRenderer.vTmpDraw2, pathToRender.get(i), pathToRender.get(i + 1), p) < 0.0001)
i++;
while (i < pathToRender.size) {
Vector3 q = pathToRender.get(i++);
shapeRenderer.line(p, q);
p = q;
}
// Draw stationarity rays
if (stationarityRayColor != null) {
shapeRenderer.setColor(stationarityRayColor);
shapeRenderer.line(stationarityRayLow.origin, tmpVec1.set(stationarityRayLow.origin).mulAdd(stationarityRayLow.direction, stationarityRayLength));
shapeRenderer.line(stationarityRayHigh.origin, tmpVec1.set(stationarityRayHigh.origin).mulAdd(stationarityRayHigh.direction, stationarityRayLength));
}
shapeRenderer.end();
}
}
}