330 lines
11 KiB
C#
330 lines
11 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using Unity.Profiling;
|
|
|
|
namespace MBT
|
|
{
|
|
[DisallowMultipleComponent]
|
|
// [RequireComponent(typeof(Blackboard))]
|
|
public class MonoBehaviourTree : MonoBehaviour
|
|
{
|
|
private static readonly ProfilerMarker _TickMarker = new ProfilerMarker("MonoBehaviourTree.Tick");
|
|
|
|
[HideInInspector]
|
|
public Node selectedEditorNode;
|
|
public bool repeatOnFinish = false;
|
|
public int maxExecutionsPerTick = 1000;
|
|
public MonoBehaviourTree parent;
|
|
|
|
/// <summary>
|
|
/// Event triggered when tree is about to be updated
|
|
/// </summary>
|
|
public event UnityAction onTick;
|
|
private List<IMonoBehaviourTreeTickListener> tickListeners = new List<IMonoBehaviourTreeTickListener>();
|
|
private Root rootNode;
|
|
private List<Node> executionStack;
|
|
private List<Node> executionLog;
|
|
private List<Decorator> interruptingNodes = new List<Decorator>();
|
|
public float LastTick { get; internal set; }
|
|
|
|
void Awake()
|
|
{
|
|
rootNode = GetComponent<Root>();
|
|
if (rootNode == null) {
|
|
Debug.LogWarning("Missing Root node in behaviour tree.", this);
|
|
}
|
|
|
|
// Find master parent tree and all nodes
|
|
MonoBehaviourTree masterTree = this.GetMasterTree();
|
|
Node[] nodes = GetComponents<Node>();
|
|
if(masterTree == this)
|
|
{
|
|
// Create lists with capicity
|
|
executionStack = new List<Node>(8);
|
|
executionLog = new List<Node>(nodes.Length);
|
|
// Set start node when tree is created first time
|
|
executionStack.Add(rootNode);
|
|
executionLog.Add(rootNode);
|
|
}
|
|
// Initialize nodes of tree/subtree
|
|
for (int i = 0; i < nodes.Length; i++)
|
|
{
|
|
Node n = nodes[i];
|
|
n.behaviourTree = masterTree;
|
|
n.runningNodeResult = new NodeResult(Status.Running, n);
|
|
}
|
|
}
|
|
|
|
private void EvaluateInterruptions()
|
|
{
|
|
if (interruptingNodes.Count == 0) {
|
|
return;
|
|
}
|
|
|
|
// Find node with highest priority - closest to the root (the smallest number)
|
|
Decorator abortingNode = interruptingNodes[0];
|
|
for (int i = 1; i < interruptingNodes.Count; i++)
|
|
{
|
|
Decorator d = interruptingNodes[i];
|
|
if (d.runtimePriority < abortingNode.runtimePriority) {
|
|
abortingNode = d;
|
|
}
|
|
}
|
|
|
|
// Revert stack
|
|
executionStack.Clear();
|
|
executionStack.AddRange(abortingNode.GetStoredTreeSnapshot());
|
|
// Restore flow of events in nodes after abort
|
|
for (int i = 0; i < executionStack.Count; i++)
|
|
{
|
|
Node node = executionStack[i];
|
|
if (node.status == Status.Running)
|
|
{
|
|
// This node is still running and might need to restore the state
|
|
node.OnBehaviourTreeAbort();
|
|
}
|
|
else if (node.status == Status.Success || node.status == Status.Failure)
|
|
{
|
|
// This node returned failure or success, so reenter it and call OnEnter
|
|
node.OnEnter();
|
|
}
|
|
// All nodes in execution stack should be in running state
|
|
node.status = Status.Running;
|
|
}
|
|
|
|
int nodeIndex = abortingNode.runtimePriority - 1;
|
|
// Sanity check
|
|
if (abortingNode != executionLog[nodeIndex]) {
|
|
Debug.LogWarning("Priority of node does not match with exectuion log");
|
|
}
|
|
// Abort nodes in log
|
|
ResetNodesTo(abortingNode, true);
|
|
// Reset aborting node
|
|
abortingNode.status = Status.Ready;
|
|
// Reset list and wait for new interruptions
|
|
interruptingNodes.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update tree state.
|
|
/// </summary>
|
|
public void Tick()
|
|
{
|
|
_TickMarker.Begin();
|
|
// Fire Tick event and notify listeners
|
|
onTick?.Invoke();
|
|
for (int i = 0; i < tickListeners.Count; i++)
|
|
{
|
|
tickListeners[i].OnBehaviourTreeTick();
|
|
}
|
|
|
|
// Check if there are any interrupting nodes
|
|
EvaluateInterruptions();
|
|
|
|
// Max number of traversed nodes
|
|
int executionLimit = maxExecutionsPerTick;
|
|
// Traverse tree
|
|
while (executionStack.Count > 0)
|
|
{
|
|
if (executionLimit == 0) {
|
|
LastTick = Time.time;
|
|
_TickMarker.End();
|
|
return;
|
|
}
|
|
executionLimit -= 1;
|
|
|
|
// Execute last element in stack
|
|
Node currentNode = executionStack[executionStack.Count - 1];
|
|
NodeResult nodeResult = currentNode.Execute();
|
|
// Set new status
|
|
currentNode.status = nodeResult.status;
|
|
if (nodeResult.status == Status.Running) {
|
|
// If node is running, then stop execution or continue children
|
|
Node child = nodeResult.child;
|
|
if (child == null) {
|
|
// Stop execution and continue next tick
|
|
LastTick = Time.time;
|
|
_TickMarker.End();
|
|
return;
|
|
} else {
|
|
// Add child to execution stack and execute it in next loop
|
|
executionStack.Add(child);
|
|
executionLog.Add(child);
|
|
// IMPORTANT: Priority must be > 0 and assigned in this order
|
|
child.runtimePriority = executionLog.Count;
|
|
child.OnAllowInterrupt();
|
|
child.OnEnter();
|
|
#if UNITY_EDITOR
|
|
// Stop execution if breakpoint is set on this node
|
|
if (child.breakpoint)
|
|
{
|
|
Debug.Break();
|
|
UnityEditor.Selection.activeGameObject = this.gameObject;
|
|
Debug.Log("MBT Breakpoint: " + child.title, this);
|
|
LastTick = Time.time;
|
|
_TickMarker.End();
|
|
return;
|
|
}
|
|
#endif
|
|
continue;
|
|
}
|
|
} else {
|
|
// Remove last node from stack and move up (closer to root)
|
|
currentNode.OnExit();
|
|
executionStack.RemoveAt(executionStack.Count - 1);
|
|
}
|
|
}
|
|
|
|
// Run this when execution stack is empty and BT should repeat
|
|
if (repeatOnFinish) {
|
|
Restart();
|
|
}
|
|
|
|
LastTick = Time.time;
|
|
_TickMarker.End();
|
|
}
|
|
|
|
public void AddTickListener(IMonoBehaviourTreeTickListener listener)
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (tickListeners.Contains(listener))
|
|
{
|
|
Debug.LogErrorFormat(this, "Tick listener {0} has been already added.", listener);
|
|
}
|
|
#endif
|
|
tickListeners.Add(listener);
|
|
}
|
|
|
|
public void RemoveTickListener(IMonoBehaviourTreeTickListener listener)
|
|
{
|
|
tickListeners.Remove(listener);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method should be called to abort tree to given node
|
|
/// </summary>
|
|
/// <param name="node">Abort and revert tree to this node</param>
|
|
internal void Interrupt(Decorator node)
|
|
{
|
|
if (!interruptingNodes.Contains(node)) {
|
|
interruptingNodes.Add(node);
|
|
}
|
|
}
|
|
|
|
internal void ResetNodesTo(Node node, bool aborted = false)
|
|
{
|
|
int i = executionLog.Count - 1;
|
|
// Reset status and get index of node
|
|
while (i >= 0)
|
|
{
|
|
Node n = executionLog[i];
|
|
if (n == node) {
|
|
break;
|
|
}
|
|
// If node is running (on exec stack) then call exit
|
|
if (n.status == Status.Running) {
|
|
n.OnExit();
|
|
// IMPROVEMENT: Abort event can be added or abort param onExit
|
|
}
|
|
n.status = Status.Ready;
|
|
n.OnDisallowInterrupt();
|
|
i -= 1;
|
|
}
|
|
// Reset log
|
|
i += 1;
|
|
if (i >= executionLog.Count) {
|
|
return;
|
|
}
|
|
executionLog.RemoveRange(i, executionLog.Count - i);
|
|
}
|
|
|
|
private void ResetNodes()
|
|
{
|
|
for (int i = 0; i < executionLog.Count; i++)
|
|
{
|
|
Node node = executionLog[i];
|
|
if (node.status == Status.Running)
|
|
{
|
|
node.OnExit();
|
|
}
|
|
node.OnDisallowInterrupt();
|
|
node.status = Status.Ready;
|
|
}
|
|
executionLog.Clear();
|
|
executionStack.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets state to root node
|
|
/// </summary>
|
|
public void Restart()
|
|
{
|
|
ResetNodes();
|
|
executionStack.Add(rootNode);
|
|
executionLog.Add(rootNode);
|
|
}
|
|
|
|
internal void GetStack(ref Node[] stack)
|
|
{
|
|
// Resize array when size is too small
|
|
if (executionStack.Count > stack.Length)
|
|
{
|
|
// Node should not change priority and position during runtime
|
|
// It means the array will be resized once during first call of this method
|
|
Array.Resize<Node>(ref stack, executionStack.Count);
|
|
}
|
|
#if UNITY_EDITOR
|
|
// Additional sanity check in case nodes are reordered or changed in editor
|
|
if (stack.Length > executionStack.Count)
|
|
{
|
|
Debug.LogError("Changing order of MBT nodes during runtime might cause errors or unpredictable results.");
|
|
}
|
|
#endif
|
|
// Copy elements to provided array
|
|
executionStack.CopyTo(stack);
|
|
}
|
|
|
|
public Node GetRoot()
|
|
{
|
|
return rootNode;
|
|
}
|
|
|
|
public MonoBehaviourTree GetMasterTree()
|
|
{
|
|
if (parent == null)
|
|
{
|
|
return this;
|
|
}
|
|
return parent.GetMasterTree();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
void OnValidate()
|
|
{
|
|
if (maxExecutionsPerTick <= 0)
|
|
{
|
|
maxExecutionsPerTick = 1;
|
|
}
|
|
|
|
if (parent != null)
|
|
{
|
|
if (parent == this)
|
|
{
|
|
parent = null;
|
|
Debug.LogWarning("This tree cannot be its own parent.");
|
|
return;
|
|
}
|
|
if (transform.parent == null || parent.gameObject != transform.parent.gameObject)
|
|
{
|
|
// parent = null;
|
|
Debug.LogWarning("Parent tree should be also parent of this gameobject.", this.gameObject);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|