孟宇 3 місяців тому
батько
коміт
9bae471941

+ 711 - 1
Ability/Plugins/EzAbility/Source/EzAbility/Private/EzAbilityContext.cpp

@@ -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)

+ 62 - 1
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityContext.h

@@ -54,7 +54,7 @@ struct EZABILITY_API FEzAbilityContext
 public:
 	FEzAbilityContext() = default;
 	FEzAbilityContext(UObject& InOwner, const UEzAbility& InAbility, FEzAbilityInstanceData& InInstanceData, const FOnCollectEzAbilityExternalData& CollectExternalDataCallback = {});
-	~FEzAbilityContext();
+	virtual ~FEzAbilityContext();
 	
 	void InitContext(UEzAbilityComponent* Component);
 	bool IsLocallyControlled() const;
@@ -96,6 +96,9 @@ protected:
 	void Reset();
 	FString GetInstanceDescription() const;
 	void UpdateInstanceData(TConstArrayView<FEzAbilityExecutionFrame> CurrentActiveFrames, TArrayView<FEzAbilityExecutionFrame> NextActiveFrames);
+
+	/** Callback when delayed transition is triggered. Contexts that are event based can use this to trigger a future event. */
+	virtual void BeginDelayedTransition(const FEzAbilityTransitionDelayedState& DelayedState) {};
 	
 	/** Starts temporary instances of global evaluators and tasks for a given frame. */
 	EAbilityRunStatus StartTemporaryEvaluatorsAndGlobalTasks(const FEzAbilityExecutionFrame* CurrentParentFrame, const FEzAbilityExecutionFrame& CurrentFrame);
@@ -167,6 +170,15 @@ protected:
 	 * @returns index in ContextAndExternalDataViews for the first external data.
 	 */
 	FEzAbilityIndex16 CollectExternalData(const UEzAbility* Ability);
+
+	/**
+	 * Triggers transitions based on current run status. CurrentStatus is used to select which transitions events are triggered.
+	 * If CurrentStatus is "Running", "Conditional" transitions pass, "Completed/Failed" will trigger "OnCompleted/OnSucceeded/OnFailed" transitions.
+	 * Transition target state can point to a selector state. For that reason the result contains both the target state, as well ass
+	 * the actual next state returned by the selector.
+	 * @return Transition result describing the source state, state transitioned to, and next selected state.
+	 */
+	bool TriggerTransitions();
 	
 	/**
 	 * Runs state selection logic starting at the specified state, walking towards the leaf states.
@@ -195,12 +207,32 @@ protected:
 		const FEzAbilityStateHandle NextStateHandle,
 		TArray<FEzAbilityExecutionFrame, TFixedAllocator<MaxExecutionFrames>>& OutNextActiveFrames);
 
+	/**
+	 * Ticks tasks of all active states starting from current state by delta time.
+	 * @return Run status returned by the tasks.
+	 */
+	EAbilityRunStatus TickTasks(const float DeltaTime);
+	
 	/**
 	 * Checks all conditions at given range
 	 * @return True if all conditions pass.
 	 */
 	bool TestAllConditions(const FEzAbilityExecutionFrame* CurrentParentFrame, const FEzAbilityExecutionFrame& CurrentFrame, const int32 ConditionsOffset, const int32 ConditionsNum);
 
+	/**
+	 * Requests transition to a specified state with specified priority.
+	 */
+	bool RequestTransition(
+		const FEzAbilityExecutionFrame& CurrentFrame,
+		const FEzAbilityStateHandle NextState,
+		const EEzAbilityTransitionPriority Priority,
+		const EEzAbilitySelectionFallback Fallback = EEzAbilitySelectionFallback::None);
+
+	/**
+	 * Sets up NextTransition based on the provided parameters and the current execution status. 
+	 */
+	void SetupNextTransition(const FEzAbilityExecutionFrame& CurrentFrame, const FEzAbilityStateHandle NextState, const EEzAbilityTransitionPriority Priority);
+	
 	/** @return events to process this tick. */
 	TConstArrayView<FEzAbilityEvent> GetEventsToProcess() const { return EventsToProcess; }
 
@@ -256,6 +288,12 @@ protected:
 	UPROPERTY(Transient)
 	FEzAbilityTarget				Target;
 
+	/** Next transition, used by RequestTransition(). */
+	FEzAbilityTransitionResult NextTransition;
+	
+	/** Structure describing the origin of the state transition that caused the state change. */
+	FEzAbilityTransitionSource NextTransitionSource;
+	
 	FOnCollectEzAbilityExternalData CollectExternalDataDelegate;
 
 	/** Data view of the context data. */
@@ -344,6 +382,29 @@ public:
 	FEzAbilityDataHandle CurrentNodeDataHandle;
 	FEzAbilityDataView CurrentNodeInstanceData;
 
+	/** True if transitions are allowed to be requested directly instead of buffering. */
+	bool bAllowDirectTransitions = false;
+
+	/** Helper struct to track when it is allowed to request transitions. */
+	struct FAllowDirectTransitionsScope
+	{
+		FAllowDirectTransitionsScope(FEzAbilityContext& InContext)
+			: Context(InContext)
+		{
+			bSavedAllowDirectTransitions = Context.bAllowDirectTransitions; 
+			Context.bAllowDirectTransitions = true;
+		}
+
+		~FAllowDirectTransitionsScope()
+		{
+			Context.bAllowDirectTransitions = bSavedAllowDirectTransitions;
+		}
+
+	private:
+		FEzAbilityContext& Context;
+		bool bSavedAllowDirectTransitions = false;
+	};
+	
 protected:
 #if WITH_EZABILITY_DEBUGGER
 	FEzAbilityInstanceDebugId GetInstanceDebugId() const;

+ 7 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/EzAbilityInstanceData.h

@@ -156,6 +156,13 @@ public:
 			FEzAbilityExecutionState*	GetMutableExecutionState()	const { return &GetMutableStorage().GetMutableExecutionState(); }
 	const	FEzAbilityEventQueue&		GetEventQueue()				const { return GetStorage().EventQueue; }
 			FEzAbilityEventQueue&		GetMutableEventQueue()		const { return GetMutableStorage().EventQueue; }
+
+	/** @return currently pending transition requests. */
+	TConstArrayView<FEzAbilityTransitionRequest> GetTransitionRequests() const { return GetStorage().GetTransitionRequests(); }
+
+	/** Reset all pending transition requests. */
+	void ResetTransitionRequests() { GetMutableStorage().ResetTransitionRequests(); }
+	
 	
 	void ResetTemporaryInstances() const { return GetMutableStorage().ResetTemporaryInstances(); }
 

+ 7 - 0
Ability/Plugins/EzAbility/Source/EzAbility/Public/Task/EzAbilityTask.h

@@ -49,6 +49,13 @@ struct EZABILITY_API FEzAbilityTask : public FEzAbilityNodeBase
 	 * @param CompletedActiveStates Active states at the time of completion.
 	 */
 	virtual void StateCompleted(FEzAbilityContext& Context, const EAbilityRunStatus CompletionStatus, const FEzAbilityActiveStates& CompletedActiveStates) const {}
+
+	/**
+	 * Called when state tree triggers transitions. This method is called during transition handling, before state's tick and event transitions are handled.
+	 * Note: the method is called only if bShouldAffectTransitions is set.
+	 * @param Context Reference to current execution context.
+	 */
+	virtual void TriggerTransitions(FEzAbilityContext& Context) const {};
 	
 	/**
 	 * If set to true, the task will receive EnterState/ExitState even if the state was previously active.