/******************************************************************************* * Copyright 2015 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.btree; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.ai.btree.BehaviorTree; import com.badlogic.gdx.ai.btree.Task; import com.badlogic.gdx.ai.btree.annotation.TaskAttribute; import com.badlogic.gdx.ai.btree.utils.DistributionAdapters; import com.badlogic.gdx.ai.utils.random.Distribution; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.ui.Label; import com.badlogic.gdx.scenes.scene2d.ui.Skin; import com.badlogic.gdx.scenes.scene2d.ui.Slider; import com.badlogic.gdx.scenes.scene2d.ui.Table; import com.badlogic.gdx.scenes.scene2d.ui.TextButton; import com.badlogic.gdx.scenes.scene2d.ui.TextTooltip; import com.badlogic.gdx.scenes.scene2d.ui.Tree; import com.badlogic.gdx.scenes.scene2d.ui.Tree.Node; import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.IntArray; import com.badlogic.gdx.utils.ObjectMap; import com.badlogic.gdx.utils.StringBuilder; import com.badlogic.gdx.utils.reflect.Annotation; import com.badlogic.gdx.utils.reflect.ClassReflection; import com.badlogic.gdx.utils.reflect.Field; import com.badlogic.gdx.utils.reflect.ReflectionException; /** A simple behavior tree viewer actor with serialization capability that lets you play with task navigation. This should make * learning and debugging easier. * * @author davebaol */ public class BehaviorTreeViewer<E> extends Table { private static final int BT_SUSPENDED = 0; private static final int BT_RUNNING = 1; private static final int BT_STEP = 2; private static String LABEL_STEP = "Step: "; private BehaviorTree<E> tree; private ObjectMap<Task<E>, TaskNode> taskNodes; private int step; private Label stepLabel; private Slider runDelaySlider; private TextButton runButton; private TextButton stepButton; private TextButton resetButton; private TextButton saveButton; private TextButton loadButton; private Tree displayTree; private int treeStatus; boolean saved; public BehaviorTreeViewer (BehaviorTree<E> tree, Skin skin) { this(tree, true, skin); } public BehaviorTreeViewer (BehaviorTree<E> tree, boolean loadAndSave, Skin skin) { super(skin); this.tree = tree; step = 0; taskNodes = new ObjectMap<Task<E>, TaskNode>(); tree.addListener(new BehaviorTree.Listener<E>() { @Override public void statusUpdated (Task<E> task, Task.Status previousStatus) { TaskNode tn = taskNodes.get(task); if (tn!= null) tn.updateStatus(previousStatus, step); } @Override public void childAdded (Task<E> task, int index) { TaskNode parentNode = taskNodes.get(task); Task<E> child = task.getChild(index); addToTree(displayTree, parentNode, child, null, 0); displayTree.expandAll(); } }); treeStatus = BT_SUSPENDED; runDelaySlider = new Slider(0, 5, 0.01f, false, skin); runDelaySlider.setValue(.5f); runButton = new TextButton("Run", skin); runButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { if (treeStatus == BT_SUSPENDED) { treeStatus = BT_RUNNING; runDelayAccumulator = runDelaySlider.getValue(); // this makes it start immediately runButton.setText("Suspend"); stepButton.setDisabled(true); if (saveButton != null) saveButton.setDisabled(true); if (loadButton != null) loadButton.setDisabled(true); } else { treeStatus = BT_SUSPENDED; runButton.setText("Run"); stepButton.setDisabled(false); if (saveButton != null) saveButton.setDisabled(false); if (loadButton != null) loadButton.setDisabled(!saved); } } }); stepButton = new TextButton("Step", skin); stepButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { treeStatus = BT_STEP; } }); resetButton = new TextButton("Reset", skin); resetButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { BehaviorTreeViewer.this.tree.reset(); rebuildDisplayTree(); } }); if (loadAndSave) { loadButton = new TextButton("Load", skin); loadButton.setDisabled(true); loadButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { load(); } }); saveButton = new TextButton("Save", skin); saveButton.addListener(new ChangeListener() { @Override public void changed (ChangeEvent event, Actor actor) { save(); saved = true; loadButton.setDisabled(false); } }); } stepLabel = new Label(new StringBuilder(LABEL_STEP + step), skin); this.row().height(26).fillX(); this.add(runDelaySlider); this.add(runButton); this.add(stepButton); this.add(resetButton); if (loadAndSave) { this.add(saveButton); this.add(loadButton); } this.add(stepLabel); this.row(); displayTree = new Tree(skin); rebuildDisplayTree(); this.add(displayTree).colspan(5 + (loadAndSave ? 2 : 0)).grow(); } public BehaviorTree<E> getBehaviorTree () { return tree; } public void step () { step++; Gdx.app.log("BTV(" + getName() + ")", "Step " + step); updateStepLabel(); tree.step(); } private void updateStepLabel () { StringBuilder sb = stepLabel.getText(); sb.setLength(LABEL_STEP.length()); sb.append(step); stepLabel.invalidateHierarchy(); } public void save () { Array<BehaviorTree.Listener<E>> listeners = tree.listeners; tree.listeners = null; IntArray taskSteps = new IntArray(); fill(taskSteps, (TaskNode)displayTree.getNodes().get(0)); KryoUtils.save(new SaveObject<E>(tree, step, taskSteps)); tree.listeners = listeners; } public void load () { @SuppressWarnings("unchecked") SaveObject<E> saveObject = KryoUtils.load(SaveObject.class); BehaviorTree<E> oldTree = tree; tree = saveObject.tree; tree.listeners = oldTree.listeners; step = saveObject.step; updateStepLabel(); rebuildDisplayTree(saveObject.taskSteps); } private void fill (IntArray taskSteps, TaskNode taskNode) { taskSteps.add(taskNode.step); for (Node child : taskNode.getChildren()) { fill(taskSteps, (TaskNode)child); } } private float runDelayAccumulator; @Override public void act (float delta) { Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); if (treeStatus == BT_RUNNING) { runDelayAccumulator += delta; if (runDelayAccumulator > runDelaySlider.getValue()) { runDelayAccumulator = 0; step(); } } else if (treeStatus == BT_STEP) { step(); treeStatus = BT_SUSPENDED; } super.act(delta); } private void rebuildDisplayTree () { rebuildDisplayTree(null); } private void rebuildDisplayTree (IntArray taskSteps) { displayTree.clear(); taskNodes.clear(); Task<E> root = tree.getChild(0); addToTree(displayTree, null, root, taskSteps, 0); displayTree.expandAll(); } private static class TaskNode extends Tree.Node { public Task<?> task; public BehaviorTreeViewer<?> btViewer; public int step; public TaskNode (Task<?> task, BehaviorTreeViewer<?> btViewer, int step, Skin skin) { super(new View(task, skin)); ((View)getActor()).taskNode = this; this.task = task; this.btViewer = btViewer; this.step = step; updateStatus(null, step); } private void updateStatus (Task.Status previousStatus, int step) { this.step = step; Task.Status status = task.getStatus(); if (status != previousStatus) { View view = (View)getActor(); view.status.setText(status == Task.Status.FRESH ? "" : status.name()); } } public boolean hasJustRun () { return step == btViewer.step; } private static class View extends Table { Label name; Label status; TaskNode taskNode; public View (Task<?> task, Skin skin) { super(skin); this.name = new Label(task.getClass().getSimpleName(), skin); this.status = new Label("", skin); add(name); add(status).padLeft(10); StringBuilder attrs = new StringBuilder(task.getClass().getSimpleName()).append('\n'); addListener(new TextTooltip(appendTaskAttributes(attrs, task).toString(), skin)); } @Override public void act (float delta) { status.setColor(taskNode.hasJustRun() ? Color.YELLOW : Color.GRAY); } private static StringBuilder appendTaskAttributes (StringBuilder attrs, Task<?> task) { Class<?> aClass = task.getClass(); Field[] fields = ClassReflection.getFields(aClass); for (Field f : fields) { Annotation a = f.getDeclaredAnnotation(TaskAttribute.class); if (a == null) continue; TaskAttribute annotation = a.getAnnotation(TaskAttribute.class); attrs.append('\n'); appendFieldString(attrs, task, annotation, f); } return attrs; } // TODO: should be configurable private static DistributionAdapters DAs = new DistributionAdapters(); private static void appendFieldString (StringBuilder sb, Task<?> task, TaskAttribute ann, Field field) { // prefer name from annotation if there is one String name = ann.name(); if (name == null || name.length() == 0) name = field.getName(); Object value; try { field.setAccessible(true); value = field.get(task); } catch (ReflectionException e) { Gdx.app.error("", "Failed to get field", e); return; } sb.append(name).append(":"); Class<?> fieldType = field.getType(); if (fieldType.isEnum() || fieldType == String.class) { sb.append('\"').append(value).append('\"'); } else if (Distribution.class.isAssignableFrom(fieldType)) { sb.append('\"').append(DAs.toString((Distribution)value)).append('\"'); } else sb.append(value); } } } private int addToTree (Tree displayTree, TaskNode parentNode, Task<E> task, IntArray taskSteps, int taskStepIndex) { TaskNode node = new TaskNode(task, this, taskSteps == null ? step - 1 : taskSteps.get(taskStepIndex), getSkin()); taskNodes.put(task, node); if (parentNode == null) { displayTree.add(node); } else { parentNode.add(node); } taskStepIndex++; for (int i = 0; i < task.getChildCount(); i++) { Task<E> child = task.getChild(i); taskStepIndex = addToTree(displayTree, node, child, taskSteps, taskStepIndex); } return taskStepIndex; } static class SaveObject<T> { BehaviorTree<T> tree; int step; IntArray taskSteps; SaveObject (BehaviorTree<T> tree, int step, IntArray taskSteps) { this.tree = tree; this.step = step; this.taskSteps = taskSteps; } } }