|
@@ -327,7 +327,153 @@ EAbilityRunStatus FEzAbilityContext::GetAbilityRunStatus() const
|
|
|
|
|
|
EAbilityRunStatus FEzAbilityContext::Tick(float DeltaTime)
|
|
|
{
|
|
|
- return EAbilityRunStatus::Failed;
|
|
|
+ CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EzAbility_Tick);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_PHASE(EEzAbilityUpdatePhase::Tick);
|
|
|
+
|
|
|
+ if (!IsValid())
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(Warning, TEXT("%hs: EzAbility context is not initialized properly ('%s' using EzAbility '%s')"),
|
|
|
+ __FUNCTION__, *GetNameSafe(Owner), *GetFullNameSafe(GetAbility()));
|
|
|
+ return EAbilityRunStatus::Failed;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!CollectActiveExternalData())
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(Warning, TEXT("%hs: Failed to collect external data ('%s' using EzAbility '%s')"),
|
|
|
+ __FUNCTION__, *GetNameSafe(Owner), *GetFullNameSafe(GetAbility()));
|
|
|
+ return EAbilityRunStatus::Failed;
|
|
|
+ }
|
|
|
+
|
|
|
+ FEzAbilityEventQueue& EventQueue = InstanceData.GetMutableEventQueue();
|
|
|
+ FEzAbilityExecutionState& Exec = GetExecState();
|
|
|
+
|
|
|
+ // No ticking if the tree is done or stopped.
|
|
|
+ if (Exec.TreeRunStatus != EAbilityRunStatus::Running)
|
|
|
+ {
|
|
|
+ return Exec.TreeRunStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!ensureMsgf(Exec.CurrentPhase == EEzAbilityUpdatePhase::Unset, TEXT("%hs can't be called while already in %s ('%s' using EzAbility '%s')."),
|
|
|
+ __FUNCTION__, *UEnum::GetDisplayValueAsText(Exec.CurrentPhase).ToString(), *GetNameSafe(Owner), *GetFullNameSafe(GetAbility())))
|
|
|
+ {
|
|
|
+ return EAbilityRunStatus::Failed;
|
|
|
+ }
|
|
|
+
|
|
|
+ // From this point any calls to Stop should be deferred.
|
|
|
+ Exec.CurrentPhase = EEzAbilityUpdatePhase::Tick;
|
|
|
+
|
|
|
+ // Capture events added between ticks.
|
|
|
+ EventsToProcess = EventQueue.GetEvents();
|
|
|
+ EventQueue.Reset();
|
|
|
+
|
|
|
+ // Update the delayed transitions.
|
|
|
+ for (FEzAbilityTransitionDelayedState& DelayedState : Exec.DelayedTransitions)
|
|
|
+ {
|
|
|
+ DelayedState.TimeLeft -= DeltaTime;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Tick global evaluators and tasks.
|
|
|
+ const EAbilityRunStatus EvalAndGlobalTaskStatus = TickEvaluatorsAndGlobalTasks(DeltaTime);
|
|
|
+ if (EvalAndGlobalTaskStatus == EAbilityRunStatus::Running)
|
|
|
+ {
|
|
|
+ if (Exec.LastTickStatus == EAbilityRunStatus::Running)
|
|
|
+ {
|
|
|
+ // Tick tasks on active states.
|
|
|
+ Exec.LastTickStatus = TickTasks(DeltaTime);
|
|
|
+
|
|
|
+ // Report state completed immediately.
|
|
|
+ if (Exec.LastTickStatus != EAbilityRunStatus::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 EzAbilitys 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())
|
|
|
+ {
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_PHASE(EEzAbilityUpdatePhase::ApplyTransitions);
|
|
|
+ EZ_ABILITY_TRACE_TRANSITION_EVENT(NextTransitionSource, EEzAbilityTraceEventType::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 EAbilityRunStatus 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 != EAbilityRunStatus::Running)
|
|
|
+ {
|
|
|
+ StateCompleted();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Stop as soon as have found a running state.
|
|
|
+ if (Exec.LastTickStatus == EAbilityRunStatus::Running)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ EZ_ABILITY_TRACE_LOG_EVENT(TEXT("Global tasks completed (%s), stopping the tree"), *UEnum::GetDisplayValueAsText(EvalAndGlobalTaskStatus).ToString());
|
|
|
+ Exec.RequestedStop = EvalAndGlobalTaskStatus;
|
|
|
+ }
|
|
|
+
|
|
|
+ EventsToProcess.Reset();
|
|
|
+
|
|
|
+ // Reset phase since we are now safe to stop.
|
|
|
+ Exec.CurrentPhase = EEzAbilityUpdatePhase::Unset;
|
|
|
+
|
|
|
+ // Use local for resulting run state since Stop will reset the instance data.
|
|
|
+ EAbilityRunStatus Result = Exec.TreeRunStatus;
|
|
|
+
|
|
|
+ if (Exec.RequestedStop != EAbilityRunStatus::Unset)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG_AND_TRACE(VeryVerbose, TEXT("Processing Deferred Stop"));
|
|
|
+ Result = Stop(Exec.RequestedStop);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Result;
|
|
|
}
|
|
|
|
|
|
FEzAbilityDataView FEzAbilityContext::GetDataView(FEzAbilityInstanceStorage& InstanceDataStorage,
|
|
@@ -1823,6 +1969,376 @@ FEzAbilityIndex16 FEzAbilityContext::CollectExternalData(const UEzAbility* InAbi
|
|
|
return FEzAbilityIndex16(Result);
|
|
|
}
|
|
|
|
|
|
+bool FEzAbilityContext::TriggerTransitions()
|
|
|
+{
|
|
|
+ CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EzAbility_TriggerTransition);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_PHASE(EEzAbilityUpdatePhase::TriggerTransitions);
|
|
|
+
|
|
|
+ FAllowDirectTransitionsScope AllowDirectTransitionsScope(*this); // Set flag for the scope of this function to allow direct transitions without buffering.
|
|
|
+ FEzAbilityExecutionState& Exec = GetExecState();
|
|
|
+
|
|
|
+ if (EventsToProcess.Num() > 0)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG_AND_TRACE(Verbose, TEXT("Trigger transitions with events [%s]"), *DebugGetEventsAsString());
|
|
|
+ }
|
|
|
+
|
|
|
+ NextTransition.Reset();
|
|
|
+
|
|
|
+ //
|
|
|
+ // Process transition requests
|
|
|
+ //
|
|
|
+ for (const FEzAbilityTransitionRequest& Request : InstanceData.GetTransitionRequests())
|
|
|
+ {
|
|
|
+ // Find frame associated with the request.
|
|
|
+ const int32 FrameIndex = Exec.ActiveFrames.IndexOfByPredicate([&Request](const FEzAbilityExecutionFrame& Frame)
|
|
|
+ {
|
|
|
+ return Frame.Ability == Request.SourceAbility && Frame.RootState == Request.SourceRootState;
|
|
|
+ });
|
|
|
+
|
|
|
+ if (FrameIndex != INDEX_NONE)
|
|
|
+ {
|
|
|
+ const FEzAbilityExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
|
+ if (RequestTransition(CurrentFrame, Request.TargetState, Request.Priority))
|
|
|
+ {
|
|
|
+ NextTransitionSource = FEzAbilityTransitionSource(EEzAbilityTransitionSourceType::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++)
|
|
|
+ {
|
|
|
+ FEzAbilityExecutionFrame* CurrentParentFrame = FrameIndex > 0 ? &Exec.ActiveFrames[FrameIndex - 1] : nullptr;
|
|
|
+ FEzAbilityExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
|
+ const UEzAbility* CurrentEzAbility = CurrentFrame.Ability;
|
|
|
+
|
|
|
+ FCurrentlyProcessedFrameScope FrameScope(*this, CurrentParentFrame, CurrentFrame);
|
|
|
+
|
|
|
+ for (int32 StateIndex = CurrentFrame.ActiveStates.Num() - 1; StateIndex >= 0; StateIndex--)
|
|
|
+ {
|
|
|
+ const FEzAbilityStateHandle StateHandle = CurrentFrame.ActiveStates[StateIndex];
|
|
|
+ const FCompactEzAbilityState& State = CurrentEzAbility->States[StateHandle.Index];
|
|
|
+
|
|
|
+ // Do not process any transitions from a disabled state
|
|
|
+ if (!State.bEnabled)
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ FCurrentlyProcessedStateScope StateScope(*this, StateHandle);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_STATE(StateHandle);
|
|
|
+
|
|
|
+ if (State.bHasTransitionTasks)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_CLOG(State.TasksNum > 0, VeryVerbose, TEXT("%*sTrigger task transitions in state '%s'"), StateIndex*UE::EzAbility::DebugIndentSize, TEXT(""), *DebugGetStatePath(Exec.ActiveFrames, &CurrentFrame, StateIndex));
|
|
|
+
|
|
|
+ for (int32 TaskIndex = (State.TasksBegin + State.TasksNum) - 1; TaskIndex >= State.TasksBegin; TaskIndex--)
|
|
|
+ {
|
|
|
+ const FEzAbilityTask& Task = CurrentEzAbility->Nodes[TaskIndex].Get<const FEzAbilityTask>();
|
|
|
+ const FEzAbilityDataView TaskInstanceView = GetDataView(CurrentParentFrame, CurrentFrame, Task.InstanceDataHandle);
|
|
|
+ FNodeInstanceDataScope DataScope(*this, Task.InstanceDataHandle, TaskInstanceView);
|
|
|
+
|
|
|
+ // Ignore disabled task
|
|
|
+ if (Task.bTaskEnabled == false)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("%*sSkipped 'TriggerTransitions' for disabled Task: '%s'"), UE::EzAbility::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Task.bShouldAffectTransitions)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("%*sTriggerTransitions: '%s'"), UE::EzAbility::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
|
+ EZ_ABILITY_TRACE_TASK_EVENT(TaskIndex, TaskInstanceView, EEzAbilityTraceEventType::OnEvaluating, EAbilityRunStatus::Running);
|
|
|
+ check(TaskInstanceView.IsValid());
|
|
|
+ Task.TriggerTransitions(*this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ for (uint8 i = 0; i < State.TransitionsNum; i++)
|
|
|
+ {
|
|
|
+ // All transition conditions must pass
|
|
|
+ const int16 TransitionIndex = State.TransitionsBegin + i;
|
|
|
+ const FCompactEzAbilityTransition& Transition = CurrentEzAbility->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, EEzAbilityTransitionTrigger::OnStateCompleted))
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If a delayed transition has passed the delay, and remove it from the queue, and try trigger it.
|
|
|
+ FEzAbilityTransitionDelayedState* DelayedState = nullptr;
|
|
|
+ if (Transition.HasDelay())
|
|
|
+ {
|
|
|
+ DelayedState = Exec.FindDelayedTransition(CurrentFrame.Ability, FEzAbilityIndex16(TransitionIndex));
|
|
|
+ if (DelayedState != nullptr && DelayedState->TimeLeft <= 0.0f)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_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([EzAbility = CurrentFrame.Ability, TransitionIndex](const FEzAbilityTransitionDelayedState& DelayedState)
|
|
|
+ {
|
|
|
+ return DelayedState.Ability == EzAbility && DelayedState.TransitionIndex.Get() == TransitionIndex;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Trigger Delayed Transition when the delay has passed.
|
|
|
+ if (RequestTransition(CurrentFrame, Transition.State, Transition.Priority, Transition.Fallback))
|
|
|
+ {
|
|
|
+ NextTransitionSource = FEzAbilityTransitionSource(FEzAbilityIndex16(TransitionIndex), Transition.State, Transition.Priority);
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const bool bShouldTrigger = Transition.Trigger == EEzAbilityTransitionTrigger::OnTick
|
|
|
+ || (Transition.Trigger == EEzAbilityTransitionTrigger::OnEvent
|
|
|
+ && HasEventToProcess(Transition.EventTag));
|
|
|
+
|
|
|
+ bool bPassed = false;
|
|
|
+ if (bShouldTrigger)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_TRACE_TRANSITION_EVENT(FEzAbilityTransitionSource(FEzAbilityIndex16(TransitionIndex), Transition.State, Transition.Priority), EEzAbilityTraceEventType::OnEvaluating);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_PHASE(EEzAbilityUpdatePhase::TransitionConditions);
|
|
|
+ bPassed = TestAllConditions(CurrentParentFrame, 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->Ability = CurrentFrame.Ability;
|
|
|
+ DelayedState->TransitionIndex = FEzAbilityIndex16(TransitionIndex);
|
|
|
+ DelayedState->TimeLeft = DelayDuration;
|
|
|
+ BeginDelayedTransition(*DelayedState);
|
|
|
+ EZ_ABILITY_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 = FEzAbilityTransitionSource(FEzAbilityIndex16(TransitionIndex), Transition.State, Transition.Priority);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (CurrentFrame.bIsGlobalFrame)
|
|
|
+ {
|
|
|
+ // Global frame
|
|
|
+ if (CurrentFrame.Ability->bHasGlobalTransitionTasks)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("Trigger global task transitions"));
|
|
|
+ for (int32 TaskIndex = (CurrentEzAbility->GlobalTasksBegin + CurrentEzAbility->GlobalTasksNum) - 1; TaskIndex >= CurrentFrame.Ability->GlobalTasksBegin; TaskIndex--)
|
|
|
+ {
|
|
|
+ const FEzAbilityTask& Task = CurrentEzAbility->Nodes[TaskIndex].Get<const FEzAbilityTask>();
|
|
|
+ const FEzAbilityDataView TaskInstanceView = GetDataView(CurrentParentFrame, CurrentFrame, Task.InstanceDataHandle);
|
|
|
+ FNodeInstanceDataScope DataScope(*this, Task.InstanceDataHandle, TaskInstanceView);
|
|
|
+
|
|
|
+ // Ignore disabled task
|
|
|
+ if (Task.bTaskEnabled == false)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("%*sSkipped 'TriggerTransitions' for disabled Task: '%s'"), UE::EzAbility::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Task.bShouldAffectTransitions)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("%*sTriggerTransitions: '%s'"), UE::EzAbility::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
|
+ EZ_ABILITY_TRACE_TASK_EVENT(TaskIndex, TaskInstanceView, EEzAbilityTraceEventType::OnEvaluating, EAbilityRunStatus::Running);
|
|
|
+ check(TaskInstanceView.IsValid());
|
|
|
+ Task.TriggerTransitions(*this);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ //
|
|
|
+ // Check state completion transitions.
|
|
|
+ //
|
|
|
+ bool bProcessSubTreeCompletion = true;
|
|
|
+
|
|
|
+ if (NextTransition.Priority == EEzAbilityTransitionPriority::None
|
|
|
+ && Exec.LastTickStatus != EAbilityRunStatus::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--)
|
|
|
+ {
|
|
|
+ FEzAbilityExecutionFrame* CurrentParentFrame = FrameIndex > 0 ? &Exec.ActiveFrames[FrameIndex - 1] : nullptr;
|
|
|
+ FEzAbilityExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
|
+ const UEzAbility* CurrentEzAbility = CurrentFrame.Ability;
|
|
|
+
|
|
|
+ FCurrentlyProcessedFrameScope FrameScope(*this, CurrentParentFrame, CurrentFrame);
|
|
|
+
|
|
|
+ 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 EEzAbilityTransitionTrigger CompletionTrigger = Exec.LastTickStatus == EAbilityRunStatus::Succeeded ? EEzAbilityTransitionTrigger::OnStateSucceeded : EEzAbilityTransitionTrigger::OnStateFailed;
|
|
|
+
|
|
|
+ // Check completion transitions
|
|
|
+ for (int32 StateIndex = StateStartIndex; StateIndex >= 0; StateIndex--)
|
|
|
+ {
|
|
|
+ const FEzAbilityStateHandle StateHandle = CurrentFrame.ActiveStates[StateIndex];
|
|
|
+ const FCompactEzAbilityState& State = CurrentEzAbility->States[StateHandle.Index];
|
|
|
+
|
|
|
+ FCurrentlyProcessedStateScope StateScope(*this, StateHandle);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_STATE_PHASE(StateHandle, EEzAbilityUpdatePhase::TriggerTransitions);
|
|
|
+
|
|
|
+ for (uint8 i = 0; i < State.TransitionsNum; i++)
|
|
|
+ {
|
|
|
+ // All transition conditions must pass
|
|
|
+ const int16 TransitionIndex = State.TransitionsBegin + i;
|
|
|
+ const FCompactEzAbilityTransition& Transition = CurrentEzAbility->Transitions[TransitionIndex];
|
|
|
+
|
|
|
+ // Skip disabled transitions
|
|
|
+ if (Transition.bTransitionEnabled == false)
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (EnumHasAnyFlags(Transition.Trigger, CompletionTrigger))
|
|
|
+ {
|
|
|
+ bool bPassed = false;
|
|
|
+ {
|
|
|
+ EZ_ABILITY_TRACE_TRANSITION_EVENT(FEzAbilityTransitionSource(FEzAbilityIndex16(TransitionIndex), Transition.State, Transition.Priority), EEzAbilityTraceEventType::OnEvaluating);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_PHASE(EEzAbilityUpdatePhase::TransitionConditions);
|
|
|
+ bPassed = TestAllConditions(CurrentParentFrame, 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, EEzAbilityTransitionPriority::Normal, Transition.Fallback))
|
|
|
+ {
|
|
|
+ NextTransitionSource = FEzAbilityTransitionSource(FEzAbilityIndex16(TransitionIndex), Transition.State, Transition.Priority);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (NextTransition.Priority != EEzAbilityTransitionPriority::None)
|
|
|
+ {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle the case where no transition was found.
|
|
|
+ if (NextTransition.Priority == EEzAbilityTransitionPriority::None)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG_AND_TRACE(Verbose, TEXT("Could not trigger completion transition, jump back to root state."));
|
|
|
+
|
|
|
+ check(!Exec.ActiveFrames.IsEmpty());
|
|
|
+ FEzAbilityExecutionFrame& RootFrame = Exec.ActiveFrames[0];
|
|
|
+ FCurrentlyProcessedFrameScope RootFrameScope(*this, nullptr, RootFrame);
|
|
|
+ FCurrentlyProcessedStateScope RootStateScope(*this, FEzAbilityStateHandle::Root);
|
|
|
+
|
|
|
+ if (RequestTransition(RootFrame, FEzAbilityStateHandle::Root, EEzAbilityTransitionPriority::Normal))
|
|
|
+ {
|
|
|
+ NextTransitionSource = FEzAbilityTransitionSource(EEzAbilityTransitionSourceType::Internal, FEzAbilityStateHandle::Root, EEzAbilityTransitionPriority::Normal);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG_AND_TRACE(Warning, TEXT("Failed to select root state. Stopping the tree with failure."));
|
|
|
+
|
|
|
+ SetupNextTransition(RootFrame, FEzAbilityStateHandle::Failed, EEzAbilityTransitionPriority::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 FEzAbilityExecutionFrame& Frame)
|
|
|
+ {
|
|
|
+ return Frame.Ability == NextTransition.SourceAbility && 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 FEzAbilityExecutionFrame& SourceFrame = Exec.ActiveFrames[SourceFrameIndex];
|
|
|
+ const int32 ParentFrameIndex = SourceFrameIndex - 1;
|
|
|
+ const FEzAbilityExecutionFrame& ParentFrame = Exec.ActiveFrames[ParentFrameIndex];
|
|
|
+ const FEzAbilityStateHandle ParentLinkedState = ParentFrame.ActiveStates.Last();
|
|
|
+
|
|
|
+ if (ParentLinkedState.IsValid())
|
|
|
+ {
|
|
|
+ const EAbilityRunStatus RunStatus = NextTransition.TargetState.ToCompletionStatus();
|
|
|
+ EZ_ABILITY_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 = FEzAbilityIndex16(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 FEzAbilityContext::SelectState(const FEzAbilityExecutionFrame& CurrentFrame, const FEzAbilityStateHandle NextState, TArray<FEzAbilityExecutionFrame, TFixedAllocator<MaxExecutionFrames>>& OutNextActiveFrames, const EEzAbilitySelectionFallback Fallback)
|
|
|
{
|
|
|
const FEzAbilityExecutionState& Exec = GetExecState();
|
|
@@ -2321,6 +2837,133 @@ bool FEzAbilityContext::SelectStateInternal(const FEzAbilityExecutionFrame* Curr
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+EAbilityRunStatus FEzAbilityContext::TickTasks(const float DeltaTime)
|
|
|
+{
|
|
|
+ CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EzAbility_TickTasks);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_PHASE(EEzAbilityUpdatePhase::TickingTasks);
|
|
|
+
|
|
|
+ FEzAbilityExecutionState& Exec = GetExecState();
|
|
|
+
|
|
|
+ if (Exec.ActiveFrames.IsEmpty())
|
|
|
+ {
|
|
|
+ return EAbilityRunStatus::Failed;
|
|
|
+ }
|
|
|
+
|
|
|
+ EAbilityRunStatus Result = EAbilityRunStatus::Running;
|
|
|
+ int32 NumTotalTasks = 0;
|
|
|
+
|
|
|
+ const bool bHasEvents = !EventsToProcess.IsEmpty();
|
|
|
+
|
|
|
+ Exec.CompletedFrameIndex = FEzAbilityIndex16::Invalid;
|
|
|
+ Exec.CompletedStateHandle = FEzAbilityStateHandle::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;
|
|
|
+
|
|
|
+ EZ_ABILITY_CLOG(Exec.ActiveFrames.Num() > 0, VeryVerbose, TEXT("Ticking Tasks"));
|
|
|
+
|
|
|
+ for (int32 FrameIndex = 0; FrameIndex < Exec.ActiveFrames.Num(); FrameIndex++)
|
|
|
+ {
|
|
|
+ const FEzAbilityExecutionFrame* CurrentParentFrame = FrameIndex > 0 ? &Exec.ActiveFrames[FrameIndex - 1] : nullptr;
|
|
|
+ const FEzAbilityExecutionFrame& CurrentFrame = Exec.ActiveFrames[FrameIndex];
|
|
|
+ const UEzAbility* CurrentEzAbility = CurrentFrame.Ability;
|
|
|
+
|
|
|
+ FCurrentlyProcessedFrameScope FrameScope(*this, CurrentParentFrame, CurrentFrame);
|
|
|
+
|
|
|
+ for (int32 Index = 0; Index < CurrentFrame.ActiveStates.Num(); Index++)
|
|
|
+ {
|
|
|
+ const FEzAbilityStateHandle CurrentHandle = CurrentFrame.ActiveStates[Index];
|
|
|
+ const FCompactEzAbilityState& State = CurrentEzAbility->States[CurrentHandle.Index];
|
|
|
+
|
|
|
+ FCurrentlyProcessedStateScope StateScope(*this, CurrentHandle);
|
|
|
+ EZ_ABILITY_TRACE_SCOPED_STATE(CurrentHandle);
|
|
|
+
|
|
|
+ EZ_ABILITY_CLOG(State.TasksNum > 0, VeryVerbose, TEXT("%*sState '%s'"), Index*UE::EzAbility::DebugIndentSize, TEXT(""), *DebugGetStatePath(Exec.ActiveFrames, &CurrentFrame, Index));
|
|
|
+
|
|
|
+ if (State.Type == EEzAbilityStateType::Linked
|
|
|
+ || State.Type == EEzAbilityStateType::LinkedAsset)
|
|
|
+ {
|
|
|
+ if (State.ParameterDataHandle.IsValid()
|
|
|
+ && State.ParameterBindingsBatch.IsValid())
|
|
|
+ {
|
|
|
+ const FEzAbilityDataView StateParamsDataView = GetDataView(CurrentParentFrame, CurrentFrame, State.ParameterDataHandle);
|
|
|
+ CopyBatchOnActiveInstances(CurrentParentFrame, 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 FEzAbilityTask& Task = CurrentEzAbility->Nodes[TaskIndex].Get<const FEzAbilityTask>();
|
|
|
+ const FEzAbilityDataView TaskInstanceView = GetDataView(CurrentParentFrame, CurrentFrame, Task.InstanceDataHandle);
|
|
|
+ FNodeInstanceDataScope DataScope(*this, Task.InstanceDataHandle, TaskInstanceView);
|
|
|
+
|
|
|
+ // Ignore disabled task
|
|
|
+ if (Task.bTaskEnabled == false)
|
|
|
+ {
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("%*sSkipped 'Tick' for disabled Task: '%s'"), UE::EzAbility::DebugIndentSize, TEXT(""), *Task.Name.ToString());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ const bool bNeedsTick = bShouldTickTasks && (Task.bShouldCallTick || (bHasEvents && Task.bShouldCallTickOnlyOnEvents));
|
|
|
+ EZ_ABILITY_LOG(VeryVerbose, TEXT("%*s Tick: '%s' %s"), Index*UE::EzAbility::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)
|
|
|
+ {
|
|
|
+ CopyBatchOnActiveInstances(CurrentParentFrame, CurrentFrame, TaskInstanceView, Task.BindingsBatch);
|
|
|
+ }
|
|
|
+
|
|
|
+ //EZ_ABILITY_TRACE_TASK_EVENT(TaskIndex, TaskDataView, EEzAbilityTraceEventType::OnTickingTask, EAbilityRunStatus::Running);
|
|
|
+ EAbilityRunStatus TaskResult = EAbilityRunStatus::Unset;
|
|
|
+ {
|
|
|
+ QUICK_SCOPE_CYCLE_COUNTER(EzAbility_Task_Tick);
|
|
|
+ CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EzAbility_Task_Tick);
|
|
|
+
|
|
|
+ TaskResult = Task.Tick(*this, DeltaTime);
|
|
|
+ }
|
|
|
+
|
|
|
+ EZ_ABILITY_TRACE_TASK_EVENT(TaskIndex, TaskInstanceView,
|
|
|
+ TaskResult != EAbilityRunStatus::Running ? EEzAbilityTraceEventType::OnTaskCompleted : EEzAbilityTraceEventType::OnTicked,
|
|
|
+ TaskResult);
|
|
|
+
|
|
|
+ // TODO: Add more control over which states can control the failed/succeeded result.
|
|
|
+ if (TaskResult != EAbilityRunStatus::Running)
|
|
|
+ {
|
|
|
+ // Store the first state that completed, will be used to decide where to trigger transitions.
|
|
|
+ if (!Exec.CompletedStateHandle.IsValid())
|
|
|
+ {
|
|
|
+ Exec.CompletedFrameIndex = FEzAbilityIndex16(FrameIndex);
|
|
|
+ Exec.CompletedStateHandle = CurrentHandle;
|
|
|
+ }
|
|
|
+ Result = TaskResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (TaskResult == EAbilityRunStatus::Failed)
|
|
|
+ {
|
|
|
+ bShouldTickTasks = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ NumTotalTasks += State.TasksNum;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (NumTotalTasks == 0)
|
|
|
+ {
|
|
|
+ // No tasks, done ticking.
|
|
|
+ Result = EAbilityRunStatus::Succeeded;
|
|
|
+ Exec.CompletedFrameIndex = FEzAbilityIndex16(0);
|
|
|
+ Exec.CompletedStateHandle = Exec.ActiveFrames[0].ActiveStates.GetStateSafe(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Result;
|
|
|
+}
|
|
|
+
|
|
|
bool FEzAbilityContext::TestAllConditions(const FEzAbilityExecutionFrame* CurrentParentFrame, const FEzAbilityExecutionFrame& CurrentFrame, const int32 ConditionsOffset, const int32 ConditionsNum)
|
|
|
{
|
|
|
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(EzAbility_TestConditions);
|
|
@@ -2413,6 +3056,73 @@ bool FEzAbilityContext::TestAllConditions(const FEzAbilityExecutionFrame* Curren
|
|
|
return Values[0];
|
|
|
}
|
|
|
|
|
|
+bool FEzAbilityContext::RequestTransition(const FEzAbilityExecutionFrame& CurrentFrame,
|
|
|
+ const FEzAbilityStateHandle NextState, const EEzAbilityTransitionPriority Priority,
|
|
|
+ const EEzAbilitySelectionFallback Fallback)
|
|
|
+{
|
|
|
+ // Skip lower priority transitions.
|
|
|
+ if (NextTransition.Priority >= Priority)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (NextState.IsCompletionState())
|
|
|
+ {
|
|
|
+ SetupNextTransition(CurrentFrame, NextState, Priority);
|
|
|
+ EZ_ABILITY_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, FEzAbilityStateHandle::Invalid, Priority);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ TArray<FEzAbilityExecutionFrame, TFixedAllocator<MaxExecutionFrames>> NewNextActiveFrames;
|
|
|
+ if (SelectState(CurrentFrame, NextState, NewNextActiveFrames, Fallback))
|
|
|
+ {
|
|
|
+ SetupNextTransition(CurrentFrame, NextState, Priority);
|
|
|
+ NextTransition.NextActiveFrames = NewNextActiveFrames;
|
|
|
+
|
|
|
+ EZ_ABILITY_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 FEzAbilityContext::SetupNextTransition(const FEzAbilityExecutionFrame& CurrentFrame,
|
|
|
+ const FEzAbilityStateHandle NextState, const EEzAbilityTransitionPriority Priority)
|
|
|
+{
|
|
|
+ const FEzAbilityExecutionState& Exec = GetExecState();
|
|
|
+
|
|
|
+ NextTransition.CurrentRunStatus = Exec.LastTickStatus;
|
|
|
+ NextTransition.SourceState = CurrentlyProcessedState;
|
|
|
+ NextTransition.SourceAbility = CurrentFrame.Ability;
|
|
|
+ NextTransition.SourceRootState = CurrentFrame.ActiveStates.GetStateSafe(0);
|
|
|
+ NextTransition.TargetState = NextState;
|
|
|
+ NextTransition.Priority = Priority;
|
|
|
+
|
|
|
+ FEzAbilityExecutionFrame& NewFrame = NextTransition.NextActiveFrames.AddDefaulted_GetRef();
|
|
|
+ NewFrame.Ability = CurrentFrame.Ability;
|
|
|
+ NewFrame.RootState = CurrentFrame.RootState;
|
|
|
+
|
|
|
+ if (NextState == FEzAbilityStateHandle::Invalid)
|
|
|
+ {
|
|
|
+ NewFrame.ActiveStates = {};
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ NewFrame.ActiveStates = FEzAbilityActiveStates(NextState);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
FString FEzAbilityContext::GetStateStatusString(const FEzAbilityExecutionState& ExecState) const
|
|
|
{
|
|
|
if (ExecState.TreeRunStatus != EAbilityRunStatus::Running)
|