Run Sub State Tree Assets from State Tree

We have this ability with Behavior Trees.

It would be extremely useful to have sub state trees that can run as a task within a parent state tree.

It would make it possible to have some modular reusable behavior without having to copy paste chunks of trees between other trees.

I thought it wouldn’t be too hard to add this myself with a simple task node with the requirement that the subtree context type has to be a super class of the parent, and the parent can pass down the context. However after looking through the code that drives the State Tree Component I realized it may be quite involved to properly run another subtree instance correctly so I’d leave this functionality to the engine team to implement.

It’s definitely a very useful thing to have once it works.

In the meantime I may still try to build a task that works.

Well I managed to make a task that runs a sub tree.

I’m not 100% I’m correctly copying data into the subtree correctly but so far it’s working beautifully.

A transition can do a subtree success/fail and it tells the task running the subtree asset that the task failed/succeeded.

It all works so beautifully so far.

Hi IllYay!

I also created a simple “play state tree” task plays a state tree and the task finishes when the state tree finishes.

I have been wondering how to handle State Tree events. Events are really useful for state transition triggers, but you need to get to the State Tree instance. The State Tree Component is where the first state tree instance lives and has helper variables for sending events to it.

The other downside I see for having a “run state tree” task is that all the work we do in this state tree cannot be passed down to the sub state trees. Say I have an evaluator/task in the parent state tree that is doing something expensive and the result is useful in a bunch of states. If I have a task that starts a new state tree, then I can’t bind to the data in the parent state tree.

Did you work out a solution for either of these?

I’m not sure if I tried something like that myself yet. I’d think maybe a pointer to an object could be a painful way to get things out if you do some stuff with C++?

Could you share the code for the task for calling sub tree ? Thank you.

I’m not sure if this works anymore and I have the code currently commented out, but it’s something like this:

SubStateTreeTask.h

#pragma once

// #include "StateTreeReference.h"
// #include "StateTreeInstanceData.h"
// #include "Blueprint/StateTreeTaskBlueprintBase.h"
//
// #include "SubtreeStateTreeTask.generated.h"
//
// /**
//  * FSubtreeStateTreeTask instance data
//  * @see FSubtreeStateTreeTask
//  */
// USTRUCT()
// struct RDBASEFRAMEWORKCORE_API FSubtreeStateTreeTaskInstanceData
// {
//     GENERATED_BODY()
//
//     UPROPERTY(EditAnywhere, Category = "Parameter", meta=(Schema="/Script/GameplayStateTreeModule.StateTreeComponentSchema"))
//     FStateTreeReference StateTreeRef;
//
//     UPROPERTY(Transient)
//     FStateTreeInstanceData InstanceData;
// };
//
// /*
//  * Runs a sub state tree asset whose schema class should at least be a super class of the parent's schema
//  */
// USTRUCT(meta = (DisplayName = "Run Subtree Asset Task"))
// struct RDBASEFRAMEWORKCORE_API FSubtreeStateTreeTask : public FStateTreeTaskCommonBase
// {
//     GENERATED_BODY()
//
//     using FInstanceDataType = FSubtreeStateTreeTaskInstanceData;
//
// protected:
//     virtual const UStruct* GetInstanceDataType() const override { return FInstanceDataType::StaticStruct(); }
//
//     virtual EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
//     virtual EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;
//     virtual void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const override;
//
//     static bool SetContextRequirements(FStateTreeExecutionContext& Context, FStateTreeExecutionContext& ChildContext, bool bLogErrors = false);
// };

SubStateTreeTask.cpp

#include "StateTree/SubtreeStateTreeTask.h"

// #include "VisualLogger/VisualLogger.h"
//
// #include "StateTreeExecutionContext.h"
//
// #define STATETREE_LOG(Verbosity, Format, ...) UE_VLOG(Context.GetOwner(), LogStateTree, Verbosity, Format, ##__VA_ARGS__)
// #define STATETREE_CLOG(Condition, Verbosity, Format, ...) UE_CVLOG((Condition), Context.GetOwner(), LogStateTree, Verbosity, Format, ##__VA_ARGS__)
//
// EStateTreeRunStatus FSubtreeStateTreeTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
// {
//     FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
//
//     if (!InstanceData.StateTreeRef.IsValid())
//     {
//         STATETREE_LOG(Error, TEXT("%s: StateTree asset is not set, cannot enter subtree task state."), ANSI_TO_TCHAR(__FUNCTION__));
//         return EStateTreeRunStatus::Failed;
//     }
//
//     FStateTreeExecutionContext ChildContext(*Context.GetOwner(), *InstanceData.StateTreeRef.GetStateTree(), InstanceData.InstanceData);
//     if (SetContextRequirements(Context, ChildContext, true))
//     {
//         ChildContext.SetParameters(InstanceData.StateTreeRef.GetParameters());
//         return ChildContext.Start();
//     }
//
//     return EStateTreeRunStatus::Failed;
// }
//
// EStateTreeRunStatus FSubtreeStateTreeTask::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
// {
//     FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
//
//     FStateTreeExecutionContext ChildContext(*Context.GetOwner(), *InstanceData.StateTreeRef.GetStateTree(), InstanceData.InstanceData);
//     if (SetContextRequirements(Context, ChildContext, true))
//     {
//         return ChildContext.Tick(DeltaTime);
//     }
//
//     return EStateTreeRunStatus::Failed;
// }
//
// void FSubtreeStateTreeTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const
// {
//     FInstanceDataType& InstanceData = Context.GetInstanceData(*this);
//
//     FStateTreeExecutionContext ChildContext(*Context.GetOwner(), *InstanceData.StateTreeRef.GetStateTree(), InstanceData.InstanceData);
//     if (SetContextRequirements(Context, ChildContext, true))
//     {
//         ChildContext.SetParameters(InstanceData.StateTreeRef.GetParameters());
//         ChildContext.Stop();
//     }
// }
//
// bool FSubtreeStateTreeTask::SetContextRequirements(FStateTreeExecutionContext& Context, FStateTreeExecutionContext& ChildContext, bool bLogErrors)
// {
//     if (!ChildContext.IsValid())
//     {
//         return false;
//     }
//
//     // Make sure the schema of the child is at least a super class of the parent tree so things can copy correctly
//     if (!Context.GetStateTree()->GetSchema()->IsA(ChildContext.GetStateTree()->GetSchema()->GetClass()))
//     {
//         return false;
//     }
//
//     {
//         TMap<FName, const FStateTreeExternalDataDesc*> ContextDataNameToDescriptor;
//
//         // Build lookups of named ItemDescriptors in the parent context so they can be transferred to the child
//         for (const FStateTreeExternalDataDesc& ItemDesc : Context.GetContextDataDescs())
//         {
//             if (ItemDesc.Name != NAME_None)
//             {
//                 ContextDataNameToDescriptor.Add(ItemDesc.Name, &ItemDesc);
//             }
//         }
//
//         // Copy data with the same name from parent to child
//         for (const FStateTreeExternalDataDesc& ItemDesc : ChildContext.GetContextDataDescs())
//         {
//             if (auto Desc = ContextDataNameToDescriptor.Find(ItemDesc.Name))
//             {
//                 ChildContext.SetExternalData(ItemDesc.Handle, Context.GetExternalDataView((*Desc)->Handle));
//             }
//         }
//     }
//
//     {
//         TMap<const UStruct*, const FStateTreeExternalDataDesc*> ExternalDataStructToDescriptor;
//
//         // Build lookups of named ItemDescriptors in the parent context so they can be transferred to the child
//         for (const FStateTreeExternalDataDesc& ItemDesc : Context.GetExternalDataDescs())
//         {
//             if (ItemDesc.Struct)
//             {
//                 ExternalDataStructToDescriptor.Add(ItemDesc.Struct, &ItemDesc);
//             }
//         }
//
//         // Copy data with the same struct from parent to child
//         for (const FStateTreeExternalDataDesc& ItemDesc : ChildContext.GetExternalDataDescs())
//         {
//             if (auto Desc = ExternalDataStructToDescriptor.Find(ItemDesc.Struct))
//             {
//                 ChildContext.SetExternalData(ItemDesc.Handle, Context.GetExternalDataView((*Desc)->Handle));
//             }
//         }
//     }
//
//     bool bResult = ChildContext.AreExternalDataViewsValid();
//
//     if (!bResult && bLogErrors)
//     {
//         STATETREE_LOG(Error, TEXT("%s: Missing external data requirements. StateTree will not update."), ANSI_TO_TCHAR(__FUNCTION__));
//     }
//
//     // Pass along events
//     if (bResult)
//     {
//         Context.ForEachEvent([&ChildContext](const FStateTreeEvent& Event)
//         {
//             ChildContext.SendEvent(Event);
//             return EStateTreeLoopEvents::Next;
//         });
//     }
//
//     return bResult;
// }
//
// #undef STATETREE_LOG
// #undef STATETREE_CLOG

Thank you sharing