You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
- Remove DataView concept - Added FStateTreeExecutionFrame, which defines an active branch of a specific state tree that is running - Added FStateTreeDataHandle, which allows frame relative access of instance data (replaces the DataViews) - Changed linked states to spawn new frames (will later extend to handle linked trees on other assets) - Bumped StateTree data version number, requires recomplation of StateTrees #rb Yoan.StAmant [CL 29805087 by mikko mononen in ue5-main branch]
3015 lines
118 KiB
C++
3015 lines
118 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "StateTreeExecutionContext.h"
|
|
#include "StateTreeTaskBase.h"
|
|
#include "StateTreeEvaluatorBase.h"
|
|
#include "StateTreeConditionBase.h"
|
|
#include "Containers/StaticArray.h"
|
|
#include "Debugger/StateTreeTrace.h"
|
|
#include "Debugger/StateTreeTraceTypes.h"
|
|
#include "Misc/ScopeExit.h"
|
|
#include "VisualLogger/VisualLogger.h"
|
|
#include "ProfilingDebugging/CsvProfiler.h"
|
|
#include "Logging/LogScopedVerbosityOverride.h"
|
|
|
|
#define STATETREE_LOG(Verbosity, Format, ...) UE_VLOG_UELOG(GetOwner(), LogStateTree, Verbosity, TEXT("%s: ") Format, *GetInstanceDescription(), ##__VA_ARGS__)
|
|
#define STATETREE_CLOG(Condition, Verbosity, Format, ...) UE_CVLOG_UELOG((Condition), GetOwner(), LogStateTree, Verbosity, TEXT("%s: ") Format, *GetInstanceDescription(), ##__VA_ARGS__)
|
|
|
|
#define STATETREE_LOG_AND_TRACE(Verbosity, Format, ...) \
|
|
UE_VLOG_UELOG(GetOwner(), LogStateTree, Verbosity, TEXT("%s: ") Format, *GetInstanceDescription(), ##__VA_ARGS__); \
|
|
STATETREE_TRACE_LOG_EVENT(Format, ##__VA_ARGS__)
|
|
|
|
#if WITH_STATETREE_DEBUGGER
|
|
#define ID_NAME PREPROCESSOR_JOIN(InstanceId,__LINE__) \
|
|
|
|
#define STATETREE_TRACE_SCOPED_PHASE(Phase) \
|
|
FStateTreeInstanceDebugId ID_NAME = GetInstanceDebugId(); \
|
|
TRACE_STATETREE_PHASE_EVENT(ID_NAME, Phase, EStateTreeTraceEventType::Push, FStateTreeStateHandle::Invalid) \
|
|
ON_SCOPE_EXIT { TRACE_STATETREE_PHASE_EVENT(ID_NAME, Phase, EStateTreeTraceEventType::Pop, FStateTreeStateHandle::Invalid) }
|
|
|
|
#define STATETREE_TRACE_SCOPED_STATE(StateHandle) \
|
|
FStateTreeInstanceDebugId ID_NAME = GetInstanceDebugId(); \
|
|
TRACE_STATETREE_PHASE_EVENT(ID_NAME, EStateTreeUpdatePhase::Unset, EStateTreeTraceEventType::Push, StateHandle) \
|
|
ON_SCOPE_EXIT { TRACE_STATETREE_PHASE_EVENT(ID_NAME, EStateTreeUpdatePhase::Unset, EStateTreeTraceEventType::Pop, StateHandle) }
|
|
|
|
#define STATETREE_TRACE_SCOPED_STATE_PHASE(StateHandle, Phase) \
|
|
FStateTreeInstanceDebugId ID_NAME = GetInstanceDebugId(); \
|
|
TRACE_STATETREE_PHASE_EVENT(ID_NAME, Phase, EStateTreeTraceEventType::Push, StateHandle) \
|
|
ON_SCOPE_EXIT { TRACE_STATETREE_PHASE_EVENT(ID_NAME, Phase, EStateTreeTraceEventType::Pop, StateHandle) }
|
|
|
|
#define STATETREE_TRACE_INSTANCE_EVENT(EventType) TRACE_STATETREE_INSTANCE_EVENT(GetInstanceDebugId(), GetStateTree(), *GetInstanceDescription(), EventType);
|
|
#define STATETREE_TRACE_ACTIVE_STATES_EVENT(ActiveStates) TRACE_STATETREE_ACTIVE_STATES_EVENT(GetInstanceDebugId(), ActiveStates);
|
|
#define STATETREE_TRACE_LOG_EVENT(Format, ...) TRACE_STATETREE_LOG_EVENT(GetInstanceDebugId(), Format, ##__VA_ARGS__)
|
|
#define STATETREE_TRACE_STATE_EVENT(StateHandle, EventType) TRACE_STATETREE_STATE_EVENT(GetInstanceDebugId(), StateHandle, EventType, EStateTreeStateSelectionBehavior::None);
|
|
#define STATETREE_TRACE_TASK_EVENT(Index, DataView, EventType, Status) TRACE_STATETREE_TASK_EVENT(GetInstanceDebugId(), FStateTreeIndex16(Index), DataView, EventType, Status);
|
|
#define STATETREE_TRACE_EVALUATOR_EVENT(Index, DataView, EventType) TRACE_STATETREE_EVALUATOR_EVENT(GetInstanceDebugId(), FStateTreeIndex16(Index), DataView, EventType);
|
|
#define STATETREE_TRACE_CONDITION_EVENT(Index, DataView, EventType) TRACE_STATETREE_CONDITION_EVENT(GetInstanceDebugId(), FStateTreeIndex16(Index), DataView, EventType);
|
|
#define STATETREE_TRACE_TRANSITION_EVENT(Source, EventType) TRACE_STATETREE_TRANSITION_EVENT(GetInstanceDebugId(), Source, EventType);
|
|
#else
|
|
#define STATETREE_TRACE_SCOPED_PHASE(Phase)
|
|
#define STATETREE_TRACE_SCOPED_STATE(StateHandle)
|
|
#define STATETREE_TRACE_SCOPED_STATE_PHASE(StateHandle, Phase)
|
|
#define STATETREE_TRACE_INSTANCE_EVENT(EventType)
|
|
#define STATETREE_TRACE_ACTIVE_STATES_EVENT(ActiveStates)
|
|
#define STATETREE_TRACE_LOG_EVENT(Format, ...)
|
|
#define STATETREE_TRACE_STATE_EVENT(StateHandle, EventType)
|
|
#define STATETREE_TRACE_TASK_EVENT(Index, DataView, EventType, Status)
|
|
#define STATETREE_TRACE_EVALUATOR_EVENT(Index, DataView, EventType)
|
|
#define STATETREE_TRACE_CONDITION_EVENT(Index, DataView, EventType)
|
|
#define STATETREE_TRACE_TRANSITION_EVENT(Source, EventType)
|
|
#endif // WITH_STATETREE_DEBUGGER
|
|
|
|
namespace UE::StateTree
|
|
{
|
|
constexpr int32 DebugIndentSize = 2; // Debug printing indent for hierarchical data.
|
|
}; // UE::StateTree
|
|
|
|
FStateTreeExecutionContext::FStateTreeExecutionContext(UObject& InOwner, const UStateTree& InStateTree, FStateTreeInstanceData& InInstanceData)
|
|
: Owner(InOwner)
|
|
, RootStateTree(InStateTree)
|
|
, InstanceData(InInstanceData)
|
|
{
|
|
if (InStateTree.IsReadyToRun())
|
|
{
|
|
// Initialize data views for all possible items.
|
|
ContextDataViews.SetNum(RootStateTree.GetNumContextDataViews());
|
|
|
|
// Set data views associated to the parameters using the default values
|
|
SetDefaultParameters();
|
|
|
|
InstanceDataStorage = &InstanceData.GetMutableStorage();
|
|
check(InstanceDataStorage);
|
|
}
|
|
else
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree asset is not valid ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
}
|
|
}
|
|
|
|
FStateTreeExecutionContext::~FStateTreeExecutionContext()
|
|
{
|
|
}
|
|
|
|
void FStateTreeExecutionContext::SetDefaultParameters()
|
|
{
|
|
if (ContextDataViews.IsValidIndex(RootStateTree.ParametersDataHandle.GetIndex()))
|
|
{
|
|
// @todo: Handle constness correctly.
|
|
const FConstStructView ConstParameters = RootStateTree.GetDefaultParameters().GetValue();
|
|
ContextDataViews[RootStateTree.ParametersDataHandle.GetIndex()] = FStateTreeDataView(ConstParameters.GetScriptStruct(), const_cast<uint8*>(ConstParameters.GetMemory()));
|
|
}
|
|
}
|
|
|
|
void FStateTreeExecutionContext::SetParameters(const FInstancedPropertyBag& Parameters)
|
|
{
|
|
if (ensureMsgf(RootStateTree.GetDefaultParameters().GetPropertyBagStruct() == Parameters.GetPropertyBagStruct(),
|
|
TEXT("Parameters must be of the same struct type. Make sure to migrate the provided parameters to the same type as the StateTree default parameters."))
|
|
&& ContextDataViews.IsValidIndex(RootStateTree.ParametersDataHandle.GetIndex()))
|
|
{
|
|
// @todo: Handle constness correctly.
|
|
const FConstStructView ConstParameters = Parameters.GetValue();
|
|
ContextDataViews[RootStateTree.ParametersDataHandle.GetIndex()] = FStateTreeDataView(ConstParameters.GetScriptStruct(), const_cast<uint8*>(ConstParameters.GetMemory()));
|
|
}
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::AreExternalDataViewsValid() const
|
|
{
|
|
if (!IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bResult = true;
|
|
for (const FStateTreeExternalDataDesc& DataDesc : RootStateTree.ExternalDataDescs)
|
|
{
|
|
const FStateTreeDataView& DataView = ContextDataViews[DataDesc.Handle.DataHandle.GetIndex()];
|
|
|
|
auto IsAssignmentValid = [](const FStateTreeExternalDataDesc& DataDesc, const FStateTreeDataView& DataView)
|
|
{
|
|
const UClass* DataDescClass = Cast<UClass>(DataDesc.Struct);
|
|
const UClass* DataViewClass = Cast<UClass>(DataView.GetStruct());
|
|
return DataView.GetStruct()->IsChildOf(DataDesc.Struct) || (DataViewClass && DataDescClass && DataViewClass->ImplementsInterface(DataDescClass));
|
|
};
|
|
|
|
if (DataDesc.Requirement == EStateTreeExternalDataRequirement::Required)
|
|
{
|
|
// Required items must have valid pointer of the expected type.
|
|
if (!DataView.IsValid() || !IsAssignmentValid(DataDesc, DataView))
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Optional items must have same type if they are set.
|
|
if (DataView.IsValid() && !IsAssignmentValid(DataDesc, DataView))
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const FStateTreeExternalDataDesc& DataDesc : RootStateTree.GetContextDataDescs())
|
|
{
|
|
const FStateTreeDataView& DataView = ContextDataViews[DataDesc.Handle.DataHandle.GetIndex()];
|
|
|
|
// Items must have valid pointer of the expected type.
|
|
if (!DataView.IsValid() || !DataView.GetStruct()->IsChildOf(DataDesc.Struct))
|
|
{
|
|
bResult = false;
|
|
break;
|
|
}
|
|
}
|
|
return bResult;
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::Start()
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Start);
|
|
|
|
if (!IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree context is not initialized properly ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
if (InstanceData.IsValid())
|
|
{
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
if (!ensureMsgf(Exec.CurrentPhase == EStateTreeUpdatePhase::Unset, TEXT("%hs can't be called while already in %s ('%s' using StateTree '%s')."),
|
|
__FUNCTION__, *UEnum::GetDisplayValueAsText(Exec.CurrentPhase).ToString(), *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree)))
|
|
{
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
// Stop if still running previous state.
|
|
if (Exec.TreeRunStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
// Initialize instance data. No active states yet, so we'll initialize the evals and global tasks.
|
|
InstanceData.Reset();
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
// Initialize for the init frame.
|
|
FStateTreeExecutionFrame& InitFrame = Exec.ActiveFrames.AddDefaulted_GetRef();
|
|
InitFrame.StateTree = &RootStateTree;
|
|
InitFrame.RootState = FStateTreeStateHandle::Root;
|
|
InitFrame.ActiveStates = {};
|
|
InitFrame.bIsGlobalFrame = true;
|
|
|
|
UpdateInstanceData({}, Exec.ActiveFrames);
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: Failed to initialize instance data on '%s' using StateTree '%s'. Try to recompile the StateTree asset."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
// Must sent instance creation event first
|
|
STATETREE_TRACE_INSTANCE_EVENT(EStateTreeTraceEventType::Push);
|
|
|
|
// Set scoped phase only for properly initialized context with valid Instance data
|
|
// since we need it to output the InstanceId
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::StartTree);
|
|
|
|
// From this point any calls to Stop should be deferred.
|
|
Exec.CurrentPhase = EStateTreeUpdatePhase::StartTree;
|
|
|
|
// Start evaluators and global tasks. Fail the execution if any global task fails.
|
|
FStateTreeIndex16 LastInitializedTaskIndex;
|
|
const EStateTreeRunStatus GlobalTasksRunStatus = StartEvaluatorsAndGlobalTasks(LastInitializedTaskIndex);
|
|
if (GlobalTasksRunStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
// First tick.
|
|
// Tasks are not ticked here, since their behavior is that EnterState() (called above) is treated as a tick.
|
|
TickEvaluatorsAndGlobalTasks(0.0f, /*bTickGlobalTasks*/false);
|
|
|
|
// Initialize to unset running state.
|
|
Exec.TreeRunStatus = EStateTreeRunStatus::Running;
|
|
Exec.LastTickStatus = EStateTreeRunStatus::Unset;
|
|
|
|
static const FStateTreeStateHandle RootState = FStateTreeStateHandle(0);
|
|
|
|
TArray<FStateTreeExecutionFrame, TFixedAllocator<MaxExecutionFrames>> NextActiveFrames;
|
|
if (SelectState(InitFrame, RootState, NextActiveFrames))
|
|
{
|
|
check(!NextActiveFrames.IsEmpty());
|
|
if (NextActiveFrames.Last().ActiveStates.Last().IsCompletionState())
|
|
{
|
|
// Transition to a terminal state (succeeded/failed).
|
|
STATETREE_LOG(Warning, TEXT("%hs: Tree %s at StateTree start on '%s' using StateTree '%s'."),
|
|
__FUNCTION__, NextActiveFrames.Last().ActiveStates.Last() == FStateTreeStateHandle::Succeeded ? TEXT("succeeded") : TEXT("failed"), *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
Exec.TreeRunStatus = NextActiveFrames.Last().ActiveStates.Last().ToCompletionStatus();
|
|
}
|
|
else
|
|
{
|
|
// Enter state tasks can fail/succeed, treat it same as tick.
|
|
FStateTreeTransitionResult Transition;
|
|
Transition.TargetState = RootState;
|
|
Transition.CurrentRunStatus = Exec.LastTickStatus;
|
|
Transition.NextActiveFrames = NextActiveFrames; // Enter state will update Exec.ActiveFrames.
|
|
const EStateTreeRunStatus LastTickStatus = EnterState(Transition);
|
|
|
|
Exec.LastTickStatus = LastTickStatus;
|
|
|
|
// Report state completed immediately.
|
|
if (Exec.LastTickStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
StateCompleted();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Exec.LastTickStatus == EStateTreeRunStatus::Unset)
|
|
{
|
|
// Should not happen. This may happen if initial state could not be selected.
|
|
STATETREE_LOG(Error, TEXT("%hs: Failed to select initial state on '%s' using StateTree '%s'. This should not happen, check that the StateTree logic can always select a state at start."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
Exec.TreeRunStatus = EStateTreeRunStatus::Failed;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
StopEvaluatorsAndGlobalTasks(GlobalTasksRunStatus, LastInitializedTaskIndex);
|
|
|
|
STATETREE_LOG(VeryVerbose, TEXT("%hs: Global tasks completed the StateTree %s on start in status '%s'."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree), *UEnum::GetDisplayValueAsText(GlobalTasksRunStatus).ToString());
|
|
|
|
// No active states or global tasks anymore, reset frames.
|
|
Exec.ActiveFrames.Reset();
|
|
|
|
// We are not considered as running yet so we only set the status without requiring a stop.
|
|
Exec.TreeRunStatus = GlobalTasksRunStatus;
|
|
}
|
|
|
|
// Reset phase since we are now safe to stop.
|
|
Exec.CurrentPhase = EStateTreeUpdatePhase::Unset;
|
|
|
|
// Use local for resulting run state since Stop will reset the instance data.
|
|
EStateTreeRunStatus Result = Exec.TreeRunStatus;
|
|
|
|
if (Exec.RequestedStop != EStateTreeRunStatus::Unset)
|
|
{
|
|
STATETREE_LOG_AND_TRACE(VeryVerbose, TEXT("Processing Deferred Stop"));
|
|
Result = Stop(Exec.RequestedStop);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::Stop(EStateTreeRunStatus CompletionStatus)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Stop);
|
|
|
|
if (!IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree context is not initialized properly ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
// Set scoped phase only for properly initialized context with valid Instance data
|
|
// since we need it to output the InstanceId
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::StopTree);
|
|
|
|
// Make sure that we return a valid completion status (i.e. Succeeded, Failed or Stopped)
|
|
if (CompletionStatus == EStateTreeRunStatus::Unset
|
|
|| CompletionStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
CompletionStatus = EStateTreeRunStatus::Stopped;
|
|
}
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
// A reentrant call to Stop or a call from Start or Tick must be deferred.
|
|
if (Exec.CurrentPhase != EStateTreeUpdatePhase::Unset)
|
|
{
|
|
STATETREE_LOG_AND_TRACE(VeryVerbose, TEXT("Deferring Stop at end of %s"), *UEnum::GetDisplayValueAsText(Exec.CurrentPhase).ToString());
|
|
|
|
Exec.RequestedStop = CompletionStatus;
|
|
return EStateTreeRunStatus::Running;
|
|
}
|
|
|
|
// No need to clear on exit since we reset all the instance data before leaving the function.
|
|
Exec.CurrentPhase = EStateTreeUpdatePhase::StopTree;
|
|
|
|
EStateTreeRunStatus Result = Exec.TreeRunStatus;
|
|
|
|
// Capture events added between ticks.
|
|
FStateTreeEventQueue& EventQueue = InstanceData.GetMutableEventQueue();
|
|
EventsToProcess = EventQueue.GetEvents();
|
|
EventQueue.Reset();
|
|
|
|
// Exit states if still in some valid state.
|
|
if (Exec.TreeRunStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
// Transition to Succeeded state.
|
|
FStateTreeTransitionResult Transition;
|
|
Transition.TargetState = FStateTreeStateHandle::FromCompletionStatus(CompletionStatus);
|
|
Transition.CurrentRunStatus = CompletionStatus;
|
|
FStateTreeExecutionFrame& NewFrame = Transition.NextActiveFrames.AddDefaulted_GetRef();
|
|
NewFrame.StateTree = &RootStateTree;
|
|
NewFrame.RootState = FStateTreeStateHandle::Root;
|
|
NewFrame.ActiveStates = {};
|
|
|
|
if (Exec.TreeRunStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
ExitState(Transition);
|
|
}
|
|
|
|
// Stop evaluators and global tasks.
|
|
StopEvaluatorsAndGlobalTasks(CompletionStatus);
|
|
|
|
// No active states or global tasks anymore, reset frames.
|
|
Exec.ActiveFrames.Reset();
|
|
|
|
Result = CompletionStatus;
|
|
}
|
|
|
|
// Trace before resetting the instance data since it is required to provide all the event information
|
|
STATETREE_TRACE_ACTIVE_STATES_EVENT(FStateTreeActiveStates());
|
|
STATETREE_TRACE_INSTANCE_EVENT(EStateTreeTraceEventType::Pop);
|
|
|
|
// Destruct all allocated instance data (does not shrink the buffer). This will invalidate Exec too.
|
|
InstanceData.Reset();
|
|
|
|
return Result;
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::Tick(const float DeltaTime)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Tick);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TickStateTree);
|
|
|
|
if (!IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree context is not initialized properly ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Tick called on %s using StateTree %s with invalid instance data. Start() must be called before Tick()."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
FStateTreeEventQueue& EventQueue = InstanceData.GetMutableEventQueue();
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
// No ticking if the tree is done or stopped.
|
|
if (Exec.TreeRunStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
return Exec.TreeRunStatus;
|
|
}
|
|
|
|
if (!ensureMsgf(Exec.CurrentPhase == EStateTreeUpdatePhase::Unset, TEXT("%hs can't be called while already in %s ('%s' using StateTree '%s')."),
|
|
__FUNCTION__, *UEnum::GetDisplayValueAsText(Exec.CurrentPhase).ToString(), *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree)))
|
|
{
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
// From this point any calls to Stop should be deferred.
|
|
Exec.CurrentPhase = EStateTreeUpdatePhase::TickStateTree;
|
|
|
|
// Capture events added between ticks.
|
|
EventsToProcess = EventQueue.GetEvents();
|
|
EventQueue.Reset();
|
|
|
|
// Update the delayed transitions.
|
|
for (FStateTreeTransitionDelayedState& DelayedState : Exec.DelayedTransitions)
|
|
{
|
|
DelayedState.TimeLeft -= DeltaTime;
|
|
}
|
|
|
|
// Tick global evaluators and tasks.
|
|
const EStateTreeRunStatus EvalAndGlobalTaskStatus = TickEvaluatorsAndGlobalTasks(DeltaTime);
|
|
if (EvalAndGlobalTaskStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
if (Exec.LastTickStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
// Tick tasks on active states.
|
|
Exec.LastTickStatus = TickTasks(DeltaTime);
|
|
|
|
// Report state completed immediately.
|
|
if (Exec.LastTickStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
StateCompleted();
|
|
}
|
|
}
|
|
|
|
// The state selection is repeated up to MaxIteration time. This allows failed EnterState() to potentially find a new state immediately.
|
|
// This helps event driven StateTrees to not require another event/tick to find a suitable state.
|
|
static constexpr int32 MaxIterations = 5;
|
|
for (int32 Iter = 0; Iter < MaxIterations; Iter++)
|
|
{
|
|
// Append events accumulated during the tick, so that transitions can immediately act on them.
|
|
// We'll consume the events only if they lead to state change below (EnterState is treated the same as Tick),
|
|
// or let them be processed next frame if no transition.
|
|
EventsToProcess.Append(EventQueue.GetEvents());
|
|
|
|
// Trigger conditional transitions or state succeed/failed transitions. First tick transition is handled here too.
|
|
if (TriggerTransitions())
|
|
{
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::ApplyTransitions);
|
|
STATETREE_TRACE_TRANSITION_EVENT(NextTransitionSource, EStateTreeTraceEventType::OnTransition);
|
|
NextTransitionSource.Reset();
|
|
|
|
// We have committed to state change, consume events that were accumulated during the tick above.
|
|
EventQueue.Reset();
|
|
|
|
ExitState(NextTransition);
|
|
|
|
// Tree succeeded or failed.
|
|
if (NextTransition.TargetState.IsCompletionState())
|
|
{
|
|
// Transition to a terminal state (succeeded/failed), or default transition failed.
|
|
Exec.TreeRunStatus = NextTransition.TargetState.ToCompletionStatus();
|
|
|
|
// Stop evaluators and global tasks.
|
|
StopEvaluatorsAndGlobalTasks(Exec.TreeRunStatus);
|
|
|
|
// No active states or global tasks anymore, reset frames.
|
|
Exec.ActiveFrames.Reset();
|
|
|
|
break;
|
|
}
|
|
|
|
// Append and consume the events accumulated during the state exit.
|
|
EventsToProcess.Append(EventQueue.GetEvents());
|
|
EventQueue.Reset();
|
|
|
|
// Enter state tasks can fail/succeed, treat it same as tick.
|
|
const EStateTreeRunStatus LastTickStatus = EnterState(NextTransition);
|
|
|
|
NextTransition.Reset();
|
|
|
|
Exec.LastTickStatus = LastTickStatus;
|
|
|
|
// Consider events so far processed. Events sent during EnterState went into EventQueue, and are processed in next iteration.
|
|
EventsToProcess.Reset();
|
|
|
|
// Report state completed immediately.
|
|
if (Exec.LastTickStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
StateCompleted();
|
|
}
|
|
}
|
|
|
|
// Stop as soon as have found a running state.
|
|
if (Exec.LastTickStatus == EStateTreeRunStatus::Running)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// If global tasks succeed or fail, stop the tree.
|
|
Exec.RequestedStop = EvalAndGlobalTaskStatus;
|
|
}
|
|
|
|
EventsToProcess.Reset();
|
|
|
|
// Reset phase since we are now safe to stop.
|
|
Exec.CurrentPhase = EStateTreeUpdatePhase::Unset;
|
|
|
|
// Use local for resulting run state since Stop will reset the instance data.
|
|
EStateTreeRunStatus Result = Exec.TreeRunStatus;
|
|
|
|
if (Exec.RequestedStop != EStateTreeRunStatus::Unset)
|
|
{
|
|
STATETREE_LOG_AND_TRACE(VeryVerbose, TEXT("Processing Deferred Stop"));
|
|
Result = Stop(Exec.RequestedStop);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::GetStateTreeRunStatus() const
|
|
{
|
|
if (!IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree context is not initialized properly ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
if (const FStateTreeExecutionState* Exec = InstanceData.GetExecutionState())
|
|
{
|
|
return Exec->TreeRunStatus;
|
|
}
|
|
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
void FStateTreeExecutionContext::SendEvent(const FStateTreeEvent& Event) const
|
|
{
|
|
SendEvent(Event.Tag, Event.Payload, Event.Origin);
|
|
}
|
|
|
|
void FStateTreeExecutionContext::SendEvent(const FGameplayTag Tag, const FConstStructView Payload, const FName Origin) const
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_SendEvent);
|
|
|
|
if (!IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree context is not initialized properly ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return;
|
|
}
|
|
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: SendEvent called on %s using StateTree %s with invalid instance data. Start() must be called before sending events."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return;
|
|
}
|
|
|
|
STATETREE_LOG_AND_TRACE(Verbose, TEXT("Send Event '%s'"), *Tag.ToString());
|
|
|
|
FStateTreeEventQueue& EventQueue = InstanceData.GetMutableEventQueue();
|
|
EventQueue.SendEvent(&Owner, Tag, Payload, Origin);
|
|
}
|
|
|
|
void FStateTreeExecutionContext::RequestTransition(const FStateTreeTransitionRequest& Request)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_RequestTransition);
|
|
|
|
if (!IsValid())
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: StateTree context is not initialized properly ('%s' using StateTree '%s')"),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return;
|
|
}
|
|
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: RequestTransition called on %s using StateTree %s with invalid instance data. Start() must be called before requesting transition."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return;
|
|
}
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (bAllowDirectTransitions)
|
|
{
|
|
checkf(CurrentlyProcessedFrame, TEXT("Expecting CurrentlyProcessedFrame to be valid when called during TriggerTransitions()."));
|
|
|
|
STATETREE_LOG(Verbose, TEXT("Request transition to '%s' at priority %s"), *GetSafeStateName(*CurrentlyProcessedFrame, Request.TargetState), *UEnum::GetDisplayValueAsText(Request.Priority).ToString());
|
|
|
|
if (RequestTransition(*CurrentlyProcessedFrame, Request.TargetState, Request.Priority))
|
|
{
|
|
NextTransitionSource = FStateTreeTransitionSource(EStateTreeTransitionSourceType::ExternalRequest, Request.TargetState, Request.Priority);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const FStateTreeExecutionFrame* RootFrame = &Exec.ActiveFrames[0];
|
|
if (CurrentlyProcessedFrame)
|
|
{
|
|
RootFrame = CurrentlyProcessedFrame;
|
|
}
|
|
|
|
if (!RootFrame)
|
|
{
|
|
STATETREE_LOG(Warning, TEXT("%hs: RequestTransition called on %s using StateTree %s without active state. Start() must be called before requesting transition."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return;
|
|
}
|
|
|
|
STATETREE_LOG(Verbose, TEXT("Request transition to '%s' at priority %s"), *GetSafeStateName(*RootFrame, Request.TargetState), *UEnum::GetDisplayValueAsText(Request.Priority).ToString());
|
|
|
|
FStateTreeTransitionRequest RequestWithSource = Request;
|
|
RequestWithSource.SourceStateTree = RootFrame->StateTree;
|
|
RequestWithSource.SourceRootState = RootFrame->ActiveStates[0];
|
|
RequestWithSource.SourceState = CurrentlyProcessedState;
|
|
|
|
InstanceData.AddTransitionRequest(&Owner, RequestWithSource);
|
|
}
|
|
}
|
|
|
|
#if WITH_STATETREE_DEBUGGER
|
|
FStateTreeInstanceDebugId FStateTreeExecutionContext::GetInstanceDebugId() const
|
|
{
|
|
FStateTreeInstanceDebugId& InstanceDebugId = GetExecState().InstanceDebugId;
|
|
if (!InstanceDebugId.IsValid())
|
|
{
|
|
static std::atomic<uint32> SerialNumber = 0;
|
|
InstanceDebugId = FStateTreeInstanceDebugId(GetTypeHash(GetInstanceDescription()), ++SerialNumber);
|
|
}
|
|
return InstanceDebugId;
|
|
}
|
|
#endif // WITH_STATETREE_DEBUGGER
|
|
|
|
void FStateTreeExecutionContext::UpdateInstanceData(TConstArrayView<FStateTreeExecutionFrame> CurrentActiveFrames, TArrayView<FStateTreeExecutionFrame> NextActiveFrames)
|
|
{
|
|
// Estimate how many new instance data items we might have.
|
|
int32 EstimatedNumStructs = 0;
|
|
for (int32 FrameIndex = 0; FrameIndex < NextActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
const FStateTreeExecutionFrame& NextFrame = NextActiveFrames[FrameIndex];
|
|
if (NextFrame.bIsGlobalFrame)
|
|
{
|
|
EstimatedNumStructs += NextFrame.StateTree->NumGlobalInstanceData;
|
|
}
|
|
// States
|
|
for (int32 StateIndex = 0; StateIndex < NextFrame.ActiveStates.Num(); StateIndex++)
|
|
{
|
|
const FStateTreeStateHandle StateHandle = NextFrame.ActiveStates[StateIndex];
|
|
const FCompactStateTreeState& State = NextFrame.StateTree->States[StateHandle.Index];
|
|
EstimatedNumStructs += State.InstanceDataNum;
|
|
}
|
|
}
|
|
|
|
TArray<FConstStructView, TConcurrentLinearArrayAllocator<FDefaultBlockAllocationTag>> InstanceStructs;
|
|
InstanceStructs.Reserve(EstimatedNumStructs);
|
|
|
|
// Find next instance data sources and find common/existing section of instance data at start.
|
|
int32 CurrentGlobalInstanceIndexBase = 0;
|
|
int32 NumCommonInstanceData = 0;
|
|
|
|
const UStruct* NextStateParameterDataStruct = nullptr;
|
|
FStateTreeIndex16 NextStateParameterDataIndex = FStateTreeIndex16::Invalid;
|
|
|
|
bool bAreCommon = true;
|
|
for (int32 FrameIndex = 0; FrameIndex < NextActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
const bool bIsCurrentFrameValid = CurrentActiveFrames.IsValidIndex(FrameIndex)
|
|
&& CurrentActiveFrames[FrameIndex].IsSameFrame(NextActiveFrames[FrameIndex]);
|
|
|
|
bAreCommon &= bIsCurrentFrameValid;
|
|
|
|
const FStateTreeExecutionFrame* CurrentFrame = bIsCurrentFrameValid ? &CurrentActiveFrames[FrameIndex] : nullptr;
|
|
FStateTreeExecutionFrame& NextFrame = NextActiveFrames[FrameIndex];
|
|
|
|
check(NextFrame.StateTree);
|
|
|
|
if (NextFrame.bIsGlobalFrame)
|
|
{
|
|
// Global Evals
|
|
const int32 BaseIndex = InstanceStructs.Num();
|
|
|
|
NextFrame.GlobalInstanceIndexBase = FStateTreeIndex16(BaseIndex);
|
|
NextFrame.ActiveInstanceIndexBase = FStateTreeIndex16(BaseIndex);
|
|
CurrentGlobalInstanceIndexBase = BaseIndex;
|
|
|
|
InstanceStructs.AddDefaulted(NextFrame.StateTree->NumGlobalInstanceData);
|
|
|
|
for (int32 EvalIndex = NextFrame.StateTree->EvaluatorsBegin; EvalIndex < (NextFrame.StateTree->EvaluatorsBegin + NextFrame.StateTree->EvaluatorsNum); EvalIndex++)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = NextFrame.StateTree->Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
const FConstStructView EvalInstanceData = NextFrame.StateTree->DefaultInstanceData.GetStruct(Eval.InstanceTemplateIndex.Get());
|
|
InstanceStructs[BaseIndex + Eval.InstanceDataHandle.GetIndex()] = EvalInstanceData;
|
|
}
|
|
|
|
// Global tasks
|
|
for (int32 TaskIndex = NextFrame.StateTree->GlobalTasksBegin; TaskIndex < (NextFrame.StateTree->GlobalTasksBegin + NextFrame.StateTree->GlobalTasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = NextFrame.StateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FConstStructView TaskInstanceData = NextFrame.StateTree->DefaultInstanceData.GetStruct(Task.InstanceTemplateIndex.Get());
|
|
InstanceStructs[BaseIndex + Task.InstanceDataHandle.GetIndex()] = TaskInstanceData;
|
|
}
|
|
|
|
if (bAreCommon)
|
|
{
|
|
NumCommonInstanceData = InstanceStructs.Num();
|
|
}
|
|
}
|
|
|
|
// States
|
|
const int32 BaseIndex = InstanceStructs.Num();
|
|
|
|
NextFrame.GlobalInstanceIndexBase = FStateTreeIndex16(CurrentGlobalInstanceIndexBase);
|
|
NextFrame.ActiveInstanceIndexBase = FStateTreeIndex16(BaseIndex);
|
|
|
|
for (int32 StateIndex = 0; StateIndex < NextFrame.ActiveStates.Num(); StateIndex++)
|
|
{
|
|
// Check if the next state is still same as current state, GetStateSafe() will return invalid state if passed out of bounds index.
|
|
bAreCommon = bAreCommon && (CurrentFrame && CurrentFrame->ActiveStates.GetStateSafe(StateIndex) == NextFrame.ActiveStates[StateIndex]);
|
|
|
|
const FStateTreeStateHandle StateHandle = NextFrame.ActiveStates[StateIndex];
|
|
const FCompactStateTreeState& State = NextFrame.StateTree->States[StateHandle.Index];
|
|
|
|
InstanceStructs.AddDefaulted(State.InstanceDataNum);
|
|
|
|
if (State.Type == EStateTreeStateType::Linked)
|
|
{
|
|
// Linked state's instance data is the parameters.
|
|
check(State.ParameterDataHandle.IsValid());
|
|
const FConstStructView ParamsInstanceData = NextFrame.StateTree->DefaultInstanceData.GetStruct(State.ParameterTemplateIndex.Get());
|
|
InstanceStructs[BaseIndex + State.ParameterDataHandle.GetIndex()] = ParamsInstanceData;
|
|
|
|
// Store the index of the parameter data, so that we can point the linked state to it.
|
|
check(State.ParameterDataHandle.GetSource() == EStateTreeDataSourceType::LinkedStateParameterData);
|
|
checkf(!NextStateParameterDataIndex.IsValid(), TEXT("NextStateParameterDataIndex not should be set yet when we encounter a linked state."));
|
|
NextStateParameterDataIndex = FStateTreeIndex16(BaseIndex + State.ParameterDataHandle.GetIndex());
|
|
NextStateParameterDataStruct = ParamsInstanceData.GetScriptStruct();
|
|
}
|
|
else if (State.Type == EStateTreeStateType::Subtree)
|
|
{
|
|
check(State.ParameterDataHandle.IsValid());
|
|
const FConstStructView ParamsInstanceData = NextFrame.StateTree->DefaultInstanceData.GetStruct(State.ParameterTemplateIndex.Get());
|
|
if (!NextStateParameterDataIndex.IsValid())
|
|
{
|
|
// Parameters are not set by a linked state, create instance data.
|
|
InstanceStructs[BaseIndex + State.ParameterDataHandle.GetIndex()] = ParamsInstanceData;
|
|
NextFrame.StateParameterDataIndex = FStateTreeIndex16(BaseIndex + State.ParameterDataHandle.GetIndex());
|
|
}
|
|
else
|
|
{
|
|
// Point to the parameter block set by linked state.
|
|
check(NextStateParameterDataStruct == ParamsInstanceData.GetScriptStruct());
|
|
NextFrame.StateParameterDataIndex = NextStateParameterDataIndex;
|
|
NextStateParameterDataIndex = FStateTreeIndex16(); // Mark as used.
|
|
}
|
|
}
|
|
|
|
for (int32 TaskIndex = State.TasksBegin; TaskIndex < (State.TasksBegin + State.TasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = NextFrame.StateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FConstStructView TaskInstanceData = NextFrame.StateTree->DefaultInstanceData.GetStruct(Task.InstanceTemplateIndex.Get());
|
|
InstanceStructs[BaseIndex + Task.InstanceDataHandle.GetIndex()] = TaskInstanceData;
|
|
}
|
|
|
|
if (bAreCommon)
|
|
{
|
|
NumCommonInstanceData = InstanceStructs.Num();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Common section should match.
|
|
// @todo: put this behind a define when enough testing has been done.
|
|
for (int32 Index = 0; Index < NumCommonInstanceData; Index++)
|
|
{
|
|
check(Index < InstanceData.Num());
|
|
|
|
FConstStructView ExistingInstanceDataView = InstanceData.GetStruct(Index);
|
|
FConstStructView NewInstanceDataView = InstanceStructs[Index];
|
|
|
|
check(NewInstanceDataView.GetScriptStruct() == ExistingInstanceDataView.GetScriptStruct());
|
|
|
|
const FStateTreeInstanceObjectWrapper* ExistingWrapper = ExistingInstanceDataView.GetPtr<const FStateTreeInstanceObjectWrapper>();
|
|
const FStateTreeInstanceObjectWrapper* NewWrapper = ExistingInstanceDataView.GetPtr<const FStateTreeInstanceObjectWrapper>();
|
|
if (ExistingWrapper && NewWrapper)
|
|
{
|
|
check(ExistingWrapper->InstanceObject && NewWrapper->InstanceObject);
|
|
check(ExistingWrapper->InstanceObject->GetClass() == NewWrapper->InstanceObject->GetClass());
|
|
}
|
|
}
|
|
|
|
// Remove instance data that was not common.
|
|
InstanceData.ShrinkTo(NumCommonInstanceData);
|
|
|
|
// Add new instance data.
|
|
InstanceData.Append(Owner, MakeArrayView(InstanceStructs.GetData() + NumCommonInstanceData, InstanceStructs.Num() - NumCommonInstanceData));
|
|
}
|
|
|
|
FStateTreeDataView FStateTreeExecutionContext::GetDataView(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeDataHandle Handle) const
|
|
{
|
|
switch (Handle.GetSource())
|
|
{
|
|
case EStateTreeDataSourceType::None:
|
|
return {};
|
|
|
|
case EStateTreeDataSourceType::GlobalInstanceData:
|
|
return InstanceDataStorage->GetMutableStruct(CurrentFrame.GlobalInstanceIndexBase.Get() + Handle.GetIndex());
|
|
case EStateTreeDataSourceType::GlobalInstanceDataObject:
|
|
return InstanceDataStorage->GetMutableObject(CurrentFrame.GlobalInstanceIndexBase.Get() + Handle.GetIndex());
|
|
|
|
case EStateTreeDataSourceType::ActiveInstanceData:
|
|
return InstanceDataStorage->GetMutableStruct(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex());
|
|
case EStateTreeDataSourceType::ActiveInstanceDataObject:
|
|
return InstanceDataStorage->GetMutableObject(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex());
|
|
|
|
case EStateTreeDataSourceType::SharedInstanceData:
|
|
check(CurrentlyProcessedSharedInstanceStorage);
|
|
return CurrentlyProcessedSharedInstanceStorage->GetMutableStruct(Handle.GetIndex());
|
|
case EStateTreeDataSourceType::SharedInstanceDataObject:
|
|
check(CurrentlyProcessedSharedInstanceStorage);
|
|
return CurrentlyProcessedSharedInstanceStorage->GetMutableStruct(Handle.GetIndex());
|
|
|
|
case EStateTreeDataSourceType::ContextData:
|
|
return ContextDataViews[Handle.GetIndex()];
|
|
|
|
case EStateTreeDataSourceType::SubtreeParameterData:
|
|
{
|
|
FCompactStateTreeParameters& Params = InstanceDataStorage->GetMutableStruct(CurrentFrame.StateParameterDataIndex.Get()).Get<FCompactStateTreeParameters>();
|
|
return Params.Parameters.GetMutableValue();
|
|
}
|
|
case EStateTreeDataSourceType::LinkedStateParameterData:
|
|
{
|
|
FCompactStateTreeParameters& Params = InstanceDataStorage->GetMutableStruct(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex()).Get<FCompactStateTreeParameters>();
|
|
return Params.Parameters.GetMutableValue();
|
|
}
|
|
|
|
default:
|
|
checkf(false, TEXT("Unhandle case %s"), *UEnum::GetValueAsString(Handle.GetSource()));
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::IsHandleSourceValid(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeDataHandle Handle) const
|
|
{
|
|
// Checks that the instance data is valid for specific handle types.
|
|
//
|
|
// The CurrentFrame may not be yet properly initialized, for that reason we need to check
|
|
// that the path to the handle makes sense (it's part of the active states) as well as that
|
|
// we actually have instance data for the handle (index is valid).
|
|
//
|
|
// The (base) indices can be invalid if the frame/state is not entered yet.
|
|
// For active instance data we need to check that the frame is initialized for a specific state,
|
|
// as well as that the instance data is initialized.
|
|
|
|
switch (Handle.GetSource())
|
|
{
|
|
case EStateTreeDataSourceType::None:
|
|
return true;
|
|
|
|
case EStateTreeDataSourceType::GlobalInstanceData:
|
|
case EStateTreeDataSourceType::GlobalInstanceDataObject:
|
|
return CurrentFrame.GlobalInstanceIndexBase.IsValid()
|
|
&& InstanceDataStorage->IsValidIndex(CurrentFrame.GlobalInstanceIndexBase.Get() + Handle.GetIndex());
|
|
|
|
case EStateTreeDataSourceType::ActiveInstanceData:
|
|
case EStateTreeDataSourceType::ActiveInstanceDataObject:
|
|
return CurrentFrame.ActiveInstanceIndexBase.IsValid()
|
|
&& CurrentFrame.ActiveStates.Contains(Handle.GetState())
|
|
&& InstanceDataStorage->IsValidIndex(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex());
|
|
|
|
case EStateTreeDataSourceType::SharedInstanceData:
|
|
case EStateTreeDataSourceType::SharedInstanceDataObject:
|
|
return true;
|
|
|
|
case EStateTreeDataSourceType::ContextData:
|
|
return true;
|
|
|
|
case EStateTreeDataSourceType::SubtreeParameterData:
|
|
return CurrentFrame.StateParameterDataIndex.IsValid()
|
|
&& InstanceDataStorage->IsValidIndex(CurrentFrame.StateParameterDataIndex.Get());
|
|
|
|
case EStateTreeDataSourceType::LinkedStateParameterData:
|
|
return CurrentFrame.ActiveInstanceIndexBase.IsValid()
|
|
&& CurrentFrame.ActiveStates.Contains(Handle.GetState())
|
|
&& InstanceDataStorage->IsValidIndex(CurrentFrame.ActiveInstanceIndexBase.Get() + Handle.GetIndex());
|
|
|
|
default:
|
|
checkf(false, TEXT("Unhandle case %s"), *UEnum::GetValueAsString(Handle.GetSource()));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
bool FStateTreeExecutionContext::CopyBatch(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeDataView TargetView, const FStateTreeIndex16 BindingsBatch) const
|
|
{
|
|
const FStateTreePropertyCopyBatch& Batch = CurrentFrame.StateTree->PropertyBindings.GetBatch(BindingsBatch);
|
|
check(TargetView.GetStruct() == Batch.TargetStruct.Struct);
|
|
|
|
bool bSucceed = true;
|
|
for (const FStateTreePropertyCopy& Copy : CurrentFrame.StateTree->PropertyBindings.GetBatchCopies(Batch))
|
|
{
|
|
const FStateTreeDataView SourceView = GetDataView(CurrentFrame, Copy.SourceDataHandle);
|
|
bSucceed &= CurrentFrame.StateTree->PropertyBindings.CopyProperty(Copy, SourceView, TargetView);
|
|
}
|
|
return bSucceed;
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::CopyBatchWithValidation(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeDataView TargetView, const FStateTreeIndex16 BindingsBatch) const
|
|
{
|
|
const FStateTreePropertyCopyBatch& Batch = CurrentFrame.StateTree->PropertyBindings.GetBatch(BindingsBatch);
|
|
check(TargetView.GetStruct() == Batch.TargetStruct.Struct);
|
|
|
|
bool bSucceed = true;
|
|
for (const FStateTreePropertyCopy& Copy : CurrentFrame.StateTree->PropertyBindings.GetBatchCopies(Batch))
|
|
{
|
|
if (!IsHandleSourceValid(CurrentFrame, Copy.SourceDataHandle))
|
|
{
|
|
bSucceed = false;
|
|
break;
|
|
}
|
|
|
|
const FStateTreeDataView SourceView = GetDataView(CurrentFrame, Copy.SourceDataHandle);
|
|
bSucceed &= CurrentFrame.StateTree->PropertyBindings.CopyProperty(Copy, SourceView, TargetView);
|
|
}
|
|
return bSucceed;
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::EnterState(FStateTreeTransitionResult& Transition)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_EnterState);
|
|
|
|
if (Transition.NextActiveFrames.IsEmpty())
|
|
{
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
// Allocate new tasks.
|
|
UpdateInstanceData(Exec.ActiveFrames, Transition.NextActiveFrames);
|
|
|
|
Exec.StateChangeCount++;
|
|
Exec.CompletedFrameIndex = FStateTreeIndex16::Invalid;
|
|
Exec.CompletedStateHandle = FStateTreeStateHandle::Invalid;
|
|
Exec.EnterStateFailedFrameIndex = FStateTreeIndex16::Invalid; // This will make all tasks to be accepted.
|
|
Exec.EnterStateFailedTaskIndex = FStateTreeIndex16::Invalid; // This will make all tasks to be accepted.
|
|
|
|
// On target branch means that the state is the target of current transition or child of it.
|
|
// States which were active before and will remain active, but are not on target branch will not get
|
|
// EnterState called. That is, a transition is handled as "replan from this state".
|
|
bool bOnTargetBranch = false;
|
|
FStateTreeTransitionResult CurrentTransition = Transition;
|
|
EStateTreeRunStatus Result = EStateTreeRunStatus::Running;
|
|
|
|
STATETREE_LOG(Log, TEXT("Enter state '%s' (%d)"), *DebugGetStatePath(Transition.NextActiveFrames), Exec.StateChangeCount);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::EnterStates);
|
|
|
|
// The previous active frames are needed for state enter logic.
|
|
TArray<FStateTreeExecutionFrame, TConcurrentLinearArrayAllocator<FDefaultBlockAllocationTag>> PreviousActiveFrames;
|
|
PreviousActiveFrames = Exec.ActiveFrames;
|
|
|
|
// Reset the current active frames, new ones are added one by one.
|
|
Exec.ActiveFrames.Reset();
|
|
|
|
for (int32 FrameIndex = 0; FrameIndex < Transition.NextActiveFrames.Num() && Result != EStateTreeRunStatus::Failed; FrameIndex++)
|
|
{
|
|
const FStateTreeExecutionFrame& NextFrame = Transition.NextActiveFrames[FrameIndex];
|
|
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames.Add_GetRef(NextFrame);
|
|
// We'll add new states one by one, so that active states contain only the states which have EnterState called.
|
|
CurrentFrame.ActiveStates.Reset();
|
|
|
|
// Get previous active states, they are used to calculate transition type.
|
|
FStateTreeActiveStates PreviousActiveStates;
|
|
if (PreviousActiveFrames.IsValidIndex(FrameIndex)
|
|
&& PreviousActiveFrames[FrameIndex].IsSameFrame(NextFrame))
|
|
{
|
|
PreviousActiveStates = PreviousActiveFrames[FrameIndex].ActiveStates;
|
|
}
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
const UStateTree* CurrentStateTree = NextFrame.StateTree;
|
|
|
|
for (int32 Index = 0; Index < NextFrame.ActiveStates.Num() && Result != EStateTreeRunStatus::Failed; Index++)
|
|
{
|
|
const FStateTreeStateHandle CurrentHandle = NextFrame.ActiveStates[Index];
|
|
const FStateTreeStateHandle PreviousHandle = PreviousActiveStates.GetStateSafe(Index);
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[CurrentHandle.Index];
|
|
|
|
FCurrentlyProcessedStateScope StateScope(*this, CurrentHandle);
|
|
|
|
// Add only enabled States to the list of active States
|
|
if (State.bEnabled && !CurrentFrame.ActiveStates.Push(CurrentHandle))
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Reached max execution depth when trying to enter state '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
break;
|
|
}
|
|
|
|
if (State.Type == EStateTreeStateType::Linked)
|
|
{
|
|
if (State.ParameterDataHandle.IsValid()
|
|
&& State.ParameterBindingsBatch.IsValid())
|
|
{
|
|
const FStateTreeDataView StateParamsDataView = GetDataView(CurrentFrame, State.ParameterDataHandle);
|
|
CopyBatch(CurrentFrame, StateParamsDataView, State.ParameterBindingsBatch);
|
|
}
|
|
}
|
|
|
|
bOnTargetBranch = bOnTargetBranch || CurrentHandle == Transition.TargetState;
|
|
const bool bWasActive = PreviousHandle == CurrentHandle;
|
|
|
|
// Do not enter a disabled State tasks but maintain property bindings
|
|
const bool bIsEnteringState = (!bWasActive || bOnTargetBranch) && State.bEnabled;
|
|
|
|
CurrentTransition.CurrentState = CurrentHandle;
|
|
CurrentTransition.ChangeType = bWasActive ? EStateTreeStateChangeType::Sustained : EStateTreeStateChangeType::Changed;
|
|
|
|
if (bIsEnteringState)
|
|
{
|
|
STATETREE_TRACE_STATE_EVENT(CurrentHandle, EStateTreeTraceEventType::OnEntering);
|
|
STATETREE_LOG(Log, TEXT("%*sState '%s' %s"), Index*UE::StateTree::DebugIndentSize, TEXT(""),
|
|
*DebugGetStatePath(Transition.NextActiveFrames, &NextFrame, Index),
|
|
*UEnum::GetDisplayValueAsText(CurrentTransition.ChangeType).ToString());
|
|
}
|
|
|
|
// Activate tasks on current state.
|
|
for (int32 TaskIndex = State.TasksBegin; TaskIndex < (State.TasksBegin + State.TasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = NextFrame.StateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskInstanceView = GetDataView(NextFrame, Task.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Task.BindingsBatch.IsValid())
|
|
{
|
|
CopyBatch(NextFrame, TaskInstanceView, Task.BindingsBatch);
|
|
}
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'EnterState' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
const bool bShouldCallStateChange = CurrentTransition.ChangeType == EStateTreeStateChangeType::Changed
|
|
|| (CurrentTransition.ChangeType == EStateTreeStateChangeType::Sustained && Task.bShouldStateChangeOnReselect);
|
|
|
|
if (bIsEnteringState && bShouldCallStateChange)
|
|
{
|
|
STATETREE_LOG(Verbose, TEXT("%*s Task '%s'"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
|
|
EStateTreeRunStatus Status = EStateTreeRunStatus::Unset;
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Task_EnterState);
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Task_EnterState);
|
|
|
|
Status = Task.EnterState(*this, CurrentTransition);
|
|
}
|
|
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskInstanceView, EStateTreeTraceEventType::OnEntered, Status);
|
|
|
|
if (Status != EStateTreeRunStatus::Running)
|
|
{
|
|
// Store the first state that completed, will be used to decide where to trigger transitions.
|
|
if (!Exec.CompletedStateHandle.IsValid())
|
|
{
|
|
Exec.CompletedFrameIndex = FStateTreeIndex16(FrameIndex);
|
|
Exec.CompletedStateHandle = CurrentHandle;
|
|
}
|
|
Result = Status;
|
|
}
|
|
|
|
if (Status == EStateTreeRunStatus::Failed)
|
|
{
|
|
// Store how far in the enter state we got. This will be used to match the StateCompleted() and ExitState() calls.
|
|
Exec.EnterStateFailedFrameIndex = FStateTreeIndex16(FrameIndex);
|
|
Exec.EnterStateFailedTaskIndex = FStateTreeIndex16(TaskIndex);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bIsEnteringState)
|
|
{
|
|
STATETREE_TRACE_STATE_EVENT(CurrentHandle, EStateTreeTraceEventType::OnEntered);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if WITH_STATETREE_DEBUGGER
|
|
// @todo: implement support for frames in debugger
|
|
FStateTreeActiveStates AllActiveStates;
|
|
for (const FStateTreeExecutionFrame& ActiveFrame : Exec.ActiveFrames)
|
|
{
|
|
for (const FStateTreeStateHandle StateHandle : ActiveFrame.ActiveStates)
|
|
{
|
|
AllActiveStates.Push(StateHandle);
|
|
}
|
|
}
|
|
STATETREE_TRACE_ACTIVE_STATES_EVENT(AllActiveStates);
|
|
#endif // WITH_STATETREE_DEBUGGER
|
|
|
|
return Result;
|
|
}
|
|
|
|
void FStateTreeExecutionContext::ExitState(const FStateTreeTransitionResult& Transition)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_ExitState);
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (Exec.ActiveFrames.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// On target branch means that the state is the target of current transition or child of it.
|
|
// States which were active before and will remain active, but are not on target branch will not get
|
|
// EnterState called. That is, a transition is handled as "replan from this state".
|
|
bool bOnTargetBranch = false;
|
|
|
|
struct FExitStateCall
|
|
{
|
|
FExitStateCall() = default;
|
|
FExitStateCall(const EStateTreeStateChangeType InChangeType, const bool bInShouldCall)
|
|
: ChangeType(InChangeType)
|
|
, bShouldCall(bInShouldCall)
|
|
{
|
|
}
|
|
|
|
EStateTreeStateChangeType ChangeType = EStateTreeStateChangeType::None;
|
|
bool bShouldCall = false;
|
|
};
|
|
|
|
TArray<FExitStateCall, TConcurrentLinearArrayAllocator<FDefaultBlockAllocationTag>> ExitStateCalls;
|
|
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
FStateTreeInstanceStorage& SharedInstanceDataStorage = CurrentStateTree->GetSharedInstanceData()->GetMutableStorage();
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
const FStateTreeExecutionFrame* NextFrame = nullptr;
|
|
if (Transition.NextActiveFrames.IsValidIndex(FrameIndex)
|
|
&& Transition.NextActiveFrames[FrameIndex].IsSameFrame(CurrentFrame))
|
|
{
|
|
NextFrame = &Transition.NextActiveFrames[FrameIndex];
|
|
}
|
|
|
|
for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
|
|
{
|
|
const FStateTreeStateHandle CurrentHandle = CurrentFrame.ActiveStates[Index];
|
|
const FStateTreeStateHandle NextHandle = NextFrame ? NextFrame->ActiveStates.GetStateSafe(Index) : FStateTreeStateHandle::Invalid;
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[CurrentHandle.Index];
|
|
|
|
FCurrentlyProcessedStateScope StateScope(*this, CurrentHandle);
|
|
|
|
if (State.Type == EStateTreeStateType::Linked)
|
|
{
|
|
if (State.ParameterDataHandle.IsValid()
|
|
&& State.ParameterBindingsBatch.IsValid())
|
|
{
|
|
const FStateTreeDataView StateParamsDataView = GetDataView(CurrentFrame, State.ParameterDataHandle);
|
|
CopyBatch(CurrentFrame, StateParamsDataView, State.ParameterBindingsBatch);
|
|
}
|
|
}
|
|
|
|
const bool bRemainsActive = NextHandle == CurrentHandle;
|
|
bOnTargetBranch = bOnTargetBranch || NextHandle == Transition.TargetState;
|
|
const EStateTreeStateChangeType ChangeType = bRemainsActive ? EStateTreeStateChangeType::Sustained : EStateTreeStateChangeType::Changed;
|
|
|
|
// Should call ExitState() on this state.
|
|
const bool bShouldCall = !bRemainsActive || bOnTargetBranch;
|
|
ExitStateCalls.Emplace(ChangeType, bShouldCall);
|
|
|
|
// Do property copies, ExitState() is called below.
|
|
for (int32 TaskIndex = State.TasksBegin; TaskIndex < (State.TasksBegin + State.TasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskInstanceView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Task.BindingsBatch.IsValid() && Task.bShouldCopyBoundPropertiesOnExitState)
|
|
{
|
|
CopyBatch(CurrentFrame, TaskInstanceView, Task.BindingsBatch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call in reverse order.
|
|
STATETREE_LOG(Log, TEXT("Exit state '%s' (%d)"), *DebugGetStatePath(Exec.ActiveFrames), Exec.StateChangeCount);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::ExitStates);
|
|
|
|
FStateTreeTransitionResult CurrentTransition = Transition;
|
|
int32 CallIndex = ExitStateCalls.Num() - 1;
|
|
|
|
for (int32 FrameIndex = Exec.ActiveFrames.Num() - 1; FrameIndex >= 0; FrameIndex--)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
for (int32 Index = CurrentFrame.ActiveStates.Num() - 1; Index >= 0; Index--)
|
|
{
|
|
const FStateTreeStateHandle CurrentHandle = CurrentFrame.ActiveStates[Index];
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[CurrentHandle.Index];
|
|
|
|
const FExitStateCall& ExitCall = ExitStateCalls[CallIndex--];
|
|
CurrentTransition.ChangeType = ExitCall.ChangeType;
|
|
|
|
STATETREE_LOG(Log, TEXT("%*sState '%s' %s"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *DebugGetStatePath(Exec.ActiveFrames, &CurrentFrame, CurrentHandle.Index), *UEnum::GetDisplayValueAsText(CurrentTransition.ChangeType).ToString());
|
|
|
|
STATETREE_TRACE_STATE_EVENT(CurrentHandle, EStateTreeTraceEventType::OnExiting);
|
|
|
|
if (ExitCall.bShouldCall)
|
|
{
|
|
FCurrentlyProcessedStateScope StateScope(*this, CurrentHandle);
|
|
|
|
// Remove any delayed transitions that belong to this state.
|
|
Exec.DelayedTransitions.RemoveAllSwap(
|
|
[StateTree = CurrentFrame.StateTree, Begin = State.TransitionsBegin, End = State.TransitionsBegin + State.TransitionsNum](const FStateTreeTransitionDelayedState& DelayedState)
|
|
{
|
|
return DelayedState.StateTree == StateTree && DelayedState.TransitionIndex.Get() >= Begin && DelayedState.TransitionIndex.Get() < End;
|
|
});
|
|
|
|
CurrentTransition.CurrentState = CurrentHandle;
|
|
|
|
// Do property copies, ExitState() is called below.
|
|
for (int32 TaskIndex = (State.TasksBegin + State.TasksNum) - 1; TaskIndex >= State.TasksBegin; TaskIndex--)
|
|
{
|
|
|
|
// Call task completed only if EnterState() was called.
|
|
// The task order in the tree (BF) allows us to use the comparison.
|
|
// Relying here that invalid value of Exec.EnterStateFailedTaskIndex == MAX_uint16.
|
|
if (TaskIndex <= Exec.EnterStateFailedTaskIndex.Get())
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskInstanceView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'ExitState' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
const bool bShouldCallStateChange = CurrentTransition.ChangeType == EStateTreeStateChangeType::Changed
|
|
|| (CurrentTransition.ChangeType == EStateTreeStateChangeType::Sustained && Task.bShouldStateChangeOnReselect);
|
|
|
|
if (bShouldCallStateChange)
|
|
{
|
|
|
|
STATETREE_LOG(Verbose, TEXT("%*s Task '%s'"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Task_ExitState);
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Task_ExitState);
|
|
Task.ExitState(*this, CurrentTransition);
|
|
}
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskInstanceView, EStateTreeTraceEventType::OnExited, Transition.CurrentRunStatus);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
STATETREE_TRACE_STATE_EVENT(CurrentHandle, EStateTreeTraceEventType::OnExited);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void FStateTreeExecutionContext::StateCompleted()
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_StateCompleted);
|
|
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (Exec.ActiveFrames.IsEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
STATETREE_LOG(Verbose, TEXT("State Completed %s (%d)"), *UEnum::GetDisplayValueAsText(Exec.LastTickStatus).ToString(), Exec.StateChangeCount);
|
|
|
|
// Call from child towards root to allow to pass results back.
|
|
// Note: Completed is assumed to be called immediately after tick or enter state, so there's no property copying.
|
|
|
|
for (int32 FrameIndex = Exec.ActiveFrames.Num() - 1; FrameIndex >= 0; FrameIndex--)
|
|
{
|
|
const FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
if (FrameIndex <= Exec.EnterStateFailedFrameIndex.Get())
|
|
{
|
|
for (int32 Index = CurrentFrame.ActiveStates.Num() - 1; Index >= 0; Index--)
|
|
{
|
|
const FStateTreeStateHandle CurrentHandle = CurrentFrame.ActiveStates[Index];
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[CurrentHandle.Index];
|
|
|
|
FCurrentlyProcessedStateScope StateScope(*this, CurrentHandle);
|
|
|
|
STATETREE_LOG(Verbose, TEXT("%*sState '%s'"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *DebugGetStatePath(Exec.ActiveFrames, &CurrentFrame, Index));
|
|
STATETREE_TRACE_STATE_EVENT(CurrentHandle, EStateTreeTraceEventType::OnStateCompleted);
|
|
|
|
// Notify Tasks
|
|
for (int32 TaskIndex = (State.TasksBegin + State.TasksNum) - 1; TaskIndex >= State.TasksBegin; TaskIndex--)
|
|
{
|
|
// Call task completed only if EnterState() was called.
|
|
// The task order in the tree (BF) allows us to use the comparison.
|
|
// Relying here that invalid value of Exec.EnterStateFailedTaskIndex == MAX_uint16.
|
|
if (TaskIndex <= Exec.EnterStateFailedTaskIndex.Get())
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'StateCompleted' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
STATETREE_LOG(Verbose, TEXT("%*s Task '%s'"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
Task.StateCompleted(*this, Exec.LastTickStatus, CurrentFrame.ActiveStates);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::TickEvaluatorsAndGlobalTasks(const float DeltaTime, const bool bTickGlobalTasks)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_TickEvaluators);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TickingGlobalTasks);
|
|
|
|
STATETREE_LOG(VeryVerbose, TEXT("Ticking Evaluators & Global Tasks"));
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
EStateTreeRunStatus Result = EStateTreeRunStatus::Running;
|
|
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
if (CurrentFrame.bIsGlobalFrame)
|
|
{
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
// Tick evaluators
|
|
for (int32 EvalIndex = CurrentStateTree->EvaluatorsBegin; EvalIndex < (CurrentStateTree->EvaluatorsBegin + CurrentStateTree->EvaluatorsNum); EvalIndex++)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = CurrentStateTree->Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
const FStateTreeDataView EvalInstanceView = GetDataView(CurrentFrame, Eval.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Eval.BindingsBatch.IsValid())
|
|
{
|
|
CopyBatch(CurrentFrame, EvalInstanceView, Eval.BindingsBatch);
|
|
}
|
|
STATETREE_LOG(VeryVerbose, TEXT(" Tick: '%s'"), *Eval.Name.ToString());
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Eval_Tick);
|
|
Eval.Tick(*this, DeltaTime);
|
|
|
|
STATETREE_TRACE_EVALUATOR_EVENT(EvalIndex, EvalInstanceView, EStateTreeTraceEventType::OnTicked);
|
|
}
|
|
}
|
|
|
|
if (bTickGlobalTasks)
|
|
{
|
|
// Used to stop ticking tasks after one fails, but we still want to keep updating the data views so that property binding works properly.
|
|
bool bShouldTickTasks = true;
|
|
const bool bHasEvents = !EventsToProcess.IsEmpty();
|
|
|
|
for (int32 TaskIndex = CurrentStateTree->GlobalTasksBegin; TaskIndex < (CurrentStateTree->GlobalTasksBegin + CurrentStateTree->GlobalTasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'Tick' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
const bool bNeedsTick = bShouldTickTasks && (Task.bShouldCallTick || (bHasEvents && Task.bShouldCallTickOnlyOnEvents));
|
|
STATETREE_LOG(VeryVerbose, TEXT(" Tick: '%s' %s"), *Task.Name.ToString(), !bNeedsTick ? TEXT("[not ticked]") : TEXT(""));
|
|
if (!bNeedsTick)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Copy bound properties.
|
|
// Only copy properties when the task is actually ticked, and copy properties at tick is requested.
|
|
if (Task.BindingsBatch.IsValid() && Task.bShouldCopyBoundPropertiesOnTick)
|
|
{
|
|
CopyBatch(CurrentFrame, TaskDataView, Task.BindingsBatch);
|
|
}
|
|
|
|
//STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EStateTreeTraceEventType::OnTickingTask, EStateTreeRunStatus::Running);
|
|
EStateTreeRunStatus TaskResult = EStateTreeRunStatus::Unset;
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Task_Tick);
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Task_Tick);
|
|
|
|
TaskResult = Task.Tick(*this, DeltaTime);
|
|
}
|
|
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView,
|
|
TaskResult != EStateTreeRunStatus::Running ? EStateTreeTraceEventType::OnTaskCompleted : EStateTreeTraceEventType::OnTicked,
|
|
TaskResult);
|
|
|
|
// If a global task succeeds or fails, it will stop the whole tree.
|
|
if (TaskResult != EStateTreeRunStatus::Running)
|
|
{
|
|
Result = TaskResult;
|
|
}
|
|
|
|
if (TaskResult == EStateTreeRunStatus::Failed)
|
|
{
|
|
bShouldTickTasks = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::StartEvaluatorsAndGlobalTasks(FStateTreeIndex16& OutLastInitializedTaskIndex)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_StartEvaluators);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::StartGlobalTasks);
|
|
|
|
STATETREE_LOG(Verbose, TEXT("Start Evaluators & Global tasks"));
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
OutLastInitializedTaskIndex = FStateTreeIndex16();
|
|
EStateTreeRunStatus Result = EStateTreeRunStatus::Running;
|
|
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
if (CurrentFrame.bIsGlobalFrame)
|
|
{
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
// Start evaluators
|
|
for (int32 EvalIndex = CurrentStateTree->EvaluatorsBegin; EvalIndex < (CurrentStateTree->EvaluatorsBegin + CurrentStateTree->EvaluatorsNum); EvalIndex++)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = CurrentStateTree->Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
const FStateTreeDataView EvalInstanceView = GetDataView(CurrentFrame, Eval.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Eval.BindingsBatch.IsValid())
|
|
{
|
|
CopyBatch(CurrentFrame, EvalInstanceView, Eval.BindingsBatch);
|
|
}
|
|
STATETREE_LOG(Verbose, TEXT(" Start: '%s'"), *Eval.Name.ToString());
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Eval_TreeStart);
|
|
Eval.TreeStart(*this);
|
|
|
|
STATETREE_TRACE_EVALUATOR_EVENT(EvalIndex, EvalInstanceView, EStateTreeTraceEventType::OnTreeStarted);
|
|
}
|
|
}
|
|
|
|
// Start Global tasks
|
|
// Even if we call Enter/ExitState() on global tasks, they do not enter any specific state.
|
|
const FStateTreeTransitionResult Transition = {}; // Empty transition
|
|
|
|
for (int32 TaskIndex = CurrentStateTree->GlobalTasksBegin; TaskIndex < (CurrentStateTree->GlobalTasksBegin + CurrentStateTree->GlobalTasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Task.BindingsBatch.IsValid())
|
|
{
|
|
CopyBatch(CurrentFrame, TaskDataView, Task.BindingsBatch);
|
|
}
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'EnterState' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
STATETREE_LOG(Verbose, TEXT(" Start: '%s'"), *Task.Name.ToString());
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Task_TreeStart);
|
|
const EStateTreeRunStatus TaskStatus = Task.EnterState(*this, Transition);
|
|
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EStateTreeTraceEventType::OnEntered, TaskStatus);
|
|
|
|
if (TaskStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
OutLastInitializedTaskIndex = FStateTreeIndex16(TaskIndex);
|
|
Result = TaskStatus;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
void FStateTreeExecutionContext::StopEvaluatorsAndGlobalTasks(const EStateTreeRunStatus CompletionStatus, const FStateTreeIndex16 LastInitializedTaskIndex)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_StopEvaluators);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::StopGlobalTasks);
|
|
|
|
STATETREE_LOG(Verbose, TEXT("Stop Evaluators & Global Tasks"));
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
// Update bindings
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
if (CurrentFrame.bIsGlobalFrame)
|
|
{
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
for (int32 EvalIndex = CurrentStateTree->EvaluatorsBegin; EvalIndex < (CurrentStateTree->EvaluatorsBegin + CurrentStateTree->EvaluatorsNum); EvalIndex++)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = CurrentStateTree->Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
const FStateTreeDataView EvalInstanceView = GetDataView(CurrentFrame, Eval.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Eval.BindingsBatch.IsValid())
|
|
{
|
|
CopyBatch(CurrentFrame, EvalInstanceView, Eval.BindingsBatch);
|
|
}
|
|
}
|
|
|
|
for (int32 TaskIndex = CurrentStateTree->GlobalTasksBegin; TaskIndex < (CurrentStateTree->GlobalTasksBegin + CurrentStateTree->GlobalTasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Copy bound properties.
|
|
if (Task.BindingsBatch.IsValid() && Task.bShouldCopyBoundPropertiesOnExitState)
|
|
{
|
|
CopyBatch(CurrentFrame, TaskDataView, Task.BindingsBatch);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call in reverse order.
|
|
FStateTreeTransitionResult Transition;
|
|
Transition.TargetState = FStateTreeStateHandle::FromCompletionStatus(CompletionStatus);
|
|
Transition.CurrentRunStatus = CompletionStatus;
|
|
|
|
for (int32 FrameIndex = Exec.ActiveFrames.Num() - 1; FrameIndex >= 0; FrameIndex--)
|
|
{
|
|
const FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
if (CurrentFrame.bIsGlobalFrame)
|
|
{
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
for (int32 TaskIndex = (CurrentStateTree->GlobalTasksBegin + CurrentStateTree->GlobalTasksNum) - 1; TaskIndex >= CurrentStateTree->GlobalTasksBegin; TaskIndex--)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'ExitState' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
// Relying here that invalid value of LastInitializedTaskIndex == MAX_uint16.
|
|
if (TaskIndex <= LastInitializedTaskIndex.Get())
|
|
{
|
|
STATETREE_LOG(Verbose, TEXT(" Stop: '%s'"), *Task.Name.ToString());
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Task_TreeStop);
|
|
Task.ExitState(*this, Transition);
|
|
}
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EStateTreeTraceEventType::OnExited, Transition.CurrentRunStatus);
|
|
}
|
|
}
|
|
|
|
for (int32 EvalIndex = (CurrentStateTree->EvaluatorsBegin + CurrentStateTree->EvaluatorsNum) - 1; EvalIndex >= CurrentStateTree->EvaluatorsBegin; EvalIndex--)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = CurrentStateTree->Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
const FStateTreeDataView EvalInstanceView = GetDataView(CurrentFrame, Eval.InstanceDataHandle);
|
|
|
|
STATETREE_LOG(Verbose, TEXT(" Stop: '%s'"), *Eval.Name.ToString());
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Eval_TreeStop);
|
|
Eval.TreeStop(*this);
|
|
|
|
STATETREE_TRACE_EVALUATOR_EVENT(EvalIndex, EvalInstanceView, EStateTreeTraceEventType::OnTreeStopped);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::TickTasks(const float DeltaTime)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_TickTasks);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TickingTasks);
|
|
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (Exec.ActiveFrames.IsEmpty())
|
|
{
|
|
return EStateTreeRunStatus::Failed;
|
|
}
|
|
|
|
EStateTreeRunStatus Result = EStateTreeRunStatus::Running;
|
|
int32 NumTotalTasks = 0;
|
|
|
|
const bool bHasEvents = !EventsToProcess.IsEmpty();
|
|
|
|
Exec.CompletedFrameIndex = FStateTreeIndex16::Invalid;
|
|
Exec.CompletedStateHandle = FStateTreeStateHandle::Invalid;
|
|
|
|
// Used to stop ticking tasks after one fails, but we still want to keep updating the data views so that property binding works properly.
|
|
bool bShouldTickTasks = true;
|
|
|
|
STATETREE_CLOG(Exec.ActiveFrames.Num() > 0, VeryVerbose, TEXT("Ticking Tasks"));
|
|
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
const FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
|
|
{
|
|
const FStateTreeStateHandle CurrentHandle = CurrentFrame.ActiveStates[Index];
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[CurrentHandle.Index];
|
|
|
|
FCurrentlyProcessedStateScope StateScope(*this, CurrentHandle);
|
|
STATETREE_TRACE_SCOPED_STATE(CurrentHandle);
|
|
|
|
STATETREE_CLOG(State.TasksNum > 0, VeryVerbose, TEXT("%*sState '%s'"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *DebugGetStatePath(Exec.ActiveFrames, &CurrentFrame, Index));
|
|
|
|
if (State.Type == EStateTreeStateType::Linked)
|
|
{
|
|
if (State.ParameterDataHandle.IsValid()
|
|
&& State.ParameterBindingsBatch.IsValid())
|
|
{
|
|
const FStateTreeDataView StateParamsDataView = GetDataView(CurrentFrame, State.ParameterDataHandle);
|
|
CopyBatch(CurrentFrame, StateParamsDataView, State.ParameterBindingsBatch);
|
|
}
|
|
}
|
|
|
|
// Update Tasks data and tick if possible (ie. if no task has yet failed and so bShouldTickTasks is true)
|
|
for (int32 TaskIndex = State.TasksBegin; TaskIndex < (State.TasksBegin + State.TasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'Tick' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
const bool bNeedsTick = bShouldTickTasks && (Task.bShouldCallTick || (bHasEvents && Task.bShouldCallTickOnlyOnEvents));
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*s Tick: '%s' %s"), Index*UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString(), !bNeedsTick ? TEXT("[not ticked]") : TEXT(""));
|
|
if (!bNeedsTick)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Copy bound properties.
|
|
// Only copy properties when the task is actually ticked, and copy properties at tick is requested.
|
|
if (Task.BindingsBatch.IsValid() && Task.bShouldCopyBoundPropertiesOnTick)
|
|
{
|
|
CopyBatch(CurrentFrame, TaskDataView, Task.BindingsBatch);
|
|
}
|
|
|
|
//STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EStateTreeTraceEventType::OnTickingTask, EStateTreeRunStatus::Running);
|
|
EStateTreeRunStatus TaskResult = EStateTreeRunStatus::Unset;
|
|
{
|
|
QUICK_SCOPE_CYCLE_COUNTER(StateTree_Task_Tick);
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_Task_Tick);
|
|
|
|
TaskResult = Task.Tick(*this, DeltaTime);
|
|
}
|
|
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView,
|
|
TaskResult != EStateTreeRunStatus::Running ? EStateTreeTraceEventType::OnTaskCompleted : EStateTreeTraceEventType::OnTicked,
|
|
TaskResult);
|
|
|
|
// TODO: Add more control over which states can control the failed/succeeded result.
|
|
if (TaskResult != EStateTreeRunStatus::Running)
|
|
{
|
|
// Store the first state that completed, will be used to decide where to trigger transitions.
|
|
if (!Exec.CompletedStateHandle.IsValid())
|
|
{
|
|
Exec.CompletedFrameIndex = FStateTreeIndex16(FrameIndex);
|
|
Exec.CompletedStateHandle = CurrentHandle;
|
|
}
|
|
Result = TaskResult;
|
|
}
|
|
|
|
if (TaskResult == EStateTreeRunStatus::Failed)
|
|
{
|
|
bShouldTickTasks = false;
|
|
}
|
|
}
|
|
NumTotalTasks += State.TasksNum;
|
|
}
|
|
}
|
|
|
|
if (NumTotalTasks == 0)
|
|
{
|
|
// No tasks, done ticking.
|
|
Result = EStateTreeRunStatus::Succeeded;
|
|
Exec.CompletedFrameIndex = FStateTreeIndex16(0);
|
|
Exec.CompletedStateHandle = Exec.ActiveFrames[0].ActiveStates.GetStateSafe(0);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::TestAllConditions(const FStateTreeExecutionFrame& CurrentFrame, const int32 ConditionsOffset, const int32 ConditionsNum)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_TestConditions);
|
|
|
|
if (ConditionsNum == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
TStaticArray<EStateTreeConditionOperand, UE::StateTree::MaxConditionIndent + 1> Operands(InPlace, EStateTreeConditionOperand::Copy);
|
|
TStaticArray<bool, UE::StateTree::MaxConditionIndent + 1> Values(InPlace, false);
|
|
|
|
int32 Level = 0;
|
|
|
|
for (int32 Index = 0; Index < ConditionsNum; Index++)
|
|
{
|
|
const int32 ConditionIndex = ConditionsOffset + Index;
|
|
const FStateTreeConditionBase& Cond = CurrentFrame.StateTree->Nodes[ConditionIndex].Get<const FStateTreeConditionBase>();
|
|
const FStateTreeDataView ConditionDataView = GetDataView(CurrentFrame, Cond.InstanceDataHandle);
|
|
|
|
bool bValue = false;
|
|
if (Cond.EvaluationMode == EStateTreeConditionEvaluationMode::Evaluated)
|
|
{
|
|
// Copy bound properties.
|
|
if (Cond.BindingsBatch.IsValid())
|
|
{
|
|
// Use validated copy, since we test in situations where the sources are not always valid (e.g. enter conditions may try to access inactive parent state).
|
|
if (!CopyBatchWithValidation(CurrentFrame, ConditionDataView, Cond.BindingsBatch))
|
|
{
|
|
// If the source data cannot be accessed, the whole expression evaluates to false.
|
|
Values[0] = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bValue = Cond.TestCondition(*this);
|
|
STATETREE_TRACE_CONDITION_EVENT(ConditionIndex, ConditionDataView, bValue ? EStateTreeTraceEventType::Passed : EStateTreeTraceEventType::Failed);
|
|
|
|
// Reset copied properties that might contain object references.
|
|
if (Cond.BindingsBatch.IsValid())
|
|
{
|
|
CurrentFrame.StateTree->PropertyBindings.ResetObjects(Cond.BindingsBatch, ConditionDataView);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bValue = Cond.EvaluationMode == EStateTreeConditionEvaluationMode::ForcedTrue ? true : /* EStateTreeConditionEvaluationMode::AlwaysFalse */ false;
|
|
STATETREE_TRACE_CONDITION_EVENT(ConditionIndex, FStateTreeDataView{}, bValue ? EStateTreeTraceEventType::Passed : EStateTreeTraceEventType::Failed);
|
|
}
|
|
|
|
const int32 DeltaIndent = Cond.DeltaIndent;
|
|
const int32 OpenParens = FMath::Max(0, DeltaIndent) + 1; // +1 for the current value that is stored at the empty slot at the top of the value stack.
|
|
const int32 ClosedParens = FMath::Max(0, -DeltaIndent) + 1;
|
|
|
|
// Store the operand to apply when merging higher level down when returning to this level.
|
|
// @todo: remove this conditions in 5.1, needs resaving existing StateTrees.
|
|
const EStateTreeConditionOperand Operand = Index == 0 ? EStateTreeConditionOperand::Copy : Cond.Operand;
|
|
Operands[Level] = Operand;
|
|
|
|
// Store current value at the top of the stack.
|
|
Level += OpenParens;
|
|
Values[Level] = bValue;
|
|
|
|
// Evaluate and merge down values based on closed braces.
|
|
// The current value is placed in parens (see +1 above), which makes merging down and applying the new value consistent.
|
|
// The default operand is copy, so if the value is needed immediately, it is just copied down, or if we're on the same level,
|
|
// the operand storing above gives handles with the right logic.
|
|
for (int32 Paren = 0; Paren < ClosedParens; Paren++)
|
|
{
|
|
Level--;
|
|
switch (Operands[Level])
|
|
{
|
|
case EStateTreeConditionOperand::Copy:
|
|
Values[Level] = Values[Level + 1];
|
|
break;
|
|
case EStateTreeConditionOperand::And:
|
|
Values[Level] &= Values[Level + 1];
|
|
break;
|
|
case EStateTreeConditionOperand::Or:
|
|
Values[Level] |= Values[Level + 1];
|
|
break;
|
|
}
|
|
Operands[Level] = EStateTreeConditionOperand::Copy;
|
|
}
|
|
}
|
|
|
|
return Values[0];
|
|
}
|
|
|
|
FString FStateTreeExecutionContext::DebugGetEventsAsString() const
|
|
{
|
|
FString Result;
|
|
for (const FStateTreeEvent& Event : EventsToProcess)
|
|
{
|
|
if (!Result.IsEmpty())
|
|
{
|
|
Result += TEXT(", ");
|
|
}
|
|
Result += Event.Tag.ToString();
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::RequestTransition(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeStateHandle NextState, const EStateTreeTransitionPriority Priority, const EStateTreeSelectionFallback Fallback)
|
|
{
|
|
// Skip lower priority transitions.
|
|
if (NextTransition.Priority >= Priority)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (NextState.IsCompletionState())
|
|
{
|
|
SetupNextTransition(CurrentFrame, NextState, Priority);
|
|
STATETREE_LOG(Verbose, TEXT("Transition on state '%s' -> state '%s'"),
|
|
*GetSafeStateName(CurrentFrame, CurrentFrame.ActiveStates.Last()), *NextState.Describe());
|
|
return true;
|
|
}
|
|
if (!NextState.IsValid())
|
|
{
|
|
// NotSet is no-operation, but can be used to mask a transition at parent state. Returning unset keeps updating current state.
|
|
SetupNextTransition(CurrentFrame, FStateTreeStateHandle::Invalid, Priority);
|
|
return true;
|
|
}
|
|
|
|
TArray<FStateTreeExecutionFrame, TFixedAllocator<MaxExecutionFrames>> NewNextActiveFrames;
|
|
if (SelectState(CurrentFrame, NextState, NewNextActiveFrames, Fallback))
|
|
{
|
|
SetupNextTransition(CurrentFrame, NextState, Priority);
|
|
NextTransition.NextActiveFrames = NewNextActiveFrames;
|
|
|
|
STATETREE_LOG(Verbose, TEXT("Transition on state '%s' -[%s]-> state '%s'"),
|
|
*GetSafeStateName(CurrentFrame, CurrentFrame.ActiveStates.Last()),
|
|
*GetSafeStateName(CurrentFrame, NextState),
|
|
*GetSafeStateName(NextTransition.NextActiveFrames.Last(), NextTransition.NextActiveFrames.Last().ActiveStates.Last()));
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FStateTreeExecutionContext::SetupNextTransition(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeStateHandle NextState, const EStateTreeTransitionPriority Priority)
|
|
{
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
NextTransition.CurrentRunStatus = Exec.LastTickStatus;
|
|
NextTransition.SourceState = CurrentlyProcessedState;
|
|
NextTransition.SourceStateTree = CurrentFrame.StateTree;
|
|
NextTransition.SourceRootState = CurrentFrame.ActiveStates.GetStateSafe(0);
|
|
NextTransition.TargetState = NextState;
|
|
NextTransition.Priority = Priority;
|
|
|
|
FStateTreeExecutionFrame& NewFrame = NextTransition.NextActiveFrames.AddDefaulted_GetRef();
|
|
NewFrame.StateTree = CurrentFrame.StateTree;
|
|
NewFrame.RootState = CurrentFrame.RootState;
|
|
|
|
if (NextState == FStateTreeStateHandle::Invalid)
|
|
{
|
|
NewFrame.ActiveStates = {};
|
|
}
|
|
else
|
|
{
|
|
NewFrame.ActiveStates = FStateTreeActiveStates(NextState);
|
|
}
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::TriggerTransitions()
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_TriggerTransition);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TriggerTransitions);
|
|
|
|
FAllowDirectTransitionsScope AllowDirectTransitionsScope(*this); // Set flag for the scope of this function to allow direct transitions without buffering.
|
|
FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (EventsToProcess.Num() > 0)
|
|
{
|
|
STATETREE_LOG_AND_TRACE(Verbose, TEXT("Trigger transitions with events [%s]"), *DebugGetEventsAsString());
|
|
}
|
|
|
|
NextTransition.Reset();
|
|
|
|
//
|
|
// Process transition requests
|
|
//
|
|
for (const FStateTreeTransitionRequest& Request : InstanceData.GetTransitionRequests())
|
|
{
|
|
// Find frame associated with the request.
|
|
const int32 FrameIndex = Exec.ActiveFrames.IndexOfByPredicate([&Request](const FStateTreeExecutionFrame& Frame)
|
|
{
|
|
return Frame.StateTree == Request.SourceStateTree && Frame.RootState == Request.SourceRootState;
|
|
});
|
|
|
|
if (FrameIndex != INDEX_NONE)
|
|
{
|
|
const FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
if (RequestTransition(CurrentFrame, Request.TargetState, Request.Priority))
|
|
{
|
|
NextTransitionSource = FStateTreeTransitionSource(EStateTreeTransitionSourceType::ExternalRequest, Request.TargetState, Request.Priority);
|
|
}
|
|
}
|
|
}
|
|
InstanceData.ResetTransitionRequests();
|
|
|
|
//
|
|
// Check tick, event, and task based transitions first.
|
|
//
|
|
if (Exec.ActiveFrames.Num() > 0)
|
|
{
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
for (int32 StateIndex = CurrentFrame.ActiveStates.Num() - 1; StateIndex >= 0; StateIndex--)
|
|
{
|
|
const FStateTreeStateHandle StateHandle = CurrentFrame.ActiveStates[StateIndex];
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[StateHandle.Index];
|
|
|
|
// Do not process any transitions from a disabled state
|
|
if (!State.bEnabled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FCurrentlyProcessedStateScope StateScope(*this, StateHandle);
|
|
STATETREE_TRACE_SCOPED_STATE(StateHandle);
|
|
|
|
if (State.bHasTransitionTasks)
|
|
{
|
|
STATETREE_CLOG(State.TasksNum > 0, VeryVerbose, TEXT("%*sTrigger task transitions in state '%s'"), StateIndex*UE::StateTree::DebugIndentSize, TEXT(""), *DebugGetStatePath(Exec.ActiveFrames, &CurrentFrame, StateIndex));
|
|
|
|
for (int32 TaskIndex = (State.TasksBegin + State.TasksNum) - 1; TaskIndex >= State.TasksBegin; TaskIndex--)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'TriggerTransitions' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
if (Task.bShouldAffectTransitions)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sTriggerTransitions: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EStateTreeTraceEventType::OnEvaluating, EStateTreeRunStatus::Running);
|
|
check(TaskDataView.IsValid());
|
|
Task.TriggerTransitions(*this);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (uint8 i = 0; i < State.TransitionsNum; i++)
|
|
{
|
|
// All transition conditions must pass
|
|
const int16 TransitionIndex = State.TransitionsBegin + i;
|
|
const FCompactStateTransition& Transition = CurrentStateTree->Transitions[TransitionIndex];
|
|
|
|
// Skip disabled transitions
|
|
if (Transition.bTransitionEnabled == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// No need to test the transition if same or higher priority transition has already been processed.
|
|
if (Transition.Priority <= NextTransition.Priority)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip completion transitions
|
|
if (EnumHasAnyFlags(Transition.Trigger, EStateTreeTransitionTrigger::OnStateCompleted))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// If a delayed transition has passed the delay, and remove it from the queue, and try trigger it.
|
|
FStateTreeTransitionDelayedState* DelayedState = nullptr;
|
|
if (Transition.HasDelay())
|
|
{
|
|
DelayedState = Exec.FindDelayedTransition(CurrentFrame.StateTree, FStateTreeIndex16(TransitionIndex));
|
|
if (DelayedState != nullptr && DelayedState->TimeLeft <= 0.0f)
|
|
{
|
|
STATETREE_LOG(Verbose, TEXT("Passed delayed transition from '%s' (%s) -> '%s'"),
|
|
*GetSafeStateName(CurrentFrame, CurrentFrame.ActiveStates.Last()), *State.Name.ToString(), *GetSafeStateName(CurrentFrame, Transition.State));
|
|
|
|
Exec.DelayedTransitions.RemoveAllSwap([StateTree = CurrentFrame.StateTree, TransitionIndex](const FStateTreeTransitionDelayedState& DelayedState)
|
|
{
|
|
return DelayedState.StateTree == StateTree && DelayedState.TransitionIndex.Get() == TransitionIndex;
|
|
});
|
|
|
|
// Trigger Delayed Transition when the delay has passed.
|
|
if (RequestTransition(CurrentFrame, Transition.State, Transition.Priority, Transition.Fallback))
|
|
{
|
|
NextTransitionSource = FStateTreeTransitionSource(FStateTreeIndex16(TransitionIndex), Transition.State, Transition.Priority);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const bool bShouldTrigger = Transition.Trigger == EStateTreeTransitionTrigger::OnTick
|
|
|| (Transition.Trigger == EStateTreeTransitionTrigger::OnEvent
|
|
&& HasEventToProcess(Transition.EventTag));
|
|
|
|
bool bPassed = false;
|
|
if (bShouldTrigger)
|
|
{
|
|
STATETREE_TRACE_TRANSITION_EVENT(FStateTreeTransitionSource(FStateTreeIndex16(TransitionIndex), Transition.State, Transition.Priority), EStateTreeTraceEventType::OnEvaluating);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TransitionConditions);
|
|
bPassed = TestAllConditions(CurrentFrame, Transition.ConditionsBegin, Transition.ConditionsNum);
|
|
}
|
|
|
|
if (bPassed)
|
|
{
|
|
// If the transitions is delayed, set up the delay.
|
|
if (Transition.HasDelay())
|
|
{
|
|
if (DelayedState == nullptr)
|
|
{
|
|
// Initialize new delayed transition.
|
|
const float DelayDuration = Transition.Delay.GetRandomDuration();
|
|
if (DelayDuration > 0.0f)
|
|
{
|
|
DelayedState = &Exec.DelayedTransitions.AddDefaulted_GetRef();
|
|
DelayedState->StateTree = CurrentFrame.StateTree;
|
|
DelayedState->TransitionIndex = FStateTreeIndex16(TransitionIndex);
|
|
DelayedState->TimeLeft = DelayDuration;
|
|
BeginDelayedTransition(*DelayedState);
|
|
STATETREE_LOG(Verbose, TEXT("Delayed transition triggered from '%s' (%s) -> '%s' %.1fs"),
|
|
*GetSafeStateName(CurrentFrame, CurrentFrame.ActiveStates.Last()), *State.Name.ToString(), *GetSafeStateName(CurrentFrame, Transition.State), DelayedState->TimeLeft);
|
|
|
|
// Delay state added, skip requesting the transition.
|
|
continue;
|
|
}
|
|
// Fallthrough to request transition if duration was zero.
|
|
}
|
|
else
|
|
{
|
|
// We get here if the transitions re-triggers during the delay, on which case we'll just ignore it.
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (RequestTransition(CurrentFrame, Transition.State, Transition.Priority, Transition.Fallback))
|
|
{
|
|
NextTransitionSource = FStateTreeTransitionSource(FStateTreeIndex16(TransitionIndex), Transition.State, Transition.Priority);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (CurrentFrame.bIsGlobalFrame)
|
|
{
|
|
// Global frame
|
|
if (CurrentFrame.StateTree->bHasGlobalTransitionTasks)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("Trigger global task transitions"));
|
|
for (int32 TaskIndex = (CurrentStateTree->GlobalTasksBegin + CurrentStateTree->GlobalTasksNum) - 1; TaskIndex >= CurrentFrame.StateTree->GlobalTasksBegin; TaskIndex--)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
const FStateTreeDataView TaskDataView = GetDataView(CurrentFrame, Task.InstanceDataHandle);
|
|
|
|
// Ignore disabled task
|
|
if (Task.bTaskEnabled == false)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sSkipped 'TriggerTransitions' for disabled Task: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
continue;
|
|
}
|
|
|
|
if (Task.bShouldAffectTransitions)
|
|
{
|
|
STATETREE_LOG(VeryVerbose, TEXT("%*sTriggerTransitions: '%s'"), UE::StateTree::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
STATETREE_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EStateTreeTraceEventType::OnEvaluating, EStateTreeRunStatus::Running);
|
|
check(TaskDataView.IsValid());
|
|
Task.TriggerTransitions(*this);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Check state completion transitions.
|
|
//
|
|
bool bProcessSubTreeCompletion = true;
|
|
|
|
if (NextTransition.Priority == EStateTreeTransitionPriority::None
|
|
&& Exec.LastTickStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
// Start from the last completed state if specified.
|
|
const int32 FrameStartIndex = Exec.CompletedFrameIndex.IsValid() ? Exec.CompletedFrameIndex.AsInt32() : (Exec.ActiveFrames.Num() - 1);
|
|
check(FrameStartIndex >= 0 && FrameStartIndex < Exec.ActiveFrames.Num());
|
|
|
|
for (int32 FrameIndex = FrameStartIndex; FrameIndex >= 0; FrameIndex--)
|
|
{
|
|
FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, FrameIndex);
|
|
|
|
int32 StateStartIndex = CurrentFrame.ActiveStates.Num() - 1; // This is ok, even if the ActiveStates is 0, -1 will skip the whole state loop below.
|
|
if (FrameIndex == FrameStartIndex
|
|
&& Exec.CompletedStateHandle.IsValid())
|
|
{
|
|
StateStartIndex = CurrentFrame.ActiveStates.IndexOfReverse(Exec.CompletedStateHandle);
|
|
// INDEX_NONE (-1) will skip the whole state loop below. We still want to warn.
|
|
ensureMsgf(StateStartIndex != INDEX_NONE, TEXT("If CompletedFrameIndex and CompletedStateHandle are specified, we expect that the state is found"));
|
|
}
|
|
|
|
const EStateTreeTransitionTrigger CompletionTrigger = Exec.LastTickStatus == EStateTreeRunStatus::Succeeded ? EStateTreeTransitionTrigger::OnStateSucceeded : EStateTreeTransitionTrigger::OnStateFailed;
|
|
|
|
// Check completion transitions
|
|
for (int32 StateIndex = StateStartIndex; StateIndex >= 0; StateIndex--)
|
|
{
|
|
const FStateTreeStateHandle StateHandle = CurrentFrame.ActiveStates[StateIndex];
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[StateHandle.Index];
|
|
|
|
FCurrentlyProcessedStateScope StateScope(*this, StateHandle);
|
|
STATETREE_TRACE_SCOPED_STATE_PHASE(StateHandle, EStateTreeUpdatePhase::TriggerTransitions);
|
|
|
|
for (uint8 i = 0; i < State.TransitionsNum; i++)
|
|
{
|
|
// All transition conditions must pass
|
|
const int16 TransitionIndex = State.TransitionsBegin + i;
|
|
const FCompactStateTransition& Transition = CurrentStateTree->Transitions[TransitionIndex];
|
|
|
|
// Skip disabled transitions
|
|
if (Transition.bTransitionEnabled == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (EnumHasAnyFlags(Transition.Trigger, CompletionTrigger))
|
|
{
|
|
bool bPassed = false;
|
|
{
|
|
STATETREE_TRACE_TRANSITION_EVENT(FStateTreeTransitionSource(FStateTreeIndex16(TransitionIndex), Transition.State, Transition.Priority), EStateTreeTraceEventType::OnEvaluating);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TransitionConditions);
|
|
bPassed = TestAllConditions(CurrentFrame, Transition.ConditionsBegin, Transition.ConditionsNum);
|
|
}
|
|
|
|
if (bPassed)
|
|
{
|
|
// No delay allowed on completion conditions.
|
|
// No priority on completion transitions, use the priority to signal that state is selected.
|
|
if (RequestTransition(CurrentFrame, Transition.State, EStateTreeTransitionPriority::Normal, Transition.Fallback))
|
|
{
|
|
NextTransitionSource = FStateTreeTransitionSource(FStateTreeIndex16(TransitionIndex), Transition.State, Transition.Priority);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (NextTransition.Priority != EStateTreeTransitionPriority::None)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (NextTransition.Priority == EStateTreeTransitionPriority::None)
|
|
{
|
|
STATETREE_LOG_AND_TRACE(Verbose, TEXT("Could not trigger completion transition, jump back to root state."));
|
|
FCurrentlyProcessedStateScope StateScope(*this, FStateTreeStateHandle::Root);
|
|
if (RequestTransition(CurrentFrame, FStateTreeStateHandle::Root, EStateTreeTransitionPriority::Normal))
|
|
{
|
|
NextTransitionSource = FStateTreeTransitionSource(EStateTreeTransitionSourceType::Internal, FStateTreeStateHandle::Root, EStateTreeTransitionPriority::Normal);
|
|
}
|
|
else
|
|
{
|
|
STATETREE_LOG_AND_TRACE(Warning, TEXT("Failed to select root state. Stopping the tree with failure."));
|
|
|
|
SetupNextTransition(CurrentFrame, FStateTreeStateHandle::Failed, EStateTreeTransitionPriority::Critical);
|
|
|
|
// In this case we don't want to complete subtrees, we want to force the whole tree to stop.
|
|
bProcessSubTreeCompletion = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if the transition was succeed/failed, if we're on a sub-tree, complete the subtree instead of transition.
|
|
if (NextTransition.TargetState.IsCompletionState() && bProcessSubTreeCompletion)
|
|
{
|
|
const int32 SourceFrameIndex = Exec.ActiveFrames.IndexOfByPredicate([&NextTransition = NextTransition](const FStateTreeExecutionFrame& Frame)
|
|
{
|
|
return Frame.StateTree == NextTransition.SourceStateTree && Frame.RootState == NextTransition.SourceRootState;
|
|
});
|
|
// Check that the transition source frame is a sub-tree, the first frame (0 index) is not a subtree.
|
|
if (SourceFrameIndex > 0)
|
|
{
|
|
const FStateTreeExecutionFrame& SourceFrame = Exec.ActiveFrames[SourceFrameIndex];
|
|
const int32 ParentFrameIndex = SourceFrameIndex - 1;
|
|
const FStateTreeExecutionFrame& ParentFrame = Exec.ActiveFrames[ParentFrameIndex];
|
|
const FStateTreeStateHandle ParentLinkedState = ParentFrame.ActiveStates.Last();
|
|
|
|
if (ParentLinkedState.IsValid())
|
|
{
|
|
const EStateTreeRunStatus RunStatus = NextTransition.TargetState.ToCompletionStatus();
|
|
STATETREE_LOG(Verbose, TEXT("Completed subtree '%s' from state '%s': %s"),
|
|
*GetSafeStateName(ParentFrame, ParentLinkedState), *GetSafeStateName(SourceFrame, NextTransition.SourceState), *UEnum::GetDisplayValueAsText(RunStatus).ToString());
|
|
|
|
// Set the parent linked state as last completed state, and update tick status to the status from the transition.
|
|
Exec.CompletedFrameIndex = FStateTreeIndex16(ParentFrameIndex);
|
|
Exec.CompletedStateHandle = ParentLinkedState;
|
|
Exec.LastTickStatus = RunStatus;
|
|
|
|
// Clear the transition and return that no transition took place.
|
|
// Since the LastTickStatus != running, the transition loop will try another transition
|
|
// now starting from the linked parent state. If we run out of retires in the selection loop (e.g. very deep hierarchy)
|
|
// we will continue on next tick.
|
|
NextTransition.Reset();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return NextTransition.TargetState.IsValid();
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::SelectState(const FStateTreeExecutionFrame& CurrentFrame,
|
|
const FStateTreeStateHandle NextState,
|
|
TArray<FStateTreeExecutionFrame, TFixedAllocator<MaxExecutionFrames>>& OutNextActiveFrames,
|
|
const EStateTreeSelectionFallback Fallback)
|
|
{
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (Exec.ActiveFrames.IsEmpty())
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: SelectState can only be called on initialized tree. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return false;
|
|
}
|
|
|
|
if (!NextState.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Walk towards the root from current state.
|
|
TStaticArray<FStateTreeStateHandle, FStateTreeActiveStates::MaxStates> ParentStates;
|
|
int32 NumParentStates = 0;
|
|
FStateTreeStateHandle CurrState = NextState;
|
|
while (CurrState.IsValid())
|
|
{
|
|
if (NumParentStates == FStateTreeActiveStates::MaxStates)
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Reached max execution depth when trying to select state %s from '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetSafeStateName(CurrentFrame, NextState), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return false;
|
|
}
|
|
// Store the states that are in between the 'NextState' and common ancestor.
|
|
ParentStates[NumParentStates++] = CurrState;
|
|
CurrState = CurrentFrame.StateTree->States[CurrState.Index].Parent;
|
|
}
|
|
|
|
const UStateTree* NextStateTree = CurrentFrame.StateTree;
|
|
const FStateTreeStateHandle NextRootState = ParentStates[NumParentStates - 1];
|
|
|
|
// Find the frame that the next state belongs to.
|
|
int32 CurrentFrameIndex = INDEX_NONE;
|
|
int32 CurrentStateTreeIndex = INDEX_NONE;
|
|
|
|
for (int32 FrameIndex = Exec.ActiveFrames.Num() - 1; FrameIndex >= 0; FrameIndex--)
|
|
{
|
|
const FStateTreeExecutionFrame& Frame = Exec.ActiveFrames[FrameIndex];
|
|
if (Frame.StateTree == NextStateTree)
|
|
{
|
|
CurrentStateTreeIndex = FrameIndex;
|
|
if (Frame.RootState == NextRootState)
|
|
{
|
|
CurrentFrameIndex = FrameIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy common frames over.
|
|
if (CurrentFrameIndex != INDEX_NONE)
|
|
{
|
|
const int32 NumCommonFrames = CurrentFrameIndex + 1;
|
|
OutNextActiveFrames = MakeArrayView(Exec.ActiveFrames.GetData(), NumCommonFrames);
|
|
}
|
|
else if (CurrentStateTreeIndex != INDEX_NONE)
|
|
{
|
|
// If we could not find a common frame, we assume that we jumped to different subtree in same asset.
|
|
const int32 NumCommonFrames = CurrentStateTreeIndex + 1;
|
|
OutNextActiveFrames = MakeArrayView(Exec.ActiveFrames.GetData(), NumCommonFrames);
|
|
}
|
|
else
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Encountered unrecognized state %s during state selection from '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetNameSafe(NextStateTree), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(&RootStateTree));
|
|
return false;
|
|
}
|
|
|
|
// Append in between state in reverse order, they were collected from leaf towards the root.
|
|
// Note: NextState will be added by SelectStateInternal() if conditions pass.
|
|
const int32 LastFrameIndex = OutNextActiveFrames.Num() - 1;
|
|
FStateTreeExecutionFrame& LastFrame = OutNextActiveFrames[LastFrameIndex];
|
|
|
|
LastFrame.ActiveStates.Reset();
|
|
for (int32 Index = NumParentStates - 1; Index > 0; Index--)
|
|
{
|
|
LastFrame.ActiveStates.Push(ParentStates[Index]);
|
|
}
|
|
|
|
TArray<FStateTreeExecutionFrame, TFixedAllocator<MaxExecutionFrames>> InitialNextActiveFrames;
|
|
|
|
if (Fallback == EStateTreeSelectionFallback::NextSelectableSibling)
|
|
{
|
|
InitialNextActiveFrames = OutNextActiveFrames;
|
|
}
|
|
|
|
// We take copy of the last frame and assign it later, as SelectStateInternal() might change the array and invalidate the pointer.
|
|
if (SelectStateInternal(OutNextActiveFrames[LastFrameIndex], NextState, OutNextActiveFrames))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Failed to Select Next State, handle fallback here
|
|
// Return true on the first next sibling that gets selected successfully
|
|
if (Fallback == EStateTreeSelectionFallback::NextSelectableSibling && NumParentStates >= 2)
|
|
{
|
|
// InBetweenStates is in reversed order (i.e. from leaf to root)
|
|
const FStateTreeStateHandle Parent = ParentStates[1];
|
|
if (Parent.IsValid())
|
|
{
|
|
const FCompactStateTreeState& ParentState = CurrentFrame.StateTree->States[Parent.Index];
|
|
|
|
uint16 ChildState = CurrentFrame.StateTree->States[NextState.Index].GetNextSibling();
|
|
for (; ChildState < ParentState.ChildrenEnd; ChildState = CurrentFrame.StateTree->States[ChildState].GetNextSibling())
|
|
{
|
|
FStateTreeStateHandle ChildStateHandle = FStateTreeStateHandle(ChildState);
|
|
|
|
// Start selection from blank slate.
|
|
OutNextActiveFrames = InitialNextActiveFrames;
|
|
|
|
// We take copy of the last frame and assign it later, as SelectStateInternal() might change the array and invalidate the pointer.
|
|
if (SelectStateInternal(OutNextActiveFrames[LastFrameIndex], ChildStateHandle, OutNextActiveFrames))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FStateTreeExecutionContext::SelectStateInternal(FStateTreeExecutionFrame& CurrentFrame, const FStateTreeStateHandle NextState,
|
|
TArray<FStateTreeExecutionFrame, TFixedAllocator<MaxExecutionFrames>>& OutNextActiveFrames)
|
|
{
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(StateTree_SelectState);
|
|
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
if (!NextState.IsValid())
|
|
{
|
|
// Trying to select non-existing state.
|
|
STATETREE_LOG(Error, TEXT("%hs: Trying to select invalid state from '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
|
|
return false;
|
|
}
|
|
|
|
FCurrentlyProcessedFrameScope FrameScope(*this, CurrentFrame, INDEX_NONE);
|
|
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
const FCompactStateTreeState& CurrentState = CurrentStateTree->States[NextState.Index];
|
|
|
|
if (CurrentState.bEnabled == false)
|
|
{
|
|
// Do not select disabled state
|
|
STATETREE_LOG(VeryVerbose, TEXT("%hs: Ignoring disabled state '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetSafeStateName(CurrentFrame, NextState), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
|
|
return false;
|
|
}
|
|
|
|
STATETREE_TRACE_SCOPED_STATE_PHASE(NextState, EStateTreeUpdatePhase::StateSelection);
|
|
|
|
// The state cannot be directly selected.
|
|
if (CurrentState.SelectionBehavior == EStateTreeStateSelectionBehavior::None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check that the state can be entered
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::EnterConditions);
|
|
const bool bEnterConditionsPassed = TestAllConditions(CurrentFrame, CurrentState.EnterConditionsBegin, CurrentState.EnterConditionsNum);
|
|
|
|
if (bEnterConditionsPassed)
|
|
{
|
|
if (!CurrentFrame.ActiveStates.Push(NextState))
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Reached max execution depth when trying to select state %s from '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetSafeStateName(CurrentFrame, NextState), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
|
|
return false;
|
|
}
|
|
|
|
if (CurrentState.Type == EStateTreeStateType::Linked && CurrentState.LinkedState.IsValid())
|
|
{
|
|
if (OutNextActiveFrames.Num() == MaxExecutionFrames)
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Reached max execution depth when trying to select state %s from '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetSafeStateName(CurrentFrame, NextState), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
|
|
return false;
|
|
}
|
|
|
|
FStateTreeExecutionFrame NewFrame;
|
|
NewFrame.StateTree = CurrentFrame.StateTree;
|
|
NewFrame.RootState = CurrentState.LinkedState;
|
|
NewFrame.GlobalInstanceIndexBase = CurrentFrame.GlobalInstanceIndexBase;
|
|
|
|
// If the Frame already exists, copy instance indices so that conditions that rely on active states work correctly.
|
|
const FStateTreeExecutionFrame* ExistingFrame = Exec.ActiveFrames.FindByPredicate(
|
|
[StateTree = CurrentFrame.StateTree, RootState = CurrentState.LinkedState](const FStateTreeExecutionFrame& Frame)
|
|
{
|
|
return Frame.StateTree == StateTree && Frame.RootState == RootState;
|
|
});
|
|
if (ExistingFrame)
|
|
{
|
|
NewFrame.ActiveInstanceIndexBase = ExistingFrame->ActiveInstanceIndexBase;
|
|
NewFrame.StateParameterDataIndex = ExistingFrame->StateParameterDataIndex;
|
|
// No need to update the global instance index base since we're on the same asset.
|
|
}
|
|
|
|
// Check and prevent recursion.
|
|
const bool bNewFrameAlreadySelected = OutNextActiveFrames.ContainsByPredicate([&NewFrame](const FStateTreeExecutionFrame& Frame) {
|
|
return Frame.IsSameFrame(NewFrame);
|
|
});
|
|
|
|
if (bNewFrameAlreadySelected)
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Trying to recursively enter subtree '%s' from '%s'. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetSafeStateName(NewFrame, NewFrame.RootState), *GetStateStatusString(Exec), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
|
|
return false;
|
|
}
|
|
|
|
OutNextActiveFrames.Push(NewFrame);
|
|
|
|
// If State is linked, proceed to the linked state.
|
|
if (SelectStateInternal(OutNextActiveFrames.Last(), CurrentState.LinkedState, OutNextActiveFrames))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
OutNextActiveFrames.Pop();
|
|
}
|
|
else if (CurrentState.SelectionBehavior == EStateTreeStateSelectionBehavior::TryEnterState)
|
|
{
|
|
// Select this state.
|
|
STATETREE_TRACE_STATE_EVENT(NextState, EStateTreeTraceEventType::OnStateSelected);
|
|
return true;
|
|
}
|
|
else if (CurrentState.SelectionBehavior == EStateTreeStateSelectionBehavior::TryFollowTransitions)
|
|
{
|
|
EStateTreeTransitionPriority CurrentPriority = EStateTreeTransitionPriority::None;
|
|
|
|
for (uint8 i = 0; i < CurrentState.TransitionsNum; i++)
|
|
{
|
|
const int16 TransitionIndex = CurrentState.TransitionsBegin + i;
|
|
const FCompactStateTransition& Transition = RootStateTree.Transitions[TransitionIndex];
|
|
|
|
// Skip disabled transitions
|
|
if (Transition.bTransitionEnabled == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// No need to test the transition if same or higher priority transition has already been processed.
|
|
if (Transition.Priority <= CurrentPriority)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip completion transitions
|
|
if (EnumHasAnyFlags(Transition.Trigger, EStateTreeTransitionTrigger::OnStateCompleted))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Cannot follow transitions with delay.
|
|
if (Transition.HasDelay())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Try to prevent (infinite) loops in the selection.
|
|
if (CurrentFrame.ActiveStates.Contains(Transition.State))
|
|
{
|
|
STATETREE_LOG(Error, TEXT("%hs: Loop detected when trying to select state %s from '%s'. Prior states: %s. '%s' using StateTree '%s'."),
|
|
__FUNCTION__, *GetSafeStateName(CurrentFrame, NextState), *GetStateStatusString(Exec), *DebugGetStatePath(OutNextActiveFrames, &CurrentFrame), *GetNameSafe(&Owner), *GetFullNameSafe(CurrentFrame.StateTree));
|
|
continue;
|
|
}
|
|
|
|
const bool bShouldTrigger = Transition.Trigger == EStateTreeTransitionTrigger::OnTick
|
|
|| (Transition.Trigger == EStateTreeTransitionTrigger::OnEvent
|
|
&& HasEventToProcess(Transition.EventTag));
|
|
|
|
bool bTransitionConditionsPassed = false;
|
|
if (bShouldTrigger)
|
|
{
|
|
STATETREE_TRACE_TRANSITION_EVENT(FStateTreeTransitionSource(FStateTreeIndex16(TransitionIndex), Transition.State, Transition.Priority), EStateTreeTraceEventType::OnEvaluating);
|
|
STATETREE_TRACE_SCOPED_PHASE(EStateTreeUpdatePhase::TransitionConditions);
|
|
bTransitionConditionsPassed = bShouldTrigger && TestAllConditions(CurrentFrame, Transition.ConditionsBegin, Transition.ConditionsNum);
|
|
}
|
|
|
|
if (bTransitionConditionsPassed)
|
|
{
|
|
// Using SelectState() instead of SelectStateInternal to treat the transitions the same way as regular transitions,
|
|
// e.g. it may jump to a completely different branch.
|
|
TArray<FStateTreeExecutionFrame, TFixedAllocator<MaxExecutionFrames>> NewActiveFrames;
|
|
if (SelectState(CurrentFrame, Transition.State, NewActiveFrames, Transition.Fallback))
|
|
{
|
|
// Selection succeeded.
|
|
// Cannot break yet because higher priority transitions may override the selection.
|
|
OutNextActiveFrames = NewActiveFrames;
|
|
CurrentPriority = Transition.Priority;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (CurrentPriority != EStateTreeTransitionPriority::None)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else if (CurrentState.SelectionBehavior == EStateTreeStateSelectionBehavior::TrySelectChildrenInOrder)
|
|
{
|
|
if (CurrentState.HasChildren())
|
|
{
|
|
// If the state has children, proceed to select children.
|
|
for (uint16 ChildState = CurrentState.ChildrenBegin; ChildState < CurrentState.ChildrenEnd; ChildState = CurrentStateTree->States[ChildState].GetNextSibling())
|
|
{
|
|
if (SelectStateInternal(CurrentFrame, FStateTreeStateHandle(ChildState), OutNextActiveFrames))
|
|
{
|
|
// Selection succeeded
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Select this state (For backwards compatibility)
|
|
STATETREE_TRACE_STATE_EVENT(NextState, EStateTreeTraceEventType::OnStateSelected);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
CurrentFrame.ActiveStates.Pop();
|
|
}
|
|
|
|
// Nothing got selected.
|
|
return false;
|
|
}
|
|
|
|
FString FStateTreeExecutionContext::GetSafeStateName(const FStateTreeExecutionFrame& CurrentFrame, const FStateTreeStateHandle State) const
|
|
{
|
|
if (State == FStateTreeStateHandle::Invalid)
|
|
{
|
|
return TEXT("(State Invalid)");
|
|
}
|
|
else if (State == FStateTreeStateHandle::Succeeded)
|
|
{
|
|
return TEXT("(State Succeeded)");
|
|
}
|
|
else if (State == FStateTreeStateHandle::Failed)
|
|
{
|
|
return TEXT("(State Failed)");
|
|
}
|
|
else if (CurrentFrame.StateTree && CurrentFrame.StateTree->States.IsValidIndex(State.Index))
|
|
{
|
|
return *CurrentFrame.StateTree->States[State.Index].Name.ToString();
|
|
}
|
|
return TEXT("(Unknown)");
|
|
}
|
|
|
|
FString FStateTreeExecutionContext::DebugGetStatePath(TConstArrayView<FStateTreeExecutionFrame> ActiveFrames, const FStateTreeExecutionFrame* CurrentFrame, const int32 ActiveStateIndex) const
|
|
{
|
|
FString StatePath;
|
|
const UStateTree* LastStateTree = &RootStateTree;
|
|
|
|
for (const FStateTreeExecutionFrame& Frame : ActiveFrames)
|
|
{
|
|
if (!ensure(Frame.StateTree))
|
|
{
|
|
return StatePath;
|
|
}
|
|
|
|
// If requested up the active state, clamp count.
|
|
int32 Num = Frame.ActiveStates.Num();
|
|
if (CurrentFrame == &Frame && Frame.ActiveStates.IsValidIndex(ActiveStateIndex))
|
|
{
|
|
Num = ActiveStateIndex + 1;
|
|
}
|
|
|
|
if (Frame.StateTree != LastStateTree)
|
|
{
|
|
StatePath.Appendf(TEXT("[%s]"), *GetNameSafe(Frame.StateTree));
|
|
LastStateTree = Frame.StateTree;
|
|
}
|
|
|
|
for (int32 i = 0; i < Num; i++)
|
|
{
|
|
const FCompactStateTreeState& State = Frame.StateTree->States[Frame.ActiveStates[i].Index];
|
|
StatePath.Appendf(TEXT("%s%s"), i == 0 ? TEXT("") : TEXT("."), *State.Name.ToString());
|
|
}
|
|
}
|
|
|
|
return StatePath;
|
|
}
|
|
|
|
FString FStateTreeExecutionContext::GetStateStatusString(const FStateTreeExecutionState& ExecState) const
|
|
{
|
|
if (ExecState.TreeRunStatus != EStateTreeRunStatus::Running)
|
|
{
|
|
return TEXT("--:") + UEnum::GetDisplayValueAsText(ExecState.LastTickStatus).ToString();
|
|
}
|
|
return GetSafeStateName(ExecState.ActiveFrames.Last(), ExecState.ActiveFrames.Last().ActiveStates.Last()) + TEXT(":") + UEnum::GetDisplayValueAsText(ExecState.LastTickStatus).ToString();
|
|
}
|
|
|
|
EStateTreeRunStatus FStateTreeExecutionContext::GetLastTickStatus() const
|
|
{
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
return Exec.LastTickStatus;
|
|
}
|
|
|
|
FString FStateTreeExecutionContext::GetInstanceDescription() const
|
|
{
|
|
return FString::Printf(TEXT("%s"), *GetNameSafe(&Owner));
|
|
}
|
|
|
|
TConstArrayView<FStateTreeExecutionFrame> FStateTreeExecutionContext::GetActiveFrames() const
|
|
{
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
return Exec.ActiveFrames;
|
|
}
|
|
|
|
|
|
#if WITH_GAMEPLAY_DEBUGGER
|
|
|
|
FString FStateTreeExecutionContext::GetDebugInfoString() const
|
|
{
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
FString DebugString = FString::Printf(TEXT("StateTree (asset: '%s')\n"), *GetFullNameSafe(&RootStateTree));
|
|
|
|
DebugString += TEXT("Status: ");
|
|
switch (Exec.TreeRunStatus)
|
|
{
|
|
case EStateTreeRunStatus::Failed:
|
|
DebugString += TEXT("Failed\n");
|
|
break;
|
|
case EStateTreeRunStatus::Succeeded:
|
|
DebugString += TEXT("Succeeded\n");
|
|
break;
|
|
case EStateTreeRunStatus::Running:
|
|
DebugString += TEXT("Running\n");
|
|
break;
|
|
default:
|
|
DebugString += TEXT("--\n");
|
|
}
|
|
|
|
// Active States
|
|
DebugString += TEXT("Current State:\n");
|
|
for (const FStateTreeExecutionFrame& CurrentFrame : Exec.ActiveFrames)
|
|
{
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
if (CurrentFrame.bIsGlobalFrame)
|
|
{
|
|
DebugString += FString::Printf(TEXT("\nEvaluators\n [ %-30s | %8s | %15s ]\n"),
|
|
TEXT("Name"), TEXT("Bindings"), TEXT("Data Handle"));
|
|
for (int32 EvalIndex = CurrentStateTree->EvaluatorsBegin; EvalIndex < (CurrentStateTree->EvaluatorsBegin + CurrentStateTree->EvaluatorsNum); EvalIndex++)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = CurrentStateTree->Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
DebugString += FString::Printf(TEXT("| %-30s | %8d | %15s |\n"),
|
|
*Eval.Name.ToString(), Eval.BindingsBatch.Get(), *Eval.InstanceDataHandle.Describe());
|
|
}
|
|
|
|
DebugString += FString::Printf(TEXT("\nGlobal Tasks\n"));
|
|
for (int32 TaskIndex = CurrentStateTree->GlobalTasksBegin; TaskIndex < (CurrentStateTree->GlobalTasksBegin + CurrentStateTree->GlobalTasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = CurrentStateTree->Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
if (Task.bTaskEnabled)
|
|
{
|
|
Task.AppendDebugInfoString(DebugString, *this);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
|
|
{
|
|
FStateTreeStateHandle Handle = CurrentFrame.ActiveStates[Index];
|
|
if (Handle.IsValid())
|
|
{
|
|
const FCompactStateTreeState& State = RootStateTree.States[Handle.Index];
|
|
DebugString += FString::Printf(TEXT("[%s]\n"), *State.Name.ToString());
|
|
|
|
if (State.TasksNum > 0)
|
|
{
|
|
DebugString += TEXT("\nTasks:\n");
|
|
for (int32 TaskIndex = State.TasksBegin; TaskIndex < (State.TasksBegin + State.TasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = RootStateTree.Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
if (Task.bTaskEnabled)
|
|
{
|
|
Task.AppendDebugInfoString(DebugString, *this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return DebugString;
|
|
}
|
|
#endif // WITH_GAMEPLAY_DEBUGGER
|
|
|
|
#if WITH_STATETREE_DEBUG
|
|
void FStateTreeExecutionContext::DebugPrintInternalLayout()
|
|
{
|
|
LOG_SCOPE_VERBOSITY_OVERRIDE(LogStateTree, ELogVerbosity::Log);
|
|
|
|
// @todo: this looks like more a UStateTree thing...
|
|
|
|
FString DebugString = FString::Printf(TEXT("StateTree (asset: '%s')\n"), *GetFullNameSafe(&RootStateTree));
|
|
|
|
// Tree items (e.g. tasks, evaluators, conditions)
|
|
DebugString += FString::Printf(TEXT("\nItems(%d)\n"), RootStateTree.Nodes.Num());
|
|
for (int32 Index = 0; Index < RootStateTree.Nodes.Num(); Index++)
|
|
{
|
|
const FConstStructView Node = RootStateTree.Nodes[Index];
|
|
DebugString += FString::Printf(TEXT(" %s\n"), Node.IsValid() ? *Node.GetScriptStruct()->GetName() : TEXT("null"));
|
|
}
|
|
|
|
// Instance InstanceData data (e.g. tasks)
|
|
DebugString += FString::Printf(TEXT("\nInstance Data(%d)\n"), RootStateTree.DefaultInstanceData.Num());
|
|
for (int32 Index = 0; Index < RootStateTree.DefaultInstanceData.Num(); Index++)
|
|
{
|
|
if (RootStateTree.DefaultInstanceData.IsObject(Index))
|
|
{
|
|
const UObject* Data = RootStateTree.DefaultInstanceData.GetObject(Index);
|
|
DebugString += FString::Printf(TEXT(" %s\n"), *GetNameSafe(Data));
|
|
}
|
|
else
|
|
{
|
|
const FConstStructView Data = RootStateTree.DefaultInstanceData.GetStruct(Index);
|
|
DebugString += FString::Printf(TEXT(" %s\n"), Data.IsValid() ? *Data.GetScriptStruct()->GetName() : TEXT("null"));
|
|
}
|
|
}
|
|
|
|
// External data (e.g. fragments, subsystems)
|
|
DebugString += FString::Printf(TEXT("\nExternal Data(%d)\n [ %-40s | %-8s | %15s ]\n"), RootStateTree.ExternalDataDescs.Num(), TEXT("Name"), TEXT("Optional"), TEXT("Handle"));
|
|
for (const FStateTreeExternalDataDesc& Desc : RootStateTree.ExternalDataDescs)
|
|
{
|
|
DebugString += FString::Printf(TEXT(" | %-40s | %8s | %15s |\n"), Desc.Struct ? *Desc.Struct->GetName() : TEXT("null"), *UEnum::GetDisplayValueAsText(Desc.Requirement).ToString(), *Desc.Handle.DataHandle.Describe());
|
|
}
|
|
|
|
// Bindings
|
|
RootStateTree.PropertyBindings.DebugPrintInternalLayout(DebugString);
|
|
|
|
// Transitions
|
|
DebugString += FString::Printf(TEXT("\nTransitions(%d)\n [ %-3s | %15s | %-20s | %-40s | %-8s ]\n"), RootStateTree.Transitions.Num()
|
|
, TEXT("Idx"), TEXT("State"), TEXT("Transition Trigger"), TEXT("Transition Event Tag"), TEXT("Num Cond"));
|
|
for (const FCompactStateTransition& Transition : RootStateTree.Transitions)
|
|
{
|
|
DebugString += FString::Printf(TEXT(" | %3d | %15s | %-20s | %-40s | %8d |\n"),
|
|
Transition.ConditionsBegin, *Transition.State.Describe(),
|
|
*UEnum::GetDisplayValueAsText(Transition.Trigger).ToString(),
|
|
*Transition.EventTag.ToString(),
|
|
Transition.ConditionsNum);
|
|
}
|
|
|
|
// States
|
|
DebugString += FString::Printf(TEXT("\nStates(%d)\n"
|
|
" [ %-30s | %15s | %5s [%3s:%-3s[ | Begin Idx : %4s %4s %4s %4s | Num : %4s %4s %4s %4s | Transitions : %-16s %-40s %-16s %-40s ]\n"),
|
|
RootStateTree.States.Num(),
|
|
TEXT("Name"), TEXT("Parent"), TEXT("Child"), TEXT("Beg"), TEXT("End"),
|
|
TEXT("Cond"), TEXT("Tr"), TEXT("Tsk"), TEXT("Evt"), TEXT("Cond"), TEXT("Tr"), TEXT("Tsk"), TEXT("Evt"),
|
|
TEXT("Done State"), TEXT("Done Type"), TEXT("Failed State"), TEXT("Failed Type")
|
|
);
|
|
for (const FCompactStateTreeState& State : RootStateTree.States)
|
|
{
|
|
DebugString += FString::Printf(TEXT(" | %-30s | %15s | %5s [%3d:%-3d[ | %9s %4d %4d %4d | %3s %4d %4d %4d\n"),
|
|
*State.Name.ToString(), *State.Parent.Describe(),
|
|
TEXT(""), State.ChildrenBegin, State.ChildrenEnd,
|
|
TEXT(""), State.EnterConditionsBegin, State.TransitionsBegin, State.TasksBegin,
|
|
TEXT(""), State.EnterConditionsNum, State.TransitionsNum, State.TasksNum);
|
|
}
|
|
|
|
// Evaluators
|
|
if (RootStateTree.EvaluatorsNum)
|
|
{
|
|
DebugString += FString::Printf(TEXT("\nEvaluators\n [ %-30s | %8s | %10s ]\n"),
|
|
TEXT("Name"), TEXT("Bindings"), TEXT("Struct Idx"));
|
|
for (int32 EvalIndex = RootStateTree.EvaluatorsBegin; EvalIndex < (RootStateTree.EvaluatorsBegin + RootStateTree.EvaluatorsNum); EvalIndex++)
|
|
{
|
|
const FStateTreeEvaluatorBase& Eval = RootStateTree.Nodes[EvalIndex].Get<const FStateTreeEvaluatorBase>();
|
|
DebugString += FString::Printf(TEXT("| %-30s | %8d | %10s |\n"),
|
|
*Eval.Name.ToString(), Eval.BindingsBatch.Get(), *Eval.InstanceDataHandle.Describe());
|
|
}
|
|
}
|
|
|
|
|
|
DebugString += FString::Printf(TEXT("\nTasks\n [ %-30s | %-30s | %8s | %10s ]\n"),
|
|
TEXT("State"), TEXT("Name"), TEXT("Bindings"), TEXT("Struct Idx"));
|
|
for (const FCompactStateTreeState& State : RootStateTree.States)
|
|
{
|
|
// Tasks
|
|
if (State.TasksNum)
|
|
{
|
|
for (int32 TaskIndex = State.TasksBegin; TaskIndex < (State.TasksBegin + State.TasksNum); TaskIndex++)
|
|
{
|
|
const FStateTreeTaskBase& Task = RootStateTree.Nodes[TaskIndex].Get<const FStateTreeTaskBase>();
|
|
DebugString += FString::Printf(TEXT(" | %-30s | %-30s | %8d | %10s |\n"), *State.Name.ToString(),
|
|
*Task.Name.ToString(), Task.BindingsBatch.Get(), *Task.InstanceDataHandle.Describe());
|
|
}
|
|
}
|
|
}
|
|
|
|
UE_LOG(LogStateTree, Log, TEXT("%s"), *DebugString);
|
|
}
|
|
|
|
int32 FStateTreeExecutionContext::GetStateChangeCount() const
|
|
{
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
return 0;
|
|
}
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
return Exec.StateChangeCount;
|
|
}
|
|
|
|
#endif // WITH_STATETREE_DEBUG
|
|
|
|
FString FStateTreeExecutionContext::GetActiveStateName() const
|
|
{
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
return FString(TEXT("<None>"));
|
|
}
|
|
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
FString FullStateName;
|
|
|
|
const UStateTree* LastStateTree = &RootStateTree;
|
|
int32 Indent = 0;
|
|
|
|
for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
{
|
|
const FStateTreeExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
|
|
// Append linked state marker at the end of the previous line.
|
|
if (Indent > 0)
|
|
{
|
|
FullStateName += TEXT(" >");
|
|
}
|
|
// If tree has changed, append that too.
|
|
if (CurrentFrame.StateTree != LastStateTree)
|
|
{
|
|
FullStateName.Appendf(TEXT(" [%s]"), *GetNameSafe(CurrentFrame.StateTree));
|
|
LastStateTree = CurrentFrame.StateTree;
|
|
}
|
|
|
|
for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
|
|
{
|
|
const FStateTreeStateHandle Handle = CurrentFrame.ActiveStates[Index];
|
|
if (Handle.IsValid())
|
|
{
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[Handle.Index];
|
|
if (Indent > 0)
|
|
{
|
|
FullStateName += TEXT("\n");
|
|
}
|
|
FullStateName += FString::Printf(TEXT("%*s-"), Indent * 3, TEXT("")); // Indent
|
|
FullStateName += *State.Name.ToString();
|
|
Indent++;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (Exec.TreeRunStatus)
|
|
{
|
|
case EStateTreeRunStatus::Failed:
|
|
FullStateName += TEXT(" FAILED\n");
|
|
break;
|
|
case EStateTreeRunStatus::Succeeded:
|
|
FullStateName += TEXT(" SUCCEEDED\n");
|
|
break;
|
|
case EStateTreeRunStatus::Running:
|
|
// Empty
|
|
break;
|
|
default:
|
|
FullStateName += TEXT("--\n");
|
|
}
|
|
|
|
return FullStateName;
|
|
}
|
|
|
|
TArray<FName> FStateTreeExecutionContext::GetActiveStateNames() const
|
|
{
|
|
TArray<FName> Result;
|
|
|
|
if (!InstanceData.IsValid())
|
|
{
|
|
return Result;
|
|
}
|
|
|
|
const FStateTreeExecutionState& Exec = GetExecState();
|
|
|
|
// Active States
|
|
for (const FStateTreeExecutionFrame& CurrentFrame : Exec.ActiveFrames)
|
|
{
|
|
const UStateTree* CurrentStateTree = CurrentFrame.StateTree;
|
|
for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
|
|
{
|
|
const FStateTreeStateHandle Handle = CurrentFrame.ActiveStates[Index];
|
|
if (Handle.IsValid())
|
|
{
|
|
const FCompactStateTreeState& State = CurrentStateTree->States[Handle.Index];
|
|
Result.Add(State.Name);
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
#undef STATETREE_LOG
|
|
#undef STATETREE_CLOG
|