2025-11-25 08:19:33 -05:00
#if GRAPH_DESIGNER
/// ---------------------------------------------
/// Behavior Designer
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.BehaviorDesigner.Runtime
{
using Opsive.BehaviorDesigner.Runtime.Tasks ;
using Opsive.BehaviorDesigner.Runtime.Tasks.Events ;
using Opsive.GraphDesigner.Runtime ;
using Opsive.GraphDesigner.Runtime.Utility ;
using Opsive.GraphDesigner.Runtime.Variables ;
using Opsive.Shared.Utility ;
using System ;
using System.Collections ;
using System.Collections.Generic ;
using System.Reflection ;
using UnityEngine ;
/// <summary>
/// Storage class for the graph data.
/// </summary>
[System.Serializable]
2026-01-03 18:19:39 -05:00
public partial class BehaviorTreeData
2025-11-25 08:19:33 -05:00
{
[Tooltip("The serialized Task data.")]
[SerializeField] private Serialization [ ] m_TaskData ;
[Tooltip("The serialized EventTask data.")]
[SerializeField] private Serialization [ ] m_EventTaskData ;
[Tooltip("The serialized SharedVariable data.")]
[SerializeField] private Serialization [ ] m_SharedVariableData ;
[Tooltip("The serialized disabled event nodes data.")]
[SerializeField] private Serialization [ ] m_DisabledEventNodesData ;
[Tooltip("The serialized disabled logic nodes data.")]
[SerializeField] private Serialization [ ] m_DisabledLogicNodesData ;
[Tooltip("The unique ID of the data.")]
[SerializeField] private int m_UniqueID ;
private ITreeLogicNode [ ] m_Tasks ;
private IEventNode [ ] m_EventTasks ;
private SharedVariable [ ] m_SharedVariables ;
private ushort [ ] m_DisabledLogicNodes ;
private ushort [ ] m_DisabledEventNodes ;
private Dictionary < VariableAssignment , SharedVariable > m_VariableByNameMap ;
private int m_RuntimeUniqueID ;
2026-05-10 11:47:55 -04:00
[System.NonSerialized] private ResizableArray < InjectedGraphReference > m_InjectedGraphReferences ;
[System.NonSerialized] private HashSet < IEventNode > m_InjectedSubtreeEventNodes ;
2025-11-25 08:19:33 -05:00
public ITreeLogicNode [ ] LogicNodes
{
get = > m_Tasks ;
set {
if ( value = = null ) {
m_Tasks = null ;
} else {
if ( m_Tasks = = null ) {
m_Tasks = new ITreeLogicNode [ value . Length ] ;
} else if ( m_Tasks . Length ! = value . Length ) {
Array . Resize ( ref m_Tasks , value . Length ) ;
}
for ( int i = 0 ; i < value . Length ; + + i ) {
m_Tasks [ i ] = value [ i ] ;
}
}
}
}
public IEventNode [ ] EventNodes
{
get = > m_EventTasks ;
set {
if ( value = = null ) {
m_EventTasks = null ;
} else {
if ( m_EventTasks = = null ) {
m_EventTasks = new IEventNode [ value . Length ] ;
} else if ( m_EventTasks . Length ! = value . Length ) {
Array . Resize ( ref m_EventTasks , value . Length ) ;
}
for ( int i = 0 ; i < value . Length ; + + i ) {
m_EventTasks [ i ] = value [ i ] ;
}
}
}
}
public SharedVariable [ ] SharedVariables { get = > m_SharedVariables ; set = > m_SharedVariables = value ; }
2026-05-10 11:47:55 -04:00
public int UniqueID { get = > m_UniqueID ; }
public int RuntimeUniqueID { get = > m_RuntimeUniqueID ; internal set = > m_RuntimeUniqueID = value ; }
2025-11-25 08:19:33 -05:00
public ushort [ ] DisabledLogicNodes { get = > m_DisabledLogicNodes ; set = > m_DisabledLogicNodes = value ; }
public ushort [ ] DisabledEventNodes { get = > m_DisabledEventNodes ; set = > m_DisabledEventNodes = value ; }
internal Dictionary < VariableAssignment , SharedVariable > VariableByNameMap { get = > m_VariableByNameMap ; set = > m_VariableByNameMap = value ; }
2026-05-10 11:47:55 -04:00
internal ResizableArray < InjectedGraphReference > InjectedGraphReferences { get = > m_InjectedGraphReferences ; }
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
[Tooltip("The serialized logic node properties data.")]
[SerializeField] private Serialization [ ] m_LogicNodePropertiesData ;
[Tooltip("The serialized event node properties data.")]
[SerializeField] private Serialization [ ] m_EventNodePropertiesData ;
[Tooltip("The serialized group properties data.")]
[SerializeField] private Serialization [ ] m_GroupPropertiesData ;
[Tooltip("The serialized shared variables group data.")]
[SerializeField] private Serialization [ ] m_SharedVariableGroupsData ;
private LogicNodeProperties [ ] m_LogicNodeProperties ;
private NodeProperties [ ] m_EventNodeProperties ;
private GroupProperties [ ] m_GroupProperties ;
[System.NonSerialized] private SharedVariableGroup [ ] m_SharedVariableGroups ;
public LogicNodeProperties [ ] LogicNodeProperties { get = > m_LogicNodeProperties ; set { m_LogicNodeProperties = value ; } }
public NodeProperties [ ] EventNodeProperties { get = > m_EventNodeProperties ; set { m_EventNodeProperties = value ; } }
public GroupProperties [ ] GroupProperties { get = > m_GroupProperties ; set = > m_GroupProperties = value ; }
public SharedVariableGroup [ ] SharedVariableGroups { get = > m_SharedVariableGroups ; set = > m_SharedVariableGroups = value ; }
#endif
2026-05-10 11:47:55 -04:00
private ResizableArray < InjectedSubtreeReference > m_InjectedSubtreeReference ;
2026-01-03 18:19:39 -05:00
private ResizableArray < VariableField > m_VariableFields ;
2025-11-25 08:19:33 -05:00
[System.NonSerialized] private bool m_Deserializing ;
2026-05-10 11:47:55 -04:00
internal ResizableArray < InjectedSubtreeReference > InjectedSubtreeReferences { get = > m_InjectedSubtreeReference ; set = > m_InjectedSubtreeReference = value ; }
2025-11-25 08:19:33 -05:00
/// <summary>
/// Default constructor.
/// </summary>
public BehaviorTreeData ( )
{
m_UniqueID = Guid . NewGuid ( ) . GetHashCode ( ) ;
}
/// <summary>
/// Adds the specified node.
/// </summary>
/// <param name="node">The node that should be added.</param>
public void AddNode ( ITreeLogicNode node )
{
if ( m_Tasks = = null ) {
m_Tasks = new ITreeLogicNode [ 1 ] ;
} else {
Array . Resize ( ref m_Tasks , m_Tasks . Length + 1 ) ;
}
node . Index = ( ushort ) ( m_Tasks . Length - 1 ) ;
node . ParentIndex = ushort . MaxValue ;
node . SiblingIndex = ushort . MaxValue ;
node . RuntimeIndex = ushort . MaxValue ;
m_Tasks [ m_Tasks . Length - 1 ] = node ;
}
/// <summary>
/// Removes the specified logic node.
/// </summary>
/// <param name="node">The node that should be removed.</param>
/// <returns>True if the node was removed.</returns>
public bool RemoveNode ( ITreeLogicNode node )
{
if ( m_Tasks = = null | | node . Index > = m_Tasks . Length ) {
return false ;
}
var dest = new ITreeLogicNode [ m_Tasks . Length - 1 ] ;
Array . Copy ( m_Tasks , dest , node . Index ) ;
Array . Copy ( m_Tasks , node . Index + 1 , dest , node . Index , m_Tasks . Length - node . Index - 1 ) ;
m_Tasks = dest ;
return true ;
}
/// <summary>
/// Adds the specified event node.
/// </summary>
/// <param name="eventNode">The event node that should be added.</param>
public void AddNode ( IEventNode eventNode )
{
if ( m_EventTasks = = null ) {
m_EventTasks = new IEventNode [ 1 ] ;
} else {
Array . Resize ( ref m_EventTasks , m_EventTasks . Length + 1 ) ;
}
2026-05-10 11:47:55 -04:00
eventNode . Index = ( ushort ) ( m_EventTasks . Length - 1 ) ;
2025-11-25 08:19:33 -05:00
m_EventTasks [ m_EventTasks . Length - 1 ] = 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 )
{
if ( m_EventTasks = = null ) {
return false ;
}
var index = m_EventTasks . IndexOf ( eventNode ) ;
if ( index = = - 1 ) {
return false ;
}
var dest = new IEventNode [ m_EventTasks . Length - 1 ] ;
Array . Copy ( m_EventTasks , dest , index ) ;
Array . Copy ( m_EventTasks , index + 1 , dest , index , m_EventTasks . Length - index - 1 ) ;
m_EventTasks = dest ;
return true ;
}
/// <summary>
/// Serializes the behavior tree.
/// </summary>
public void Serialize ( )
{
2026-01-03 18:19:39 -05:00
if ( Application . isPlaying ) {
return ;
}
2025-11-25 08:19:33 -05:00
m_TaskData = Serialization . Serialize < ITreeLogicNode > ( m_Tasks , ValidateSerializedObject ) ;
m_EventTaskData = Serialization . Serialize < IEventNode > ( m_EventTasks , ValidateSerializedObject ) ;
SerializeSharedVariables ( ) ;
2026-05-10 11:47:55 -04:00
// Disabled array removed in version 3.0.
m_DisabledLogicNodesData = null ;
m_DisabledEventNodesData = null ;
2025-11-25 08:19:33 -05:00
m_UniqueID = Guid . NewGuid ( ) . GetHashCode ( ) ;
#if UNITY_EDITOR
// Ensure the node data is up to date.
if ( m_LogicNodeProperties ! = null & & m_Tasks ! = null & & m_LogicNodeProperties . Length < = m_Tasks . Length ) {
for ( int i = 0 ; i < m_LogicNodeProperties . Length ; + + i ) {
var nodeData = m_LogicNodeProperties [ i ] . Data ;
nodeData . ParentIndex = m_Tasks [ i ] . ParentIndex ;
nodeData . SiblingIndex = m_Tasks [ i ] . SiblingIndex ;
nodeData . IsParent = m_Tasks [ i ] is IParentNode ;
m_LogicNodeProperties [ i ] . Data = nodeData ;
}
}
m_LogicNodePropertiesData = Serialization . Serialize < LogicNodeProperties > ( m_LogicNodeProperties ) ;
m_EventNodePropertiesData = Serialization . Serialize < NodeProperties > ( m_EventNodeProperties ) ;
m_GroupPropertiesData = Serialization . Serialize < GroupProperties > ( m_GroupProperties ) ;
#endif
}
/// <summary>
/// Validates the serialized object.
/// </summary>
/// <param name="type">The type of object.</param>
/// <param name="field">The field that the object belongs to.</param>
/// <param name="value">The value of the object</param>
/// <returns>The validated object.</returns>
public static Serialization . ValidatedObject ValidateSerializedObject ( Type type , FieldInfo field , object value )
{
if ( value = = null ) {
return new Serialization . ValidatedObject ( ) { Type = type , Obj = value } ;
}
// Replace ILogicNode with ushort index values.
if ( typeof ( IList ) . IsAssignableFrom ( type ) ) {
var elementType = Serializer . GetElementType ( type ) ;
if ( typeof ( ILogicNode ) . IsAssignableFrom ( elementType ) ) {
if ( field = = null | | field . GetCustomAttribute < InspectNodeAttribute > ( ) = = null ) {
var tasks = value as IList ;
if ( tasks = = null ) {
return new Serialization . ValidatedObject ( ) { Type = type , Obj = value } ;
}
var indexValues = new ushort [ tasks . Count ] ;
for ( int i = 0 ; i < indexValues . Length ; + + i ) {
indexValues [ i ] = ( ( ILogicNode ) tasks [ i ] ) . Index ;
}
return new Serialization . ValidatedObject ( ) { Type = typeof ( ushort [ ] ) , Obj = indexValues } ;
}
} else if ( Application . isPlaying & & ( typeof ( GameObject ) . IsAssignableFrom ( elementType ) | | typeof ( Component ) . IsAssignableFrom ( elementType ) ) ) { // Scene objects cannot be serialized at runtime.
var listValue = value as IList ;
if ( listValue ! = null ) {
IList objects ;
if ( type . IsArray ) {
objects = Array . CreateInstance ( elementType , listValue . Count ) ;
} else {
if ( type . IsGenericType ) {
objects = Activator . CreateInstance ( typeof ( List < > ) . MakeGenericType ( elementType ) ) as IList ;
} else {
objects = Activator . CreateInstance ( type ) as IList ;
}
}
for ( int i = 0 ; i < listValue . Count ; + + i ) {
GameObject gameObjectValue = null ;
if ( listValue [ i ] is Component componentValue ) {
gameObjectValue = componentValue . gameObject ;
} else {
gameObjectValue = listValue [ i ] as GameObject ;
}
if ( gameObjectValue ! = null & & gameObjectValue . scene . IsValid ( ) ) {
if ( type . IsArray ) {
objects [ i ] = null ;
} else {
objects . Add ( null ) ;
}
} else {
if ( type . IsArray ) {
objects [ i ] = listValue [ i ] ;
} else {
objects . Add ( listValue [ i ] ) ;
}
}
}
listValue = objects ;
}
return new Serialization . ValidatedObject ( ) { Type = type , Obj = listValue } ;
}
} else if ( typeof ( ILogicNode ) . IsAssignableFrom ( type ) ) {
if ( field = = null | | field . GetCustomAttribute < InspectNodeAttribute > ( ) = = null ) {
return new Serialization . ValidatedObject ( ) { Type = typeof ( ushort ) , Obj = ( ( ILogicNode ) value ) . Index } ;
}
} else if ( Application . isPlaying & & ( typeof ( GameObject ) . IsAssignableFrom ( type ) | | typeof ( Component ) . IsAssignableFrom ( type ) ) ) { // Scene objects cannot be serialized at runtime.
GameObject gameObjectValue = null ;
if ( value is Component componentValue ) {
gameObjectValue = componentValue . gameObject ;
} else {
gameObjectValue = value as GameObject ;
}
if ( gameObjectValue ! = null & & gameObjectValue . scene . IsValid ( ) ) {
return new Serialization . ValidatedObject ( ) { Type = type , Obj = null } ;
}
}
return new Serialization . ValidatedObject ( ) { Type = type , Obj = value } ;
}
/// <summary>
/// Serializes the SharedVariables. This allows the SharedVariables to be serialized independently.
/// </summary>
public void SerializeSharedVariables ( )
{
2026-01-03 18:19:39 -05:00
if ( Application . isPlaying ) {
return ;
}
2025-11-25 08:19:33 -05:00
m_SharedVariableData = Serialization . Serialize < SharedVariable > ( m_SharedVariables ) ;
#if UNITY_EDITOR
m_SharedVariableGroupsData = Serialization . Serialize < SharedVariableGroup > ( m_SharedVariableGroups ) ;
#endif
// Update the mapping for any variable name changes.
if ( m_VariableByNameMap = = null ) {
m_VariableByNameMap = new Dictionary < VariableAssignment , SharedVariable > ( ) ;
} else {
m_VariableByNameMap . Clear ( ) ;
}
if ( m_SharedVariables ! = null ) {
for ( int i = 0 ; i < m_SharedVariables . Length ; + + i ) {
2026-02-13 09:22:11 -05:00
if ( m_SharedVariables [ i ] = = null ) {
continue ;
}
2025-11-25 08:19:33 -05:00
m_VariableByNameMap . Add ( new VariableAssignment ( m_SharedVariables [ i ] . Name , m_SharedVariables [ i ] . Scope ) , m_SharedVariables [ i ] ) ;
}
}
}
/// <summary>
/// Deserialize the behavior tree.
/// </summary>
/// <param name="graphComponent">The component that the graph is being deserialized from.</param>
/// <param name="graph">The graph that is being deserialized.</param>
/// <param name="force">Should the behavior tree be force deserialized?</param>
/// <param name="forceSharedVariables">Should the shared variables be force deserialized?</param>
/// <param name="injectSubtrees">Should the subtrees be injected into the behavior tree?</param>
/// <param name="canDeepCopyVariables">Can the SharedVariables be deep copied?</param>
/// <param name="sharedVariableOverrides">A list of SharedVariables that should override the current SharedVariable value.</param>
/// <returns>True if the tree was deserialized.</returns>
public bool Deserialize ( IGraphComponent graphComponent , IGraph graph , bool force , bool forceSharedVariables , bool injectSubtrees , bool canDeepCopyVariables = true , SharedVariableOverride [ ] sharedVariableOverrides = null )
{
// No need to deserialize if the data is already deserialized.
2026-05-10 11:47:55 -04:00
if ( ! force & & ( ( m_Tasks ! = null & & m_TaskData ! = null ) | | ( m_EventTasks ! = null & & m_EventTaskData ! = null ) ) ) {
2025-11-25 08:19:33 -05:00
// SharedVariables may still need to be deserialized separately.
2026-01-03 18:19:39 -05:00
DeserializeSharedVariables ( graph , false , canDeepCopyVariables , sharedVariableOverrides ) ;
2025-11-25 08:19:33 -05:00
if ( Application . isPlaying & & m_RuntimeUniqueID = = 0 ) {
m_RuntimeUniqueID = m_UniqueID ;
}
return true ;
}
2026-05-10 11:47:55 -04:00
var deserialized = DeserializeInternal ( graphComponent , graph , force , forceSharedVariables , injectSubtrees , canDeepCopyVariables , sharedVariableOverrides ) ;
#if UNITY_EDITOR
if ( deserialized ) {
UpdateInjectedGraphReferences ( ) ;
} else {
m_InjectedGraphReferences = null ;
}
#endif
return deserialized ;
2025-11-25 08:19:33 -05:00
}
/// <summary>
/// Internal method which deserialize the behavior tree.
/// </summary>
/// <param name="graphComponent">The component that the graph is being deserialized from.</param>
/// <param name="graph">The graph that is being deserialized.</param>
/// <param name="force">Should the behavior tree be force deserialized?</param>
/// <param name="forceSharedVariables">Should the shared variables be force deserialized?</param>
/// <param name="injectSubtrees">Should the subtrees be injected into the behavior tree?</param>
/// <param name="canDeepCopyVariables">Can the SharedVariables be deep copied?</param>
/// <param name="sharedVariableOverrides">A list of SharedVariables that should override the current SharedVariable value.</param>
/// <returns>True if the tree was deserialized.</returns>
private bool DeserializeInternal ( IGraphComponent graphComponent , IGraph graph , bool force , bool forceSharedVariables , bool injectSubtrees , bool canDeepCopyVariables , SharedVariableOverride [ ] sharedVariableOverrides = null )
{
2026-05-10 11:47:55 -04:00
// Prevent the tree from being deserialized recursively.
2025-11-25 08:19:33 -05:00
if ( m_Deserializing ) {
Debug . LogError ( $"Error: Unable to deserialize {graph}. This can be caused by recursive subtree references." ) ;
return false ;
}
m_Deserializing = true ;
m_RuntimeUniqueID = Application . isPlaying ? m_UniqueID : 0 ;
2026-01-03 18:19:39 -05:00
m_VariableFields = null ;
2025-11-25 08:19:33 -05:00
var errorState = false ;
#if UNITY_EDITOR
// Deserialize the properties first so it can be used elsewhere.
if ( m_LogicNodePropertiesData ! = null & & m_LogicNodePropertiesData . Length > 0 ) {
m_LogicNodeProperties = new LogicNodeProperties [ m_LogicNodePropertiesData . Length ] ;
for ( int i = 0 ; i < m_LogicNodePropertiesData . Length ; + + i ) {
try {
m_LogicNodeProperties [ i ] = m_LogicNodePropertiesData [ i ] . DeserializeFields ( MemberVisibility . Public ) as LogicNodeProperties ;
} catch ( Exception e ) {
m_LogicNodeProperties [ i ] = new LogicNodeProperties ( ) ;
Debug . LogError ( $"Error: Unable to load task editor data at index {i} due to exception:\n{e}" ) ;
}
}
} else {
m_LogicNodeProperties = null ;
}
if ( m_EventNodePropertiesData ! = null & & m_EventNodePropertiesData . Length > 0 ) {
m_EventNodeProperties = new NodeProperties [ m_EventNodePropertiesData . Length ] ;
for ( int i = 0 ; i < m_EventNodePropertiesData . Length ; + + i ) {
m_EventNodeProperties [ i ] = m_EventNodePropertiesData [ i ] . DeserializeFields ( MemberVisibility . Public ) as NodeProperties ;
}
} else {
m_EventNodeProperties = null ;
}
if ( m_GroupPropertiesData ! = null & & m_GroupPropertiesData . Length > 0 ) {
m_GroupProperties = new GroupProperties [ m_GroupPropertiesData . Length ] ;
for ( int i = 0 ; i < m_GroupPropertiesData . Length ; + + i ) {
m_GroupProperties [ i ] = m_GroupPropertiesData [ i ] . DeserializeFields ( MemberVisibility . Public ) as GroupProperties ;
}
} else {
m_GroupProperties = null ;
}
#endif
2026-01-03 18:19:39 -05:00
DeserializeSharedVariables ( graph , forceSharedVariables , canDeepCopyVariables , sharedVariableOverrides ) ;
2025-11-25 08:19:33 -05:00
// The disabled node indicies need to be deserialized before the nodes.
if ( m_DisabledEventNodesData ! = null & & m_DisabledEventNodesData . Length > 0 & & m_EventTaskData ! = null ) {
m_DisabledEventNodes = new ushort [ m_DisabledEventNodesData . Length ] ;
var offset = 0 ;
for ( int i = 0 ; i < m_DisabledEventNodesData . Length ; + + i ) {
m_DisabledEventNodes [ i ] = ( ushort ) m_DisabledEventNodesData [ i ] . DeserializeFields ( MemberVisibility . Public ) ;
// The node index may no longer be valid.
if ( m_DisabledEventNodes [ i - offset ] > = m_EventTaskData . Length ) {
offset + + ;
}
}
if ( offset > 0 ) {
Array . Resize ( ref m_DisabledEventNodes , m_DisabledEventNodes . Length - offset ) ;
}
} else {
m_DisabledEventNodes = null ;
}
if ( m_DisabledLogicNodesData ! = null & & m_DisabledLogicNodesData . Length > 0 & & m_TaskData ! = null ) {
m_DisabledLogicNodes = new ushort [ m_DisabledLogicNodesData . Length ] ;
var offset = 0 ;
for ( int i = 0 ; i < m_DisabledLogicNodesData . Length ; + + i ) {
m_DisabledLogicNodes [ i - offset ] = ( ushort ) m_DisabledLogicNodesData [ i ] . DeserializeFields ( MemberVisibility . Public ) ;
// The node index may no longer be valid.
if ( m_DisabledLogicNodes [ i - offset ] > = m_TaskData . Length ) {
offset + + ;
}
}
if ( offset > 0 ) {
Array . Resize ( ref m_DisabledLogicNodes , m_DisabledLogicNodes . Length - offset ) ;
}
} else {
m_DisabledLogicNodes = null ;
}
ResizableArray < TaskAssignment > taskReferences = null ;
2026-05-10 11:47:55 -04:00
if ( m_InjectedSubtreeReference ! = null ) {
m_InjectedSubtreeReference . Clear ( ) ;
2025-11-25 08:19:33 -05:00
}
if ( m_TaskData ! = null & & m_TaskData . Length > 0 ) {
m_Tasks = new ITreeLogicNode [ m_TaskData . Length ] ;
for ( int i = 0 ; i < m_TaskData . Length ; + + i ) {
try {
var task = m_TaskData [ i ] . DeserializeFields ( MemberVisibility . Public , ValidateDeserializedTypeObject , ( object fieldInfoObj , object task , object value ) = >
{
2026-01-03 18:19:39 -05:00
var validatedValue = ValidateDeserializedObject ( fieldInfoObj , task , value , ref m_VariableByNameMap , ref taskReferences , sharedVariableOverrides ) ;
if ( validatedValue ! = null & & validatedValue is SharedVariable sharedVariable & & sharedVariable . Scope = = SharedVariable . SharingScope . Graph ) {
if ( m_VariableFields = = null ) { m_VariableFields = new ResizableArray < VariableField > ( ) ; }
m_VariableFields . Add ( new VariableField ( ) { Field = fieldInfoObj as FieldInfo , Task = task , Name = sharedVariable . Name } ) ;
}
return validatedValue ;
2025-11-25 08:19:33 -05:00
} ) as ILogicNode ;
if ( task is ITreeLogicNode treeLogicNode ) {
m_Tasks [ i ] = treeLogicNode ;
} else if ( task is ILogicNode ) {
Debug . LogError ( $"Error: The task {m_TaskData[i].ObjectType} at index {i} must implement ITreeLogicNode." ) ;
}
} catch ( Exception e ) {
Debug . LogError ( $"Error: Unable to load task {m_TaskData[i].ObjectType} at index {i} due to exception:\n{e}" ) ;
}
// Account for tasks where the object no longer exists.
if ( m_Tasks [ i ] = = null ) {
2026-05-10 11:47:55 -04:00
// Check if the type has moved using the MovedFrom attribute.
var taskType = TypeUtility . GetType ( m_TaskData [ i ] . ObjectType ) ;
if ( taskType ! = null ) {
// The type was found (possibly via MovedFrom), try to deserialize again.
try {
m_TaskData [ i ] . ObjectType = taskType . FullName ;
var task = m_TaskData [ i ] . DeserializeFields ( MemberVisibility . Public , ValidateDeserializedTypeObject , ( object fieldInfoObj , object taskObj , object value ) = >
{
var validatedValue = ValidateDeserializedObject ( fieldInfoObj , taskObj , value , ref m_VariableByNameMap , ref taskReferences , sharedVariableOverrides ) ;
if ( validatedValue ! = null & & validatedValue is SharedVariable sharedVariable & & sharedVariable . Scope = = SharedVariable . SharingScope . Graph ) {
if ( m_VariableFields = = null ) { m_VariableFields = new ResizableArray < VariableField > ( ) ; }
m_VariableFields . Add ( new VariableField ( ) { Field = fieldInfoObj as FieldInfo , Task = taskObj , Name = sharedVariable . Name } ) ;
}
return validatedValue ;
} ) as ILogicNode ;
if ( task is ITreeLogicNode treeLogicNode ) {
m_Tasks [ i ] = treeLogicNode ;
} else if ( task is ILogicNode ) {
Debug . LogError ( $"Error: The task {m_TaskData[i].ObjectType} at index {i} must implement ITreeLogicNode." ) ;
}
} catch ( Exception e ) {
Debug . LogError ( $"Error: Unable to load task {m_TaskData[i].ObjectType} at index {i} after MovedFrom resolution due to exception:\n{e}" ) ;
}
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
// If still null, create an unknown task.
if ( m_Tasks [ i ] = = null ) {
#if UNITY_EDITOR
if ( m_LogicNodeProperties [ i ] . Data . IsParent ) {
m_Tasks [ i ] = new UnknownParentTaskNode ( m_TaskData [ i ] . ObjectType ) ;
} else {
m_Tasks [ i ] = new UnknownTaskNode ( m_TaskData [ i ] . ObjectType ) ;
}
m_Tasks [ i ] . Index = ( ushort ) i ;
m_Tasks [ i ] . ParentIndex = m_LogicNodeProperties [ i ] . Data . ParentIndex ;
m_Tasks [ i ] . SiblingIndex = m_LogicNodeProperties [ i ] . Data . SiblingIndex ;
2025-11-25 08:19:33 -05:00
#else
2026-05-10 11:47:55 -04:00
if ( i + 1 < m_Tasks . Length & & m_Tasks [ i + 1 ] ! = null & & m_Tasks [ i + 1 ] . ParentIndex = = i ) {
m_Tasks [ i ] = new UnknownParentTaskNode ( m_TaskData [ i ] . ObjectType ) ;
} else {
m_Tasks [ i ] = new UnknownTaskNode ( m_TaskData [ i ] . ObjectType ) ;
}
m_Tasks [ i ] . Index = ( ushort ) i ;
m_Tasks [ i ] . ParentIndex = ushort . MaxValue ;
m_Tasks [ i ] . SiblingIndex = ushort . MaxValue ;
2025-11-25 08:19:33 -05:00
#endif
2026-05-10 11:47:55 -04:00
Debug . LogError ( $"Error: Unable to deserialize task of type {m_TaskData[i].ObjectType}. Use the [MovedFrom] attribute for refactoring." ) ;
}
2025-11-25 08:19:33 -05:00
}
// The RuntimeIndex is assigned later when the tree is initialized.
m_Tasks [ i ] . RuntimeIndex = ushort . MaxValue ;
#if UNITY_EDITOR
// Sanity checks.
if ( m_Tasks [ i ] . Index > = m_TaskData . Length ) { m_Tasks [ i ] . Index = ( ushort ) i ; }
if ( m_Tasks [ i ] . ParentIndex ! = ushort . MaxValue & & m_Tasks [ i ] . ParentIndex > = m_TaskData . Length ) { m_Tasks [ i ] . ParentIndex = ushort . MaxValue ; }
if ( m_Tasks [ i ] . SiblingIndex ! = ushort . MaxValue & & m_Tasks [ i ] . SiblingIndex > = m_TaskData . Length ) { m_Tasks [ i ] . SiblingIndex = ushort . MaxValue ; }
#endif
2026-05-10 11:47:55 -04:00
// Migrate from the deprecated disabled array to the Enabled property.
if ( m_DisabledLogicNodes ! = null & & m_DisabledLogicNodes . Length > 0 ) {
for ( int j = 0 ; j < m_DisabledLogicNodes . Length ; + + j ) {
if ( m_DisabledLogicNodes [ j ] = = i ) {
m_Tasks [ i ] . Enabled = false ;
break ;
}
}
}
2025-11-25 08:19:33 -05:00
if ( injectSubtrees ) {
// If the previous task is a parent the current task has to be a child otherwise the tree is in an error state. The error will also occur
// if there is only one task and that task is a parent task.
if ( ( m_Tasks [ i ] . ParentIndex ! = ushort . MaxValue & & ( i > 0 & & m_Tasks [ i - 1 ] is IParentNode & & m_Tasks [ i ] . ParentIndex ! = m_Tasks [ i - 1 ] . Index ) ) | | ( m_Tasks [ i ] is IParentNode & & i + 1 = = m_Tasks . Length ) ) {
Debug . LogError ( $"Error: {graph} contains the parent task {m_Tasks[i].GetType().Name} which does not have any children. All parent tasks must contain at least one child." , graph . Parent ) ;
errorState = true ;
continue ;
}
// Subtrees will be evaluated after all tasks are assigned.
2026-05-10 11:47:55 -04:00
if ( m_Tasks [ i ] is ISubtreeReferenceNode subtreeReference ) {
2025-11-25 08:19:33 -05:00
// Subtrees can be nested.
2026-05-10 11:47:55 -04:00
subtreeReference . EvaluateSubgraphs ( graphComponent ) ;
2025-11-25 08:19:33 -05:00
var subtrees = subtreeReference . Subtrees ;
if ( subtrees ! = null ) {
// The parent must be able to accept the number of subtrees that there are.
var parentIndex = m_Tasks [ i ] . ParentIndex ;
IParentNode parentNode = null ;
if ( parentIndex ! = ushort . MaxValue ) {
parentNode = m_Tasks [ parentIndex ] as IParentNode ;
}
if ( ( parentNode = = null & & subtrees . Length > 1 ) | | ( parentNode ! = null & & subtrees . Length > parentNode . MaxChildCount ) ) {
Debug . LogError ( $"Error: {graph} on object {graph.Parent} contains multiple subtrees as the starting task or as a child of a parent task which cannot contain so many children (such as a decorator)." , graph . Parent ) ;
errorState = true ;
continue ;
}
var deserializedNodes = new ITreeLogicNode [ subtrees . Length ] [ ] ;
for ( int j = 0 ; j < subtrees . Length ; + + j ) {
if ( subtrees [ j ] = = null ) {
continue ;
}
2026-01-03 18:19:39 -05:00
if ( ! subtrees [ j ] . Deserialize ( graphComponent , force & & ! subtrees [ j ] . Pooled , forceSharedVariables & & ! subtrees [ j ] . Pooled , true , true , subtreeReference . SharedVariableOverrides ) ) {
2025-11-25 08:19:33 -05:00
errorState = true ;
break ;
} ;
2026-01-03 18:19:39 -05:00
if ( subtrees [ j ] . Data . m_VariableFields ! = null & & subtrees [ j ] . Data . m_VariableFields . Count > 0 ) {
if ( m_VariableFields = = null ) { m_VariableFields = new ResizableArray < VariableField > ( ) ; ; }
m_VariableFields . AddRange ( subtrees [ j ] . Data . m_VariableFields ) ;
}
2025-11-25 08:19:33 -05:00
// Keep a reference to the deserialized nodes. This will ensure they are unique and do not get overwritten.
deserializedNodes [ j ] = subtrees [ j ] . TreeLogicNodes ;
// Add any new subtree variables to the current tree.
if ( subtrees [ j ] . SharedVariables ! = null ) {
// In order to reduce allocations the first loop will determine the number of variables that need to be added.
var length = subtrees [ j ] . SharedVariables . Length ;
var variableCount = 0 ;
for ( int k = 0 ; k < length ; + + k ) {
var subtreeVariable = subtrees [ j ] . SharedVariables [ k ] ;
if ( GetVariable ( graph , subtreeVariable . Name , SharedVariable . SharingScope . Graph ) = = null ) {
variableCount + + ;
}
}
// And the second loop will actually add the variables.
if ( variableCount > 0 ) {
var insertIndex = 0 ;
if ( m_SharedVariables = = null ) {
m_SharedVariables = new SharedVariable [ variableCount ] ;
m_VariableByNameMap = new Dictionary < VariableAssignment , SharedVariable > ( ) ;
} else {
insertIndex = m_SharedVariables . Length ;
Array . Resize ( ref m_SharedVariables , m_SharedVariables . Length + variableCount ) ;
}
for ( int k = 0 ; k < length ; + + k ) {
var subtreeVariable = subtrees [ j ] . SharedVariables [ k ] ;
if ( ! m_VariableByNameMap . ContainsKey ( new VariableAssignment ( subtreeVariable . Name , SharedVariable . SharingScope . Graph ) ) ) {
m_SharedVariables [ insertIndex ] = subtreeVariable ;
m_VariableByNameMap . Add ( new VariableAssignment ( subtreeVariable . Name , SharedVariable . SharingScope . Graph ) , subtreeVariable ) ;
insertIndex + + ;
}
}
}
}
}
// Do not add the subtree if it causes an error.
if ( ! errorState ) {
2026-05-10 11:47:55 -04:00
if ( m_InjectedSubtreeReference = = null ) { m_InjectedSubtreeReference = new ResizableArray < InjectedSubtreeReference > ( ) ; }
m_InjectedSubtreeReference . Add ( new InjectedSubtreeReference ( )
2025-11-25 08:19:33 -05:00
{
2026-05-10 11:47:55 -04:00
GraphReference = subtreeReference ,
2025-11-25 08:19:33 -05:00
NodeIndex = ( ushort ) i ,
Subtrees = subtrees ,
2026-05-10 11:47:55 -04:00
Nodes = deserializedNodes ,
#if UNITY_EDITOR
GraphReferenceNodeProperties = m_LogicNodeProperties [ i ] ,
#endif
2025-11-25 08:19:33 -05:00
} ) ;
}
}
}
}
}
2026-05-10 11:47:55 -04:00
// Migrate from the deprecated disabled array to the Enabled property.
m_DisabledLogicNodes = null ;
2025-11-25 08:19:33 -05:00
} else {
m_Tasks = null ;
}
2026-05-10 11:47:55 -04:00
// Add the event tasks before the subtrees are injected. Connected indices will be adjusted after injection.
var baseEventTaskCount = 0 ;
2025-11-25 08:19:33 -05:00
if ( m_EventTaskData ! = null & & m_EventTaskData . Length > 0 ) {
m_EventTasks = new IEventNode [ m_EventTaskData . Length ] ;
for ( int i = 0 ; i < m_EventTaskData . Length ; + + i ) {
2026-05-10 11:47:55 -04:00
try {
var eventTaskObj = m_EventTaskData [ i ] . DeserializeFields ( MemberVisibility . Public , ValidateDeserializedTypeObject , ( object fieldInfoObj , object task , object value ) = >
{
var validatedValue = ValidateDeserializedObject ( fieldInfoObj , task , value , ref m_VariableByNameMap , ref taskReferences , sharedVariableOverrides ) ;
if ( validatedValue ! = null & & validatedValue is SharedVariable sharedVariable & & sharedVariable . Scope = = SharedVariable . SharingScope . Graph ) {
if ( m_VariableFields = = null ) { m_VariableFields = new ResizableArray < VariableField > ( ) ; }
m_VariableFields . Add ( new VariableField ( ) { Field = fieldInfoObj as FieldInfo , Task = task , Name = sharedVariable . Name } ) ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
return validatedValue ;
} ) ;
if ( eventTaskObj is IEventNode eventNode ) {
m_EventTasks [ i ] = eventNode ;
} else if ( eventTaskObj ! = null ) {
Debug . LogError ( $"Error: The event task {m_EventTaskData[i].ObjectType} at index {i} must implement IEventNode." ) ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
} catch ( Exception e ) {
Debug . LogError ( $"Error: Unable to load event task {m_EventTaskData[i].ObjectType} at index {i} due to exception:\n{e}" ) ;
2025-11-25 08:19:33 -05:00
}
if ( m_EventTasks [ i ] = = null ) {
2026-05-10 11:47:55 -04:00
m_EventTasks [ i ] = new UnknownEventTask ( m_EventTaskData [ i ] . ObjectType ) ;
Debug . LogError ( $"Error: Unable to deserialize event of type {m_EventTaskData[i].ObjectType}." ) ;
}
m_EventTasks [ i ] . Index = ( ushort ) i ;
// Migrate from the deprecated disabled array to the Enabled property.
if ( m_DisabledEventNodes ! = null & & m_DisabledEventNodes . Length > 0 ) {
for ( int j = 0 ; j < m_DisabledEventNodes . Length ; + + j ) {
if ( m_DisabledEventNodes [ j ] = = i ) {
m_EventTasks [ i ] . Enabled = false ;
break ;
}
}
2025-11-25 08:19:33 -05:00
}
}
2026-05-10 11:47:55 -04:00
// Migrate from the deprecated disabled array to the Enabled property.
m_DisabledEventNodes = null ;
2025-11-25 08:19:33 -05:00
} else {
m_EventTasks = null ;
}
2026-05-10 11:47:55 -04:00
baseEventTaskCount = m_EventTasks ! = null ? m_EventTasks . Length : 0 ;
// Subtrees should be injected into the tree.
InjectSubtrees ( ) ;
// Modify the ConnectedIndex to match the injection for the base event tasks.
if ( m_EventTasks ! = null & & m_InjectedSubtreeReference ! = null & & baseEventTaskCount > 0 ) {
for ( int i = 0 ; i < baseEventTaskCount ; + + i ) {
// A subtree may have injected nodes before the originally connected index. Modify the index to match the injection.
var offset = 0 ;
for ( int j = 0 ; j < m_InjectedSubtreeReference . Count ; + + j ) {
if ( m_InjectedSubtreeReference [ j ] . NodeIndex > = m_EventTasks [ i ] . ConnectedIndex ) {
break ;
}
offset + = m_InjectedSubtreeReference [ j ] . NodeCount > 0 ? m_InjectedSubtreeReference [ j ] . NodeCount - 1 : 0 ;
}
if ( offset > 0 ) {
m_EventTasks [ i ] . ConnectedIndex + = ( ushort ) offset ;
}
}
}
2025-11-25 08:19:33 -05:00
// After the tree has been deserialized the task references need to be assigned.
AssignTaskReferences ( m_Tasks , taskReferences ) ;
m_Deserializing = false ;
return ! errorState ;
}
/// <summary>
/// Validates the object type when deserializing.
/// </summary>
/// <param name="type">The type of object that should be validated.</param>
/// <param name="field">The field that contains the object.</param>
/// <returns>The validated type.</returns>
public static Type ValidateDeserializedTypeObject ( Type type , FieldInfo field )
{
if ( typeof ( IList ) . IsAssignableFrom ( type ) ) {
var elementType = Serializer . GetElementType ( type ) ;
if ( typeof ( ILogicNode ) . IsAssignableFrom ( elementType ) & & ( field = = null | | field . GetCustomAttribute < InspectNodeAttribute > ( ) = = null ) ) {
return typeof ( ushort [ ] ) ;
}
} else if ( typeof ( ILogicNode ) . IsAssignableFrom ( type ) & & ( field = = null | | field . GetCustomAttribute < InspectNodeAttribute > ( ) = = null ) ) {
return typeof ( ushort ) ;
}
2026-05-10 11:47:55 -04:00
var formerlySerializedType = field ? . GetCustomAttribute < FormerlySerializedTypeAttribute > ( false ) ;
if ( formerlySerializedType ! = null ) {
return formerlySerializedType . Type ;
}
2025-11-25 08:19:33 -05:00
return type ;
}
/// <summary>
/// Validates the object when deserializing.
/// </summary>
/// <param name="fieldInfoObj">The FieldInfo that is being deserialized.</param>
/// <param name="target">The object being deserialized.</param>
/// <param name="value">The value of the field.</param>
/// <param name="variableByNameMap">A reference to the map between the VariableAssignment and SharedVariable.</param>
/// <param name="taskReferences">A reference to the list of task references that need to be resolved later.</param>
/// <param name="sharedVariableOverrides">A list of SharedVariables that should override the current SharedVariable value.</param>
/// <returns>The validated object.</returns>
public static object ValidateDeserializedObject ( object fieldInfoObj , object target , object value , ref Dictionary < VariableAssignment , SharedVariable > variableByNameMap ,
ref ResizableArray < TaskAssignment > taskReferences , SharedVariableOverride [ ] sharedVariableOverrides = null )
{
var fieldInfo = fieldInfoObj as FieldInfo ;
if ( fieldInfo = = null ) {
return value ;
}
var type = fieldInfo . FieldType ;
if ( value = = null ) {
// A SharedVariable object should always exist.
if ( ! type . IsAbstract & & typeof ( SharedVariable ) . IsAssignableFrom ( type ) ) {
return Activator . CreateInstance ( type ) ;
}
return null ;
}
if ( typeof ( IList ) . IsAssignableFrom ( type ) ) {
var elementType = Serializer . GetElementType ( type ) ;
if ( typeof ( ILogicNode ) . IsAssignableFrom ( elementType ) & & fieldInfo . GetCustomAttribute < InspectNodeAttribute > ( ) = = null ) {
// The task reference will be assigned after all of the tasks have been deserialized.
if ( taskReferences = = null ) { taskReferences = new ResizableArray < TaskAssignment > ( ) ; }
taskReferences . Add ( new TaskAssignment ( ) { Field = fieldInfo , Target = target , Value = value } ) ;
} else if ( typeof ( SharedVariable ) . IsAssignableFrom ( elementType ) ) {
var listValue = value as IList ;
if ( listValue ! = null ) {
for ( int i = 0 ; i < listValue . Count ; + + i ) {
var sharedVariableElement = listValue [ i ] as SharedVariable ;
if ( variableByNameMap ! = null & & sharedVariableElement ! = null & & ! string . IsNullOrEmpty ( sharedVariableElement . Name ) ) {
if ( variableByNameMap . TryGetValue ( new VariableAssignment ( sharedVariableElement . Name , sharedVariableElement . Scope ) , out var mappedSharedVariable ) ) {
if ( Application . isPlaying & & sharedVariableElement . Scope = = SharedVariable . SharingScope . Dynamic & & sharedVariableElement . GetType ( ) ! = mappedSharedVariable . GetType ( ) ) {
Debug . LogError ( $"Error: The dynamic variables with name {sharedVariableElement.Name} have different types. All dynamic variables must have the same type." ) ;
listValue [ i ] = sharedVariableElement ;
} else {
listValue [ i ] = GetOverrideVariable ( sharedVariableOverrides , mappedSharedVariable , false ) ;
}
} else if ( sharedVariableElement . Scope = = SharedVariable . SharingScope . Dynamic ) {
// New dynamic variables should have the default value.
var sharedVariableValueType = sharedVariableElement . GetType ( ) . GetGenericArguments ( ) [ 0 ] ;
if ( sharedVariableValueType . IsValueType ) {
sharedVariableElement . SetValue ( Activator . CreateInstance ( sharedVariableValueType ) ) ;
} else {
sharedVariableElement . SetValue ( null ) ;
}
// Dynamic variables are created when the task is deserialized. The variable needs to be added to the mapping so it can be reused.
variableByNameMap . Add ( new VariableAssignment ( sharedVariableElement . Name , sharedVariableElement . Scope ) , sharedVariableElement ) ;
listValue [ i ] = sharedVariableElement ;
}
}
}
return listValue ;
}
}
} else if ( typeof ( ILogicNode ) . IsAssignableFrom ( type ) & & fieldInfo . GetCustomAttribute < InspectNodeAttribute > ( ) = = null ) {
// The task reference will be assigned after all of the tasks have been deserialized.
if ( taskReferences = = null ) { taskReferences = new ResizableArray < TaskAssignment > ( ) ; }
taskReferences . Add ( new TaskAssignment ( ) { Field = fieldInfo , Target = target , Value = value } ) ;
} else if ( typeof ( SharedVariable ) . IsAssignableFrom ( type ) ) {
var sharedVariable = value as SharedVariable ;
2026-05-10 11:47:55 -04:00
if ( sharedVariable = = null ) {
sharedVariable = CreateLegacySharedVariable ( type , value ) ;
}
2025-11-25 08:19:33 -05:00
if ( variableByNameMap ! = null & & sharedVariable ! = null & & ! string . IsNullOrEmpty ( sharedVariable . Name ) ) {
if ( variableByNameMap . TryGetValue ( new VariableAssignment ( sharedVariable . Name , sharedVariable . Scope ) , out var mappedSharedVariable ) ) {
if ( Application . isPlaying & & sharedVariable . Scope = = SharedVariable . SharingScope . Dynamic & & sharedVariable . GetType ( ) ! = mappedSharedVariable . GetType ( ) ) {
Debug . LogError ( $"Error: The dynamic variables with name {sharedVariable.Name} have different types. Dynamic variables with the same name must have the same type." ) ;
return sharedVariable ;
}
2026-05-10 11:47:55 -04:00
return GetOverrideVariable ( sharedVariableOverrides , mappedSharedVariable , false ) ;
2025-11-25 08:19:33 -05:00
} else if ( Application . isPlaying & & sharedVariable . Scope = = SharedVariable . SharingScope . Dynamic ) {
// New dynamic variables should have the default value.
var sharedVariableValueType = sharedVariable . GetType ( ) . GetGenericArguments ( ) [ 0 ] ;
if ( sharedVariableValueType . IsValueType ) {
sharedVariable . SetValue ( Activator . CreateInstance ( sharedVariableValueType ) ) ;
} else {
sharedVariable . SetValue ( null ) ;
}
// Dynamic variables are created when the task is deserialized. The variable needs to be added to the mapping so it can be reused.
variableByNameMap . Add ( new VariableAssignment ( sharedVariable . Name , sharedVariable . Scope ) , sharedVariable ) ;
return sharedVariable ;
}
}
2026-05-10 11:47:55 -04:00
if ( sharedVariable ! = null ) {
return sharedVariable ;
}
2025-11-25 08:19:33 -05:00
}
return value ;
}
2026-05-10 11:47:55 -04:00
/// <summary>
/// Creates a SharedVariable from a legacy non-shared serialized value.
/// </summary>
/// <param name="sharedVariableType">The current SharedVariable field type.</param>
/// <param name="value">The legacy serialized value.</param>
/// <returns>The wrapped SharedVariable, or null if the value could not be converted.</returns>
private static SharedVariable CreateLegacySharedVariable ( Type sharedVariableType , object value )
{
if ( sharedVariableType = = null | | sharedVariableType . IsAbstract | | value = = null ) {
return null ;
}
try {
var sharedVariable = Activator . CreateInstance ( sharedVariableType ) as SharedVariable ;
if ( sharedVariable = = null ) {
return null ;
}
var valueType = sharedVariable . ElementType ;
if ( ! valueType . IsInstanceOfType ( value ) ) {
if ( valueType . IsEnum ) {
value = value is string enumName ? Enum . Parse ( valueType , enumName ) : Enum . ToObject ( valueType , value ) ;
} else if ( valueType = = typeof ( string ) ) {
value = value . ToString ( ) ;
} else {
value = Convert . ChangeType ( value , valueType ) ;
}
}
sharedVariable . Scope = SharedVariable . SharingScope . Self ;
sharedVariable . SetValue ( value ) ;
return sharedVariable ;
} catch {
return null ;
}
}
2025-11-25 08:19:33 -05:00
/// <summary>
/// Deserializes the SharedVariables. This allows the SharedVariables to be deserialized independently.
/// </summary>
2026-01-03 18:19:39 -05:00
/// <param name="graph">The graph that is being deserialized.</param>
2025-11-25 08:19:33 -05:00
/// <param name="force">Should the variables be forced deserialized?</param>
2026-01-03 18:19:39 -05:00
/// <param name="canDeepCopy">Can the SharedVariables be deep copied?</param>
2025-11-25 08:19:33 -05:00
/// <param name="sharedVariableOverrides">A list of SharedVariables that should override the current SharedVariable value.</param>
/// <returns>True if the SharedVariables were deserialized.</returns>
2026-01-03 18:19:39 -05:00
public bool DeserializeSharedVariables ( IGraph graph , bool force , bool canDeepCopy , SharedVariableOverride [ ] sharedVariableOverrides = null )
2025-11-25 08:19:33 -05:00
{
// No need to deserialize if the data is already deserialized.
2026-01-03 18:19:39 -05:00
if ( ! force & & ( m_SharedVariables ! = null | | m_VariableByNameMap ! = null
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
2026-01-03 18:19:39 -05:00
| | m_SharedVariableGroups ! = null
2025-11-25 08:19:33 -05:00
#endif
) ) {
return false ;
}
if ( m_SharedVariableData ! = null & & m_SharedVariableData . Length > 0 ) {
m_SharedVariables = new SharedVariable [ m_SharedVariableData . Length ] ;
for ( int i = 0 ; i < m_SharedVariableData . Length ; + + i ) {
try {
m_SharedVariables [ i ] = m_SharedVariableData [ i ] . DeserializeFields ( MemberVisibility . Public ) as SharedVariable ;
} catch ( Exception e ) {
Debug . LogError ( $"Error: Unable to load variable {m_SharedVariableData[i].ObjectType} at index {i} due to exception:\n{e}" ) ;
}
if ( m_SharedVariables [ i ] = = null ) {
2026-05-10 11:47:55 -04:00
var originalTypeName = m_SharedVariableData [ i ] . ObjectType ;
m_SharedVariableData [ i ] . ObjectType = typeof ( UnknownSharedVariable ) . FullName ;
m_SharedVariables [ i ] = m_SharedVariableData [ i ] . DeserializeFields ( MemberVisibility . Public ) as SharedVariable ;
m_SharedVariableData [ i ] . ObjectType = originalTypeName ;
// Store the original type name in the unknown variable.
if ( m_SharedVariables [ i ] is UnknownSharedVariable unknownVar ) {
unknownVar . UnknownType = originalTypeName ;
}
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
Debug . LogError ( $"Error: Unable to deserialize SharedVariable {m_SharedVariables[i].Name} of type {originalTypeName}." ) ;
2025-11-25 08:19:33 -05:00
}
// The override variable can set a value specific for the subtree.
if ( Application . isPlaying ) {
m_SharedVariables [ i ] . Initialize ( ) ;
var overrideVariable = GetOverrideVariable ( sharedVariableOverrides , m_SharedVariables [ i ] , true ) ;
// If the overridden scope is self then only the value should be overridden and not the SharedVariable reference.
if ( overrideVariable ! = null & & overrideVariable . Scope = = SharedVariable . SharingScope . Self ) {
m_SharedVariables [ i ] . SetValue ( overrideVariable . GetValue ( ) ) ;
}
}
}
} else {
m_SharedVariables = null ;
}
2026-01-03 18:19:39 -05:00
m_VariableByNameMap = PopulateSharedVariablesMapping ( graph , canDeepCopy ) ;
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
if ( m_SharedVariableGroupsData ! = null & & m_SharedVariableGroupsData . Length > 0 ) {
m_SharedVariableGroups = new SharedVariableGroup [ m_SharedVariableGroupsData . Length ] ;
for ( int i = 0 ; i < m_SharedVariableGroupsData . Length ; + + i ) {
m_SharedVariableGroups [ i ] = m_SharedVariableGroupsData [ i ] . DeserializeFields ( MemberVisibility . Public ) as SharedVariableGroup ;
}
} else {
m_SharedVariableGroups = null ;
}
#endif
return true ;
}
/// <summary>
/// Returns the override SharedVariable from the source SharedVariable.
/// </summary>
/// <param name="sharedVariableOverrides">The list of override SharedVariables.</param>
/// <param name="graphVariable">The variable that should be overridden.</param>
/// <param name="deserialize">Is the method being called when the variables are being deserialized?</param>
/// <returns>The override SharedVariable (can be null).</returns>
private static SharedVariable GetOverrideVariable ( SharedVariableOverride [ ] sharedVariableOverrides , SharedVariable graphVariable , bool deserialize )
{
if ( sharedVariableOverrides = = null ) {
return deserialize ? null : graphVariable ;
}
for ( int i = 0 ; i < sharedVariableOverrides . Length ; + + i ) {
var overrideVariable = sharedVariableOverrides [ i ] . Override ;
// Empty variables indicate that the variable should not be overridden.
if ( overrideVariable = = null | | overrideVariable . Scope = = SharedVariable . SharingScope . Empty ) {
continue ;
}
// The override variable should be used if the name and the type matches.
var sourceVariable = sharedVariableOverrides [ i ] . Source ;
if ( sourceVariable . GetType ( ) ! = graphVariable . GetType ( ) | | sourceVariable . Name ! = graphVariable . Name ) {
continue ;
}
// If the scope is self then the graphVariable value should be updated instead of completely replaced.
if ( overrideVariable . Scope = = SharedVariable . SharingScope . Self ) {
graphVariable . SetValue ( overrideVariable . GetValue ( ) ) ;
return graphVariable ;
}
return overrideVariable ;
}
return graphVariable ;
}
/// <summary>
/// Populates the SharedVariable Mapping at runtime.
/// </summary>
/// <param name="graph">The graph that is being deserialized.</param>
/// <param name="canDeepCopy">Can the SharedVariables be deep copied?</param>
/// <returns>A reference to the map between the VariableAssignment and SharedVariable.</returns>
public static Dictionary < VariableAssignment , SharedVariable > PopulateSharedVariablesMapping ( IGraph graph , bool canDeepCopy )
2026-05-10 11:47:55 -04:00
{
return PopulateSharedVariablesMapping ( graph , graph . SharedVariables , canDeepCopy ) ;
}
/// <summary>
/// Populates the SharedVariable Mapping at runtime.
/// </summary>
/// <param name="graph">The graph that is being deserialized.</param>
/// <param name="graphSharedVariables">The SharedVariables that should be used for graph scope variables.</param>
/// <param name="canDeepCopy">Can the SharedVariables be deep copied?</param>
/// <returns>A reference to the map between the VariableAssignment and SharedVariable.</returns>
private static Dictionary < VariableAssignment , SharedVariable > PopulateSharedVariablesMapping ( IGraph graph , SharedVariable [ ] graphSharedVariables , bool canDeepCopy )
2025-11-25 08:19:33 -05:00
{
var variableByNameMap = new Dictionary < VariableAssignment , SharedVariable > ( ) ;
2026-05-10 11:47:55 -04:00
PopulateSharedVariablesMapping ( graph , graphSharedVariables , SharedVariable . SharingScope . Graph , canDeepCopy , ref variableByNameMap ) ;
2025-11-25 08:19:33 -05:00
if ( graph . Parent is GameObject parentGameObject ) {
var gameObjectSharedVariablesContainer = parentGameObject . GetComponent < GameObjectSharedVariables > ( ) ;
if ( gameObjectSharedVariablesContainer ! = null ) {
gameObjectSharedVariablesContainer . Deserialize ( false ) ;
PopulateSharedVariablesMapping ( graph , gameObjectSharedVariablesContainer . SharedVariables , SharedVariable . SharingScope . GameObject , canDeepCopy , ref variableByNameMap ) ;
}
}
var sceneSharedVariablesContainer = SceneSharedVariables . Instance ;
if ( sceneSharedVariablesContainer ! = null ) {
sceneSharedVariablesContainer . Deserialize ( false ) ;
PopulateSharedVariablesMapping ( graph , sceneSharedVariablesContainer . SharedVariables , SharedVariable . SharingScope . Scene , canDeepCopy , ref variableByNameMap ) ;
}
var projectSharedVariablesContainer = ProjectSharedVariables . Instance ;
if ( projectSharedVariablesContainer ! = null ) {
projectSharedVariablesContainer . Deserialize ( false ) ;
PopulateSharedVariablesMapping ( graph , projectSharedVariablesContainer . SharedVariables , SharedVariable . SharingScope . Project , canDeepCopy , ref variableByNameMap ) ;
}
return variableByNameMap ;
}
/// <summary>
/// Populates the name variables mapping with the specified SharedVariables.
/// </summary>
/// <param name="graph">The graph that is being deserialized.</param>
/// <param name="sharedVariables">The SharedVariables that should be populated.</param>
/// <param name="scope">The scope of SharedVariables.</param>
/// <param name="canDeepCopy">Can the SharedVariables be deep copied?</param>
/// <param name="variableByNameMap">A reference to the map between the VariableAssignment and SharedVariable.</param>
private static void PopulateSharedVariablesMapping ( IGraph graph , SharedVariable [ ] sharedVariables , SharedVariable . SharingScope scope , bool canDeepCopy , ref Dictionary < VariableAssignment , SharedVariable > variableByNameMap )
{
if ( sharedVariables = = null ) {
return ;
}
2026-01-03 18:19:39 -05:00
var deepCopy = canDeepCopy & & graph is Subtree & & scope = = SharedVariable . SharingScope . Graph ; // Deep copy variables so the instance is not bound to the subtree.
2025-11-25 08:19:33 -05:00
for ( int i = 0 ; i < sharedVariables . Length ; + + i ) {
if ( sharedVariables [ i ] = = null ) {
continue ;
}
if ( variableByNameMap . ContainsKey ( new VariableAssignment ( sharedVariables [ i ] . Name , scope ) ) ) {
#if UNITY_EDITOR
Debug . LogWarning ( "Warning: Multiple SharedVariables with the same name have been added. Please email support@opsive.com with the steps to reproduce this warning. Thank you." ) ;
#endif
continue ;
}
2026-01-03 18:19:39 -05:00
var val = new VariableAssignment ( sharedVariables [ i ] . Name , scope ) ;
variableByNameMap . Add ( val , deepCopy ? CopyUtility . DeepCopy ( sharedVariables [ i ] ) as SharedVariable : sharedVariables [ i ] ) ;
2025-11-25 08:19:33 -05:00
}
}
/// <summary>
/// Injects the subtree into the task list.
/// </summary>
private void InjectSubtrees ( )
{
2026-05-10 11:47:55 -04:00
if ( m_InjectedSubtreeReference = = null | | m_InjectedSubtreeReference . Count = = 0 ) {
2025-11-25 08:19:33 -05:00
return ;
}
// The behavior tree must generate a new ID when subtrees are injected.
m_RuntimeUniqueID = Guid . NewGuid ( ) . GetHashCode ( ) ;
var taskCount = 0 ;
var subtreeReferenceCount = 0 ;
var subtreeAssignments = new ResizableArray < SubtreeAssignment > ( ) ;
2026-05-10 11:47:55 -04:00
var eventAssignments = new ResizableArray < SubtreeAssignment > ( ) ;
var lastParentIndex = m_Tasks [ m_InjectedSubtreeReference [ 0 ] . NodeIndex ] . ParentIndex ;
2025-11-25 08:19:33 -05:00
var parentIndexOffset = 0 ;
2026-05-10 11:47:55 -04:00
for ( int i = 0 ; i < m_InjectedSubtreeReference . Count ; + + i ) {
var subtreeReference = m_Tasks [ m_InjectedSubtreeReference [ i ] . NodeIndex ] as ISubtreeReferenceNode ;
2025-11-25 08:19:33 -05:00
var subtrees = subtreeReference . Subtrees ;
if ( subtrees ! = null ) {
2026-05-10 11:47:55 -04:00
var indexOffset = ( ushort ) 0 ; // The index offset is relative to each individual ISubtreeReferenceNode task.
2025-11-25 08:19:33 -05:00
// The parent index will change based on the number of tasks that have been added.
2026-05-10 11:47:55 -04:00
var parentIndex = m_Tasks [ m_InjectedSubtreeReference [ i ] . NodeIndex ] . ParentIndex ;
2025-11-25 08:19:33 -05:00
if ( parentIndex ! = ushort . MaxValue & & ( parentIndex > lastParentIndex | | ( i > 0 & & lastParentIndex = = ushort . MaxValue ) ) ) {
parentIndexOffset = ( ushort ) ( taskCount - subtreeReferenceCount ) ;
lastParentIndex = parentIndex ;
2026-01-03 18:19:39 -05:00
} else if ( parentIndex < lastParentIndex ) {
parentIndexOffset = 0 ;
lastParentIndex = parentIndex ;
2025-11-25 08:19:33 -05:00
}
// Calculate the parent index offset based on previously injected subtrees
for ( int j = 0 ; j < subtrees . Length ; + + j ) {
if ( subtrees [ j ] = = null | | subtrees [ j ] . LogicNodes = = null | | subtrees [ j ] . EventNodes = = null ) {
continue ;
}
2026-05-10 11:47:55 -04:00
var eventNodes = subtrees [ j ] . EventNodes ;
if ( eventNodes = = null ) {
2025-11-25 08:19:33 -05:00
continue ;
}
2026-05-10 11:47:55 -04:00
for ( int k = 0 ; k < eventNodes . Length ; + + k ) {
var eventNode = eventNodes [ k ] ;
if ( eventNode = = null ) {
continue ;
}
if ( eventNode . ConnectedIndex = = ushort . MaxValue | | ! subtrees [ j ] . IsNodeEnabled ( false , k ) ) {
continue ;
}
var sourceIndex = eventNode . ConnectedIndex ;
var subtreeNodes = m_InjectedSubtreeReference [ i ] . TreeNodes [ j ] ;
if ( subtreeNodes = = null | | sourceIndex > = subtreeNodes . Length ) {
continue ;
}
var firstNode = subtreeNodes [ sourceIndex ] ;
var subtreeNodeCount = GetChildCount ( firstNode , subtreeNodes ) + 1 ; // firstNode should be included in addition to the children.
if ( eventNode . GetType ( ) = = typeof ( Start ) ) {
taskCount + = subtreeNodeCount ;
subtreeAssignments . Add ( new SubtreeAssignment ( )
{
EventNodeType = eventNode . GetType ( ) ,
EventNodeIndex = ( ushort ) k ,
SourceIndex = sourceIndex ,
ReferenceIndex = i ,
NodeIndex = m_InjectedSubtreeReference [ i ] . NodeIndex ,
SubtreeIndex = j ,
Subtree = subtrees [ j ] ,
NodeCount = ( ushort ) subtreeNodeCount ,
IndexOffset = indexOffset ,
ParentIndex = ( ushort ) ( parentIndex + parentIndexOffset ) ,
SiblingIndex = m_Tasks [ m_InjectedSubtreeReference [ i ] . NodeIndex ] . SiblingIndex ,
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
2026-05-10 11:47:55 -04:00
NodePropertiesPosition = m_LogicNodeProperties [ m_InjectedSubtreeReference [ i ] . NodeIndex ] . Position ,
Collapsed = m_LogicNodeProperties [ m_InjectedSubtreeReference [ i ] . NodeIndex ] . Collapsed
2025-11-25 08:19:33 -05:00
#endif
2026-05-10 11:47:55 -04:00
} ) ;
indexOffset + = ( ushort ) subtreeNodeCount ;
} else {
eventAssignments . Add ( new SubtreeAssignment ( )
{
EventNodeType = eventNode . GetType ( ) ,
EventNodeIndex = ( ushort ) k ,
SourceIndex = sourceIndex ,
ReferenceIndex = i ,
NodeIndex = m_InjectedSubtreeReference [ i ] . NodeIndex ,
SubtreeIndex = j ,
Subtree = subtrees [ j ] ,
NodeCount = ( ushort ) subtreeNodeCount ,
ParentIndex = ushort . MaxValue ,
SiblingIndex = ushort . MaxValue
} ) ;
}
}
2025-11-25 08:19:33 -05:00
}
// Update the parent index offset for the next subtree reference
if ( indexOffset > 0 ) { // Subtree References may not contain any valid subtrees.
subtreeReferenceCount + + ;
}
2026-05-10 11:47:55 -04:00
var subtreeNodesReferenceOrig = m_InjectedSubtreeReference [ i ] ;
2025-11-25 08:19:33 -05:00
subtreeNodesReferenceOrig . NodeCount = indexOffset ;
2026-05-10 11:47:55 -04:00
m_InjectedSubtreeReference [ i ] = subtreeNodesReferenceOrig ;
2025-11-25 08:19:33 -05:00
}
}
2026-05-10 11:47:55 -04:00
if ( taskCount > 0 ) {
var targetCount = m_Tasks . Length + taskCount - subtreeReferenceCount ;
var originalTaskCount = m_Tasks . Length ;
if ( m_Tasks . Length ! = targetCount ) {
Array . Resize ( ref m_Tasks , targetCount ) ;
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
2026-05-10 11:47:55 -04:00
Array . Resize ( ref m_LogicNodeProperties , targetCount ) ;
2025-11-25 08:19:33 -05:00
#endif
2026-05-10 11:47:55 -04:00
}
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
// Make space for all of the subtree tasks.
var addedTasks = 0 ;
for ( int i = 0 ; i < subtreeAssignments . Count ; + + i ) {
var subtreeIndex = ( ushort ) ( subtreeAssignments [ i ] . NodeIndex + addedTasks ) ;
var subtreeTaskCount = subtreeAssignments [ i ] . NodeCount - ( subtreeAssignments [ i ] . IndexOffset = = 0 ? 1 : 0 ) ;
if ( subtreeTaskCount > 0 ) { // subtreeTaskCount will be zero if a single task replaces the reference task.
for ( int j = originalTaskCount - 1 + addedTasks ; j > subtreeIndex ; - - j ) {
var node = m_Tasks [ j ] ;
node . Index + = ( ushort ) subtreeTaskCount ;
if ( node . ParentIndex > subtreeIndex & & node . ParentIndex ! = ushort . MaxValue ) {
node . ParentIndex + = ( ushort ) subtreeTaskCount ;
}
if ( node . SiblingIndex > subtreeIndex & & node . SiblingIndex ! = ushort . MaxValue ) {
node . SiblingIndex + = ( ushort ) subtreeTaskCount ;
}
m_Tasks [ j + subtreeTaskCount ] = node ;
m_Tasks [ j ] = null ;
#if UNITY_EDITOR
m_LogicNodeProperties [ j + subtreeTaskCount ] = m_LogicNodeProperties [ j ] ;
#endif
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
// The parents need to adjust their sibling index offsets for the newly added nodes. This should only be done with an index offset of 0
// as grouped subtrees have the same parents.
if ( subtreeAssignments [ i ] . IndexOffset = = 0 ) {
var parentIndex = m_Tasks [ subtreeIndex ] . ParentIndex ;
while ( parentIndex ! = ushort . MaxValue ) {
var parentNode = m_Tasks [ parentIndex ] ;
if ( parentNode . SiblingIndex ! = ushort . MaxValue ) {
parentNode . SiblingIndex + = ( ushort ) subtreeTaskCount ;
m_Tasks [ parentIndex ] = parentNode ;
}
parentIndex = parentNode . ParentIndex ;
}
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
subtreeAssignments [ i ] = new SubtreeAssignment {
EventNodeType = subtreeAssignments [ i ] . EventNodeType ,
EventNodeIndex = subtreeAssignments [ i ] . EventNodeIndex ,
SourceIndex = subtreeAssignments [ i ] . SourceIndex ,
ReferenceIndex = subtreeAssignments [ i ] . ReferenceIndex ,
NodeIndex = subtreeAssignments [ i ] . NodeIndex ,
SubtreeIndex = subtreeAssignments [ i ] . SubtreeIndex ,
Subtree = subtreeAssignments [ i ] . Subtree ,
NodeCount = subtreeAssignments [ i ] . NodeCount ,
IndexOffset = subtreeAssignments [ i ] . IndexOffset ,
ParentIndex = subtreeAssignments [ i ] . ParentIndex ,
SiblingIndex = subtreeAssignments [ i ] . SiblingIndex ,
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
2026-05-10 11:47:55 -04:00
NodePropertiesPosition = subtreeAssignments [ i ] . NodePropertiesPosition ,
Collapsed = subtreeAssignments [ i ] . Collapsed ,
2025-11-25 08:19:33 -05:00
#endif
2026-05-10 11:47:55 -04:00
} ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
// Tasks were added to the tree. Update the tree to the correct indicies.
var subtreeAssignment = subtreeAssignments [ i ] ;
subtreeAssignment . IndexOffset = ( ushort ) ( addedTasks + ( subtreeAssignments [ i ] . IndexOffset = = 0 ? 0 : 1 ) ) ;
subtreeAssignments [ i ] = subtreeAssignment ;
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
addedTasks + = subtreeTaskCount ;
}
// Populate the tasks with the subtree.
for ( int i = 0 ; i < subtreeAssignments . Count ; + + i ) {
var subtreeIndex = ( ushort ) ( subtreeAssignments [ i ] . NodeIndex + subtreeAssignments [ i ] . IndexOffset ) ;
var subtreeParentIndex = subtreeAssignments [ i ] . ParentIndex ;
var rootSiblingIndex = ushort . MaxValue ;
if ( i + 1 < subtreeAssignments . Count & & subtreeAssignments [ i + 1 ] . ReferenceIndex = = subtreeAssignments [ i ] . ReferenceIndex ) {
// Point to the first node of the next subtree.
rootSiblingIndex = ( ushort ) ( subtreeAssignments [ i + 1 ] . NodeIndex + subtreeAssignments [ i + 1 ] . IndexOffset ) ;
} else {
// Use the original SiblingIndex from the reference task.
rootSiblingIndex = subtreeAssignments [ i ] . SiblingIndex ! = ushort . MaxValue ? ( ushort ) ( subtreeIndex + subtreeAssignments [ i ] . NodeCount ) : ushort . MaxValue ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
var subtreeReference = m_InjectedSubtreeReference [ subtreeAssignments [ i ] . ReferenceIndex ] ;
var subtreeNodes = subtreeReference . TreeNodes [ subtreeAssignments [ i ] . SubtreeIndex ] ;
if ( subtreeNodes = = null | | subtreeAssignments [ i ] . SourceIndex > = subtreeNodes . Length ) {
continue ;
}
InjectSubtreeLogicNodes ( subtreeAssignments [ i ] , subtreeNodes , subtreeReference . Subtrees [ subtreeAssignments [ i ] . SubtreeIndex ] . Pooled , subtreeIndex , subtreeParentIndex ,
rootSiblingIndex , subtreeReference . GraphReference . Enabled , true , false ) ;
}
}
InjectSubtreeEventNodes ( eventAssignments ) ;
}
/// <summary>
/// Injects subtree logic nodes into the task list.
/// </summary>
/// <param name="assignment">The subtree assignment.</param>
/// <param name="subtreeNodes">The subtree nodes to inject.</param>
/// <param name="pooled">Is the subtree pooled?</param>
/// <param name="branchStartIndex">The index to start inserting nodes at.</param>
/// <param name="rootParentIndex">The parent index for the root node.</param>
/// <param name="rootSiblingIndex">The sibling index for the root node.</param>
/// <param name="subtreeReferenceEnabled">Is the subtree reference enabled?</param>
/// <param name="applyPositionOffset">Should node properties be offset to match the reference?</param>
/// <param name="buildNodeMap">Should a node map be built for event node remapping?</param>
/// <returns>A map of original nodes to copied nodes (can be null).</returns>
private Dictionary < object , object > InjectSubtreeLogicNodes ( SubtreeAssignment assignment , ITreeLogicNode [ ] subtreeNodes , bool pooled , ushort branchStartIndex ,
ushort rootParentIndex , ushort rootSiblingIndex , bool subtreeReferenceEnabled , bool applyPositionOffset , bool buildNodeMap )
{
if ( subtreeNodes = = null | | assignment . SourceIndex > = subtreeNodes . Length ) {
return null ;
}
var indexOffset = ( int ) branchStartIndex - assignment . SourceIndex ;
Dictionary < object , object > nodeMap = null ;
if ( ! pooled & & buildNodeMap ) {
nodeMap = new Dictionary < object , object > ( ) ;
}
#if UNITY_EDITOR
var positionOffset = Vector2 . zero ;
#endif
for ( int j = 0 ; j < assignment . NodeCount ; + + j ) {
var node = subtreeNodes [ assignment . SourceIndex + j ] ;
// The node needs to be copied if it isn't pooled to prevent the same node from being used in multiple trees.
if ( ! pooled ) {
node = CopySubtreeLogicNode ( node , nodeMap ) ;
}
node . Index = ( ushort ) ( branchStartIndex + j ) ;
node . RuntimeIndex = ushort . MaxValue ;
if ( j = = 0 ) {
node . ParentIndex = rootParentIndex ;
node . SiblingIndex = rootSiblingIndex ;
} else {
// Adjust the subsequent subtree tasks by the location of the insertion.
if ( node . ParentIndex ! = ushort . MaxValue ) {
node . ParentIndex = ( ushort ) ( node . ParentIndex + indexOffset ) ;
}
if ( node . SiblingIndex ! = ushort . MaxValue ) {
node . SiblingIndex = ( ushort ) ( node . SiblingIndex + indexOffset ) ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
}
// If the parent reference task is disabled then all subtree nodes should be disabled.
if ( ! subtreeReferenceEnabled ) {
node . Enabled = false ;
}
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
m_Tasks [ branchStartIndex + j ] = node ;
#if UNITY_EDITOR
if ( m_LogicNodeProperties ! = null & & assignment . Subtree . LogicNodeProperties ! = null & & assignment . SourceIndex + j < assignment . Subtree . LogicNodeProperties . Length ) {
var nodeProperties = CopyUtility . DeepCopy ( assignment . Subtree . LogicNodeProperties [ assignment . SourceIndex + j ] ) as LogicNodeProperties ;
nodeProperties . GuidString = Guid . NewGuid ( ) . ToString ( ) ;
if ( applyPositionOffset ) {
if ( j = = 0 ) {
// Keep the tasks in the same relative position as the subtree reference.
positionOffset = assignment . NodePropertiesPosition - assignment . Subtree . LogicNodeProperties [ assignment . SourceIndex + j ] . Position ;
} else {
// Apply a small offset for stacked subtrees so they are not directly overlapping.
positionOffset + = new Vector2 ( 2 , 2 ) ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
nodeProperties . Position + = positionOffset ;
nodeProperties . Collapsed = assignment . Collapsed ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
m_LogicNodeProperties [ branchStartIndex + j ] = nodeProperties ;
}
#endif
}
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
return nodeMap ;
}
/// <summary>
/// Copies the subtree logic node while updating variable fields and any node maps.
/// </summary>
/// <param name="node">The node to copy.</param>
/// <param name="nodeMap">The node map to update (can be null).</param>
/// <returns>The copied node.</returns>
private ITreeLogicNode CopySubtreeLogicNode ( ITreeLogicNode node , Dictionary < object , object > nodeMap )
{
var copiedNode = CopyUtility . DeepCopy ( node ) as ITreeLogicNode ;
if ( ( m_VariableFields ! = null & & m_VariableFields . Count > 0 ) | | nodeMap ! = null ) {
// Replace the old node reference with the updated reference.
var localMap = new Dictionary < object , object > ( ) ;
localMap . Add ( node , copiedNode ) ;
if ( node is IContainerNode containerNode ) {
if ( containerNode . Nodes ! = null ) {
var copiedContainerNode = copiedNode as IContainerNode ;
for ( int k = 0 ; k < containerNode . Nodes . Length ; + + k ) {
localMap . Add ( containerNode . Nodes [ k ] , copiedContainerNode . Nodes [ k ] ) ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
}
}
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
if ( nodeMap ! = null ) {
foreach ( var pair in localMap ) {
nodeMap . Add ( pair . Key , pair . Value ) ;
}
}
if ( m_VariableFields ! = null & & m_VariableFields . Count > 0 ) {
for ( int k = 0 ; k < m_VariableFields . Count ; + + k ) {
if ( localMap . TryGetValue ( m_VariableFields [ k ] . Task , out var copiedTask ) ) {
var variableField = m_VariableFields [ k ] ;
variableField . Task = copiedTask ;
m_VariableFields [ k ] = variableField ;
2025-11-25 08:19:33 -05:00
}
}
}
2026-05-10 11:47:55 -04:00
}
copiedNode . Enabled = node . Enabled ;
return copiedNode ;
}
/// <summary>
/// Injects subtree event nodes and their connected logic nodes at the end of the task list.
/// </summary>
/// <param name="eventAssignments">The event node assignments to inject.</param>
private void InjectSubtreeEventNodes ( ResizableArray < SubtreeAssignment > eventAssignments )
{
if ( eventAssignments = = null | | eventAssignments . Count = = 0 ) {
return ;
}
var injectAssignments = new ResizableArray < SubtreeAssignment > ( ) ;
var singleInstanceEventTypes = new HashSet < Type > ( ) ;
if ( m_EventTasks ! = null & & m_EventTasks . Length > 0 ) {
for ( int i = 0 ; i < m_EventTasks . Length ; + + i ) {
var eventTask = m_EventTasks [ i ] ;
if ( eventTask = = null ) {
continue ;
}
var eventType = eventTask . GetType ( ) ;
if ( AllowsMultipleEventNodeTypes ( eventType ) ) {
continue ;
}
singleInstanceEventTypes . Add ( eventType ) ;
}
}
for ( int i = 0 ; i < eventAssignments . Count ; + + i ) {
var assignment = eventAssignments [ i ] ;
var eventType = assignment . EventNodeType ;
if ( eventType = = null ) {
continue ;
}
if ( ! AllowsMultipleEventNodeTypes ( eventType ) ) {
if ( singleInstanceEventTypes . Contains ( eventType ) ) {
continue ;
}
singleInstanceEventTypes . Add ( eventType ) ;
}
injectAssignments . Add ( assignment ) ;
}
if ( injectAssignments . Count = = 0 ) {
return ;
}
2025-11-25 08:19:33 -05:00
2026-05-10 11:47:55 -04:00
if ( m_Tasks = = null ) {
m_Tasks = new ITreeLogicNode [ 0 ] ;
}
var originalTaskCount = m_Tasks . Length ;
var addedTaskCount = 0 ;
for ( int i = 0 ; i < injectAssignments . Count ; + + i ) {
addedTaskCount + = injectAssignments [ i ] . NodeCount ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
if ( addedTaskCount > 0 ) {
Array . Resize ( ref m_Tasks , originalTaskCount + addedTaskCount ) ;
2025-11-25 08:19:33 -05:00
#if UNITY_EDITOR
2026-05-10 11:47:55 -04:00
Array . Resize ( ref m_LogicNodeProperties , originalTaskCount + addedTaskCount ) ;
2025-11-25 08:19:33 -05:00
#endif
2026-05-10 11:47:55 -04:00
}
var addedTasks = 0 ;
for ( int i = 0 ; i < injectAssignments . Count ; + + i ) {
var assignment = injectAssignments [ i ] ;
var subtreeReference = m_InjectedSubtreeReference [ assignment . ReferenceIndex ] ;
var subtree = assignment . Subtree ;
var subtreeNodes = subtreeReference . TreeNodes [ assignment . SubtreeIndex ] ;
if ( subtreeNodes = = null | | assignment . SourceIndex > = subtreeNodes . Length ) {
continue ;
}
var pooled = subtreeReference . Subtrees [ assignment . SubtreeIndex ] . Pooled ;
var subtreeReferenceEnabled = subtreeReference . GraphReference . Enabled ;
var branchStartIndex = ( ushort ) ( originalTaskCount + addedTasks ) ;
var nodeMap = InjectSubtreeLogicNodes ( assignment , subtreeNodes , pooled , branchStartIndex , ushort . MaxValue ,
ushort . MaxValue , subtreeReferenceEnabled , false , true ) ;
var eventNodes = subtree . EventNodes ;
if ( eventNodes = = null | | assignment . EventNodeIndex > = eventNodes . Length ) {
addedTasks + = assignment . NodeCount ;
continue ;
}
var eventNode = eventNodes [ assignment . EventNodeIndex ] ;
if ( eventNode = = null ) {
addedTasks + = assignment . NodeCount ;
continue ;
}
var injectedEventNode = eventNode ;
if ( ! pooled ) {
injectedEventNode = CopyUtility . DeepCopy ( eventNode ) as IEventNode ;
if ( m_VariableFields ! = null & & m_VariableFields . Count > 0 ) {
for ( int k = 0 ; k < m_VariableFields . Count ; + + k ) {
if ( ReferenceEquals ( m_VariableFields [ k ] . Task , eventNode ) ) {
var variableField = m_VariableFields [ k ] ;
variableField . Task = injectedEventNode ;
m_VariableFields [ k ] = variableField ;
2026-01-03 18:19:39 -05:00
}
2026-05-10 11:47:55 -04:00
}
}
RemapEventNodeReferences ( injectedEventNode , nodeMap ) ;
}
injectedEventNode . ConnectedIndex = branchStartIndex ;
if ( ! subtreeReferenceEnabled ) {
injectedEventNode . Enabled = false ;
}
var eventIndex = ( ushort ) ( m_EventTasks ! = null ? m_EventTasks . Length : 0 ) ;
if ( m_EventTasks = = null ) {
m_EventTasks = new IEventNode [ 1 ] ;
} else {
Array . Resize ( ref m_EventTasks , m_EventTasks . Length + 1 ) ;
}
m_EventTasks [ eventIndex ] = injectedEventNode ;
injectedEventNode . Index = eventIndex ;
if ( m_InjectedSubtreeEventNodes = = null ) {
m_InjectedSubtreeEventNodes = new HashSet < IEventNode > ( ) ;
}
m_InjectedSubtreeEventNodes . Add ( injectedEventNode ) ;
#if UNITY_EDITOR
if ( m_EventNodeProperties ! = null ) {
Array . Resize ( ref m_EventNodeProperties , m_EventTasks . Length ) ;
if ( subtree . EventNodeProperties ! = null & & assignment . EventNodeIndex < subtree . EventNodeProperties . Length ) {
var nodeProperties = CopyUtility . DeepCopy ( subtree . EventNodeProperties [ assignment . EventNodeIndex ] ) as NodeProperties ;
nodeProperties . GuidString = Guid . NewGuid ( ) . ToString ( ) ;
m_EventNodeProperties [ eventIndex ] = nodeProperties ;
}
}
#endif
addedTasks + = assignment . NodeCount ;
}
}
/// <summary>
/// Returns true if the event node type allows multiple nodes of that type.
/// </summary>
/// <param name="eventNodeType">The event node type.</param>
/// <returns>True if the type allows multiple nodes.</returns>
private bool AllowsMultipleEventNodeTypes ( Type eventNodeType )
{
return eventNodeType ! = null & & Attribute . IsDefined ( eventNodeType , typeof ( AllowMultipleTypes ) , true ) ;
}
#if UNITY_EDITOR
/// <summary>
/// Updates the cached set of injected graph references.
/// </summary>
private void UpdateInjectedGraphReferences ( )
{
if ( m_InjectedSubtreeReference = = null | | m_InjectedSubtreeReference . Count = = 0 ) {
m_InjectedGraphReferences = null ;
return ;
}
if ( m_InjectedGraphReferences = = null ) {
m_InjectedGraphReferences = new ResizableArray < InjectedGraphReference > ( m_InjectedSubtreeReference . Count ) ;
} else {
m_InjectedGraphReferences . Clear ( ) ;
}
PopulateInjectedSubtreeReferences ( ref m_InjectedGraphReferences ) ;
}
/// <summary>
/// Retrieves all of the injected subtree references and stores the result in m_InjectedSubtreeReferences.
/// </summary>
/// <param name="injectedGraphReferences">A reference to the array that the injected graph references should be added to.</param>
private void PopulateInjectedSubtreeReferences ( ref ResizableArray < InjectedGraphReference > injectedGraphReferences )
{
if ( m_InjectedSubtreeReference = = null | | injectedGraphReferences = = null ) {
return ;
}
var nodeCount = 0 ;
for ( int i = 0 ; i < m_InjectedSubtreeReference . Count ; + + i ) {
var injectedSubtreeReference = m_InjectedSubtreeReference [ i ] ;
// Emit a single top-level reference entry per SubtreeReference node. Multiple selected subtrees
// are represented by Nodes[subgraphIndex] instead of additional reference nodes.
var injectedReferenceRuntimeNodeIndex = ( ushort ) ( injectedSubtreeReference . NodeIndex + nodeCount ) ;
LogicNodeProperties graphReferenceNodeProperties = null ;
if ( m_LogicNodeProperties ! = null & & injectedReferenceRuntimeNodeIndex < m_LogicNodeProperties . Length ) {
graphReferenceNodeProperties = m_LogicNodeProperties [ injectedReferenceRuntimeNodeIndex ] ;
}
IGraph injectedReferenceGraph = null ;
if ( injectedSubtreeReference . Graphs ! = null ) {
for ( int j = 0 ; j < injectedSubtreeReference . Graphs . Length ; + + j ) {
if ( injectedSubtreeReference . Graphs [ j ] ! = null ) {
injectedReferenceGraph = injectedSubtreeReference . Graphs [ j ] ;
break ;
}
}
}
var injectedReference = new InjectedGraphReference ( ) {
Graph = injectedReferenceGraph ,
NodeIndex = injectedSubtreeReference . NodeIndex ,
RuntimeNodeIndex = injectedReferenceRuntimeNodeIndex ,
NodeCount = injectedSubtreeReference . NodeCount ,
GraphReference = injectedSubtreeReference . GraphReference ,
Nodes = injectedSubtreeReference . Nodes ,
GraphReferenceNodeProperties = graphReferenceNodeProperties
} ;
injectedGraphReferences . Add ( injectedReference ) ;
// Track how far into the injected runtime span each selected subtree starts.
var subtreeRuntimeOffset = ( ushort ) 0 ;
var nodeDelta = injectedSubtreeReference . NodeCount > 0 ? injectedSubtreeReference . NodeCount - 1 : 0 ; // The SubtreeRefrence node itself doesn't count.
if ( injectedSubtreeReference . Graphs = = null | | injectedSubtreeReference . Nodes = = null ) {
nodeCount + = nodeDelta ;
continue ;
}
for ( int j = 0 ; j < injectedSubtreeReference . Graphs . Length ; + + j ) {
var graph = injectedSubtreeReference . Graphs [ j ] ;
ushort subtreeNodeCount = 0 ;
if ( j < injectedSubtreeReference . Nodes . Length & & injectedSubtreeReference . Nodes [ j ] ! = null ) {
subtreeNodeCount = ( ushort ) injectedSubtreeReference . Nodes [ j ] . Length ;
}
if ( graph ! = null & & graph . InjectedGraphReferences ! = null ) {
for ( int k = 0 ; k < graph . InjectedGraphReferences . Length ; + + k ) {
var graphInjectedGraphReferences = graph . InjectedGraphReferences [ k ] ;
// Nested references already have a runtime-relative index within the selected subtree.
var nestedReferenceRuntimeNodeIndex = graphInjectedGraphReferences . RuntimeNodeIndex ! = ushort . MaxValue ? graphInjectedGraphReferences . RuntimeNodeIndex : graphInjectedGraphReferences . NodeIndex ;
if ( graph . LogicNodeProperties = = null | | nestedReferenceRuntimeNodeIndex > = graph . LogicNodeProperties . Length ) {
continue ;
2026-01-03 18:19:39 -05:00
}
2026-05-10 11:47:55 -04:00
var subgraphReferenceNodeProperties = graph . LogicNodeProperties [ nestedReferenceRuntimeNodeIndex ] ;
var subgraphInjectedReference = new InjectedGraphReference ( ) {
Graph = graphInjectedGraphReferences . Graph ,
NodeIndex = graphInjectedGraphReferences . NodeIndex ,
// Compose runtime index in the root graph:
// root reference start + selected subtree start + nested reference offset.
RuntimeNodeIndex = ( ushort ) ( injectedReferenceRuntimeNodeIndex + subtreeRuntimeOffset + nestedReferenceRuntimeNodeIndex ) ,
NodeCount = graphInjectedGraphReferences . NodeCount ,
GraphReference = graphInjectedGraphReferences . GraphReference ,
Nodes = graphInjectedGraphReferences . Nodes ,
GraphReferenceNodeProperties = subgraphReferenceNodeProperties
} ;
injectedGraphReferences . Add ( subgraphInjectedReference ) ;
2026-01-03 18:19:39 -05:00
}
}
2026-05-10 11:47:55 -04:00
subtreeRuntimeOffset + = subtreeNodeCount ;
}
nodeCount + = nodeDelta ; // The SubtreeRefrence node itself doesn't count.
}
}
#endif
/// <summary>
/// Remaps any ILogicNode references on the event node to the copied nodes.
/// </summary>
/// <param name="eventNode">The event node to update.</param>
/// <param name="nodeMap">A map of original nodes to copied nodes.</param>
private void RemapEventNodeReferences ( IEventNode eventNode , Dictionary < object , object > nodeMap )
{
if ( eventNode = = null | | nodeMap = = null | | nodeMap . Count = = 0 ) {
return ;
}
var fields = eventNode . GetType ( ) . GetFields ( BindingFlags . Public | BindingFlags . NonPublic | BindingFlags . Instance ) ;
for ( int i = 0 ; i < fields . Length ; + + i ) {
var field = fields [ i ] ;
var fieldType = field . FieldType ;
if ( typeof ( ILogicNode ) . IsAssignableFrom ( fieldType ) ) {
var value = field . GetValue ( eventNode ) ;
if ( value ! = null & & nodeMap . TryGetValue ( value , out var mapped ) ) {
field . SetValue ( eventNode , mapped ) ;
}
} else if ( typeof ( IList ) . IsAssignableFrom ( fieldType ) ) {
var elementType = Serializer . GetElementType ( fieldType ) ;
if ( ! typeof ( ILogicNode ) . IsAssignableFrom ( elementType ) ) {
continue ;
}
var listValue = field . GetValue ( eventNode ) as IList ;
if ( listValue = = null ) {
continue ;
}
for ( int j = 0 ; j < listValue . Count ; + + j ) {
var listItem = listValue [ j ] ;
if ( listItem ! = null & & nodeMap . TryGetValue ( listItem , out var mapped ) ) {
listValue [ j ] = mapped ;
}
}
}
}
}
/// <summary>
/// When the behavior tree loads not all tasks will be deserialized instantly. TaskA may reference TaskB but TaskB hasn't
/// been deserialized yet. The TaskAssignment data structure will store all of the references that need to be restored after
/// the behavior tree has fully been deserialized.
/// </summary>
/// <param name="tasks">The tasks that belong to the graph.</param>
/// <param name="taskReferences">The tasks that should be referenced.</param>
public static void AssignTaskReferences ( ILogicNode [ ] tasks , ResizableArray < TaskAssignment > taskReferences )
{
if ( taskReferences = = null ) {
return ;
}
for ( int i = 0 ; i < taskReferences . Count ; + + i ) {
var taskReference = taskReferences [ i ] ;
var fieldType = taskReference . Field . FieldType ;
object value = null ;
// The field can be a list or single value.
if ( typeof ( IList ) . IsAssignableFrom ( fieldType ) ) {
var elements = ( IList ) taskReferences [ i ] . Value ;
if ( fieldType . IsArray ) {
// The field type is an array. Create a new array with all of the task instances.
var array = Array . CreateInstance ( Serializer . GetElementType ( fieldType ) , elements . Count ) as ILogicNode [ ] ;
for ( int j = 0 ; j < array . Length ; + + j ) {
var index = ( ushort ) elements [ j ] ;
if ( index < tasks . Length ) {
array [ j ] = tasks [ index ] ;
}
2026-02-13 09:22:11 -05:00
}
2026-05-10 11:47:55 -04:00
value = array ;
2025-11-25 08:19:33 -05:00
} else {
2026-05-10 11:47:55 -04:00
// The field type is a list. Create a new list with all of the task instances.
IList taskList ;
if ( fieldType . IsGenericType ) {
taskList = Activator . CreateInstance ( typeof ( List < > ) . MakeGenericType ( Serializer . GetElementType ( fieldType ) ) ) as IList ;
} else {
taskList = Activator . CreateInstance ( fieldType ) as IList ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
for ( int j = 0 ; j < elements . Count ; + + j ) {
var index = ( ushort ) elements [ j ] ;
if ( index < tasks . Length ) {
taskList . Add ( tasks [ index ] ) ;
}
}
value = taskList ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
} else { // Single ILogicNode value.
var index = ( ushort ) taskReference . Value ;
if ( index < tasks . Length ) {
value = tasks [ index ] ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
}
if ( value ! = null ) {
taskReference . Field . SetValue ( taskReference . Target , value ) ;
2025-11-25 08:19:33 -05:00
}
}
}
/// <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 ITreeLogicNode GetNode ( Type type )
{
if ( m_Tasks = = null ) {
return null ;
}
for ( int i = 0 ; i < m_Tasks . Length ; + + i ) {
if ( m_Tasks [ i ] . GetType ( ) = = type ) {
return m_Tasks [ i ] ;
}
}
return null ;
}
/// <summary>
/// Returns the event node 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 )
{
if ( m_EventTasks = = null ) {
return ( null , ushort . MaxValue ) ;
}
for ( ushort i = 0 ; i < m_EventTasks . Length ; + + i ) {
if ( m_EventTasks [ i ] . GetType ( ) = = type ) {
return ( m_EventTasks [ i ] , i ) ;
}
}
return ( null , ushort . MaxValue ) ;
}
/// <summary>
/// Returns the total number of children belonging to the specified node.
/// </summary>
/// <param name="node">The node to retrieve the child count of.</param>
/// <param name="nodes">All of the nodes that belong to the graph.</param>
/// <returns>The total number of children belonging to the specified node.</returns>
public int GetChildCount ( ITreeLogicNode node , ITreeLogicNode [ ] nodes )
{
if ( node . SiblingIndex ! = ushort . MaxValue ) {
return node . SiblingIndex - node . Index - 1 ;
}
if ( node . Index + 1 = = nodes . Length ) {
return 0 ;
}
var child = nodes [ node . Index + 1 ] ;
if ( child . ParentIndex ! = node . Index ) {
return 0 ;
}
// Determine the child count based off of the sibling index.
while ( child . SiblingIndex ! = ushort . MaxValue ) {
child = nodes [ child . SiblingIndex ] ;
}
return child . Index - node . Index + GetChildCount ( child , nodes ) ;
}
/// <summary>
2026-05-10 11:47:55 -04:00
/// Reevaluates the ISubtreeReferenceNodes by calling the EvaluateSubgraphs method.
2025-11-25 08:19:33 -05:00
/// </summary>
/// <param name="graphComponent">The component that the graph is being deserialized from.</param>
/// <param name="graph">The graph that is being reevaluated.</param>
/// <param name="onBeforeReevaluationSwap">Action that should be done before the tasks are swapped.</param>
/// <returns>True if the subtree was reevaluated.</returns>
public bool ReevaluateSubtreeReferences ( IGraphComponent graphComponent , IGraph graph , Action onBeforeReevaluationSwap )
{
// The tree must contain tasks.
if ( ! Application . isPlaying | | m_Tasks = = null | | m_Tasks . Length = = 0 ) {
return false ;
}
// Subtree references must exist.
2026-05-10 11:47:55 -04:00
if ( m_InjectedSubtreeReference = = null | | m_InjectedSubtreeReference . Count = = 0 ) {
2025-11-25 08:19:33 -05:00
return false ;
}
if ( onBeforeReevaluationSwap ! = null ) {
onBeforeReevaluationSwap ( ) ;
}
2026-05-10 11:47:55 -04:00
RemoveInjectedSubtreeEventNodes ( ) ;
var baseEventTaskCount = m_EventTasks ! = null ? m_EventTasks . Length : 0 ;
2025-11-25 08:19:33 -05:00
// Find the new reevaluated nodes.
2026-05-10 11:47:55 -04:00
for ( int i = m_InjectedSubtreeReference . Count - 1 ; i > = 0 ; - - i ) {
var subtreeNodesReference = m_InjectedSubtreeReference [ i ] ;
var subtreeReference = m_InjectedSubtreeReference [ i ] . GraphReference as ISubtreeReferenceNode ;
subtreeReference . EvaluateSubgraphs ( graphComponent ) ;
2025-11-25 08:19:33 -05:00
var reevaluatedSubtrees = subtreeReference . Subtrees ;
if ( reevaluatedSubtrees = = null ) {
continue ;
}
// The parent must be able to accept the number of subtrees that there are.
2026-05-10 11:47:55 -04:00
var parentIndex = m_Tasks [ m_InjectedSubtreeReference [ i ] . NodeIndex ] . ParentIndex ;
2025-11-25 08:19:33 -05:00
IParentNode parentNode = null ;
if ( parentIndex ! = ushort . MaxValue ) {
parentNode = m_Tasks [ parentIndex ] as IParentNode ;
}
if ( ( parentNode = = null & & reevaluatedSubtrees . Length > 1 ) | | ( parentNode ! = null & & reevaluatedSubtrees . Length > parentNode . MaxChildCount ) ) {
Debug . LogError ( $"Error: the reevaluated graph contains multiple subtrees as the starting task or as a child of a parent task which cannot contain so many children (such as a decorator)." ) ;
continue ;
}
2026-05-10 11:47:55 -04:00
var reevaluatedNodes = new ITreeLogicNode [ reevaluatedSubtrees . Length ] [ ] ;
2025-11-25 08:19:33 -05:00
var errorState = false ;
for ( int j = 0 ; j < reevaluatedSubtrees . Length ; + + j ) {
if ( reevaluatedSubtrees [ j ] = = null ) {
continue ;
}
if ( ! reevaluatedSubtrees [ j ] . Deserialize ( graphComponent , true , true , true , true , subtreeReference . SharedVariableOverrides ) ) {
errorState = true ;
break ;
} ;
// Keep a reference to the deserialized nodes. This will ensure they are unique and do not get overwritten.
reevaluatedNodes [ j ] = reevaluatedSubtrees [ j ] . TreeLogicNodes ;
}
if ( errorState ) {
continue ;
}
// The subtree index will be offsetted from the original index value if there are multiple subtree references.
var nodeOffset = 0 ;
for ( int j = i - 1 ; j > = 0 ; - - j ) {
2026-05-10 11:47:55 -04:00
nodeOffset + = m_InjectedSubtreeReference [ j ] . NodeCount > 0 ? m_InjectedSubtreeReference [ j ] . NodeCount - 1 : 0 ;
2025-11-25 08:19:33 -05:00
}
// All of the reevaluated nodes have been determined. Remove the old subtree nodes.
2026-05-10 11:47:55 -04:00
var nodeCount = m_InjectedSubtreeReference [ i ] . NodeCount ;
2025-11-25 08:19:33 -05:00
// Replace the first node with the subtree reference, and remove the rest of the added nodes.
2026-05-10 11:47:55 -04:00
m_Tasks [ m_InjectedSubtreeReference [ i ] . NodeIndex + nodeOffset ] = m_InjectedSubtreeReference [ i ] . GraphReference as ITreeLogicNode ;
for ( int j = m_InjectedSubtreeReference [ i ] . NodeIndex + nodeOffset + 1 ; j < m_Tasks . Length - nodeCount + 1 ; + + j ) {
2025-11-25 08:19:33 -05:00
m_Tasks [ j ] = m_Tasks [ j + nodeCount - 1 ] ;
m_Tasks [ j ] . Index = ( ushort ) j ;
2026-05-10 11:47:55 -04:00
if ( m_Tasks [ j ] . ParentIndex ! = ushort . MaxValue & & m_Tasks [ j ] . ParentIndex > m_InjectedSubtreeReference [ i ] . NodeIndex + nodeOffset ) {
2025-11-25 08:19:33 -05:00
m_Tasks [ j ] . ParentIndex - = ( ushort ) ( nodeCount - 1 ) ;
}
2026-05-10 11:47:55 -04:00
if ( m_Tasks [ j ] . SiblingIndex ! = ushort . MaxValue & & m_Tasks [ j ] . SiblingIndex > m_InjectedSubtreeReference [ i ] . NodeIndex + nodeOffset ) {
2025-11-25 08:19:33 -05:00
m_Tasks [ j ] . SiblingIndex - = ( ushort ) ( nodeCount - 1 ) ;
}
#if UNITY_EDITOR
m_LogicNodeProperties [ j ] = m_LogicNodeProperties [ j + nodeCount - 1 ] ;
#endif
}
2026-01-03 18:19:39 -05:00
// Restore the original sibling index value for parent nodes.
2026-05-10 11:47:55 -04:00
parentIndex = m_Tasks [ m_InjectedSubtreeReference [ i ] . NodeIndex + nodeOffset ] . ParentIndex ;
2026-01-03 18:19:39 -05:00
while ( parentIndex ! = ushort . MaxValue ) {
var parentTask = m_Tasks [ parentIndex ] ;
if ( parentTask . SiblingIndex ! = ushort . MaxValue ) {
parentTask . SiblingIndex - = ( ushort ) ( nodeCount - 1 ) ;
m_Tasks [ parentIndex ] = parentTask ;
}
parentIndex = parentTask . ParentIndex ;
}
2025-11-25 08:19:33 -05:00
// Restore the original ConnectedIndex value.
if ( m_EventTasks ! = null ) {
for ( int j = 0 ; j < m_EventTasks . Length ; + + j ) {
2026-05-10 11:47:55 -04:00
if ( m_EventTasks [ j ] . ConnectedIndex > m_InjectedSubtreeReference [ i ] . NodeIndex ) {
2025-11-25 08:19:33 -05:00
m_EventTasks [ j ] . ConnectedIndex - = ( ushort ) ( nodeCount - 1 ) ;
}
}
}
Array . Resize ( ref m_Tasks , m_Tasks . Length - nodeCount + 1 ) ;
#if UNITY_EDITOR
Array . Resize ( ref m_LogicNodeProperties , m_LogicNodeProperties . Length - nodeCount + 1 ) ;
#endif
// Replace the old nodes with the new nodes.
subtreeNodesReference . Nodes = reevaluatedNodes ;
2026-05-10 11:47:55 -04:00
m_InjectedSubtreeReference [ i ] = subtreeNodesReference ;
2025-11-25 08:19:33 -05:00
}
// The tasks array has been restored to the original set of nodes with the ISubtreeReference. Inject the new nodes.
InjectSubtrees ( ) ;
2026-05-10 11:47:55 -04:00
// Modify the ConnectedIndex to match the injection for the base event tasks.
if ( m_EventTasks ! = null & & m_InjectedSubtreeReference ! = null & & baseEventTaskCount > 0 ) {
for ( int i = 0 ; i < baseEventTaskCount ; + + i ) {
2025-11-25 08:19:33 -05:00
var offset = 0 ;
2026-05-10 11:47:55 -04:00
for ( int j = 0 ; j < m_InjectedSubtreeReference . Count ; + + j ) {
if ( m_InjectedSubtreeReference [ j ] . NodeIndex > = m_EventTasks [ i ] . ConnectedIndex ) {
2025-11-25 08:19:33 -05:00
break ;
}
2026-05-10 11:47:55 -04:00
offset + = m_InjectedSubtreeReference [ j ] . NodeCount > 0 ? m_InjectedSubtreeReference [ j ] . NodeCount - 1 : 0 ;
2025-11-25 08:19:33 -05:00
}
if ( offset ! = 0 ) {
m_EventTasks [ i ] . ConnectedIndex + = ( ushort ) offset ;
}
}
}
2026-05-10 11:47:55 -04:00
#if UNITY_EDITOR
UpdateInjectedGraphReferences ( ) ;
#endif
2025-11-25 08:19:33 -05:00
return true ;
}
2026-05-10 11:47:55 -04:00
/// <summary>
/// Removes any injected subtree event nodes and their appended logic nodes.
/// </summary>
private void RemoveInjectedSubtreeEventNodes ( )
{
if ( m_EventTasks = = null | | m_EventTasks . Length = = 0 | | m_InjectedSubtreeEventNodes = = null | | m_InjectedSubtreeEventNodes . Count = = 0 ) {
return ;
}
var originalEventTasks = m_EventTasks ;
#if UNITY_EDITOR
var originalEventNodeProperties = m_EventNodeProperties ;
#endif
var totalBranchCount = 0 ;
var injectedEventTaskCount = 0 ;
for ( int i = 0 ; i < originalEventTasks . Length ; + + i ) {
var eventTask = originalEventTasks [ i ] ;
if ( eventTask = = null | | ! m_InjectedSubtreeEventNodes . Contains ( eventTask ) ) {
continue ;
}
injectedEventTaskCount + + ;
if ( eventTask . ConnectedIndex = = ushort . MaxValue | | eventTask . ConnectedIndex > = m_Tasks . Length ) {
continue ;
}
totalBranchCount + = GetChildCount ( m_Tasks [ eventTask . ConnectedIndex ] , m_Tasks ) + 1 ;
}
if ( injectedEventTaskCount = = 0 ) {
m_InjectedSubtreeEventNodes . Clear ( ) ;
return ;
}
if ( totalBranchCount > 0 & & totalBranchCount < = m_Tasks . Length ) {
Array . Resize ( ref m_Tasks , m_Tasks . Length - totalBranchCount ) ;
#if UNITY_EDITOR
Array . Resize ( ref m_LogicNodeProperties , m_LogicNodeProperties . Length - totalBranchCount ) ;
#endif
}
var retainedEventTaskCount = originalEventTasks . Length - injectedEventTaskCount ;
var retainedEventTasks = new IEventNode [ retainedEventTaskCount ] ;
var retainedIndex = 0 ;
for ( int i = 0 ; i < originalEventTasks . Length ; + + i ) {
var eventTask = originalEventTasks [ i ] ;
if ( eventTask ! = null & & m_InjectedSubtreeEventNodes . Contains ( eventTask ) ) {
continue ;
}
if ( eventTask ! = null ) {
eventTask . Index = ( ushort ) retainedIndex ;
}
retainedEventTasks [ retainedIndex ] = eventTask ;
retainedIndex + + ;
}
m_EventTasks = retainedEventTasks ;
#if UNITY_EDITOR
if ( originalEventNodeProperties ! = null ) {
var retainedEventNodeProperties = new NodeProperties [ retainedEventTaskCount ] ;
retainedIndex = 0 ;
for ( int i = 0 ; i < originalEventTasks . Length & & i < originalEventNodeProperties . Length ; + + i ) {
var eventTask = originalEventTasks [ i ] ;
if ( eventTask ! = null & & m_InjectedSubtreeEventNodes . Contains ( eventTask ) ) {
continue ;
}
retainedEventNodeProperties [ retainedIndex ] = originalEventNodeProperties [ i ] ;
retainedIndex + + ;
}
m_EventNodeProperties = retainedEventNodeProperties ;
}
#endif
m_InjectedSubtreeEventNodes . Clear ( ) ;
}
2025-11-25 08:19:33 -05:00
/// <summary>
/// Returns the SharedVariable with the specified name.
/// </summary>
/// <param name="graph">The graph that the data belongs to.</typeparam>
/// <param name="name">The name of the SharedVariable that should be retrieved.</typeparam>
/// <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 ( IGraph graph , PropertyName name , SharedVariable . SharingScope scope )
{
if ( m_VariableByNameMap = = null ) {
2026-01-03 18:19:39 -05:00
DeserializeSharedVariables ( graph , false , true , null ) ;
2025-11-25 08:19:33 -05:00
}
if ( m_VariableByNameMap ! = null & & m_VariableByNameMap . TryGetValue ( new VariableAssignment ( name , scope ) , out var variable ) ) {
return variable ;
}
return null ;
}
/// <summary>
/// Returns the SharedVariable of the specified type.
/// </summary>
/// <param name="graph">The graph that the data belongs to.</typeparam>
/// <param name="name">The name of the SharedVariable that should be retrieved.</typeparam>
/// <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 > ( IGraph graph , PropertyName name , SharedVariable . SharingScope scope )
{
return GetVariable ( graph , name , scope ) as SharedVariable < T > ;
}
/// <summary>
/// Sets the value of the SharedVariable.
/// </summary>
/// <typeparam name="T">The type of SharedVarible.</typeparam>
/// <param name="graph">The graph that the data belongs to.</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.</typeparam>
/// <returns>True if the value was set.</returns>
public bool SetVariableValue < T > ( IGraph graph , PropertyName name , T value , SharedVariable . SharingScope scope )
{
if ( m_VariableByNameMap = = null ) {
2026-01-03 18:19:39 -05:00
DeserializeSharedVariables ( graph , false , true , null ) ;
2025-11-25 08:19:33 -05:00
}
if ( m_VariableByNameMap = = null | | ! m_VariableByNameMap . TryGetValue ( new VariableAssignment ( name , scope ) , out var variable ) ) {
return false ;
}
( variable as SharedVariable < T > ) . Value = value ;
return true ;
}
/// <summary>
/// Overrides the SharedVariable binding. The name must match an exsting variable.
/// </summary>
/// <param name="graph">The graph that the data belongs to.</typeparam>
/// <param name="variable">The reference to the SharedVariable.</param>
internal void OverrideVariableBinding ( IGraph graph , SharedVariable variable )
{
if ( string . IsNullOrEmpty ( variable . Name ) ) {
return ;
}
2026-01-03 18:19:39 -05:00
DeserializeSharedVariables ( graph , false , true , null ) ;
2025-11-25 08:19:33 -05:00
var dirty = false ;
if ( m_SharedVariables ! = null ) {
for ( int i = 0 ; i < m_SharedVariables . Length ; + + i ) {
if ( m_SharedVariables [ i ] . Name = = variable . Name ) {
var variableType = variable . GetType ( ) ;
if ( variableType . IsGenericType & & variableType . GetGenericTypeDefinition ( ) . IsAssignableFrom ( typeof ( SharedVariableBinding < > ) ) ) {
m_SharedVariables [ i ] = variable . Clone ( ) as SharedVariable ;
dirty = true ;
}
break ;
}
}
}
if ( dirty ) {
2026-05-10 11:47:55 -04:00
// The graph may be a BehaviorTree pointing to local variables while this data belongs to a subtree.
// Rebuild graph-scope mappings from this data's variables to preserve the cloned binding instance.
m_VariableByNameMap = PopulateSharedVariablesMapping ( graph , m_SharedVariables , true ) ;
2025-11-25 08:19:33 -05:00
}
}
/// <summary>
/// Replaces the data with the specified BehaviorTreeData.
/// </summary>
/// <param name="graph">The graph that the current data belongs to.</param>
/// <param name="other">The data that should be replaced.</param>
/// <param name="originalSharedVariables">The SharedVariables of the current graph.</param>
2026-01-03 18:19:39 -05:00
internal void OverrideData ( IGraph graph , BehaviorTreeData other , SharedVariable [ ] originalSharedVariables , bool updateFields )
2025-11-25 08:19:33 -05:00
{
EventNodes = other . EventNodes ;
LogicNodes = other . LogicNodes ;
2026-05-10 11:47:55 -04:00
InjectedSubtreeReferences = other . InjectedSubtreeReferences ;
m_InjectedSubtreeEventNodes = other . m_InjectedSubtreeEventNodes ;
2026-01-03 18:19:39 -05:00
m_SharedVariables = other . SharedVariables ;
2025-11-25 08:19:33 -05:00
m_SharedVariableData = other . m_SharedVariableData ;
2026-01-03 18:19:39 -05:00
m_VariableByNameMap = PopulateSharedVariablesMapping ( graph , false ) ;
m_DisabledLogicNodes = other . DisabledLogicNodes ;
m_DisabledEventNodes = other . DisabledEventNodes ;
// The other tree may be pooled. Update the variable references to point to the local graph variables.
if ( updateFields & & other . m_VariableFields ! = null ) {
for ( int i = 0 ; i < other . m_VariableFields . Count ; + + i ) {
var variableField = other . m_VariableFields [ i ] ;
var localVariable = GetVariable ( graph , variableField . Name , SharedVariable . SharingScope . Graph ) ;
if ( localVariable ! = null ) {
variableField . Field . SetValue ( variableField . Task , localVariable ) ;
}
}
}
2025-11-25 08:19:33 -05:00
// The original tree variable value should override the other variable value.
if ( originalSharedVariables ! = null ) {
for ( int i = 0 ; i < originalSharedVariables . Length ; + + i ) {
2026-01-03 18:19:39 -05:00
OverrideVariableValue ( graph , originalSharedVariables [ i ] ) ;
2025-11-25 08:19:33 -05:00
}
}
#if UNITY_EDITOR
2026-01-03 18:19:39 -05:00
m_EventNodeProperties = other . EventNodeProperties ;
m_LogicNodeProperties = other . LogicNodeProperties ;
m_SharedVariableGroups = other . SharedVariableGroups ;
2025-11-25 08:19:33 -05:00
m_SharedVariableGroupsData = other . m_SharedVariableGroupsData ;
2026-01-03 18:19:39 -05:00
m_GroupProperties = other . GroupProperties ;
2026-05-10 11:47:55 -04:00
m_InjectedGraphReferences = other . m_InjectedGraphReferences ;
2025-11-25 08:19:33 -05:00
#endif
}
/// <summary>
/// Overrides the SharedVariable value. The name must match an exsting variable.
/// </summary>
/// <param name="graph">The graph that the data belongs to.</typeparam>
/// <param name="variable">The reference to the SharedVariable.</param>
/// <returns>True if the value was overridden.</returns>
2026-01-03 18:19:39 -05:00
private bool OverrideVariableValue ( IGraph graph , SharedVariable variable )
2025-11-25 08:19:33 -05:00
{
if ( string . IsNullOrEmpty ( variable . Name ) ) {
return false ;
}
var dirty = false ;
if ( m_SharedVariables ! = null ) {
for ( int i = 0 ; i < m_SharedVariables . Length ; + + i ) {
if ( m_SharedVariables [ i ] . Name = = variable . Name ) {
var variableType = variable . GetType ( ) ;
if ( m_SharedVariables [ i ] . GetType ( ) = = variableType ) {
m_SharedVariables [ i ] . SetValue ( variable . GetValue ( ) ) ;
dirty = true ;
}
break ;
}
}
}
return dirty ;
}
/// <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 )
{
if ( index = = ushort . MaxValue ) {
return true ;
}
2026-05-10 11:47:55 -04:00
// Check the Enabled property on the node directly.
if ( logicNode ) {
if ( m_Tasks = = null | | index > = m_Tasks . Length ) {
return true ;
2025-11-25 08:19:33 -05:00
}
2026-05-10 11:47:55 -04:00
return m_Tasks [ index ] . Enabled ;
} else {
if ( m_EventTasks = = null | | index > = m_EventTasks . Length ) {
return true ;
}
return m_EventTasks [ index ] . Enabled ;
2025-11-25 08:19:33 -05:00
}
}
}
}
#endif