/*******************************************************************************
* Copyright 2014 See AUTHORS file.
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.badlogic.gdx.ai.tests.pfa.tests;
import com.badlogic.gdx.InputAdapter;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.ai.pfa.HierarchicalPathFinder;
import com.badlogic.gdx.ai.pfa.PathSmoother;
import com.badlogic.gdx.ai.pfa.indexed.IndexedAStarPathFinder;
import com.badlogic.gdx.ai.tests.PathFinderTests;
import com.badlogic.gdx.ai.tests.pfa.PathFinderTestBase;
import com.badlogic.gdx.ai.tests.pfa.tests.tiled.TiledManhattanDistance;
import com.badlogic.gdx.ai.tests.pfa.tests.tiled.TiledRaycastCollisionDetector;
import com.badlogic.gdx.ai.tests.pfa.tests.tiled.TiledSmoothableGraphPath;
import com.badlogic.gdx.ai.tests.pfa.tests.tiled.flat.FlatTiledNode;
import com.badlogic.gdx.ai.tests.pfa.tests.tiled.hrchy.HierarchicalTiledGraph;
import com.badlogic.gdx.ai.tests.pfa.tests.tiled.hrchy.HierarchicalTiledNode;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.CheckBox;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.TimeUtils;
/** This test shows how the {@link HierarchicalPathFinder} can be used on a hierarchical tiled map with two levels. It also shows
* how to use a {@link PathSmoother} on the found path to reduce the zigzag.
*
* @author davebaol */
public class HierarchicalTiledAStarTest extends PathFinderTestBase {
final static float width = 8; // 5; // 10;
final static int NUM_PATHS = 10;
ShapeRenderer renderer;
Vector3 tmpUnprojection = new Vector3();
int lastScreenX;
int lastScreenY;
int lastEndTileX;
int lastEndTileY;
int startTileX;
int startTileY;
HierarchicalTiledGraph worldMap;
TiledSmoothableGraphPath<HierarchicalTiledNode>[] paths;
TiledManhattanDistance<HierarchicalTiledNode> heuristic;
HierarchicalPathFinder<HierarchicalTiledNode> pathFinder;
PathSmoother<HierarchicalTiledNode, Vector2> pathSmoother;
boolean smooth = false;
boolean metrics = false;
CheckBox checkDiagonal;
CheckBox checkSmooth;
CheckBox checkMetrics;
public HierarchicalTiledAStarTest (PathFinderTests container) {
super(container, "Hierarchical Tiled A*");
}
@SuppressWarnings("unchecked")
@Override
public void create () {
lastEndTileX = -1;
lastEndTileY = -1;
startTileX = 1;
startTileY = 1;
// Create the map
worldMap = new HierarchicalTiledGraph();
int roomCount = 100;
int roomMinSize = 2;
int roomMaxSize = 8;
int squashIterations = 100;
worldMap.init(roomCount, roomMinSize, roomMaxSize, squashIterations);
paths = (TiledSmoothableGraphPath<HierarchicalTiledNode>[])new TiledSmoothableGraphPath[NUM_PATHS];
for (int i = 0; i < NUM_PATHS; i++) {
paths[i] = new TiledSmoothableGraphPath<HierarchicalTiledNode>();
}
heuristic = new TiledManhattanDistance<HierarchicalTiledNode>();
IndexedAStarPathFinder<HierarchicalTiledNode> levelPathFinder = new IndexedAStarPathFinder<HierarchicalTiledNode>(worldMap,
true);
pathFinder = new HierarchicalPathFinder<HierarchicalTiledNode>(worldMap, levelPathFinder);
pathSmoother = new PathSmoother<HierarchicalTiledNode, Vector2>(new TiledRaycastCollisionDetector<HierarchicalTiledNode>(
worldMap));
renderer = new ShapeRenderer();
inputProcessor = new TiledHierarchicalAStarInputProcessor(this);
Table detailTable = new Table(container.skin);
detailTable.row();
checkSmooth = new CheckBox("[RED]S[]mooth Path", container.skin);
checkSmooth.setChecked(smooth);
checkSmooth.addListener(new ChangeListener() {
@Override
public void changed (ChangeEvent event, Actor actor) {
CheckBox checkBox = (CheckBox)event.getListenerActor();
smooth = checkBox.isChecked();
updatePath(true);
}
});
detailTable.add(checkSmooth);
detailTable.row();
checkDiagonal = new CheckBox("Prefer [RED]D[]iagonal", container.skin);
checkDiagonal.setChecked(worldMap.diagonal);
checkDiagonal.addListener(new ChangeListener() {
@Override
public void changed (ChangeEvent event, Actor actor) {
CheckBox checkBox = (CheckBox)event.getListenerActor();
worldMap.diagonal = checkBox.isChecked();
updatePath(true);
}
});
detailTable.add(checkDiagonal);
detailTable.row();
addSeparator(detailTable);
detailTable.row();
checkMetrics = new CheckBox("Calculate [RED]M[]etrics", container.skin);
checkMetrics.setChecked(metrics);
checkMetrics.addListener(new ChangeListener() {
@Override
public void changed (ChangeEvent event, Actor actor) {
CheckBox checkBox = (CheckBox)event.getListenerActor();
metrics = checkBox.isChecked();
updatePath(true);
}
});
detailTable.add(checkMetrics);
detailWindow = createDetailWindow(detailTable);
}
@Override
public void render () {
renderer.begin(ShapeType.Filled);
int level = 0;
worldMap.setLevel(level);
int xMax = HierarchicalTiledGraph.sizeX[level];
int yMax = HierarchicalTiledGraph.sizeY[level];
for (int x = 0; x < xMax; x++) {
for (int y = 0; y < yMax; y++) {
switch (worldMap.getNode(x, y).type) {
case FlatTiledNode.TILE_FLOOR:
renderer.setColor(Color.WHITE);
break;
case FlatTiledNode.TILE_WALL:
renderer.setColor(Color.GRAY);
break;
default:
renderer.setColor(Color.BLACK);
break;
}
renderer.rect(x * width, y * width, width, width);
}
}
// Draw path nodes
for (int p = 0; p < NUM_PATHS; p++) {
TiledSmoothableGraphPath<HierarchicalTiledNode> path = paths[p];
int nodeCount = path.getCount();
if (nodeCount == 0) break;
renderer.setColor(p % 2 == 0 ? Color.RED : Color.ORANGE);
for (int i = 0; i < nodeCount; i++) {
HierarchicalTiledNode node = path.nodes.get(i);
renderer.rect(node.x * width, node.y * width, width, width);
}
}
if (smooth) {
renderer.end();
renderer.begin(ShapeType.Line);
// Draw lines between path nodes
for (int p = 0; p < NUM_PATHS; p++) {
TiledSmoothableGraphPath<HierarchicalTiledNode> path = paths[p];
int nodeCount = path.getCount();
if (nodeCount > 0) {
float hw = width / 2f;
HierarchicalTiledNode prevNode = path.nodes.get(0);
renderer.setColor(p % 2 == 0 ? Color.RED : Color.ORANGE);
for (int i = 1; i < nodeCount; i++) {
HierarchicalTiledNode node = path.nodes.get(i);
renderer.line(node.x * width + hw, node.y * width + hw, prevNode.x * width + hw, prevNode.y * width + hw);
prevNode = node;
}
}
}
}
// Draw the lower level node of the buildings (usually a tile close to the center of mass)
level = 1;
worldMap.setLevel(level);
xMax = HierarchicalTiledGraph.sizeX[level];
yMax = HierarchicalTiledGraph.sizeY[level];
renderer.end();
renderer.begin(ShapeType.Line);
renderer.setColor(Color.MAROON);
float hw = width * .5f;
for (int x = 0; x < xMax; x++) {
for (int y = 0; y < yMax; y++) {
HierarchicalTiledNode lln = worldMap.getNode(x, y).getLowerLevelNode();
renderer.circle(lln.x * width + hw, lln.y * width + hw, hw);
}
}
renderer.end();
}
@Override
public void dispose () {
renderer.dispose();
worldMap = null;
paths = null;
heuristic = null;
pathFinder = null;
pathSmoother = null;
}
public Camera getCamera () {
return container.stage.getViewport().getCamera();
}
private void updatePath (boolean forceUpdate) {
getCamera().unproject(tmpUnprojection.set(lastScreenX, lastScreenY, 0));
int tileX = (int)(tmpUnprojection.x / width);
int tileY = (int)(tmpUnprojection.y / width);
if (forceUpdate || tileX != lastEndTileX || tileY != lastEndTileY) {
worldMap.setLevel(0);
HierarchicalTiledNode startNode = worldMap.getNode(startTileX, startTileY);
HierarchicalTiledNode endNode = worldMap.getNode(tileX, tileY);
if (forceUpdate || endNode.type == FlatTiledNode.TILE_FLOOR) {
if (endNode.type == FlatTiledNode.TILE_FLOOR) {
lastEndTileX = tileX;
lastEndTileY = tileY;
} else {
endNode = worldMap.getNode(lastEndTileX, lastEndTileY);
}
if (metrics)
System.out.println("------------ Hierarchical Indexed A* Path Finder Metrics ------------");
OUTER:
for (int p = 0; p < NUM_PATHS; p++) {
TiledSmoothableGraphPath<HierarchicalTiledNode> path = paths[p];
path.clear();
worldMap.startNode = startNode;
long startTime = nanoTime();
boolean found = pathFinder.searchNodePath(startNode, endNode, heuristic, path);
if (metrics) {
float elapsed = (TimeUtils.nanoTime() - startTime) / 1000000f;
System.out.println("<<<Subpath " + p + ">>>");
// System.out.println("Visited nodes................... = " + pathfinder.metrics.visitedNodes);
// System.out.println("Open list additions............. = " + pathfinder.metrics.openListAdditions);
// System.out.println("Open list peak.................. = " + pathfinder.metrics.openListPeak);
System.out.println("Path finding elapsed time (ms).. = " + elapsed);
}
if (!found) break;
HierarchicalTiledNode n = worldMap.convertNodeBetweenLevels(0, startNode, 1);
int nodeCount = path.getCount();
if (nodeCount > 0 && endNode == path.get(nodeCount - 1)) {
if (smooth) {
startTime = nanoTime();
pathSmoother.smoothPath(path);
if (metrics) {
float elapsed = (TimeUtils.nanoTime() - startTime) / 1000000f;
System.out.println("Path smoothing elapsed time (ms) = " + elapsed);
}
}
paths[p + 1].clear();
break;
}
for (int i = 1; i < nodeCount; i++) {
if (worldMap.convertNodeBetweenLevels(0, path.get(i), 1) != n) {
startNode = path.get(i);
path.truncatePath(i);
if (smooth) {
startTime = nanoTime();
pathSmoother.smoothPath(path);
if (metrics) {
float elapsed = (TimeUtils.nanoTime() - startTime) / 1000000f;
System.out.println("Path smoothing elapsed time (ms) = " + elapsed);
}
}
continue OUTER;
}
}
}
}
}
}
private long nanoTime () {
return metrics? TimeUtils.nanoTime() : 0;
}
/** An {@link InputProcessor} that allows you to define a path to find.
*
* @autor davebaol */
static class TiledHierarchicalAStarInputProcessor extends InputAdapter {
HierarchicalTiledAStarTest test;
public TiledHierarchicalAStarInputProcessor (HierarchicalTiledAStarTest test) {
this.test = test;
}
@Override
public boolean keyTyped (char character) {
switch (character) {
case 'm':
case 'M':
test.checkMetrics.toggle();
break;
case 'd':
case 'D':
test.checkDiagonal.toggle();
break;
case 's':
case 'S':
test.checkSmooth.toggle();
break;
}
return true;
}
@Override
public boolean touchUp (int screenX, int screenY, int pointer, int button) {
test.getCamera().unproject(test.tmpUnprojection.set(screenX, screenY, 0));
int tileX = (int)(test.tmpUnprojection.x / width);
int tileY = (int)(test.tmpUnprojection.y / width);
test.worldMap.setLevel(0);
HierarchicalTiledNode startNode = test.worldMap.getNode(tileX, tileY);
if (startNode.type == FlatTiledNode.TILE_FLOOR) {
test.startTileX = tileX;
test.startTileY = tileY;
test.updatePath(true);
}
return true;
}
@Override
public boolean mouseMoved (int screenX, int screenY) {
test.lastScreenX = screenX;
test.lastScreenY = screenY;
test.updatePath(false);
return true;
}
}
}