/******************************************************************************* * 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.msg.MessageManager; import com.badlogic.gdx.ai.msg.Telegram; import com.badlogic.gdx.ai.msg.Telegraph; import com.badlogic.gdx.ai.pfa.PathFinderQueue; import com.badlogic.gdx.ai.pfa.PathFinderRequest; import com.badlogic.gdx.ai.pfa.PathFinderRequestControl; import com.badlogic.gdx.ai.pfa.PathSmoother; import com.badlogic.gdx.ai.pfa.PathSmootherRequest; import com.badlogic.gdx.ai.pfa.indexed.IndexedAStarPathFinder; import com.badlogic.gdx.ai.pfa.indexed.IndexedAStarPathFinder.Metrics; import com.badlogic.gdx.ai.sched.LoadBalancingScheduler; 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.TiledNode; 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.FlatTiledGraph; import com.badlogic.gdx.ai.tests.pfa.tests.tiled.flat.FlatTiledNode; 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.MathUtils; 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.Label; import com.badlogic.gdx.scenes.scene2d.ui.Slider; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Pool; import com.badlogic.gdx.utils.Pool.Poolable; /** This test shows interruptible flat pathfinding through a {@link PathFinderQueue}. * * @author davebaol */ public class InterruptibleFlatTiledAStarTest extends PathFinderTestBase implements Telegraph { final static float width = 8; // 5; // 10; final static int PF_REQUEST = 1; final static int PF_RESPONSE = 2; ShapeRenderer renderer; Vector3 tmpUnprojection = new Vector3(); int lastScreenX; int lastScreenY; int lastEndTileX; int lastEndTileY; int startTileX; int startTileY; FlatTiledGraph worldMap; TiledSmoothableGraphPath<FlatTiledNode> activePath; TiledSmoothableGraphPath<FlatTiledNode> workPath; boolean isActivePathSmoothed; TiledManhattanDistance<FlatTiledNode> heuristic; IndexedAStarPathFinder<FlatTiledNode> pathFinder; PathSmoother<FlatTiledNode, Vector2> pathSmoother; Pool<MyPathFinderRequest> requestPool; LoadBalancingScheduler scheduler; boolean smooth = false; CheckBox checkDiagonal; CheckBox checkSmooth; CheckBox checkMetrics; Slider sliderMillisAvailablePerFrame; public InterruptibleFlatTiledAStarTest (PathFinderTests container) { super(container, "Interruptible Flat Tiled A*"); } @Override public void create () { lastEndTileX = -1; lastEndTileY = -1; startTileX = 1; startTileY = 1; // Create the map worldMap = new FlatTiledGraph(); int roomCount = MathUtils.random(80, 150);// 100, 260);//70, 120); int roomMinSize = 3; int roomMaxSize = 15; int squashIterations = 100; worldMap.init(roomCount, roomMinSize, roomMaxSize, squashIterations); activePath = new TiledSmoothableGraphPath<FlatTiledNode>(); workPath = new TiledSmoothableGraphPath<FlatTiledNode>(); heuristic = new TiledManhattanDistance<FlatTiledNode>(); pathFinder = new IndexedAStarPathFinder<FlatTiledNode>(worldMap, true); pathSmoother = new PathSmoother<FlatTiledNode, Vector2>(new TiledRaycastCollisionDetector<FlatTiledNode>(worldMap)); requestPool = new Pool<MyPathFinderRequest>() { @Override protected MyPathFinderRequest newObject () { return new MyPathFinderRequest(); } }; PathFinderQueue<FlatTiledNode> pathFinderQueue = new PathFinderQueue<FlatTiledNode>(pathFinder); MessageManager.getInstance().addListener(pathFinderQueue, PF_REQUEST); scheduler = new LoadBalancingScheduler(100); scheduler.add(pathFinderQueue, 1, 0); renderer = new ShapeRenderer(); inputProcessor = new TiledAStarInputProcessor(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(pathFinder.metrics != null); checkMetrics.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { CheckBox checkBox = (CheckBox)event.getListenerActor(); pathFinder.metrics = checkBox.isChecked() ? new Metrics() : null; updatePath(true); } }); detailTable.add(checkMetrics); detailTable.row(); addSeparator(detailTable); detailTable.row(); sliderMillisAvailablePerFrame = new Slider(0.1f, 40f, 0.1f, false, container.skin); sliderMillisAvailablePerFrame.setValue(16); final Label labelMillisAvailablePerFrame = new Label("Millis Available per Frame [[" + sliderMillisAvailablePerFrame.getValue() + "]", container.skin); detailTable.add(labelMillisAvailablePerFrame); detailTable.row(); sliderMillisAvailablePerFrame.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { labelMillisAvailablePerFrame.setText("Millis Available per Frame [[" + sliderMillisAvailablePerFrame.getValue() + "]"); } }); Table sliderMapfTable = new Table(); sliderMapfTable.add(new Label("[RED]-[] ", container.skin)); sliderMapfTable.add(sliderMillisAvailablePerFrame); sliderMapfTable.add(new Label(" [RED]+[]", container.skin)); detailTable.add(sliderMapfTable); detailWindow = createDetailWindow(detailTable); } @Override public void render () { long timeToRun = (long)(sliderMillisAvailablePerFrame.getValue() * 1000000f); scheduler.run(timeToRun); // Draw dungeon renderer.begin(ShapeType.Filled); for (int x = 0; x < FlatTiledGraph.sizeX; x++) { for (int y = 0; y < FlatTiledGraph.sizeY; y++) { switch (worldMap.getNode(x, y).type) { case TiledNode.TILE_FLOOR: renderer.setColor(Color.WHITE); break; case TiledNode.TILE_WALL: renderer.setColor(Color.GRAY); break; default: renderer.setColor(Color.BLACK); break; } renderer.rect(x * width, y * width, width, width); } } // Draw active path renderer.setColor(Color.RED); int nodeCount = activePath.getCount(); for (int i = 0; i < nodeCount; i++) { FlatTiledNode node = activePath.nodes.get(i); renderer.rect(node.x * width, node.y * width, width, width); } if (isActivePathSmoothed) { renderer.end(); renderer.begin(ShapeType.Line); float hw = width / 2f; if (nodeCount > 0) { FlatTiledNode prevNode = activePath.nodes.get(0); for (int i = 1; i < nodeCount; i++) { FlatTiledNode node = activePath.nodes.get(i); renderer.line(node.x * width + hw, node.y * width + hw, prevNode.x * width + hw, prevNode.y * width + hw); prevNode = node; } } } renderer.end(); } @Override public void dispose () { renderer.dispose(); worldMap = null; activePath = null; workPath = null; heuristic = null; pathFinder = null; pathSmoother = null; scheduler = null; MessageManager.getInstance().clear(); } public Camera getCamera () { return container.stage.getViewport().getCamera(); } @Override public boolean handleMessage (Telegram telegram) { switch (telegram.message) { case PF_RESPONSE: // PathFinderQueue will call us directly, no need to register for this message MyPathFinderRequest pfr = (MyPathFinderRequest)telegram.extraInfo; if (PathFinderRequestControl.DEBUG) { @SuppressWarnings("unchecked") PathFinderQueue<FlatTiledNode> pfQueue = (PathFinderQueue<FlatTiledNode>)telegram.sender; System.out.println("pfQueue.size = " + pfQueue.size() + " executionFrames = " + pfr.executionFrames); } // Swap double buffer workPath = activePath; activePath = (TiledSmoothableGraphPath<FlatTiledNode>)pfr.resultPath; isActivePathSmoothed = pfr.smoothEnabled; // Release the request requestPool.free(pfr); break; } return true; } 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) { final FlatTiledNode startNode = worldMap.getNode(startTileX, startTileY); FlatTiledNode endNode = worldMap.getNode(tileX, tileY); if (forceUpdate || endNode.type == TiledNode.TILE_FLOOR) { if (endNode.type == TiledNode.TILE_FLOOR) { lastEndTileX = tileX; lastEndTileY = tileY; } else { endNode = worldMap.getNode(lastEndTileX, lastEndTileY); } MyPathFinderRequest pfRequest = requestPool.obtain(); pfRequest.startNode = startNode; pfRequest.endNode = endNode; pfRequest.heuristic = heuristic; pfRequest.responseMessageCode = PF_RESPONSE; MessageManager.getInstance().dispatchMessage(this, PF_REQUEST, pfRequest); // worldMap.startNode = startNode; // long startTime = nanoTime(); // pathFinder.searchNodePath(startNode, endNode, heuristic, path); // if (pathFinder.metrics != null) { // float elapsed = (TimeUtils.nanoTime() - startTime) / 1000000f; // System.out.println("----------------- Indexed A* Path Finder Metrics -----------------"); // 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 (smooth) { // startTime = nanoTime(); // pathSmoother.smoothPath(path); // if (pathFinder.metrics != null) { // float elapsed = (TimeUtils.nanoTime() - startTime) / 1000000f; // System.out.println("Path smoothing elapsed time (ms) = " + elapsed); // } // } } } } /** An {@link InputProcessor} that allows you to define a path to find. * * @autor davebaol */ static class TiledAStarInputProcessor extends InputAdapter { InterruptibleFlatTiledAStarTest test; public TiledAStarInputProcessor (InterruptibleFlatTiledAStarTest 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; case '-': test.sliderMillisAvailablePerFrame.setValue(test.sliderMillisAvailablePerFrame.getValue() - test.sliderMillisAvailablePerFrame.getStepSize()); break; case '+': test.sliderMillisAvailablePerFrame.setValue(test.sliderMillisAvailablePerFrame.getValue() + test.sliderMillisAvailablePerFrame.getStepSize()); 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); FlatTiledNode startNode = test.worldMap.getNode(tileX, tileY); if (startNode.type == TiledNode.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; } } class MyPathFinderRequest extends PathFinderRequest<FlatTiledNode> implements Poolable { PathSmootherRequest<FlatTiledNode, Vector2> pathSmootherRequest; boolean smoothEnabled; boolean smoothFinished; public MyPathFinderRequest () { this.resultPath = new TiledSmoothableGraphPath<FlatTiledNode>(); pathSmootherRequest = new PathSmootherRequest<FlatTiledNode, Vector2>(); } @Override public boolean initializeSearch (long timeToRun) { resultPath = workPath; resultPath.clear(); smoothEnabled = smooth; pathSmootherRequest.refresh((TiledSmoothableGraphPath<FlatTiledNode>)resultPath); smoothFinished = false; worldMap.startNode = startNode; return true; } @Override public boolean finalizeSearch (long timeToRun) { if (pathFound && smoothEnabled && !smoothFinished) { smoothFinished = pathSmoother.smoothPath(pathSmootherRequest, timeToRun); if (!smoothFinished) return false; } return true; } @Override public void reset () { this.startNode = null; this.endNode = null; this.heuristic = null; this.client = null; } } }