/*******************************************************************************
* 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;
}
}
}