2431 lines
108 KiB
C#
2431 lines
108 KiB
C#
#if GRAPH_DESIGNER
|
|
/// ---------------------------------------------
|
|
/// Behavior Designer
|
|
/// Copyright (c) Opsive. All Rights Reserved.
|
|
/// https://www.opsive.com
|
|
/// ---------------------------------------------
|
|
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Opsive.BehaviorDesigner.Editor")]
|
|
namespace Opsive.BehaviorDesigner.Runtime
|
|
{
|
|
using Opsive.BehaviorDesigner.Runtime.Components;
|
|
using Opsive.BehaviorDesigner.Runtime.Groups;
|
|
using Opsive.BehaviorDesigner.Runtime.Systems;
|
|
using Opsive.BehaviorDesigner.Runtime.Tasks;
|
|
using Opsive.BehaviorDesigner.Runtime.Tasks.Events;
|
|
using Opsive.BehaviorDesigner.Runtime.Utility;
|
|
using Opsive.GraphDesigner.Runtime;
|
|
using Opsive.GraphDesigner.Runtime.Variables;
|
|
using Opsive.GraphDesigner.Runtime.Variables.ECS;
|
|
using Opsive.Shared.Utility;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using Unity.Entities;
|
|
using UnityEngine;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
using static Opsive.BehaviorDesigner.Runtime.BehaviorTreeData;
|
|
|
|
/// <summary>
|
|
/// Container for managing behavior tree logic.
|
|
/// </summary>
|
|
public class BehaviorTree : MonoBehaviour, IGraph, IGraphComponent, ISharedVariableContainer
|
|
{
|
|
[Tooltip("The name of the behavior tree.")]
|
|
[SerializeField] [Delayed] private string m_GraphName = "Behavior Tree";
|
|
[Tooltip("A user specified ID for the graph.")]
|
|
[SerializeField] [Delayed] private int m_Index;
|
|
[Tooltip("The graph data.")]
|
|
[SerializeField] private BehaviorTreeData m_Data = new BehaviorTreeData();
|
|
[Tooltip("Should the behavior tree start when the component is enabled?")]
|
|
[SerializeField] private bool m_StartWhenEnabled = true;
|
|
[Tooltip("Should the behavior tree pause when the tree is disabled?")]
|
|
[SerializeField] private bool m_PauseWhenDisabled;
|
|
[Tooltip("Specifies when the behavior tree should be updated.")]
|
|
[SerializeField] private UpdateMode m_UpdateMode;
|
|
[Tooltip("Specifies how many tasks should be updated during a single tick.")]
|
|
[SerializeField] private EvaluationType m_EvaluationType;
|
|
[Tooltip("The maximum number of tasks that can run if the evaluation type is set to EvaluationType.Count.")]
|
|
[SerializeField] [Range(1, ushort.MaxValue)] private int m_MaxEvaluationCount = 1;
|
|
[Tooltip("A reference to the subtree.")]
|
|
[SerializeField] private Subtree m_Subtree;
|
|
|
|
private GameObject m_GameObject;
|
|
private World m_World;
|
|
private Entity m_Entity;
|
|
private Dictionary<int, int> m_NodeIndexByRuntimeIndex = new Dictionary<int, int>();
|
|
private Task[] m_TaskByRuntimeIndex;
|
|
private ITaskObjectParentNode[] m_TaskObjectParentByRuntimeIndex;
|
|
private IConditionalReevaluation[] m_ConditionalReevaluationByRuntimeIndex;
|
|
private ECSVariableRegistry m_ECSVariableRegistry;
|
|
|
|
private static Dictionary<Entity, BehaviorTree> s_BehaviorTreeByEntity = new Dictionary<Entity, BehaviorTree>();
|
|
private static EventNodeComparer s_EventNodeComparer = new EventNodeComparer();
|
|
#if UNITY_EDITOR
|
|
private static Dictionary<Type, bool> s_AuthoringTaskSharedVariableFieldLookup = new Dictionary<Type, bool>();
|
|
#endif
|
|
private bool m_SubtreeOverride;
|
|
|
|
public static int GraphCount { get => s_BehaviorTreeByEntity.Count; }
|
|
|
|
public string Name { get => m_GraphName; set => m_GraphName = value; }
|
|
public int Index { get => m_Index; set => m_Index = value; }
|
|
public int UniqueID { get => m_Data.UniqueID; }
|
|
public UnityEngine.Object Parent { get {
|
|
if (this == null) { return null; }
|
|
return Application.isPlaying && m_GameObject != null ? m_GameObject : gameObject;
|
|
} }
|
|
private BehaviorTreeData Data { get => (m_Subtree != null && (!Application.isPlaying || !m_SubtreeOverride)) ? m_Subtree.Data : m_Data; }
|
|
public bool StartWhenEnabled { get => m_StartWhenEnabled; set => m_StartWhenEnabled = value; }
|
|
public bool PauseWhenDisabled { get => m_PauseWhenDisabled; set => m_PauseWhenDisabled = value; }
|
|
public UpdateMode UpdateMode { get => m_UpdateMode; set => m_UpdateMode = value; }
|
|
public EvaluationType EvaluationType { get => m_EvaluationType; set => m_EvaluationType = value; }
|
|
public int MaxEvaluationCount { get => m_MaxEvaluationCount; set => m_MaxEvaluationCount = Mathf.Max(1, Mathf.Min(ushort.MaxValue, value)); }
|
|
public IGraph Subgraph
|
|
{
|
|
get => m_Subtree;
|
|
set
|
|
{
|
|
if (m_Subtree == (Subtree)value) {
|
|
return;
|
|
}
|
|
|
|
var active = IsActive();
|
|
ClearTree();
|
|
m_Subtree = value as Subtree;
|
|
m_SubtreeOverride = false;
|
|
InheritSubtree(false);
|
|
if (active && !IsPaused()) {
|
|
StartBehavior();
|
|
}
|
|
#if UNITY_EDITOR
|
|
// Generate a new ID so the graph reloads.
|
|
if (Application.isPlaying) {
|
|
m_Data.RuntimeUniqueID = Guid.NewGuid().GetHashCode();
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public ILogicNode[] LogicNodes { get => Data.LogicNodes; set => Data.LogicNodes = value as ITreeLogicNode[]; }
|
|
public ITreeLogicNode[] TreeLogicNodes { get => Data.LogicNodes; set => Data.LogicNodes = value; }
|
|
public IEventNode[] EventNodes { get => Data.EventNodes; set => Data.EventNodes = value; }
|
|
public SharedVariable[] SharedVariables { get => m_Data.SharedVariables; set => m_Data.SharedVariables = value; }
|
|
public SharedVariableGroup[] SharedVariableGroups {
|
|
#if UNITY_EDITOR
|
|
get => m_Data.SharedVariableGroups;
|
|
set => m_Data.SharedVariableGroups = value;
|
|
#else
|
|
get => null;
|
|
set { }
|
|
#endif
|
|
}
|
|
public ushort[] DisabledLogicNodes { get => Data.DisabledLogicNodes; set => Data.DisabledLogicNodes = value; }
|
|
public ushort[] DisabledEventNodes { get => Data.DisabledEventNodes; set => Data.DisabledEventNodes = value; }
|
|
|
|
public SharedVariable.SharingScope VariableScope { get => SharedVariable.SharingScope.Graph; }
|
|
public World World { get => m_World; set { m_World = value; } }
|
|
public Entity Entity { get => m_Entity; set { m_Entity = value; } }
|
|
public static int BehaviorTreeCount { get => s_BehaviorTreeByEntity.Count; }
|
|
|
|
public LogicNodeProperties[] LogicNodeProperties {
|
|
#if UNITY_EDITOR
|
|
get => Data.LogicNodeProperties;
|
|
set => Data.LogicNodeProperties = value;
|
|
#else
|
|
get => null;
|
|
set { }
|
|
#endif
|
|
}
|
|
public NodeProperties[] EventNodeProperties {
|
|
#if UNITY_EDITOR
|
|
get => Data.EventNodeProperties;
|
|
set => Data.EventNodeProperties = value;
|
|
#else
|
|
get => null;
|
|
set { }
|
|
#endif
|
|
}
|
|
public GroupProperties[] GroupProperties {
|
|
#if UNITY_EDITOR
|
|
get => Data.GroupProperties;
|
|
set => Data.GroupProperties = value;
|
|
#else
|
|
get => null;
|
|
set { }
|
|
#endif
|
|
}
|
|
public InjectedGraphReference[] InjectedGraphReferences {
|
|
get {
|
|
if (m_Data.InjectedGraphReferences == null) {
|
|
return null;
|
|
}
|
|
return m_Data.InjectedGraphReferences.Array;
|
|
}
|
|
}
|
|
internal ResizableArray<InjectedSubtreeReference> InjectedSubtreeReferences {
|
|
get => m_Data.InjectedSubtreeReferences;
|
|
}
|
|
public TaskStatus Status { get
|
|
{
|
|
if (!Application.isPlaying || m_World == null || m_Entity == null || m_Data == null || m_Data.LogicNodes == null || Data.EventNodes == null) { return TaskStatus.Inactive; }
|
|
|
|
// Find the Start event node.
|
|
Start startEventNode = null;
|
|
for (int i = 0; i < m_Data.EventNodes.Length; ++i) {
|
|
if (m_Data.EventNodes[i].GetType() == typeof(Start)) {
|
|
startEventNode = m_Data.EventNodes[i] as Start;
|
|
}
|
|
}
|
|
if (startEventNode == null || startEventNode.ConnectedIndex >= m_Data.LogicNodes.Length) { return TaskStatus.Inactive; }
|
|
|
|
// The runtime index will match with the correct Entity TaskComponent.
|
|
var runtimeIndex = m_Data.LogicNodes[startEventNode.ConnectedIndex].RuntimeIndex;
|
|
var taskComponents = m_World.EntityManager.GetBuffer<TaskComponent>(m_Entity);
|
|
if (runtimeIndex >= taskComponents.Length) { return TaskStatus.Inactive; }
|
|
|
|
// Retun the status of the task that the Start node is connected to. This is the current tree status.
|
|
return taskComponents[runtimeIndex].Status;
|
|
} }
|
|
|
|
// Flow callbacks.
|
|
public Action OnBehaviorTreeStarted;
|
|
public Action<bool> OnBehaviorTreeStopped;
|
|
public Action OnBehaviorTreeDestroyed;
|
|
|
|
// Physics callbacks.
|
|
public Action<Collision> OnBehaviorTreeCollisionEnter;
|
|
public Action<Collision> OnBehaviorTreeCollisionExit;
|
|
public Action<Collision2D> OnBehaviorTreeCollisionEnter2D;
|
|
public Action<Collision2D> OnBehaviorTreeCollisionExit2D;
|
|
public Action<Collider> OnBehaviorTreeTriggerEnter;
|
|
public Action<Collider> OnBehaviorTreeTriggerExit;
|
|
public Action<Collider2D> OnBehaviorTreeTriggerEnter2D;
|
|
public Action<Collider2D> OnBehaviorTreeTriggerExit2D;
|
|
public Action<ControllerColliderHit> OnBehaviorTreeControllerColliderHit;
|
|
|
|
// Event callbacks.
|
|
public Action<BehaviorTree> OnWillSave;
|
|
public Action<BehaviorTree, bool> OnDidSave;
|
|
public Action<BehaviorTree> OnWillLoad;
|
|
public Action<BehaviorTree, bool> OnDidLoad;
|
|
|
|
// Coroutine support.
|
|
private Dictionary<string, List<TaskCoroutine>> m_ActiveTaskCoroutines;
|
|
|
|
/// <summary>
|
|
/// Serializes the behavior tree.
|
|
/// </summary>
|
|
public void Serialize()
|
|
{
|
|
Data.Serialize();
|
|
if (m_Subtree != null) {
|
|
m_Data.SerializeSharedVariables();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserialize the behavior tree.
|
|
/// </summary>
|
|
/// <param name="force">Should the behavior tree be force deserialized?</param>
|
|
/// <returns>True if the tree was deserialized.</returns>
|
|
public bool Deserialize(bool force = false)
|
|
{
|
|
if (this == null) {
|
|
return false;
|
|
}
|
|
|
|
// Force the deserialization if the tree hasn't been deserialized yet at runtime.
|
|
if (Application.isPlaying && m_GameObject == null) {
|
|
force = true;
|
|
m_GameObject = gameObject;
|
|
}
|
|
|
|
var deserialized = false;
|
|
if (m_Subtree != null) {
|
|
deserialized = InheritSubtree(force);
|
|
} else if (m_Data != null) {
|
|
deserialized = m_Data.Deserialize(this, this, force, Application.isPlaying, Application.isPlaying);
|
|
}
|
|
|
|
// Initialize tasks after deserialization. This is only needed at edit time as the tasks are initialized elsewhere at runtime.
|
|
if (!Application.isPlaying && deserialized && m_Data != null && m_Data.LogicNodes != null) {
|
|
for (int i = 0; i < m_Data.LogicNodes.Length; ++i) {
|
|
if (m_Data.LogicNodes[i] is Task task) {
|
|
task.Initialize(this, (ushort)i);
|
|
}
|
|
}
|
|
}
|
|
|
|
return deserialized;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inherits the subtree tasks and variables.
|
|
/// </summary>
|
|
/// <param name="force">Should the behavior tree be force deserialized?</param>
|
|
/// <returns>True if the subtree was inherited.</returns>
|
|
private bool InheritSubtree(bool force)
|
|
{
|
|
if (m_Subtree == null) {
|
|
if (Application.isPlaying) {
|
|
m_Data.EventNodes = null;
|
|
m_Data.LogicNodes = null;
|
|
m_Data.InjectedSubtreeReferences = null;
|
|
m_Data.DisabledLogicNodes = null;
|
|
m_Data.DisabledEventNodes = null;
|
|
#if UNITY_EDITOR
|
|
m_Data.EventNodeProperties = null;
|
|
m_Data.LogicNodeProperties = null;
|
|
m_Data.SharedVariableGroups = null;
|
|
m_Data.GroupProperties = null;
|
|
#endif
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// The local behavior tree variables should be used.
|
|
m_Data.DeserializeSharedVariables(this, force, false);
|
|
if (m_Subtree.DeserializeSharedVariables(force || (Application.isPlaying && !m_SubtreeOverride)) && Application.isPlaying && !m_SubtreeOverride && m_Data.SharedVariables != null) {
|
|
// Set the binding on the subtree before the tasks are loaded. This is necessary because a new SharedVariable instance may need to be created.
|
|
for (int i = 0; i < m_Data.SharedVariables.Length; ++i) {
|
|
m_Subtree.Data.OverrideVariableBinding(this, m_Data.SharedVariables[i]);
|
|
}
|
|
}
|
|
// Subtrees will not have access to GameObject or Scene variables. Copy the reference from the parent tree in order to allow the subtree to correctly load all of the SharedVariables.
|
|
if (Application.isPlaying && !m_SubtreeOverride && m_Data.VariableByNameMap != null) {
|
|
foreach (var variableMap in m_Data.VariableByNameMap) {
|
|
var sharedVariable = variableMap.Value;
|
|
if (sharedVariable.Scope == SharedVariable.SharingScope.GameObject || sharedVariable.Scope == SharedVariable.SharingScope.Scene) {
|
|
var variableAssignment = new BehaviorTreeData.VariableAssignment(sharedVariable.Name, sharedVariable.Scope);
|
|
if (m_Subtree.Data.VariableByNameMap.ContainsKey(variableAssignment)) {
|
|
continue;
|
|
}
|
|
m_Subtree.Data.VariableByNameMap.Add(variableAssignment, sharedVariable);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!m_Subtree.Deserialize(force || (Application.isPlaying && !m_Subtree.Pooled && !m_SubtreeOverride), false, Application.isPlaying, false)) {
|
|
return false;
|
|
}
|
|
|
|
// Copy the deserialized objects at runtime to ensure each object is unique.
|
|
if (Application.isPlaying && !m_SubtreeOverride) {
|
|
m_Data.OverrideData(this, m_Subtree.Data, m_Data.SharedVariables, m_Subtree.Pooled);
|
|
m_GameObject = gameObject;
|
|
m_SubtreeOverride = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deserializes the shared variables.
|
|
/// </summary>
|
|
/// <param name="force">Should the variables be force deserialized?</param>
|
|
/// <returns>True if the variables were deserialized.</returns>
|
|
public bool DeserializeSharedVariables(bool force = false)
|
|
{
|
|
return m_Data.DeserializeSharedVariables(this, force, Application.isPlaying);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the specified node.
|
|
/// </summary>
|
|
/// <param name="node">The node that should be added.</param>
|
|
public void AddNode(ILogicNode node)
|
|
{
|
|
Data.AddNode(node as ITreeLogicNode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the specified node.
|
|
/// </summary>
|
|
/// <param name="node">The node that should be removed.</param>
|
|
/// <returns>True if the node was removed.</returns>
|
|
public bool RemoveNode(ILogicNode node)
|
|
{
|
|
return Data.RemoveNode(node as ITreeLogicNode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the specified event node.
|
|
/// </summary>
|
|
/// <param name="eventNode">The event node that should be added.</param>
|
|
public void AddNode(IEventNode eventNode)
|
|
{
|
|
Data.AddNode(eventNode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the specified event node.
|
|
/// </summary>
|
|
/// <param name="eventNode">The event node that should be removed.</param>
|
|
/// <returns>True if the event node was removed.</returns>
|
|
public bool RemoveNode(IEventNode eventNode)
|
|
{
|
|
return Data.RemoveNode(eventNode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieves the UniqueID of the graph.
|
|
/// </summary>
|
|
/// <param name="forceDesignID">Should the non-runtime ID be retrieved?</param>
|
|
/// <returns>The UniqueID of the graph.</returns>
|
|
public int GetUniqueID(bool forceDesignID = false)
|
|
{
|
|
if (forceDesignID) {
|
|
return m_Data.UniqueID;
|
|
}
|
|
return m_Data.RuntimeUniqueID != 0 ? m_Data.RuntimeUniqueID : m_Data.UniqueID;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the Node of the specified type.
|
|
/// </summary>
|
|
/// <param name="type">The type of Node that should be retrieved.</typeparam>
|
|
/// <returns>The Node of the specified type (can be null).</returns>
|
|
public ILogicNode GetNode(Type type)
|
|
{
|
|
return Data.GetNode(type);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the EventNode of the specified type.
|
|
/// </summary>
|
|
/// <param name="type">The type of EventNode that should be retrieved.</typeparam>
|
|
/// <returns>The EventNode of the specified type (can be null). If the node is found the index will also be returned.</returns>
|
|
public (IEventNode, ushort) GetEventNode(Type type)
|
|
{
|
|
return Data.GetEventNode(type);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The component has been enabled.
|
|
/// </summary>
|
|
private void OnEnable()
|
|
{
|
|
if (m_World != null && m_StartWhenEnabled) {
|
|
StartBehavior();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The component has started.
|
|
/// </summary>
|
|
private void Start()
|
|
{
|
|
if (m_StartWhenEnabled) {
|
|
StartBehavior();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the behavior tree.
|
|
/// </summary>
|
|
/// <returns>True if the behavior tree was started.</returns>
|
|
public bool StartBehavior()
|
|
{
|
|
var world = m_World == null ? World.DefaultGameObjectInjectionWorld : m_World;
|
|
var entity = m_Entity == Entity.Null ? world.EntityManager.CreateEntity() : m_Entity;
|
|
return StartBehavior(world, entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the behavior tree.
|
|
/// </summary>
|
|
/// <param name="world">The world that should contain the behavior tree.</param>
|
|
/// <param name="entity">The entity that should contain the behavior tree.</param>
|
|
/// <returns>True if the behavior tree was started.</returns>
|
|
public bool StartBehavior(World world, Entity entity)
|
|
{
|
|
return StartBehavior(world, entity, typeof(Start));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the behavior tree.
|
|
/// </summary>
|
|
/// <param name="world">The world that contains the entity.</param>
|
|
/// <param name="entity">The entity that should contain the behavior tree.</param>
|
|
/// <param name="startBranchType">The type of branch that should be started.</param>
|
|
/// <returns>True if the behavior tree was started.</returns>
|
|
public bool StartBehavior(World world, Entity entity, Type startBranchType)
|
|
{
|
|
if (world == null) {
|
|
return false;
|
|
}
|
|
|
|
if (s_BehaviorTreeByEntity.ContainsKey(entity)) {
|
|
// The behavior tree may be paused.
|
|
if (IsPaused(world, entity)) {
|
|
world.EntityManager.SetComponentEnabled<EnabledFlag>(entity, true);
|
|
if (OnBehaviorTreeStarted != null) {
|
|
OnBehaviorTreeStarted();
|
|
}
|
|
// Tasks can implement a pause specific interface.
|
|
var tasks = Data.LogicNodes;
|
|
for (int i = 0; i < tasks.Length; ++i) {
|
|
if (!(tasks[i] is IPausableTask pausableTask)) {
|
|
continue;
|
|
}
|
|
pausableTask.Resume(world, entity);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// An active tree may have completed. Start the requested branch again if it is no longer running.
|
|
if (!IsRunning(entity)) {
|
|
var started = StartBranch(world, entity, startBranchType);
|
|
if (started && OnBehaviorTreeStarted != null) {
|
|
OnBehaviorTreeStarted();
|
|
}
|
|
return started;
|
|
}
|
|
|
|
// The tree cannot be started multiple times.
|
|
return false;
|
|
}
|
|
s_BehaviorTreeByEntity.Add(entity, this);
|
|
|
|
if (!InitializeTree(world, entity)) {
|
|
return false;
|
|
}
|
|
|
|
var branchStarted = StartBranch(startBranchType);
|
|
if (branchStarted && OnBehaviorTreeStarted != null) {
|
|
OnBehaviorTreeStarted();
|
|
}
|
|
return branchStarted;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the tree within DOTS for all of the event tasks.
|
|
/// </summary>
|
|
/// <returns>True if the behavior tree was initialized.</returns>
|
|
internal bool InitializeTree()
|
|
{
|
|
var world = m_World == null ? World.DefaultGameObjectInjectionWorld : m_World;
|
|
var entity = m_Entity == Entity.Null ? world.EntityManager.CreateEntity() : m_Entity;
|
|
return InitializeTree(world, entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the tree within DOTS for all of the event tasks.
|
|
/// </summary>
|
|
/// <param name="world">The world that contains the entity.</param>
|
|
/// <param name="entity">The entity that should contain the behavior tree.</param>
|
|
/// <returns>True if the behavior tree was initialized.</returns>
|
|
internal bool InitializeTree(World world, Entity entity)
|
|
{
|
|
if (!Deserialize(false)) {
|
|
enabled = false;
|
|
return false;
|
|
}
|
|
|
|
if (m_Data.EventNodes == null || world == null) {
|
|
return false;
|
|
}
|
|
|
|
// The tree may be reinitialized with the same world/entity.
|
|
m_World = world;
|
|
m_Entity = entity;
|
|
|
|
// The tree may have already been initialized.
|
|
if (world.EntityManager.HasComponent<EvaluateFlag>(entity)) {
|
|
return true;
|
|
}
|
|
|
|
// Initialize the branch according to the connected index. This will ensure when the task is referencing other
|
|
// tasks the index will be correct.
|
|
var eventNodes = m_Data.EventNodes;
|
|
HashSet<IEventNode> disabledNodes = null;
|
|
if (m_Data.DisabledEventNodes != null && m_Data.DisabledEventNodes.Length > 0) {
|
|
disabledNodes = new HashSet<IEventNode>();
|
|
for (int i = 0; i < m_Data.DisabledEventNodes.Length; ++i) {
|
|
disabledNodes.Add(eventNodes[m_Data.DisabledEventNodes[i]]);
|
|
}
|
|
}
|
|
#if UNITY_EDITOR
|
|
var eventNodeProperties = Data.EventNodeProperties;
|
|
Array.Sort<IEventNode, NodeProperties>(eventNodes, eventNodeProperties, s_EventNodeComparer);
|
|
Data.EventNodeProperties = eventNodeProperties;
|
|
#endif
|
|
Array.Sort(eventNodes, s_EventNodeComparer);
|
|
// The disabled event nodes array only stores the index. Update the index with the sorted value.
|
|
if (disabledNodes != null) {
|
|
var index = 0;
|
|
var disabledEventNodes = m_Data.DisabledEventNodes;
|
|
for (int i = 0; i < eventNodes.Length; ++i) {
|
|
if (disabledNodes.Contains(eventNodes[i])) {
|
|
disabledEventNodes[index] = (ushort)i;
|
|
index++;
|
|
}
|
|
}
|
|
m_Data.DisabledEventNodes = disabledEventNodes;
|
|
}
|
|
|
|
m_ECSVariableRegistry?.Dispose();
|
|
|
|
var registry = new ECSVariableRegistry();
|
|
for (int i = 0; i < eventNodes.Length; ++i) {
|
|
InitializeBranch(world, entity, registry, eventNodes[i]);
|
|
}
|
|
if (registry.Count == 0) {
|
|
registry.Dispose();
|
|
m_ECSVariableRegistry = null;
|
|
return true;
|
|
}
|
|
|
|
registry.Bake(world, entity);
|
|
m_ECSVariableRegistry = registry;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize the specified event branch within DOTS.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that the branch should be added to.</param>
|
|
/// <param name="registry">The ECS variable registry for registering SharedVariable fields.</param>
|
|
/// <param name="eventTask">The task that should be setup.</param>
|
|
private void InitializeBranch(World world, Entity entity, ECSVariableRegistry registry, IEventNode eventTask)
|
|
{
|
|
if (Data.LogicNodes == null) {
|
|
return;
|
|
}
|
|
|
|
// There must be a starting event node.
|
|
if (eventTask == null || eventTask.ConnectedIndex >= Data.LogicNodes.Length) {
|
|
return;
|
|
}
|
|
|
|
if (!world.EntityManager.HasBuffer<TaskComponent>(entity)) {
|
|
var taskComponentBuffer = world.EntityManager.AddBuffer<TaskComponent>(entity);
|
|
taskComponentBuffer.EnsureCapacity(Data.LogicNodes.Length);
|
|
}
|
|
|
|
DynamicBuffer<BranchComponent> branchComponents;
|
|
if (world.EntityManager.HasBuffer<BranchComponent>(entity)) {
|
|
branchComponents = world.EntityManager.GetBuffer<BranchComponent>(entity);
|
|
} else {
|
|
branchComponents = world.EntityManager.AddBuffer<BranchComponent>(entity);
|
|
}
|
|
var startBranchIndex = (ushort)branchComponents.Length;
|
|
branchComponents.Add(new BranchComponent() { ActiveIndex = ushort.MaxValue, NextIndex = ushort.MaxValue, LastActiveIndex = ushort.MaxValue, CanExecute = true });
|
|
|
|
ComponentUtility.AddEvaluationComponent(world, entity, m_Data.LogicNodes.Length, m_EvaluationType, m_MaxEvaluationCount);
|
|
world.EntityManager.AddComponent<EnabledFlag>(entity);
|
|
world.EntityManager.AddComponent<EvaluateFlag>(entity);
|
|
// A manual update mode will require the user to call the Tick method.
|
|
if (m_UpdateMode == UpdateMode.Manual) {
|
|
world.EntityManager.SetComponentEnabled<EnabledFlag>(entity, false);
|
|
world.EntityManager.SetComponentEnabled<EvaluateFlag>(entity, false);
|
|
}
|
|
|
|
// Get the required parent system groups.
|
|
var traversalTaskSystemGroup = world.GetOrCreateSystemManaged<TraversalTaskSystemGroup>();
|
|
var reevaluateTaskSystemGroup = world.GetOrCreateSystemManaged<ReevaluateTaskSystemGroup>();
|
|
var interruptTaskSystemGroup = world.GetOrCreateSystemManaged<InterruptTaskSystemGroup>();
|
|
|
|
// Add the necessary cleanup systems.
|
|
var behaviorTreeSystemGroup = world.GetOrCreateSystemManaged<BehaviorTreeSystemGroup>();
|
|
behaviorTreeSystemGroup.AddSystemToUpdateList(world.GetOrCreateSystem<EvaluationCleanupSystem>());
|
|
behaviorTreeSystemGroup.AddSystemToUpdateList(world.GetOrCreateSystem<InterruptedCleanupSystem>());
|
|
|
|
var canReevaluate = false;
|
|
var taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
var taskOffset = (ushort)(eventTask.ConnectedIndex - taskComponents.Length);
|
|
for (int i = eventTask.ConnectedIndex; i < m_Data.LogicNodes.Length; ++i) {
|
|
// Don't initialize tasks that aren't connected to the event node.
|
|
if (i > eventTask.ConnectedIndex && m_Data.LogicNodes[i].ParentIndex == ushort.MaxValue) {
|
|
break;
|
|
}
|
|
|
|
taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
|
|
// Determine the branch index based off of the parent. If the parent is a parallel node then the node will be part of a new branch.
|
|
var branchIndex = startBranchIndex;
|
|
if (m_Data.LogicNodes[i].ParentIndex != ushort.MaxValue) {
|
|
branchComponents = world.EntityManager.GetBuffer<BranchComponent>(entity);
|
|
var parentIndex = m_Data.LogicNodes[i].ParentIndex;
|
|
if (m_Data.LogicNodes[parentIndex] is IParallelNode) {
|
|
branchIndex = (ushort)branchComponents.Length;
|
|
} else {
|
|
branchIndex = taskComponents[parentIndex - taskOffset].BranchIndex;
|
|
}
|
|
|
|
// A new branch component may need to be added to keep track of the active task index for that branch.
|
|
if (branchIndex >= branchComponents.Length) {
|
|
branchComponents.Add(new BranchComponent() { ActiveIndex = ushort.MaxValue, NextIndex = ushort.MaxValue, LastActiveIndex = ushort.MaxValue, CanExecute = true });
|
|
}
|
|
}
|
|
|
|
// The TaskComponent index will be different from the LogicNode index if the tree has a gap of tasks that are not connected.
|
|
// The RuntimeIndex maps the LogicNode index to the TaskComponent index.
|
|
var node = m_Data.LogicNodes[i];
|
|
node.RuntimeIndex = (ushort)(node.Index - taskOffset);
|
|
m_Data.LogicNodes[i] = node;
|
|
EnsureRuntimeTaskCacheCapacity(node.RuntimeIndex);
|
|
if (!m_NodeIndexByRuntimeIndex.ContainsKey(node.RuntimeIndex)) { // The index will already exist if multiple entities use the same MonoBehaviour.
|
|
m_NodeIndexByRuntimeIndex.Add(node.RuntimeIndex, node.Index);
|
|
}
|
|
|
|
if (m_Data.LogicNodes[i] is IAuthoringTask authoringTask) {
|
|
m_TaskByRuntimeIndex[node.RuntimeIndex] = null;
|
|
m_TaskObjectParentByRuntimeIndex[node.RuntimeIndex] = null;
|
|
m_ConditionalReevaluationByRuntimeIndex[node.RuntimeIndex] = null;
|
|
authoringTask.AddBufferElement(world, entity, registry, gameObject);
|
|
taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
taskComponents.Add(new TaskComponent {
|
|
Status = TaskStatus.Inactive,
|
|
Index = node.RuntimeIndex,
|
|
ParentIndex = AdjustByIndexOffset(m_Data.LogicNodes[i].ParentIndex, taskOffset),
|
|
SiblingIndex = AdjustByIndexOffset(m_Data.LogicNodes[i].SiblingIndex, taskOffset),
|
|
ChildUpperIndex = node.RuntimeIndex,
|
|
BranchIndex = branchIndex,
|
|
FlagComponentType = authoringTask.Flag,
|
|
Enabled = IsNodeEnabled(true, m_Data.LogicNodes[i].ParentIndex) && IsNodeEnabled(true, i),
|
|
});
|
|
|
|
world.EntityManager.AddComponent(entity, authoringTask.Flag);
|
|
world.EntityManager.SetComponentEnabled(entity, authoringTask.Flag, false);
|
|
traversalTaskSystemGroup.AddSystemToUpdateList(world.GetOrCreateSystem(authoringTask.SystemType));
|
|
} else if (m_Data.LogicNodes[i] is Task taskObject) {
|
|
m_TaskByRuntimeIndex[node.RuntimeIndex] = taskObject;
|
|
m_TaskObjectParentByRuntimeIndex[node.RuntimeIndex] = (taskObject is IParentNode) ? taskObject as ITaskObjectParentNode : null;
|
|
m_ConditionalReevaluationByRuntimeIndex[node.RuntimeIndex] = taskObject as IConditionalReevaluation;
|
|
taskObject.AddBufferElement(world, entity, GetHashCode(), node.RuntimeIndex);
|
|
taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
taskComponents.Add(new TaskComponent {
|
|
Status = TaskStatus.Inactive,
|
|
Index = node.RuntimeIndex,
|
|
ParentIndex = AdjustByIndexOffset(m_Data.LogicNodes[i].ParentIndex, taskOffset),
|
|
SiblingIndex = AdjustByIndexOffset(m_Data.LogicNodes[i].SiblingIndex, taskOffset),
|
|
ChildUpperIndex = node.RuntimeIndex,
|
|
BranchIndex = branchIndex,
|
|
FlagComponentType = typeof(TaskObjectFlag),
|
|
Enabled = IsNodeEnabled(true, m_Data.LogicNodes[i].ParentIndex) && IsNodeEnabled(true, i),
|
|
});
|
|
world.EntityManager.AddComponent(entity, typeof(TaskObjectFlag));
|
|
world.EntityManager.SetComponentEnabled(entity, typeof(TaskObjectFlag), false);
|
|
traversalTaskSystemGroup.AddSystemToUpdateList(world.GetOrCreateSystem(typeof(TaskObjectSystem)));
|
|
|
|
taskObject.Initialize(this, node.RuntimeIndex);
|
|
} else {
|
|
Debug.LogError("Error: Unknown Task Type.", this);
|
|
continue;
|
|
}
|
|
|
|
// Conditional tasks can be reevaluated.
|
|
if (m_Data.LogicNodes[i] is IConditional && m_Data.LogicNodes[i].ParentIndex != ushort.MaxValue) {
|
|
var reevaluateFlag = new ComponentType();
|
|
Type reevaluateSystem;
|
|
if (m_Data.LogicNodes[i] is IAuthoringTask conditionalAuthoringTask) {
|
|
if (m_Data.LogicNodes[i] is IReevaluateResponder reevaluateTask) {
|
|
reevaluateFlag = reevaluateTask.ReevaluateFlag;
|
|
reevaluateSystem = reevaluateTask.ReevaluateSystemType;
|
|
} else {
|
|
Debug.LogWarning($"Warning: The task {m_Data.LogicNodes[i]} doesn't have a separate reevaluation tag. This may lead to unexpected results. It is recommend " +
|
|
$"that the task implements the IReevaluate interface.");
|
|
reevaluateFlag = conditionalAuthoringTask.Flag;
|
|
reevaluateSystem = conditionalAuthoringTask.SystemType;
|
|
}
|
|
} else {
|
|
reevaluateFlag = typeof(TaskObjectReevaluateFlag);
|
|
reevaluateSystem = typeof(TaskObjectReevaluateSystem);
|
|
}
|
|
world.EntityManager.AddComponent(entity, reevaluateFlag);
|
|
world.EntityManager.SetComponentEnabled(entity, reevaluateFlag, false);
|
|
ComponentUtility.AddInterruptComponents(world.EntityManager, entity);
|
|
reevaluateTaskSystemGroup.AddSystemToUpdateList(world.GetOrCreateSystem(reevaluateSystem));
|
|
|
|
// Ignore any decorator tasks when determining the parent. Composite tasks can only be a conditional abort parent.
|
|
IComposite parentComposite = null;
|
|
ushort parentIndex;
|
|
var compositeParentIndex = m_Data.LogicNodes[i].ParentIndex;
|
|
do {
|
|
parentComposite = m_Data.LogicNodes[compositeParentIndex] as IComposite;
|
|
parentIndex = compositeParentIndex;
|
|
compositeParentIndex = m_Data.LogicNodes[compositeParentIndex].ParentIndex;
|
|
} while (compositeParentIndex != ushort.MaxValue && parentComposite == null);
|
|
|
|
var abortParent = parentComposite as IConditionalAbortParent;
|
|
if (abortParent != null && abortParent.AbortType != ConditionalAbortType.None) {
|
|
canReevaluate = true;
|
|
var lowerPriorityLowerIndex = ushort.MaxValue;
|
|
var lowerPriorityUpperIndex = ushort.MaxValue;
|
|
// Lower Priority aborts are recursive allowing a nested conditional task to be reevaluated even if the direct
|
|
// parent's sibling isn't active.
|
|
if (abortParent.AbortType == ConditionalAbortType.LowerPriority || abortParent.AbortType == ConditionalAbortType.Both) {
|
|
var parentChildCount = m_Data.GetChildCount(m_Data.LogicNodes[parentIndex], m_Data.LogicNodes);
|
|
lowerPriorityLowerIndex = AdjustByIndexOffset((ushort)(parentIndex + parentChildCount), taskOffset);
|
|
var parentTranversalIndex = parentIndex;
|
|
IConditionalAbortParent parentAbortParent = null;
|
|
while (parentTranversalIndex != ushort.MaxValue && ((parentAbortParent = m_Data.LogicNodes[parentTranversalIndex] as IConditionalAbortParent) != null || m_Data.LogicNodes[parentTranversalIndex] is IDecorator)) {
|
|
if (parentAbortParent != null && parentAbortParent.AbortType != ConditionalAbortType.LowerPriority && parentAbortParent.AbortType != ConditionalAbortType.Both) {
|
|
break;
|
|
}
|
|
parentIndex = parentTranversalIndex;
|
|
parentTranversalIndex = m_Data.LogicNodes[parentTranversalIndex].ParentIndex;
|
|
}
|
|
|
|
// The conditional abort can reevaluate up to the rightmost task of the parent.
|
|
parentTranversalIndex = parentTranversalIndex != ushort.MaxValue ? parentTranversalIndex : parentIndex;
|
|
parentChildCount = m_Data.GetChildCount(m_Data.LogicNodes[parentTranversalIndex], m_Data.LogicNodes);
|
|
lowerPriorityUpperIndex = AdjustByIndexOffset((ushort)(parentTranversalIndex + parentChildCount), taskOffset);
|
|
}
|
|
var selfPriorityUpperIndex = ushort.MaxValue;
|
|
if (abortParent.AbortType == ConditionalAbortType.Self || abortParent.AbortType == ConditionalAbortType.Both) {
|
|
if (m_Data.LogicNodes[parentIndex].SiblingIndex == ushort.MaxValue) {
|
|
selfPriorityUpperIndex = (ushort)(parentIndex + m_Data.GetChildCount(m_Data.LogicNodes[parentIndex], m_Data.LogicNodes));
|
|
} else {
|
|
selfPriorityUpperIndex = (ushort)(m_Data.LogicNodes[parentIndex].SiblingIndex - 1);
|
|
}
|
|
}
|
|
|
|
var reevaluateTaskComponents = world.EntityManager.AddBuffer<ReevaluateTaskComponent>(entity);
|
|
var reevaluateTaskComponent = new ReevaluateTaskComponent()
|
|
{
|
|
Index = node.RuntimeIndex,
|
|
AbortType = abortParent.AbortType,
|
|
ReevaluateFlagComponentType = reevaluateFlag,
|
|
LowerPriorityLowerIndex = lowerPriorityLowerIndex,
|
|
LowerPriorityUpperIndex = lowerPriorityUpperIndex,
|
|
SelfPriorityUpperIndex = selfPriorityUpperIndex,
|
|
};
|
|
|
|
reevaluateTaskComponents.Add(reevaluateTaskComponent);
|
|
}
|
|
}
|
|
|
|
// Add any systems that respond to interrupts.
|
|
if (m_Data.LogicNodes[i] is IInterruptResponder interruptResponder) {
|
|
interruptTaskSystemGroup.AddSystemToUpdateList(world.GetOrCreateSystem(interruptResponder.InterruptSystemType));
|
|
}
|
|
}
|
|
|
|
// Populates each task's child upper bound for O(1) parent/child range checks.
|
|
taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
TraversalUtility.PopulateChildUpperIndices(ref taskComponents);
|
|
|
|
if (canReevaluate) {
|
|
world.GetOrCreateSystemManaged<BeforeTraversalSystemGroup>().AddSystemToUpdateList(world.GetOrCreateSystem(typeof(ReevaluateSystem)));
|
|
}
|
|
|
|
// The event task may perform its own logic.
|
|
if (eventTask is IEventNodeEntityReceiver entityReceiver) {
|
|
entityReceiver.AddBufferElement(world, entity, gameObject, taskOffset);
|
|
}
|
|
if (eventTask is IEventNodeGameObjectReceiver gameObjectReceiver) {
|
|
gameObjectReceiver.Initialize(this);
|
|
}
|
|
|
|
reevaluateTaskSystemGroup.SortSystems();
|
|
interruptTaskSystemGroup.SortSystems();
|
|
traversalTaskSystemGroup.SortSystems();
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// Registers ECS SharedVariables for all authoring tasks within the specified branch by replaying their AddBufferElement calls on a scratch entity.
|
|
/// </summary>
|
|
/// <param name="world">The world that owns the scratch entity.</param>
|
|
/// <param name="entity">The scratch entity used to replay AddBufferElement.</param>
|
|
/// <param name="registry">The variable registry that should receive the registrations.</param>
|
|
/// <param name="eventTask">The event task that starts the branch.</param>
|
|
private void RegisterBranchSharedVariables(World world, Entity entity, ECSVariableRegistry registry, IEventNode eventTask)
|
|
{
|
|
var logicNodes = Data.LogicNodes;
|
|
if (logicNodes == null) {
|
|
return;
|
|
}
|
|
|
|
if (eventTask == null || eventTask.ConnectedIndex >= logicNodes.Length) {
|
|
return;
|
|
}
|
|
|
|
for (int i = eventTask.ConnectedIndex; i < logicNodes.Length; ++i) {
|
|
if (i > eventTask.ConnectedIndex && logicNodes[i].ParentIndex == ushort.MaxValue) {
|
|
break;
|
|
}
|
|
|
|
if (logicNodes[i] is IAuthoringTask authoringTask && AuthoringTaskHasSharedVariableFields(authoringTask)) {
|
|
authoringTask.AddBufferElement(world, entity, registry, gameObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an ECS variable registry for the tree without initializing the runtime entity state.
|
|
/// </summary>
|
|
/// <param name="world">The world used to create the scratch entity for AddBufferElement.</param>
|
|
/// <returns>The populated registry, or null if the tree has no ECS SharedVariables.</returns>
|
|
internal ECSVariableRegistry CreateECSVariableSyncRegistry(World world)
|
|
{
|
|
if (world == null || !world.IsCreated || !Deserialize(false)) {
|
|
return null;
|
|
}
|
|
|
|
var eventNodes = Data.EventNodes;
|
|
if (eventNodes == null || Data.LogicNodes == null) {
|
|
return null;
|
|
}
|
|
|
|
eventNodes = (IEventNode[])eventNodes.Clone();
|
|
Array.Sort(eventNodes, s_EventNodeComparer);
|
|
|
|
var scratchEntity = world.EntityManager.CreateEntity();
|
|
try {
|
|
var registry = new ECSVariableRegistry();
|
|
for (int i = 0; i < eventNodes.Length; ++i) {
|
|
RegisterBranchSharedVariables(world, scratchEntity, registry, eventNodes[i]);
|
|
}
|
|
|
|
if (registry.Count == 0) {
|
|
registry.Dispose();
|
|
return null;
|
|
}
|
|
|
|
return registry;
|
|
} finally {
|
|
if (world.EntityManager.Exists(scratchEntity)) {
|
|
world.EntityManager.DestroyEntity(scratchEntity);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Adjusts the index by the specified offset.
|
|
/// </summary>
|
|
/// <param name="index">The index that should be adjusted.</param>
|
|
/// <param name="offset">The offset that should be adjusted by.</param>
|
|
/// <returns>THe index by the specified offset.</returns>
|
|
private ushort AdjustByIndexOffset(ushort index, ushort offset)
|
|
{
|
|
if (index == ushort.MaxValue) { return index; }
|
|
return (ushort)(index - offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stars the branch with the specified event task type.
|
|
/// </summary>
|
|
/// <param name="eventTaskType">The branch that should be started.</param>
|
|
/// <returns>True if the branch was started.</returns>
|
|
public bool StartBranch(Type eventTaskType)
|
|
{
|
|
if (m_World == null || m_Entity == Entity.Null) {
|
|
return false;
|
|
}
|
|
|
|
return StartBranch(m_World, m_Entity, eventTaskType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stars the branch with the specified event task type.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the branch.</param>
|
|
/// <param name="eventTaskType">The branch that should be started.</param>
|
|
/// <returns>True if the branch was started.</returns>
|
|
public bool StartBranch(World world, Entity entity, Type eventTaskType)
|
|
{
|
|
if (!s_BehaviorTreeByEntity.ContainsKey(entity)) {
|
|
return StartBehavior(world, entity, eventTaskType);
|
|
}
|
|
|
|
if (m_Data.EventNodes == null || entity.Index == 0 || !Application.isPlaying) {
|
|
return false;
|
|
}
|
|
|
|
for (int i = 0; i < m_Data.EventNodes.Length; ++i) {
|
|
if (m_Data.EventNodes[i].GetType() == eventTaskType) {
|
|
// The branch cannot start if it is disabled.
|
|
if (!IsNodeEnabled(false, i)) {
|
|
Debug.LogError($"Error: Unable to start the {eventTaskType.Name} branch because the node is disabled.", this);
|
|
return false;
|
|
}
|
|
|
|
return StartBranch(world, entity, m_Data.EventNodes[i]);
|
|
}
|
|
}
|
|
|
|
Debug.LogError($"Error: Unable to start the {eventTaskType.Name} branch because it can't be found.", this);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the branch with the specified event task.
|
|
/// </summary>
|
|
/// <param name="eventTask">The branch that should be started.</param>
|
|
/// <returns>True if the branch was started.</returns>
|
|
public bool StartBranch(IEventNode eventTask)
|
|
{
|
|
if (m_World == null || m_Entity == Entity.Null) {
|
|
return false;
|
|
}
|
|
|
|
return StartBranch(m_World, m_Entity, eventTask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the branch with the specified event task.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the branch.</param>
|
|
/// <param name="eventTask">The branch that should be started.</param>
|
|
/// <returns>True if the branch was started.</returns>
|
|
public bool StartBranch(World world, Entity entity, IEventNode eventTask)
|
|
{
|
|
if (!Application.isPlaying || entity == Entity.Null) {
|
|
return false;
|
|
}
|
|
|
|
// The branch can't be started if it's not connected to any tasks.
|
|
if (eventTask.ConnectedIndex == ushort.MaxValue) {
|
|
Debug.LogError($"Error: Unable to start the {eventTask.GetType().Name} branch because it doesn't have a connecting task.", this);
|
|
return false;
|
|
}
|
|
|
|
// The tree needs to be setup before the branch can start.
|
|
if (!world.EntityManager.HasBuffer<BranchComponent>(entity)) {
|
|
InitializeTree();
|
|
}
|
|
|
|
var connectedIndex = m_Data.LogicNodes[eventTask.ConnectedIndex].RuntimeIndex;
|
|
return StartBranch(world, entity, connectedIndex, m_UpdateMode == UpdateMode.EveryFrame);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the branch. This method is static allowing for alread-baked entities to be able to start the branch.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the branch.</param>
|
|
/// <param name="connectedIndex">The index of the starting task.</param>
|
|
/// <param name="startEvaluation">Should the branch start evaluation? If false the tree will manually need to be ticked.</param>
|
|
/// <returns>True if the branch was started.</returns>
|
|
internal static bool StartBranch(World world, Entity entity, ushort connectedIndex, bool startEvaluation)
|
|
{
|
|
var taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
if (connectedIndex >= taskComponents.Length) {
|
|
Debug.LogError($"Error: Unable to start the branch on entity {entity} because the start index is greater than the task count.");
|
|
return false;
|
|
}
|
|
|
|
var startTask = taskComponents[connectedIndex];
|
|
// The branch can't be started twice or if it is disabled.
|
|
if (startTask.Status == TaskStatus.Queued || startTask.Status == TaskStatus.Running || !startTask.Enabled) {
|
|
return false;
|
|
}
|
|
|
|
var systemGroup = world.GetExistingSystemManaged<BehaviorTreeSystemGroup>();
|
|
if (systemGroup == null) {
|
|
systemGroup = World.DefaultGameObjectInjectionWorld.GetExistingSystemManaged<BehaviorTreeSystemGroup>();
|
|
if (systemGroup == null) {
|
|
return false;
|
|
}
|
|
}
|
|
systemGroup.Enabled = true;
|
|
|
|
// The branch can start.
|
|
startTask.Status = TaskStatus.Queued;
|
|
taskComponents[connectedIndex] = startTask;
|
|
|
|
var activeFlag = taskComponents[connectedIndex].FlagComponentType;
|
|
world.EntityManager.SetComponentEnabled(entity, activeFlag, true);
|
|
|
|
var branchComponents = world.EntityManager.GetBuffer<BranchComponent>(entity);
|
|
var branchIndex = taskComponents[connectedIndex].BranchIndex;
|
|
var branchComponent = branchComponents[branchIndex];
|
|
branchComponent.ActiveIndex = branchComponent.NextIndex = connectedIndex;
|
|
branchComponent.LastActiveIndex = ushort.MaxValue;
|
|
branchComponent.ActiveFlagComponentType = activeFlag;
|
|
branchComponents[branchIndex] = branchComponent;
|
|
|
|
ComponentUtility.ResetEvaluationComponent(world, entity);
|
|
|
|
if (startEvaluation) {
|
|
world.EntityManager.SetComponentEnabled<EnabledFlag>(entity, true);
|
|
world.EntityManager.SetComponentEnabled<EvaluateFlag>(entity, true);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree can currently synchronize ECS-backed SharedVariables for the specified entity.
|
|
/// </summary>
|
|
/// <param name="world">The world containing the behavior tree entity.</param>
|
|
/// <param name="entity">The entity containing the behavior tree.</param>
|
|
/// <returns>True if the entity currently has ECS-backed SharedVariables that can be synchronized.</returns>
|
|
internal bool HasECSVariableSync(World world, Entity entity)
|
|
{
|
|
return m_ECSVariableRegistry != null && world != null && world.IsCreated && entity != Entity.Null && m_World == world &&
|
|
m_Entity == entity && s_BehaviorTreeByEntity.TryGetValue(entity, out var behaviorTree) && behaviorTree == this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Syncs the managed SharedVariable values into the ECS shared variable buffer for the specified entity.
|
|
/// </summary>
|
|
/// <param name="world">The world containing the behavior tree entity.</param>
|
|
/// <param name="entity">The entity containing the behavior tree.</param>
|
|
internal void SyncManagedVariablesToECS(World world, Entity entity)
|
|
{
|
|
if (!HasECSVariableSync(world, entity)) {
|
|
return;
|
|
}
|
|
|
|
m_ECSVariableRegistry.SyncToECS(world, entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Syncs the ECS shared variable buffer back into the managed SharedVariable instances for the specified entity.
|
|
/// </summary>
|
|
/// <param name="world">The world containing the behavior tree entity.</param>
|
|
/// <param name="entity">The entity containing the behavior tree.</param>
|
|
internal void SyncECSVariablesToManaged(World world, Entity entity)
|
|
{
|
|
if (!HasECSVariableSync(world, entity)) {
|
|
return;
|
|
}
|
|
|
|
m_ECSVariableRegistry.SyncToManaged(world, entity);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// Resolves the authoring BehaviorTree from a GlobalObjectId string.
|
|
/// </summary>
|
|
/// <param name="authoringBehaviorTreeGlobalObjectId">The GlobalObjectId string for the authoring BehaviorTree.</param>
|
|
/// <param name="designGraphUniqueID">The design-time graph ID.</param>
|
|
/// <returns>The resolved BehaviorTree, or null if it could not be found.</returns>
|
|
internal static BehaviorTree ResolveBehaviorTreeFromGlobalObjectId(string authoringBehaviorTreeGlobalObjectId, int designGraphUniqueID)
|
|
{
|
|
if (!GlobalObjectId.TryParse(authoringBehaviorTreeGlobalObjectId, out var globalObjectId)) {
|
|
return null;
|
|
}
|
|
|
|
return ResolveBehaviorTreeFromObject(GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalObjectId), designGraphUniqueID);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a BehaviorTree from a Unity object reference.
|
|
/// </summary>
|
|
/// <param name="obj">The Unity object to resolve from.</param>
|
|
/// <param name="designGraphUniqueID">The design-time graph ID.</param>
|
|
/// <returns>The resolved BehaviorTree, or null if it could not be found.</returns>
|
|
private static BehaviorTree ResolveBehaviorTreeFromObject(UnityEngine.Object obj, int designGraphUniqueID)
|
|
{
|
|
if (obj == null) {
|
|
return null;
|
|
}
|
|
|
|
if (obj is BehaviorTree behaviorTree) {
|
|
return designGraphUniqueID == 0 || behaviorTree.UniqueID == designGraphUniqueID ? behaviorTree : null;
|
|
}
|
|
|
|
GameObject gameObject = null;
|
|
if (obj is Component component) {
|
|
gameObject = component.gameObject;
|
|
} else if (obj is GameObject resolvedGameObject) {
|
|
gameObject = resolvedGameObject;
|
|
}
|
|
|
|
if (gameObject == null) {
|
|
return null;
|
|
}
|
|
|
|
var behaviorTrees = gameObject.GetComponents<BehaviorTree>();
|
|
for (int i = 0; i < behaviorTrees.Length; ++i) {
|
|
if (designGraphUniqueID == 0 || behaviorTrees[i].UniqueID == designGraphUniqueID) {
|
|
return behaviorTrees[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the authoring task declares any SharedVariable fields that may participate in ECS synchronization.
|
|
/// </summary>
|
|
/// <param name="authoringTask">The authoring task to inspect.</param>
|
|
/// <returns>True if the task declares any SharedVariable fields.</returns>
|
|
private static bool AuthoringTaskHasSharedVariableFields(IAuthoringTask authoringTask)
|
|
{
|
|
if (authoringTask == null) {
|
|
return false;
|
|
}
|
|
|
|
var type = authoringTask.GetType();
|
|
if (s_AuthoringTaskSharedVariableFieldLookup.TryGetValue(type, out var hasSharedVariables)) {
|
|
return hasSharedVariables;
|
|
}
|
|
|
|
var currentType = type;
|
|
while (currentType != null && currentType != typeof(object)) {
|
|
var fields = currentType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
|
for (int i = 0; i < fields.Length; ++i) {
|
|
if (typeof(SharedVariable).IsAssignableFrom(fields[i].FieldType)) {
|
|
s_AuthoringTaskSharedVariableFieldLookup.Add(type, true);
|
|
return true;
|
|
}
|
|
}
|
|
currentType = currentType.BaseType;
|
|
}
|
|
|
|
s_AuthoringTaskSharedVariableFieldLookup.Add(type, false);
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Returns the behavior tree component specified by the entity.
|
|
/// </summary>
|
|
/// <param name="entity">The entity that should be retrieved.</param>
|
|
/// <returns>The behavior tree component specified by the ID.</returns>
|
|
public static BehaviorTree GetBehaviorTree(Entity entity)
|
|
{
|
|
if (s_BehaviorTreeByEntity.TryGetValue(entity, out var behaviorTree)) {
|
|
return behaviorTree;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the task at the specified index.
|
|
/// </summary>
|
|
/// <param name="index">The index of the task.</param>
|
|
/// <returns>The task at the specified index.</returns>
|
|
public ITreeLogicNode GetTask(int index)
|
|
{
|
|
Deserialize();
|
|
|
|
if (m_Data.LogicNodes == null || index < 0 || index >= m_Data.LogicNodes.Length) {
|
|
return null;
|
|
}
|
|
if (Application.isPlaying && m_NodeIndexByRuntimeIndex.Count > 0) {
|
|
return m_Data.LogicNodes[m_NodeIndexByRuntimeIndex[index]];
|
|
}
|
|
return m_Data.LogicNodes[index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the cached task object at the specified runtime index.
|
|
/// </summary>
|
|
/// <param name="index">The runtime index of the task.</param>
|
|
/// <returns>The cached task object. Can be null.</returns>
|
|
public Task GetTaskObject(int index)
|
|
{
|
|
if (m_TaskByRuntimeIndex == null || index < 0 || index >= m_TaskByRuntimeIndex.Length) {
|
|
return null;
|
|
}
|
|
|
|
return m_TaskByRuntimeIndex[index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the cached task object parent interface at the specified runtime index.
|
|
/// </summary>
|
|
/// <param name="index">The runtime index of the task.</param>
|
|
/// <returns>The cached parent interface. Can be null.</returns>
|
|
public ITaskObjectParentNode GetTaskObjectParent(int index)
|
|
{
|
|
if (m_TaskObjectParentByRuntimeIndex == null || index < 0 || index >= m_TaskObjectParentByRuntimeIndex.Length) {
|
|
return null;
|
|
}
|
|
|
|
return m_TaskObjectParentByRuntimeIndex[index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the cached conditional reevaluation interface at the specified runtime index.
|
|
/// </summary>
|
|
/// <param name="index">The runtime index of the task.</param>
|
|
/// <returns>The cached conditional reevaluation interface. Can be null.</returns>
|
|
public IConditionalReevaluation GetConditionalReevaluationTaskObject(int index)
|
|
{
|
|
if (m_ConditionalReevaluationByRuntimeIndex == null || index < 0 || index >= m_ConditionalReevaluationByRuntimeIndex.Length) {
|
|
return null;
|
|
}
|
|
|
|
return m_ConditionalReevaluationByRuntimeIndex[index];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the task with the specified type.
|
|
/// </summary>
|
|
/// <returns>The first task found with the specified type (can be null).</returns>
|
|
public T FindTask<T>() where T : Task
|
|
{
|
|
Deserialize();
|
|
|
|
if (m_Data.LogicNodes == null || m_Data.LogicNodes.Length == 0) {
|
|
return null;
|
|
}
|
|
|
|
for (int i = 0; i < m_Data.LogicNodes.Length; ++i) {
|
|
if (m_Data.LogicNodes[i] is T task) {
|
|
return task;
|
|
}
|
|
if (m_Data.LogicNodes[i] is IContainerNode containerNode) {
|
|
if (containerNode.Nodes == null) {
|
|
continue;
|
|
}
|
|
for (int j = 0; j < containerNode.Nodes.Length; ++j) {
|
|
if (containerNode.Nodes[j] is T stackedTask) {
|
|
return stackedTask;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the tasks with the specified type. This method does not have any allocations.
|
|
/// </summary>
|
|
/// <param name="foundTasks">A pre-initialized array that will contain the found tasks.</param>
|
|
/// <returns>The number of tasks found with the specified type.</returns>
|
|
public int FindTasks<T>(T[] foundTasks) where T : Task
|
|
{
|
|
if (foundTasks == null || foundTasks.Length == 0) {
|
|
return 0;
|
|
}
|
|
|
|
Deserialize();
|
|
if (m_Data.LogicNodes == null || m_Data.LogicNodes.Length == 0) {
|
|
return 0;
|
|
}
|
|
|
|
var count = 0;
|
|
for (int i = 0; i < m_Data.LogicNodes.Length; ++i) {
|
|
if (m_Data.LogicNodes[i] is T task) {
|
|
foundTasks[count] = task;
|
|
count++;
|
|
if (count == foundTasks.Length) {
|
|
return count;
|
|
}
|
|
}
|
|
if (m_Data.LogicNodes[i] is IContainerNode containerNode) {
|
|
if (containerNode.Nodes == null) {
|
|
continue;
|
|
}
|
|
for (int j = 0; j < containerNode.Nodes.Length; ++j) {
|
|
if (containerNode.Nodes[j] is T stackedTask) {
|
|
foundTasks[count] = stackedTask;
|
|
count++;
|
|
if (count == foundTasks.Length) {
|
|
return count;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the tasks with the specified type.
|
|
/// </summary>
|
|
/// <returns>An array containing the found tasks.</returns>
|
|
public T[] FindTasks<T>() where T : Task
|
|
{
|
|
Deserialize();
|
|
|
|
if (m_Data.LogicNodes == null || m_Data.LogicNodes.Length == 0) {
|
|
return null;
|
|
}
|
|
|
|
// Assume the maximum number of tasks will be found. The array will be resized before returning.
|
|
var foundTasks = new T[m_Data.LogicNodes.Length];
|
|
var count = FindTasks<T>(foundTasks);
|
|
if (foundTasks.Length != count) {
|
|
Array.Resize<T>(ref foundTasks, count);
|
|
}
|
|
return foundTasks;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ticks the behavior tree. The UpdateMode must be set to Manual.
|
|
/// The behavior tree will not be executed instantaneously. It will instead be ticked the next time the DOTS system group is updated.
|
|
/// </summary>
|
|
public void Tick()
|
|
{
|
|
if (m_UpdateMode != UpdateMode.Manual) {
|
|
Debug.LogWarning("Warning: The behavior tree UpdateMode must be set to Manual in order to be ticked manually.", this);
|
|
return;
|
|
}
|
|
|
|
if (m_World == null || m_Entity == Entity.Null) {
|
|
Debug.LogWarning("Warning: The behavior tree must be started in order for it to be ticked manually.", this);
|
|
return;
|
|
}
|
|
|
|
Tick(m_World, m_Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ticks the behavior tree.
|
|
/// The behavior tree will not be executed instantaneously. It will instead be ticked the next time the DOTS system group is updated.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
public static void Tick(World world, Entity entity)
|
|
{
|
|
if (world == null || entity.Index == 0) {
|
|
return;
|
|
}
|
|
world.EntityManager.SetComponentEnabled<EvaluateFlag>(entity, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reevaluates the SubtreeReferences by calling the EvaluateSubgraphs method.
|
|
/// </summary>
|
|
public void ReevaluateSubtreeReferences()
|
|
{
|
|
if (!m_Data.ReevaluateSubtreeReferences(this, this, ClearTree)) {
|
|
return;
|
|
}
|
|
|
|
// Restart the tree.
|
|
if (enabled && m_GameObject.activeSelf) {
|
|
StartBehavior();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops or pauses the behavior tree.
|
|
/// </summary>
|
|
/// <param name="pause">Should the behavior tree be paused?</param>
|
|
/// <returns>True if the behavior tree was stopped or paused.</returns>
|
|
public bool StopBehavior(bool pause = false)
|
|
{
|
|
return StopBehavior(m_World, m_Entity, pause);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops or pauses the behavior tree.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
/// <param name="pause">Should the behavior tree be paused?</param>
|
|
/// <returns>True if the behavior tree was stopped or paused.</returns>
|
|
public bool StopBehavior(World world, Entity entity, bool pause = false)
|
|
{
|
|
if (world == null || !world.IsCreated || entity == Entity.Null) {
|
|
return false;
|
|
}
|
|
|
|
// The tree could be stopped after it has been paused.
|
|
if (world.EntityManager.HasComponent<EnabledFlag>(entity)) {
|
|
world.EntityManager.SetComponentEnabled<EnabledFlag>(entity, false);
|
|
}
|
|
if (world.EntityManager.HasComponent<EvaluateFlag>(entity)) {
|
|
world.EntityManager.SetComponentEnabled<EvaluateFlag>(entity, false);
|
|
}
|
|
|
|
if (!s_BehaviorTreeByEntity.ContainsKey(entity)) {
|
|
return false;
|
|
}
|
|
|
|
// Notify those interested that the behavior tree has been stopped.
|
|
if (OnBehaviorTreeStopped != null) {
|
|
OnBehaviorTreeStopped(pause);
|
|
}
|
|
|
|
// Tasks can implement a pause and end specific callback.
|
|
var tasks = Data.LogicNodes;
|
|
for (int i = 0; i < tasks.Length; ++i) {
|
|
if (pause) {
|
|
if (!(tasks[i] is IPausableTask pausableTask)) {
|
|
continue;
|
|
}
|
|
pausableTask.Pause(world, entity);
|
|
} else if (tasks[i] is Task task) {
|
|
task.OnEnd();
|
|
}
|
|
}
|
|
|
|
// Removing the EnabledFlag and EvaluationComponent is sufficient to pause the tree.
|
|
if (pause) {
|
|
return true;
|
|
}
|
|
|
|
s_BehaviorTreeByEntity.Remove(entity);
|
|
|
|
StopBehavior(world, entity);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the behavior tree. This method should only be called from an ECS system.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
public static void StopBehavior(World world, Entity entity)
|
|
{
|
|
if (world == null || entity.Index == 0) {
|
|
return;
|
|
}
|
|
|
|
var branchComponents = world.EntityManager.GetBuffer<BranchComponent>(entity);
|
|
var taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
for (int i = 0; i < branchComponents.Length; ++i) {
|
|
var branchComponent = branchComponents[i];
|
|
if (branchComponent.ActiveIndex == ushort.MaxValue) {
|
|
continue;
|
|
}
|
|
|
|
// Stop all of the active tasks within the branch.
|
|
var taskIndex = branchComponent.ActiveIndex;
|
|
while (taskIndex != ushort.MaxValue) {
|
|
var taskComponent = taskComponents[taskIndex];
|
|
taskComponent.Status = TaskStatus.Inactive;
|
|
taskComponent.Reevaluate = false;
|
|
taskComponents[taskIndex] = taskComponent;
|
|
|
|
taskIndex = taskComponent.ParentIndex;
|
|
}
|
|
|
|
world.EntityManager.SetComponentEnabled(entity, branchComponent.ActiveFlagComponentType, false);
|
|
branchComponent.ActiveIndex = branchComponent.NextIndex = branchComponent.LastActiveIndex = ushort.MaxValue;
|
|
branchComponent.ActiveFlagComponentType = new ComponentType();
|
|
branchComponent.InterruptType = InterruptType.None;
|
|
branchComponent.InterruptIndex = 0;
|
|
branchComponents[i] = branchComponent;
|
|
}
|
|
|
|
// Stop all reevaluations.
|
|
if (world.EntityManager.HasBuffer<ReevaluateTaskComponent>(entity)) {
|
|
var reevaluateTaskComponents = world.EntityManager.GetBuffer<ReevaluateTaskComponent>(entity);
|
|
for (int i = 0; i < reevaluateTaskComponents.Length; ++i) {
|
|
if (reevaluateTaskComponents[i].ReevaluateStatus == ReevaluateStatus.Inactive) {
|
|
continue;
|
|
}
|
|
|
|
var reevaluateTaskComponent = reevaluateTaskComponents[i];
|
|
world.EntityManager.SetComponentEnabled(entity, reevaluateTaskComponent.ReevaluateFlagComponentType, false);
|
|
reevaluateTaskComponent.ReevaluateStatus = ReevaluateStatus.Inactive;
|
|
reevaluateTaskComponent.OriginalStatus = TaskStatus.Inactive;
|
|
|
|
reevaluateTaskComponents[i] = reevaluateTaskComponent;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restarts the behavior tree.
|
|
/// </summary>
|
|
/// <returns>True if the behavior tree was restarted.</returns>
|
|
public bool RestartBehavior()
|
|
{
|
|
if (!IsActive()) {
|
|
return false;
|
|
}
|
|
if (!StopBehavior()) {
|
|
return false;
|
|
}
|
|
return StartBehavior();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all of the tree components.
|
|
/// </summary>
|
|
private void ClearTree()
|
|
{
|
|
ClearTree(m_World, m_Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all of the tree components.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
private void ClearTree(World world, Entity entity)
|
|
{
|
|
if (world == null || entity == Entity.Null || Data.LogicNodes == null) {
|
|
return;
|
|
}
|
|
|
|
StopBehavior(world, entity, false);
|
|
|
|
world.EntityManager.RemoveComponent<EnabledFlag>(entity);
|
|
world.EntityManager.RemoveComponent<EvaluateFlag>(entity);
|
|
ComponentUtility.RemoveEvaluationComponent(world, entity);
|
|
|
|
var branchComponents = world.EntityManager.GetBuffer<BranchComponent>(entity);
|
|
var taskComponents = world.EntityManager.GetBuffer<TaskComponent>(entity);
|
|
branchComponents.Clear();
|
|
taskComponents.Clear();
|
|
if (world.EntityManager.HasBuffer<ReevaluateTaskComponent>(entity)) {
|
|
var reevaluateTaskComponents = world.EntityManager.GetBuffer<ReevaluateTaskComponent>(entity);
|
|
reevaluateTaskComponents.Clear();
|
|
}
|
|
|
|
for (int i = 0; i < m_Data.LogicNodes.Length; ++i) {
|
|
if (m_Data.LogicNodes[i] is IAuthoringTask authoringTask) {
|
|
authoringTask.ClearBufferElement(world, entity);
|
|
if (world.EntityManager.HasComponent(entity, authoringTask.Flag)) {
|
|
world.EntityManager.RemoveComponent(entity, authoringTask.Flag);
|
|
}
|
|
if (m_Data.LogicNodes[i] is IReevaluateResponder reevaluateTask) {
|
|
if (world.EntityManager.HasComponent(entity, reevaluateTask.ReevaluateFlag)) {
|
|
world.EntityManager.RemoveComponent(entity, reevaluateTask.ReevaluateFlag);
|
|
}
|
|
}
|
|
} else if (m_Data.LogicNodes[i] is Task task) {
|
|
task.ClearBufferElement(world, entity);
|
|
if (m_Data.LogicNodes[i] is IConditional) {
|
|
if (world.EntityManager.HasComponent(entity, typeof(TaskObjectReevaluateFlag))) {
|
|
world.EntityManager.RemoveComponent(entity, typeof(TaskObjectReevaluateFlag));
|
|
}
|
|
if (world.EntityManager.HasComponent(entity, typeof(InterruptFlag))) {
|
|
world.EntityManager.RemoveComponent(entity, typeof(InterruptFlag));
|
|
}
|
|
}
|
|
if (world.EntityManager.HasComponent(entity, typeof(TaskObjectFlag))) {
|
|
world.EntityManager.RemoveComponent(entity, typeof(TaskObjectFlag));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < m_Data.EventNodes.Length; ++i) {
|
|
if (m_Data.EventNodes[i] is IEventNodeEntityReceiver entityReceiver) {
|
|
entityReceiver.ClearBufferElement(world, entity);
|
|
}
|
|
}
|
|
|
|
m_NodeIndexByRuntimeIndex.Clear();
|
|
m_TaskByRuntimeIndex = null;
|
|
m_TaskObjectParentByRuntimeIndex = null;
|
|
m_ConditionalReevaluationByRuntimeIndex = null;
|
|
m_ECSVariableRegistry?.Dispose();
|
|
m_ECSVariableRegistry = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the runtime caches can index the specified runtime task index.
|
|
/// </summary>
|
|
/// <param name="requiredIndex">The required runtime index.</param>
|
|
private void EnsureRuntimeTaskCacheCapacity(int requiredIndex)
|
|
{
|
|
if (requiredIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
var requiredLength = requiredIndex + 1;
|
|
if (m_TaskByRuntimeIndex == null) {
|
|
m_TaskByRuntimeIndex = new Task[requiredLength];
|
|
m_TaskObjectParentByRuntimeIndex = new ITaskObjectParentNode[requiredLength];
|
|
m_ConditionalReevaluationByRuntimeIndex = new IConditionalReevaluation[requiredLength];
|
|
return;
|
|
}
|
|
|
|
if (requiredLength <= m_TaskByRuntimeIndex.Length) {
|
|
return;
|
|
}
|
|
|
|
Array.Resize(ref m_TaskByRuntimeIndex, requiredLength);
|
|
Array.Resize(ref m_TaskObjectParentByRuntimeIndex, requiredLength);
|
|
Array.Resize(ref m_ConditionalReevaluationByRuntimeIndex, requiredLength);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the SharedVariable with the specified name.
|
|
/// </summary>
|
|
/// <param name="name">The name of the SharedVariable that should be retrieved.</param>
|
|
/// <returns>The SharedVariable with the specified name (can be null).</returns>
|
|
public SharedVariable GetVariable(PropertyName name)
|
|
{
|
|
return GetVariable(name, SharedVariable.SharingScope.Graph);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the SharedVariable with the specified name and scope.
|
|
/// </summary>
|
|
/// <param name="name">The name of the SharedVariable that should be retrieved.</param>
|
|
/// <param name="scope">The scope of the SharedVariable that should be retrieved.</param>
|
|
/// <returns>The SharedVariable with the specified name (can be null).</returns>
|
|
public SharedVariable GetVariable(PropertyName name, SharedVariable.SharingScope scope)
|
|
{
|
|
Deserialize();
|
|
SyncECSVariablesToManaged(m_World, m_Entity);
|
|
return m_Data.GetVariable(this, name, scope);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the SharedVariable of the specified name.
|
|
/// </summary>
|
|
/// <param name="name">The name of the SharedVariable that should be retrieved.</param>
|
|
/// <returns>The SharedVariable with the specified name (can be null).</returns>
|
|
public SharedVariable<T> GetVariable<T>(PropertyName name)
|
|
{
|
|
return GetVariable<T>(name, SharedVariable.SharingScope.Graph);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the SharedVariable of the specified name.
|
|
/// </summary>
|
|
/// <param name="name">The name of the SharedVariable that should be retrieved.</param>
|
|
/// <param name="scope">The scope of the SharedVariable that should be retrieved.</param>
|
|
/// <returns>The SharedVariable with the specified name (can be null).</returns>
|
|
public SharedVariable<T> GetVariable<T>(PropertyName name, SharedVariable.SharingScope scope)
|
|
{
|
|
Deserialize();
|
|
SyncECSVariablesToManaged(m_World, m_Entity);
|
|
return m_Data.GetVariable<T>(this, name, scope);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of the SharedVariable.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of SharedVarible.</typeparam>
|
|
/// <param name="name">The name of the SharedVariable.</param>
|
|
/// <param name="value">The value of the SharedVariable.</param>
|
|
/// <returns>True if the value was set.</returns>
|
|
public bool SetVariableValue<T>(PropertyName name, T value)
|
|
{
|
|
return SetVariableValue<T>(name, value, SharedVariable.SharingScope.Graph);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the value of the SharedVariable.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of SharedVarible.</typeparam>
|
|
/// <param name="name">The name of the SharedVariable.</param>
|
|
/// <param name="value">The value of the SharedVariable.</param>
|
|
/// <param name="scope">The scope of the SharedVariable that should be set.</param>
|
|
/// <returns>True if the value was set.</returns>
|
|
public bool SetVariableValue<T>(PropertyName name, T value, SharedVariable.SharingScope scope)
|
|
{
|
|
Deserialize();
|
|
var success = m_Data.SetVariableValue<T>(this, name, value, scope);
|
|
if (success) {
|
|
SyncManagedVariablesToECS(m_World, m_Entity);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the behavior tree save data.
|
|
/// </summary>
|
|
/// <param name="variableSaveScope">Specifies which variables should be saved. Graph variables will automatically be saved.</param>
|
|
/// <returns>The save data if the behavior tree can be saved.</returns>
|
|
public SaveManager.SaveData? Save(SaveManager.VariableSaveScope variableSaveScope = 0)
|
|
{
|
|
if (OnWillSave != null) {
|
|
OnWillSave(this);
|
|
}
|
|
var saveData = SaveManager.Save(new BehaviorTree[] { this }, variableSaveScope);
|
|
if (OnDidSave != null) {
|
|
OnDidSave(this, saveData.HasValue);
|
|
}
|
|
return saveData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the behavior tree at the specified file path.
|
|
/// </summary>
|
|
/// <param name="filePath">The file path to save the behavior tree at. The file will be replaced if it already exists.</param>
|
|
/// <param name="variableSaveScope">Specifies which variables should be saved. Graph variables will automatically be saved.</param>
|
|
/// <returns>True if the behavior tree was successfully saved.</returns>
|
|
public bool Save(string filePath, SaveManager.VariableSaveScope variableSaveScope = 0)
|
|
{
|
|
if (OnWillSave != null) {
|
|
OnWillSave(this);
|
|
}
|
|
var success = SaveManager.Save(new BehaviorTree[] { this }, filePath, variableSaveScope);
|
|
if (OnDidSave != null) {
|
|
OnDidSave(this, success);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the behavior tree from the specified file path.
|
|
/// </summary>
|
|
/// <param name="filePath">The file path to load the behavior tree at.</param>
|
|
/// <param name="afterVariablesRestored">Optional callback after the graph variables have been restored.</param>
|
|
/// <returns>True if the behavior tree was successfully loaded.</returns>
|
|
public bool Load(string filePath, Action<BehaviorTree> afterVariablesRestored = null)
|
|
{
|
|
if (OnWillLoad != null) {
|
|
OnWillLoad(this);
|
|
}
|
|
var success = SaveManager.Load(new BehaviorTree[] { this }, filePath, afterVariablesRestored);
|
|
if (OnDidLoad != null) {
|
|
OnDidLoad(this, success);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the behavior tree from the specified save data.
|
|
/// </summary>
|
|
/// <param name="saveData">The data associated with the behavior tree.</param>
|
|
/// <param name="afterVariablesRestored">Optional callback after the graph variables have been restored.</param>
|
|
/// <returns>True if the behavior tree was successfully loaded.</returns>
|
|
public bool Load(SaveManager.SaveData saveData, Action<BehaviorTree> afterVariablesRestored = null)
|
|
{
|
|
if (OnWillLoad != null) {
|
|
OnWillLoad(this);
|
|
}
|
|
var success = SaveManager.Load(new BehaviorTree[] { this }, saveData, afterVariablesRestored);
|
|
if (OnDidLoad != null) {
|
|
OnDidLoad(this, success);
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the task courtine with the specified name.
|
|
/// </summary>
|
|
/// <param name="task">The task that the coroutine belongs to.</param>
|
|
/// <param name="coroutineName">The name of the coroutine method.</param>
|
|
/// <returns>The created routine (can be null).</returns>
|
|
public Coroutine StartTaskCoroutine(Task task, string coroutineName)
|
|
{
|
|
var method = task.GetType().GetMethod(coroutineName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
if (method == null) {
|
|
Debug.LogError($"Error: The coroutine {coroutineName} cannot be started due to the method not being found on {task.GetType()}.");
|
|
return null;
|
|
}
|
|
|
|
if (m_ActiveTaskCoroutines == null) {
|
|
m_ActiveTaskCoroutines = new Dictionary<string, List<TaskCoroutine>>();
|
|
}
|
|
var taskCoroutine = new TaskCoroutine(this, (IEnumerator)method.Invoke(task, new object[] { }), coroutineName);
|
|
if (!m_ActiveTaskCoroutines.TryGetValue(coroutineName, out var taskCoroutines)) {
|
|
taskCoroutines = new List<TaskCoroutine>();
|
|
m_ActiveTaskCoroutines.Add(coroutineName, taskCoroutines);
|
|
}
|
|
taskCoroutines.Add(taskCoroutine);
|
|
|
|
return taskCoroutine.Coroutine;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the task courtine with the specified name.
|
|
/// </summary>
|
|
/// <param name="task">The task that the coroutine belongs to.</param>
|
|
/// <param name="coroutineName">The name of the coroutine method.</param>
|
|
/// <param name="value">The input parameter to the coroutine.</param>
|
|
/// <returns>The created routine (can be null).</returns>
|
|
public Coroutine StartTaskCoroutine(Task task, string coroutineName, object value)
|
|
{
|
|
var method = task.GetType().GetMethod(coroutineName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
if (method == null) {
|
|
Debug.LogError($"Error: The coroutine {coroutineName} cannot be started due to the method not being found on {task.GetType()}.");
|
|
return null;
|
|
}
|
|
|
|
if (m_ActiveTaskCoroutines == null) {
|
|
m_ActiveTaskCoroutines = new Dictionary<string, List<TaskCoroutine>>();
|
|
}
|
|
var taskCoroutine = new TaskCoroutine(this, (IEnumerator)method.Invoke(task, new object[] { value }), coroutineName);
|
|
if (!m_ActiveTaskCoroutines.TryGetValue(coroutineName, out var taskCoroutines)) {
|
|
taskCoroutines = new List<TaskCoroutine>();
|
|
m_ActiveTaskCoroutines.Add(coroutineName, taskCoroutines);
|
|
}
|
|
taskCoroutines.Add(taskCoroutine);
|
|
|
|
return taskCoroutine.Coroutine;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the task courtine with the specified name.
|
|
/// </summary>
|
|
/// <param name="coroutineName">The name of the coroutine method.</param>
|
|
public void StopTaskCoroutine(string coroutineName)
|
|
{
|
|
if (!m_ActiveTaskCoroutines.TryGetValue(coroutineName, out var taskCoroutines)) {
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < taskCoroutines.Count; ++i) {
|
|
taskCoroutines[i].Stop();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops all of the task coroutines.
|
|
/// </summary>
|
|
public void StopAllTaskCoroutines()
|
|
{
|
|
if (m_ActiveTaskCoroutines == null) {
|
|
return;
|
|
}
|
|
|
|
foreach (var entry in m_ActiveTaskCoroutines) {
|
|
var taskCoroutines = entry.Value;
|
|
for (int i = 0; i < taskCoroutines.Count; ++i) {
|
|
taskCoroutines[i].Stop();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The TaskCoroutine has ended.
|
|
/// </summary>
|
|
/// <param name="taskCoroutine">The coroutine that has ended.</param>
|
|
/// <param name="coroutineName">The name of the coroutine.</param>
|
|
public void TaskCoroutineEnded(TaskCoroutine taskCoroutine, string coroutineName)
|
|
{
|
|
if (!m_ActiveTaskCoroutines.TryGetValue(coroutineName, out var taskCoroutines)) {
|
|
return;
|
|
}
|
|
|
|
taskCoroutines.Remove(taskCoroutine);
|
|
if (taskCoroutines.Count == 0) {
|
|
m_ActiveTaskCoroutines.Remove(coroutineName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnCollisionEnter callback.
|
|
/// </summary>
|
|
/// <param name="collision">The resulting collision.</param>
|
|
private void OnCollisionEnter(Collision collision)
|
|
{
|
|
if (OnBehaviorTreeCollisionEnter != null) {
|
|
OnBehaviorTreeCollisionEnter(collision);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnCollisionExit callback.
|
|
/// </summary>
|
|
/// <param name="collision">The resulting collision.</param>
|
|
private void OnCollisionExit(Collision collision)
|
|
{
|
|
if (OnBehaviorTreeCollisionExit != null) {
|
|
OnBehaviorTreeCollisionExit(collision);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnCollisionEnter2D callback.
|
|
/// </summary>
|
|
/// <param name="collision">The resulting collision.</param>
|
|
private void OnCollisionEnter2D(Collision2D collision)
|
|
{
|
|
if (OnBehaviorTreeCollisionEnter2D != null) {
|
|
OnBehaviorTreeCollisionEnter2D(collision);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnCollisionExit2D callback.
|
|
/// </summary>
|
|
/// <param name="collision">The resulting collision.</param>
|
|
private void OnCollisionExit2D(Collision2D collision)
|
|
{
|
|
if (OnBehaviorTreeCollisionExit2D != null) {
|
|
OnBehaviorTreeCollisionExit2D(collision);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnTriggerEnter callback.
|
|
/// </summary>
|
|
/// <param name="other">The overlapping collider.</param>
|
|
private void OnTriggerEnter(Collider other)
|
|
{
|
|
if (OnBehaviorTreeTriggerEnter != null) {
|
|
OnBehaviorTreeTriggerEnter(other);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnTriggerExit callback.
|
|
/// </summary>
|
|
/// <param name="other">The collider that is no longer overlapping.</param>
|
|
private void OnTriggerExit(Collider other)
|
|
{
|
|
if (OnBehaviorTreeTriggerExit != null) {
|
|
OnBehaviorTreeTriggerExit(other);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnTriggerEnter2D callback.
|
|
/// </summary>
|
|
/// <param name="other">The overlapping collider.</param>
|
|
private void OnTriggerEnter2D(Collider2D other)
|
|
{
|
|
if (OnBehaviorTreeTriggerEnter2D != null) {
|
|
OnBehaviorTreeTriggerEnter2D(other);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnTriggerExit2D callback.
|
|
/// </summary>
|
|
/// <param name="other">The collider that is no longer overlapping.</param>
|
|
private void OnTriggerExit2D(Collider2D other)
|
|
{
|
|
if (OnBehaviorTreeTriggerExit2D != null) {
|
|
OnBehaviorTreeTriggerExit2D(other);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnControllerColliderHit callback.
|
|
/// </summary>
|
|
/// <param name="hit">The hit result.</param>
|
|
private void OnControllerColliderHit(ControllerColliderHit hit)
|
|
{
|
|
if (OnBehaviorTreeControllerColliderHit != null) {
|
|
OnBehaviorTreeControllerColliderHit(hit);
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// OnDrawGizmos callback.
|
|
/// </summary>
|
|
private void OnDrawGizmos()
|
|
{
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
|
|
if (m_Data != null && m_Data.LogicNodes != null) {
|
|
for (int i = 0; i < m_Data.LogicNodes.Length; ++i) {
|
|
if (m_Data.LogicNodes[i] is Task task) {
|
|
task.OnDrawGizmos(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// OnDrawGizmos callback.
|
|
/// </summary>
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
|
|
if (m_Data != null && m_Data.LogicNodes != null) {
|
|
for (int i = 0; i < m_Data.LogicNodes.Length; ++i) {
|
|
if (m_Data.LogicNodes[i] is Task task) {
|
|
task.OnDrawGizmosSelected(this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// The behavior tree has been disabled.
|
|
/// </summary>
|
|
private void OnDisable()
|
|
{
|
|
if (m_Entity == Entity.Null) {
|
|
return;
|
|
}
|
|
|
|
StopBehavior(m_World, m_Entity, m_PauseWhenDisabled);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The behavior tree has been destroyed.
|
|
/// </summary>
|
|
private void OnDestroy()
|
|
{
|
|
if (m_Entity == Entity.Null) {
|
|
m_ECSVariableRegistry?.Dispose();
|
|
m_ECSVariableRegistry = null;
|
|
return;
|
|
}
|
|
|
|
if (OnBehaviorTreeDestroyed != null) {
|
|
OnBehaviorTreeDestroyed();
|
|
}
|
|
StopBehavior(m_World, m_Entity, false);
|
|
m_ECSVariableRegistry?.Dispose();
|
|
m_ECSVariableRegistry = null;
|
|
m_GameObject = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the node with the specified index enabled?
|
|
/// </summary>
|
|
/// <param name="logicNode">Is the node a LogicNode?</param>
|
|
/// <param name="index">The index of the node.</param>
|
|
/// <returns>True if the node with the specified index is enabled.</returns>
|
|
public bool IsNodeEnabled(bool logicNode, int index)
|
|
{
|
|
return Data.IsNodeEnabled(logicNode, index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the node with the specified index active?
|
|
/// </summary>
|
|
/// <param name="logicNode">Is the node a LogicNode?</param>
|
|
/// <param name="index">The index of the node.</param>
|
|
/// <returns>True if the node with the specified index is active.</returns>
|
|
public bool IsNodeActive(bool logicNode, int index)
|
|
{
|
|
if (!Application.isPlaying || m_Entity == Entity.Null) {
|
|
return false;
|
|
}
|
|
|
|
var taskComponents = m_World.EntityManager.GetBuffer<TaskComponent>(m_Entity);
|
|
var logicNodeIndex = index;
|
|
if (!logicNode && m_Data.EventNodes != null && index < m_Data.EventNodes.Length) {
|
|
// Find the logic node that the event node is connected to.
|
|
logicNodeIndex = m_Data.EventNodes[index].ConnectedIndex;
|
|
}
|
|
if (logicNodeIndex >= taskComponents.Length) {
|
|
return false;
|
|
}
|
|
var taskComponent = taskComponents[logicNodeIndex];
|
|
return taskComponent.Status == TaskStatus.Running;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree is active.
|
|
/// </summary>
|
|
/// <returns>True if the behavior tree is active.</returns>
|
|
public bool IsActive()
|
|
{
|
|
return IsActive(m_Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree is active.
|
|
/// </summary>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
/// <returns>True if the behavior tree is active.</returns>
|
|
public bool IsActive(Entity entity)
|
|
{
|
|
if (entity == Entity.Null) {
|
|
return false;
|
|
}
|
|
return s_BehaviorTreeByEntity.ContainsKey(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree is running.
|
|
/// </summary>
|
|
/// <returns>True if the behavior tree is running.</returns>
|
|
public bool IsRunning()
|
|
{
|
|
return IsRunning(m_Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree is running.
|
|
/// </summary>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
/// <returns>True if the behavior tree is running.</returns>
|
|
public bool IsRunning(Entity entity)
|
|
{
|
|
if (!IsActive(entity)) {
|
|
return false;
|
|
}
|
|
|
|
if (!s_BehaviorTreeByEntity.TryGetValue(entity, out var behaviorTree) || behaviorTree == null) {
|
|
return false;
|
|
}
|
|
|
|
return behaviorTree.Status == TaskStatus.Running;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree is paused.
|
|
/// </summary>
|
|
/// <returns>True if the behavior tree is paused.</returns>
|
|
public bool IsPaused()
|
|
{
|
|
return IsPaused(m_World, m_Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the behavior tree is paused.
|
|
/// </summary>
|
|
/// <param name="world">The world that the entity exists in.</param>
|
|
/// <param name="entity">The entity that contains the behavior tree.</param>
|
|
/// <returns>True if the behavior tree is paused.</returns>
|
|
public bool IsPaused(World world, Entity entity)
|
|
{
|
|
if (!IsActive(entity)) {
|
|
return false;
|
|
}
|
|
return world.EntityManager.HasComponent<EnabledFlag>(entity) && !world.EntityManager.IsComponentEnabled<EnabledFlag>(entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the graph onto the current graph.
|
|
/// </summary>
|
|
/// <param name="other">The graph that should be copied.</param>
|
|
public void Clone(IGraph other)
|
|
{
|
|
m_Data = new BehaviorTreeData();
|
|
m_Data.EventNodes = other.EventNodes;
|
|
m_Data.LogicNodes = other.LogicNodes as ITreeLogicNode[];
|
|
m_Data.InjectedSubtreeReferences = (other as BehaviorTree).InjectedSubtreeReferences;
|
|
m_Data.SharedVariables = other.SharedVariables;
|
|
|
|
#if UNITY_EDITOR
|
|
m_Data.EventNodeProperties = other.EventNodeProperties;
|
|
m_Data.LogicNodeProperties = other.LogicNodeProperties;
|
|
m_Data.GroupProperties = other.GroupProperties;
|
|
m_Data.SharedVariableGroups = other.SharedVariableGroups;
|
|
#endif
|
|
|
|
m_Data.Serialize();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overrides ToString.
|
|
/// </summary>
|
|
/// <returns>The desired string value.</returns>
|
|
public override string ToString()
|
|
{
|
|
return $"{m_GraphName} (Index {m_Index})";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the hashcode of the graph.
|
|
/// </summary>
|
|
/// <returns>The hashcode of the graph.</returns>
|
|
public override int GetHashCode()
|
|
{
|
|
if (m_Subtree != null) {
|
|
return m_Subtree.GetHashCode();
|
|
}
|
|
return base.GetHashCode();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Callback when the domain should be reloaded.
|
|
/// </summary>
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
|
private static void Reinitialize()
|
|
{
|
|
s_BehaviorTreeByEntity = new Dictionary<Entity, BehaviorTree>();
|
|
#if UNITY_EDITOR
|
|
s_AuthoringTaskSharedVariableFieldLookup = new Dictionary<Type, bool>();
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enables the baked behavior tree system.
|
|
/// </summary>
|
|
/// <param name="world">The world that the system has been added to.</param>
|
|
[Obsolete("EnableBakedBehaviorTreeSystem is deprecated and no longer required because baked startup runs automatically. For deferred baked trees call StartBakedBehaviorTree(world, entity).", false)]
|
|
public static void EnableBakedBehaviorTreeSystem(World world)
|
|
{
|
|
if (world == null) {
|
|
return;
|
|
}
|
|
|
|
world.Unmanaged.GetExistingSystemState<StartBakedBehaviorTreeSystem>().Enabled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enables the baked behavior tree system.
|
|
/// </summary>
|
|
/// <param name="world">The unmanged world that the system has been added to.</param>
|
|
[Obsolete("EnableBakedBehaviorTreeSystem is deprecated and no longer required because baked startup runs automatically. For deferred baked trees call StartBakedBehaviorTree(world, entity).", false)]
|
|
public static void EnableBakedBehaviorTreeSystem(WorldUnmanaged world)
|
|
{
|
|
world.GetExistingSystemState<StartBakedBehaviorTreeSystem>().Enabled = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a baked behavior tree that was deferred because StartWhenEnabled is false.
|
|
/// </summary>
|
|
/// <param name="world">The world that contains the entity.</param>
|
|
/// <param name="entity">The entity that should be started.</param>
|
|
/// <returns>True if the baked behavior tree was started.</returns>
|
|
public static bool StartBakedBehaviorTree(World world, Entity entity)
|
|
{
|
|
if (world == null || entity == Entity.Null || !world.EntityManager.Exists(entity)) {
|
|
return false;
|
|
}
|
|
|
|
return StartBakedBehaviorTree(world.Unmanaged, entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts a baked behavior tree that was deferred because StartWhenEnabled is false.
|
|
/// </summary>
|
|
/// <param name="world">The unmanaged world that contains the entity.</param>
|
|
/// <param name="entity">The entity that should be started.</param>
|
|
/// <returns>True if the baked behavior tree was started.</returns>
|
|
public static bool StartBakedBehaviorTree(WorldUnmanaged world, Entity entity)
|
|
{
|
|
if (entity == Entity.Null || !world.EntityManager.Exists(entity) || !world.EntityManager.HasComponent<DeferredBakedBehaviorTreeStart>(entity)) {
|
|
return false;
|
|
}
|
|
|
|
var deferredStart = world.EntityManager.GetComponentData<DeferredBakedBehaviorTreeStart>(entity);
|
|
if (!StartBranch(world.EntityManager.World, entity, deferredStart.StartEventConnectedIndex, deferredStart.StartEvaluation)) {
|
|
return false;
|
|
}
|
|
|
|
world.EntityManager.RemoveComponent<DeferredBakedBehaviorTreeStart>(entity);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts the behavior tree to a DOTS entity.
|
|
/// </summary>
|
|
public class BehaviorTreeBaker : Baker<BehaviorTree>
|
|
{
|
|
private static MethodInfo s_GetTypeOfSystemMethod;
|
|
|
|
/// <summary>
|
|
/// Bakes the behavior tree to the DOTS entity.
|
|
/// </summary>
|
|
/// <param name="behaviorTree">The authoring behavior tree.</param>
|
|
public override void Bake(BehaviorTree behaviorTree)
|
|
{
|
|
if (!behaviorTree.enabled) {
|
|
return;
|
|
}
|
|
|
|
var entity = GetEntity(behaviorTree, TransformUsageFlags.Dynamic);
|
|
var worlds = World.All;
|
|
for (int i = 0; i < worlds.Count; ++i) {
|
|
if (worlds[i].EntityManager.Exists(entity)) {
|
|
if (!behaviorTree.InitializeTree(worlds[i], entity)) {
|
|
continue;
|
|
}
|
|
#if UNITY_EDITOR
|
|
ushort[] logicNodeRuntimeIndices = null;
|
|
var logicNodes = behaviorTree.TreeLogicNodes;
|
|
if (logicNodes != null && logicNodes.Length > 0) {
|
|
logicNodeRuntimeIndices = new ushort[logicNodes.Length];
|
|
for (int j = 0; j < logicNodes.Length; ++j) {
|
|
logicNodeRuntimeIndices[j] = logicNodes[j].RuntimeIndex;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
var connectedIndex = GetStartTaskConnectedIndex(behaviorTree);
|
|
if (connectedIndex == -1 || connectedIndex == ushort.MaxValue) {
|
|
return;
|
|
}
|
|
|
|
var taskComponents = worlds[i].EntityManager.GetBuffer<TaskComponent>(entity);
|
|
var tagStableTypeHash = new ulong[taskComponents.Length];
|
|
for (int j = 0; j < taskComponents.Length; ++j) {
|
|
tagStableTypeHash[j] = TypeManager.GetTypeInfo(taskComponents[j].FlagComponentType.TypeIndex).StableTypeHash;
|
|
}
|
|
ulong[] ReevaluateFlagStableTypeHash = null;
|
|
if (worlds[i].EntityManager.HasBuffer<ReevaluateTaskComponent>(entity)) {
|
|
var reevaluateTaskComponents = worlds[i].EntityManager.GetBuffer<ReevaluateTaskComponent>(entity);
|
|
ReevaluateFlagStableTypeHash = new ulong[reevaluateTaskComponents.Length];
|
|
for (int j = 0; j < reevaluateTaskComponents.Length; ++j) {
|
|
ReevaluateFlagStableTypeHash[j] = TypeManager.GetTypeInfo(reevaluateTaskComponents[j].ReevaluateFlagComponentType.TypeIndex).StableTypeHash;
|
|
}
|
|
}
|
|
|
|
AddComponentObject<BakedBehaviorTree>(entity, new BakedBehaviorTree
|
|
{
|
|
StartEventConnectedIndex = connectedIndex,
|
|
StartWhenEnabled = behaviorTree.StartWhenEnabled,
|
|
StartEvaluation = behaviorTree.UpdateMode == UpdateMode.EveryFrame,
|
|
ReevaluateTaskSystems = GetTaskSystems<ReevaluateTaskSystemGroup>(worlds[i]),
|
|
InterruptTaskSystems = GetTaskSystems<InterruptTaskSystemGroup>(worlds[i]),
|
|
TraversalTaskSystems = GetTaskSystems<TraversalTaskSystemGroup>(worlds[i]),
|
|
TagStableTypeHashes = tagStableTypeHash,
|
|
ReevaluateFlagStableTypeHashes = ReevaluateFlagStableTypeHash,
|
|
});
|
|
#if UNITY_EDITOR
|
|
// Stored in a separate component so StripEditorBehaviorTreeReferenceSystem can remove it during the EntitySceneOptimizations pass, keeping it out of the build.
|
|
AddComponentObject<BakedEditorReference>(entity, new BakedEditorReference
|
|
{
|
|
AuthoringBehaviorTreeGlobalObjectId = GlobalObjectId.GetGlobalObjectIdSlow(behaviorTree).ToString(),
|
|
DesignGraphUniqueID = behaviorTree.UniqueID,
|
|
LogicNodeRuntimeIndices = logicNodeRuntimeIndices,
|
|
});
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the index of the node connection for the start event task.
|
|
/// </summary>
|
|
/// <param name="behaviorTree">The interested behavior tree.</param>
|
|
/// <returns>The index of the node connection for the start event task.</returns>
|
|
private int GetStartTaskConnectedIndex(BehaviorTree behaviorTree)
|
|
{
|
|
// The behavior tree has to first be initialized.
|
|
if (behaviorTree.World == null || behaviorTree.Entity == Entity.Null) {
|
|
Debug.LogError($"Error: Unable to retrieve the connected index on behavior tree {behaviorTree}. The behavior tree has to first be initialized.");
|
|
return -1;
|
|
}
|
|
|
|
var data = behaviorTree.Data;
|
|
for (int i = 0; i < data.EventNodes.Length; ++i) {
|
|
if (data.EventNodes[i].GetType() == typeof(Start)) {
|
|
// The connected index may not be valid.
|
|
if (data.EventNodes[i].ConnectedIndex == ushort.MaxValue) {
|
|
return ushort.MaxValue;
|
|
}
|
|
|
|
// The branch cannot start if it is disabled.
|
|
if (!behaviorTree.IsNodeEnabled(false, i)) {
|
|
return -1;
|
|
}
|
|
|
|
return data.LogicNodes[data.EventNodes[i].ConnectedIndex].RuntimeIndex;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all of the system type indicies within the systems of the specified type.
|
|
/// </summary>
|
|
/// <param name="world">The world that the systems were added to.</param>
|
|
/// <returns>The system type indicies within the systems of the specified type (can be null).</returns>
|
|
private string[] GetTaskSystems<T>(World world) where T : ComponentSystemGroup
|
|
{
|
|
var systemGroup = world.GetExistingSystemManaged<T>();
|
|
if (systemGroup == null) {
|
|
return null;
|
|
}
|
|
|
|
var systems = systemGroup.GetAllSystems();
|
|
if (systems.Length == 0) {
|
|
systems.Dispose();
|
|
return null;
|
|
}
|
|
|
|
// Use reflection to call WorldUnmanaged.GetTypeOfSystem. This method is only called during baking at edit time so reflection is ok, though it would be better
|
|
// if this method was eventually made public.
|
|
if (s_GetTypeOfSystemMethod == null) {
|
|
s_GetTypeOfSystemMethod = typeof(WorldUnmanaged).GetMethod("GetTypeOfSystem", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
|
if (s_GetTypeOfSystemMethod == null) {
|
|
Debug.LogError("Error: Unable to find WorldUnmanaged.GetTypeOfSystem. Please email support@opsive.com with your Unity version and Entity package version.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
var systemTypes = new string[systems.Length];
|
|
for (int i = 0; i < systems.Length; ++i) {
|
|
var systemTypeIndex = TypeManager.GetSystemTypeIndex((Type)s_GetTypeOfSystemMethod.Invoke(world.Unmanaged, new object[] { systems[i] }));
|
|
systemTypes[i] = systemTypeIndex.ToString();
|
|
}
|
|
systems.Dispose();
|
|
return systemTypes;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif |