// Copyright Epic Games, Inc. All Rights Reserved. #include "StateTreeTest.h" #include "StateTreeTestBase.h" #include "StateTreeTestTypes.h" #include "StateTreeCompilerLog.h" #include "StateTreeEditorData.h" #include "StateTreeCompiler.h" #include "Conditions/StateTreeCommonConditions.h" #define LOCTEXT_NAMESPACE "AITestSuite_StateTreeTest" UE_DISABLE_OPTIMIZATION_SHIP namespace UE::StateTree::Tests { struct FStateTreeTest_TransitionPriority : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); /* - Root - State1 : Task1 -> Succeeded - State1A : Task1A -> Next - State1B : Task1B -> Next - State1C : Task1C Task1A completed first, transitioning to State1B. Task1, Task1B, and Task1C complete at the same time, we should take the transition on the first completed state (State1). */ UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State1A = State1.AddChildState(FName(TEXT("State1A"))); UStateTreeState& State1B = State1.AddChildState(FName(TEXT("State1B"))); UStateTreeState& State1C = State1.AddChildState(FName(TEXT("State1C"))); auto& Task1 = State1.AddTask(FName(TEXT("Task1"))); Task1.GetNode().TicksToCompletion = 2; State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded); auto& Task1A = State1A.AddTask(FName(TEXT("Task1A"))); Task1A.GetNode().TicksToCompletion = 1; State1A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState); auto& Task1B = State1B.AddTask(FName(TEXT("Task1B"))); Task1B.GetNode().TicksToCompletion = 2; State1B.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState); auto& Task1C = State1C.AddTask(FName(TEXT("Task1C"))); Task1C.GetNode().TicksToCompletion = 2; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); const TCHAR* TickStr(TEXT("Tick")); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); const TCHAR* StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree Task1 should enter state"), Exec.Expect(Task1.GetName(), EnterStateStr)); AITEST_TRUE(TEXT("StateTree Task1A should enter state"), Exec.Expect(Task1A.GetName(), EnterStateStr)); Exec.LogClear(); // Transition from Task1A to Task1B Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task1A should complete"), Exec.Expect(Task1A.GetName(), StateCompletedStr)); AITEST_TRUE(TEXT("StateTree Task1B should enter state"), Exec.Expect(Task1B.GetName(), EnterStateStr)); Exec.LogClear(); // Task1 completes, and we should take State1 transition. Status = Exec.Tick(0.1f); AITEST_TRUE("StateTree Task1 should complete", Exec.Expect(Task1.GetName(), StateCompletedStr)); AITEST_EQUAL(TEXT("Tree execution should stop on success"), Status, EStateTreeRunStatus::Succeeded); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionPriority, "System.StateTree.Transition.Priority"); struct FStateTreeTest_TransitionPriorityEnterState : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State0 = Root.AddChildState(FName(TEXT("State0"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State1A = State1.AddChildState(FName(TEXT("State1A"))); UStateTreeState& State2 = Root.AddChildState(FName(TEXT("State2"))); UStateTreeState& State3 = Root.AddChildState(FName(TEXT("State3"))); auto& Task0 = State0.AddTask(FName(TEXT("Task0"))); State0.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State1); auto& Task1 = State1.AddTask(FName(TEXT("Task1"))); Task1.GetNode().EnterStateResult = EStateTreeRunStatus::Failed; State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State2); auto& Task1A = State1A.AddTask(FName(TEXT("Task1A"))); State1A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State3); auto& Task2 = State2.AddTask(FName(TEXT("Task2"))); State2.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded); auto& Task3 = State3.AddTask(FName(TEXT("Task3"))); State3.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); const TCHAR* TickStr(TEXT("Tick")); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); const TCHAR* StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree Task0 should enter state"), Exec.Expect(Task0.GetName(), EnterStateStr)); Exec.LogClear(); // Transition from State0 to State1, it should fail (Task1), and the transition on State1->State2 (and not State1A->State3) Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should complete"), Exec.Expect(Task0.GetName(), StateCompletedStr)); AITEST_TRUE(TEXT("StateTree Task2 should enter state"), Exec.Expect(Task2.GetName(), EnterStateStr)); AITEST_FALSE(TEXT("StateTree Task3 should not enter state"), Exec.Expect(Task3.GetName(), EnterStateStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionPriorityEnterState, "System.StateTree.Transition.PriorityEnterState"); struct FStateTreeTest_TransitionNextSelectableState : FStateTreeTestBase { virtual bool InstantTest() override { // Root // State0: OnCompleted->Nextselectable // State1: EnterCondition fails // State2: EnterCondition fails // State3: EnterCondition successed. OnCompleted->Succeeded UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State0 = Root.AddChildState(FName(TEXT("State0"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State2 = Root.AddChildState(FName(TEXT("State2"))); UStateTreeState& State3 = Root.AddChildState(FName(TEXT("State3"))); auto& EvalA = EditorData.AddEvaluator(); EvalA.GetInstanceData().bBoolA = true; auto& Task0 = State0.AddTask(FName(TEXT("Task0"))); State0.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextSelectableState); // Add Task 1 with Condition that will always fail auto& Task1 = State1.AddTask(FName(TEXT("Task1"))); auto& BoolCond1 = State1.AddEnterCondition(); EditorData.AddPropertyBinding(EvalA, TEXT("bBoolA"), BoolCond1, TEXT("bLeft")); BoolCond1.GetInstanceData().bRight = !EvalA.GetInstanceData().bBoolA; // Add Task 2 with Condition that will always fail auto& Task2 = State2.AddTask(FName(TEXT("Task2"))); auto& BoolCond2 = State2.AddEnterCondition(); EditorData.AddPropertyBinding(EvalA, TEXT("bBoolA"), BoolCond2, TEXT("bLeft")); BoolCond2.GetInstanceData().bRight = !EvalA.GetInstanceData().bBoolA; // Add Task 3 with Condition that will always succeed auto& Task3 = State3.AddTask(FName(TEXT("Task3"))); auto& BoolCond3 = State3.AddEnterCondition(); State3.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded); EditorData.AddPropertyBinding(EvalA, TEXT("bBoolA"), BoolCond3, TEXT("bLeft")); BoolCond3.GetInstanceData().bRight = EvalA.GetInstanceData().bBoolA; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* TickStr = TEXT("Tick"); const TCHAR* EnterStateStr = TEXT("EnterState"); const TCHAR* ExitStateStr = TEXT("ExitState"); const TCHAR* StateCompletedStr = TEXT("StateCompleted"); // Start and enter state Exec.Start(); AITEST_TRUE(TEXT("Should be in active state"), Exec.ExpectInActiveStates("Root", "State0")); AITEST_TRUE(TEXT("StateTree Task0 should enter state"), Exec.Expect(Task0.GetName(), EnterStateStr)); Exec.LogClear(); // Transition from State0 and tries to select State1. It should fail (Task1 and Tasks2) and because transition is set to "Next Selectable", it should now select Task 3 and Enter State Exec.Tick(0.1f); AITEST_TRUE(TEXT("Should be in active state"), Exec.ExpectInActiveStates("Root", "State3")); AITEST_TRUE(TEXT("StateTree Task0 should complete"), Exec.Expect(Task0.GetName(), StateCompletedStr)); AITEST_FALSE(TEXT("StateTree Task1 should not enter state"), Exec.Expect(Task1.GetName(), EnterStateStr)); AITEST_FALSE(TEXT("StateTree Task2 should not enter state"), Exec.Expect(Task2.GetName(), EnterStateStr)); AITEST_TRUE(TEXT("StateTree Task3 should enter state"), Exec.Expect(Task3.GetName(), EnterStateStr)); Exec.LogClear(); // Complete Task3 EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should succeed"), Status, EStateTreeRunStatus::Succeeded); AITEST_TRUE(TEXT("Should be in active state"), Exec.GetActiveStateNames().Num() == 0); AITEST_TRUE(TEXT("StateTree Task3 should complete"), Exec.Expect(Task3.GetName(), StateCompletedStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionNextSelectableState, "System.StateTree.Transition.NextSelectableState"); struct FStateTreeTest_TransitionNextWithParentData : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State0 = Root.AddChildState(FName(TEXT("State0"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State1A = State1.AddChildState(FName(TEXT("State1A"))); auto& RootTask = Root.AddTask(FName(TEXT("RootTask"))); RootTask.GetInstanceData().bBoolB = true; auto& Task0 = State0.AddTask(FName(TEXT("Task0"))); State0.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState); auto& Task1A = State1A.AddTask(FName(TEXT("Task1A"))); auto& BoolCond1 = State1A.AddEnterCondition(); EditorData.AddPropertyBinding(RootTask, TEXT("bBoolB"), BoolCond1, TEXT("bLeft")); BoolCond1.GetInstanceData().bRight = true; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); const TCHAR* TickStr(TEXT("Tick")); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); const TCHAR* StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Exec.Start(); AITEST_TRUE(TEXT("StateTree Task0 should enter state"), Exec.Expect(Task0.GetName(), EnterStateStr)); Exec.LogClear(); // Transition from State0 and tries to select State1. // This tests that data from current shared active states (Root) is available during state selection. Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should complete"), Exec.Expect(Task0.GetName(), StateCompletedStr)); AITEST_TRUE(TEXT("StateTree Task1A should enter state"), Exec.Expect(Task1A.GetName(), EnterStateStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionNextWithParentData, "System.StateTree.Transition.NextWithParentData"); struct FStateTreeTest_TransitionGlobalDataView : FStateTreeTestBase { // Tests that the global eval and task dataviews are kept up to date when transitioning from virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& StateA = Root.AddChildState(FName(TEXT("A"))); UStateTreeState& StateB = Root.AddChildState(FName(TEXT("B"))); auto& EvalA = EditorData.AddEvaluator(FName(TEXT("Eval"))); EvalA.GetInstanceData().IntA = 42; auto& GlobalTask = EditorData.AddGlobalTask(FName(TEXT("Global"))); GlobalTask.GetInstanceData().Value = 123; // State A auto& Task0 = StateA.AddTask(FName(TEXT("Task0"))); StateA.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &StateB); // State B auto& Task1 = StateB.AddTask(FName(TEXT("Task1"))); EditorData.AddPropertyBinding(EvalA, TEXT("IntA"), Task1, TEXT("Value")); auto& Task2 = StateB.AddTask(FName(TEXT("Task2"))); EditorData.AddPropertyBinding(GlobalTask, TEXT("Value"), Task2, TEXT("Value")); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* EnterState42Str(TEXT("EnterState42")); const TCHAR* EnterState123Str(TEXT("EnterState123")); // Start and enter state Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree Task0 should enter state"), Exec.Expect(Task0.GetName(), EnterStateStr)); Exec.LogClear(); // Transition from StateA to StateB, Task0 should enter state with evaluator value copied. Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should enter state with value 42"), Exec.Expect(Task1.GetName(), EnterState42Str)); AITEST_TRUE(TEXT("StateTree Task1 should enter state with value 123"), Exec.Expect(Task2.GetName(), EnterState123Str)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionGlobalDataView, "System.StateTree.Transition.GlobalDataView"); struct FStateTreeTest_TransitionDelay : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); const FGameplayTag Tag = GetTestTag1(); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& StateA = Root.AddChildState(FName(TEXT("A"))); UStateTreeState& StateB = Root.AddChildState(FName(TEXT("B"))); // State A auto& Task0 = StateA.AddTask(FName(TEXT("Task0"))); Task0.GetNode().TicksToCompletion = 100; FStateTreeTransition& Transition = StateA.AddTransition(EStateTreeTransitionTrigger::OnEvent, EStateTreeTransitionType::GotoState, &StateB); Transition.bDelayTransition = true; Transition.DelayDuration = 0.15f; Transition.DelayRandomVariance = 0.0f; Transition.RequiredEvent.Tag = Tag; // State B auto& Task1 = StateB.AddTask(FName(TEXT("Task1"))); Task1.GetNode().TicksToCompletion = 100; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* TickStr(TEXT("Tick")); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); const TCHAR* StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree Task0 should enter state"), Exec.Expect(Task0.GetName(), EnterStateStr)); Exec.LogClear(); // This should cause delayed transition. Exec.SendEvent(Tag); Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should tick"), Exec.Expect(Task0.GetName(), TickStr)); Exec.LogClear(); // Should have execution frames AITEST_TRUE(TEXT("Should have active frames"), InstanceData.GetExecutionState()->ActiveFrames.Num() > 0); // Should have delayed transitions const int32 NumDelayedTransitions0 = InstanceData.GetExecutionState()->DelayedTransitions.Num(); AITEST_EQUAL(TEXT("Should have a delayed transition"), NumDelayedTransitions0, 1); // Tick and expect a delayed transition. Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should tick"), Exec.Expect(Task0.GetName(), TickStr)); Exec.LogClear(); const int32 NumDelayedTransitions1 = InstanceData.GetExecutionState()->DelayedTransitions.Num(); AITEST_EQUAL(TEXT("Should have a delayed transition"), NumDelayedTransitions1, 1); // Should complete delayed transition. Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should exit state"), Exec.Expect(Task0.GetName(), ExitStateStr)); AITEST_TRUE(TEXT("StateTree Task1 should enter state"), Exec.Expect(Task1.GetName(), EnterStateStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionDelay, "System.StateTree.Transition.Delay"); struct FStateTreeTest_TransitionDelayZero : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); const FGameplayTag Tag = GetTestTag1(); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& StateA = Root.AddChildState(FName(TEXT("A"))); UStateTreeState& StateB = Root.AddChildState(FName(TEXT("B"))); // State A auto& Task0 = StateA.AddTask(FName(TEXT("Task0"))); Task0.GetNode().TicksToCompletion = 100; FStateTreeTransition& Transition = StateA.AddTransition(EStateTreeTransitionTrigger::OnEvent, EStateTreeTransitionType::GotoState, &StateB); Transition.bDelayTransition = true; Transition.DelayDuration = 0.0f; Transition.DelayRandomVariance = 0.0f; Transition.RequiredEvent.Tag = Tag; // State B auto& Task1 = StateB.AddTask(FName(TEXT("Task1"))); Task1.GetNode().TicksToCompletion = 100; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* TickStr(TEXT("Tick")); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); const TCHAR* StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree Task0 should enter state"), Exec.Expect(Task0.GetName(), EnterStateStr)); Exec.LogClear(); // This should cause delayed transition. Because the time is 0, it should happen immediately. Exec.SendEvent(Tag); Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("StateTree Task0 should exit state"), Exec.Expect(Task0.GetName(), ExitStateStr)); AITEST_TRUE(TEXT("StateTree Task1 should enter state"), Exec.Expect(Task1.GetName(), EnterStateStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionDelayZero, "System.StateTree.Transition.DelayZero"); struct FStateTreeTest_PassingTransitionEventToStateSelection : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); FPropertyBindingPath PathToPayloadMember; { const bool bParseResult = PathToPayloadMember.FromString(TEXT("Payload.A")); AITEST_TRUE(TEXT("Parsing path should succeeed"), bParseResult); FStateTreeEvent EventWithPayload; EventWithPayload.Payload = FInstancedStruct::Make(); const bool bUpdateSegments = PathToPayloadMember.UpdateSegmentsFromValue(FStateTreeDataView(FStructView::Make(EventWithPayload))); AITEST_TRUE(TEXT("Updating segments should succeeed"), bUpdateSegments); } // This state shouldn't be selected, because transition's condition and state's enter condition exlude each other. UStateTreeState& StateA = Root.AddChildState(FName(TEXT("A"))); StateA.bHasRequiredEventToEnter = true; StateA.RequiredEventToEnter.PayloadStruct = FStateTreeTest_PropertyStructA::StaticStruct(); auto& TaskA = StateA.AddTask(FName(TEXT("TaskA"))); TStateTreeEditorNode& AIntCond = StateA.AddEnterCondition(EGenericAICheck::Equal); AIntCond.GetInstanceData().Right = 0; EditorData.AddPropertyBinding( FPropertyBindingPath(StateA.GetEventID(), PathToPayloadMember.GetSegments()), FPropertyBindingPath(AIntCond.ID, TEXT("Left"))); // This state should be selected as the sent event fullfils both transition's condition and state's enter condition. UStateTreeState& StateB = Root.AddChildState(FName(TEXT("B"))); StateB.bHasRequiredEventToEnter = true; StateB.RequiredEventToEnter.PayloadStruct = FStateTreeTest_PropertyStructA::StaticStruct(); auto& TaskB = StateB.AddTask(FName(TEXT("TaskB"))); // Test copying data from the state event. The condition properties are copied from temp instance data during selection, this gets copied from active instance data. TaskB.GetInstanceData().Value = -1; // Initially -1, expected to be overridden by property binding below. EditorData.AddPropertyBinding( FPropertyBindingPath(StateB.GetEventID(), PathToPayloadMember.GetSegments()), FPropertyBindingPath(TaskB.ID, TEXT("Value"))); TStateTreeEditorNode& BIntCond = StateB.AddEnterCondition(EGenericAICheck::Equal); BIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(StateB.GetEventID(), PathToPayloadMember.GetSegments()), FPropertyBindingPath(BIntCond.ID, TEXT("Left"))); // This state should be selected only initially when there's not event in the queue. UStateTreeState& StateInitial = Root.AddChildState(FName(TEXT("Initial"))); auto& TaskInitial = StateInitial.AddTask(FName(TEXT("TaskInitial"))); // Transition from Initial -> StateA FStateTreeTransition& TransA = StateInitial.AddTransition(EStateTreeTransitionTrigger::OnEvent, FGameplayTag(), EStateTreeTransitionType::GotoState, &StateA); TransA.RequiredEvent.PayloadStruct = FStateTreeTest_PropertyStructA::StaticStruct(); TStateTreeEditorNode& TransAIntCond = TransA.AddCondition(EGenericAICheck::Equal); TransAIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(TransA.GetEventID(), PathToPayloadMember.GetSegments()), FPropertyBindingPath(TransAIntCond.ID, TEXT("Left"))); // Transition from Initial -> StateB FStateTreeTransition& TransB = StateInitial.AddTransition(EStateTreeTransitionTrigger::OnEvent, FGameplayTag(), EStateTreeTransitionType::GotoState, &StateB); TransB.RequiredEvent.PayloadStruct = FStateTreeTest_PropertyStructA::StaticStruct(); TStateTreeEditorNode& TransBIntCond = TransB.AddCondition(EGenericAICheck::Equal); TransBIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(TransB.GetEventID(), PathToPayloadMember.GetSegments()), FPropertyBindingPath(TransBIntCond.ID, TEXT("Left"))); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* EnterStateStr(TEXT("EnterState")); Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree TaskInitial should enter state"), Exec.Expect(TaskInitial.GetName(), EnterStateStr)); Exec.LogClear(); // The conditions test for payload Value=1, the first event should not trigger transition. Exec.SendEvent(GetTestTag1(), FConstStructView::Make(FStateTreeTest_PropertyStructA{0})); Exec.SendEvent(GetTestTag1(), FConstStructView::Make(FStateTreeTest_PropertyStructA{1})); Status = Exec.Tick(0.1f); AITEST_FALSE(TEXT("StateTree TaskA should not enter state"), Exec.Expect(TaskA.GetName(), EnterStateStr)); AITEST_TRUE(TEXT("StateTree TaskB should enter state"), Exec.Expect(TaskB.GetName(), TEXT("EnterState1"))); // TaskB decorates "EnterState" with value from the payload. Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_PassingTransitionEventToStateSelection, "System.StateTree.Transition.PassingTransitionEventToStateSelection"); struct FStateTreeTest_FollowTransitions : FStateTreeTestBase { virtual bool InstantTest() override { //Root // Trans // A // B // C UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); FInstancedPropertyBag& RootPropertyBag = GetRootPropertyBag(EditorData); RootPropertyBag.AddProperty(FName(TEXT("Int")), EPropertyBagPropertyType::Int32); RootPropertyBag.SetValueInt32(FName(TEXT("Int")), 1); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& StateTrans = Root.AddChildState(FName(TEXT("Trans"))); UStateTreeState& StateA = Root.AddChildState(FName(TEXT("A"))); UStateTreeState& StateB = Root.AddChildState(FName(TEXT("B"))); UStateTreeState& StateC = Root.AddChildState(FName(TEXT("C"))); // Root // Trans { StateTrans.SelectionBehavior = EStateTreeStateSelectionBehavior::TryFollowTransitions; { // This transition should be skipped due to the condition FStateTreeTransition& TransA = StateTrans.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &StateA); TStateTreeEditorNode& TransIntCond = TransA.AddCondition(EGenericAICheck::Equal); TransIntCond.GetInstanceData().Right = 0; EditorData.AddPropertyBinding( FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Int")), FPropertyBindingPath(TransIntCond.ID, TEXT("Left"))); } { // This transition leads to selection, but will be overridden. FStateTreeTransition& TransB = StateTrans.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &StateB); TransB.Priority = EStateTreeTransitionPriority::Normal; TStateTreeEditorNode& TransIntCond = TransB.AddCondition(EGenericAICheck::Equal); TransIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Int")), FPropertyBindingPath(TransIntCond.ID, TEXT("Left"))); } { // This transition is selected, should override previous one due to priority. FStateTreeTransition& TransC = StateTrans.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &StateC); TransC.Priority = EStateTreeTransitionPriority::High; TStateTreeEditorNode& TransIntCond = TransC.AddCondition(EGenericAICheck::Equal); TransIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Int")), FPropertyBindingPath(TransIntCond.ID, TEXT("Left"))); } } auto& TaskA = StateA.AddTask(FName(TEXT("TaskA"))); auto& TaskB = StateB.AddTask(FName(TEXT("TaskB"))); auto& TaskC = StateC.AddTask(FName(TEXT("TaskC"))); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* TickStr = TEXT("Tick"); const TCHAR* EnterStateStr = TEXT("EnterState"); const TCHAR* ExitStateStr = TEXT("ExitState"); Status = Exec.Start(); AITEST_FALSE(TEXT("StateTree TaskA should not enter state"), Exec.Expect(TaskA.GetName(), EnterStateStr)); AITEST_FALSE(TEXT("StateTree TaskB should not enter state"), Exec.Expect(TaskB.GetName(), EnterStateStr)); AITEST_TRUE(TEXT("StateTree TaskC should enter state"), Exec.Expect(TaskC.GetName(), EnterStateStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_FollowTransitions, "System.StateTree.Transition.FollowTransitions"); struct FStateTreeTest_InfiniteLoop : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); FInstancedPropertyBag& RootPropertyBag = GetRootPropertyBag(EditorData); RootPropertyBag.AddProperty(FName(TEXT("Int")), EPropertyBagPropertyType::Int32); RootPropertyBag.SetValueInt32(FName(TEXT("Int")), 1); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& StateA = Root.AddChildState(FName(TEXT("A"))); UStateTreeState& StateB = StateA.AddChildState(FName(TEXT("B"))); // Root // State A { StateA.SelectionBehavior = EStateTreeStateSelectionBehavior::TryFollowTransitions; { // A -> B FStateTreeTransition& Trans = StateA.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &StateB); TStateTreeEditorNode& TransIntCond = Trans.AddCondition(EGenericAICheck::Equal); TransIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Int")), FPropertyBindingPath(TransIntCond.ID, TEXT("Left"))); } } // State B { StateB.SelectionBehavior = EStateTreeStateSelectionBehavior::TryFollowTransitions; { // B -> A FStateTreeTransition& Trans = StateB.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &StateA); TStateTreeEditorNode& TransIntCond = Trans.AddCondition(EGenericAICheck::Equal); TransIntCond.GetInstanceData().Right = 1; EditorData.AddPropertyBinding( FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Int")), FPropertyBindingPath(TransIntCond.ID, TEXT("Left"))); } } auto& TaskA = StateA.AddTask(FName(TEXT("TaskA"))); auto& TaskB = StateB.AddTask(FName(TEXT("TaskB"))); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); const TCHAR* TickStr = TEXT("Tick"); const TCHAR* EnterStateStr = TEXT("EnterState"); const TCHAR* ExitStateStr = TEXT("ExitState"); GetTestRunner().AddExpectedMessage(TEXT("Loop detected when trying to select state"), ELogVerbosity::Warning, EAutomationExpectedErrorFlags::Contains, 1); GetTestRunner().AddExpectedError(TEXT("Failed to select initial state"), EAutomationExpectedErrorFlags::Contains, 1); Status = Exec.Start(); AITEST_EQUAL(TEXT("Start should fail"), Status, EStateTreeRunStatus::Failed); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_InfiniteLoop, "System.StateTree.Transition.InfiniteLoop"); struct FStateTreeTest_RegularTransitions : FStateTreeTestBase { virtual bool InstantTest() override { //Tree 1 // Global task and parameter // RootA // StateB -> Next // StateC -> Next // StateD -> Next // StateE -> Next // StateF -> Next // StateG -> Succeeded FStateTreeCompilerLog Log; // Main asset UStateTree& StateTree = NewStateTree(); { UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); FGuid RootParameter_ValueID; { // Parameters FInstancedPropertyBag& RootPropertyBag = GetRootPropertyBag(EditorData); RootPropertyBag.AddProperty("Value", EPropertyBagPropertyType::Int32); RootPropertyBag.SetValueInt32("Value", -111); RootParameter_ValueID = RootPropertyBag.FindPropertyDescByName("Value")->ID; TStateTreeEditorNode& GlobalTask = EditorData.AddGlobalTask("GlobalTask"); GlobalTask.GetInstanceData().Value = -1; EditorData.AddPropertyBinding(FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Value")), FPropertyBindingPath(GlobalTask.ID, TEXT("Value"))); } UStateTreeState& Root = EditorData.AddSubTree("RootA"); { TStateTreeEditorNode& Task = Root.AddTask("TaskA"); Task.GetInstanceData().Value = -1; EditorData.AddPropertyBinding(FPropertyBindingPath(EditorData.GetRootParametersGuid(), TEXT("Value")), FPropertyBindingPath(Task.ID, TEXT("Value"))); } { UStateTreeState& StateB = Root.AddChildState("StateB", EStateTreeStateType::State); TStateTreeEditorNode& Task = StateB.AddTask("TaskB"); Task.GetInstanceData().Value = 1; FStateTreeTransition& Transition = StateB.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::NextState); Transition.bDelayTransition = true; Transition.DelayDuration = 1.0; } { UStateTreeState& StateB = Root.AddChildState("StateC", EStateTreeStateType::State); TStateTreeEditorNode& Task = StateB.AddTask("TaskC"); Task.GetInstanceData().Value = 2; FStateTreeTransition& Transition = StateB.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::NextState); Transition.bDelayTransition = true; Transition.DelayDuration = 1.0; } { UStateTreeState& StateD = Root.AddChildState("StateD", EStateTreeStateType::State); TStateTreeEditorNode& Task = StateD.AddTask("TaskD"); Task.GetInstanceData().Value = 3; FStateTreeTransition& Transition = StateD.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &Root); Transition.bDelayTransition = true; Transition.DelayDuration = 1.0; } FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE(TEXT("StateTree should get compiled"), bResult); } const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); { EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE(TEXT("StateTree should init"), bInitSucceeded); FInstancedPropertyBag Parameters; Parameters.MigrateToNewBagInstance(StateTree.GetDefaultParameters()); Parameters.SetValueInt32("Value", 111); Status = Exec.Start(&Parameters); AITEST_EQUAL(TEXT("Start should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should enter Global tasks"), Exec.Expect("GlobalTask", TEXT("EnterState111"))); AITEST_TRUE(TEXT("Start should enter StateA"), Exec.Expect("TaskA", TEXT("EnterState111"))); AITEST_TRUE(TEXT("Start should enter StateB"), Exec.Expect("TaskB", TEXT("EnterState1"))); Exec.LogClear(); Status = Exec.Tick(1.5f); // over tick, should trigger AITEST_EQUAL(TEXT("1st Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("1st Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("1st Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("1st Tick should tick StateB"), Exec.Expect("TaskB", TEXT("Tick1"))); Exec.LogClear(); // B go to C Status = Exec.Tick(1.0f); AITEST_EQUAL(TEXT("2nd Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("2nd Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("2nd Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("2nd Tick should tick the StateB"), Exec.Expect("TaskB", TEXT("Tick1"))); AITEST_TRUE(TEXT("2nd Tick should exit the StateB"), Exec.Expect("TaskB", TEXT("ExitState1"))); AITEST_TRUE(TEXT("2nd Tick should enter the StateC"), Exec.Expect("TaskC", TEXT("EnterState2"))); AITEST_FALSE(TEXT("2nd Tick should not exit the StateA"), Exec.Expect("TaskA", TEXT("ExitState111"))); AITEST_FALSE(TEXT("2nd Tick should not exit the Global tasks"), Exec.Expect("GlobalTask", TEXT("ExitState111"))); Exec.LogClear(); Status = Exec.Tick(1.0f); AITEST_EQUAL(TEXT("3rd Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("3rd Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("3rd Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("3rd Tick should tick StateC"), Exec.Expect("TaskC", TEXT("Tick2"))); AITEST_FALSE(TEXT("3th Tick should not exit StateC"), Exec.Expect("TaskD", TEXT("ExitState2"))); Exec.LogClear(); // C go to D Status = Exec.Tick(1.0f); AITEST_EQUAL(TEXT("4th Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("4th Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("4th Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("4th Tick should tick the StateC"), Exec.Expect("TaskC", TEXT("Tick2"))); AITEST_TRUE(TEXT("4th Tick should exit the StateC"), Exec.Expect("TaskC", TEXT("ExitState2"))); AITEST_TRUE(TEXT("4th Tick should enter the StateD"), Exec.Expect("TaskD", TEXT("EnterState3"))); AITEST_FALSE(TEXT("4th Tick should not exit the StateA"), Exec.Expect("TaskA", TEXT("ExitState111"))); AITEST_FALSE(TEXT("4th Tick should not exit the Global tasks"), Exec.Expect("GlobalTask", TEXT("ExitState111"))); Exec.LogClear(); Status = Exec.Tick(0.001f); AITEST_EQUAL(TEXT("5th Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("5th Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("5th Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("5th Tick should tick StateD"), Exec.Expect("TaskD", TEXT("Tick3"))); AITEST_FALSE(TEXT("5th Tick should not exit StateD"), Exec.Expect("TaskD", TEXT("ExitState3"))); Exec.LogClear(); // D go to root Status = Exec.Tick(1.0f); AITEST_EQUAL(TEXT("6th Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("6th Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("6th Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("6th Tick should tick StateD"), Exec.Expect("TaskD", TEXT("Tick3"))); AITEST_TRUE(TEXT("6th Tick should exit the StateD"), Exec.Expect("TaskD", TEXT("ExitState3"))); AITEST_FALSE(TEXT("6th Tick should not exit the Global tasks"), Exec.Expect("GlobalTask", TEXT("ExitState111"))); AITEST_FALSE(TEXT("6th Tick should not enter the Global tasks"), Exec.Expect("GlobalTask", TEXT("EnterState111"))); AITEST_TRUE(TEXT("6th Tick should exit the StateA"), Exec.Expect("TaskA", TEXT("ExitState=Sustained"))); AITEST_TRUE(TEXT("6th Tick should enter the StateA"), Exec.Expect("TaskA", TEXT("EnterState=Sustained"))); AITEST_TRUE(TEXT("6th Tick should enter the StateB"), Exec.Expect("TaskB", TEXT("EnterState1"))); AITEST_TRUE(TEXT("6th Tick should enter the StateB"), Exec.Expect("TaskB", TEXT("EnterState=Changed"))); Exec.LogClear(); Status = Exec.Tick(1.0f); AITEST_EQUAL(TEXT("7th Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("7th Tick should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("Tick111"))); AITEST_TRUE(TEXT("7th Tick should tick StateA"), Exec.Expect("TaskA", TEXT("Tick111"))); AITEST_TRUE(TEXT("7th Tick should tick StateB"), Exec.Expect("TaskB", TEXT("Tick1"))); Exec.LogClear(); Exec.Stop(); AITEST_TRUE(TEXT("Stop Tick should exit the StateB"), Exec.Expect("TaskB", TEXT("ExitState1"))); AITEST_TRUE(TEXT("Stop Tick should exit the StateA"), Exec.Expect("TaskA", TEXT("ExitState111"))); AITEST_TRUE(TEXT("Stop should tick Global tasks"), Exec.Expect("GlobalTask", TEXT("ExitState111"))); Exec.LogClear(); } return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_RegularTransitions, "System.StateTree.Transition.RegularTransitions"); struct FStateTreeTest_RequestTransition : FStateTreeTestBase { virtual bool InstantTest() override { //Tree 1 //RootA // StateB // StateC // StateD // StateE // StateF // StateI // StateJ // StateX // StateY //RootO // StateP FStateTreeCompilerLog Log; enum class ECustomFunctionToRun { None, TransitionD_To_E, TransitionB_To_I, TransitionB_To_C, TransitionC_To_B, TransitionB_To_X, TransitionB_To_B, TransitionB_To_P, } TransitionToExecute = ECustomFunctionToRun::None; struct FStateHandle { FStateTreeStateHandle B; FStateTreeStateHandle C; FStateTreeStateHandle E; FStateTreeStateHandle I; FStateTreeStateHandle X; FStateTreeStateHandle P; FGuid B_ID; FGuid C_ID; FGuid E_ID; FGuid I_ID; FGuid X_ID; FGuid P_ID; bool bFinishTasksB = false; bool bFinishTasksC = false; bool bFinishTasksD = false; } AllStateHandle; // Main asset UStateTree& StateTree = NewStateTree(); { UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& StateA = EditorData.AddSubTree("StateA"); StateA.AddTask("StateATask"); UStateTreeState& StateB = StateA.AddChildState("StateB", EStateTreeStateType::State); FTestTask_PrintValue& TaskB = StateB.AddTask("StateBTask").GetNode(); AllStateHandle.B_ID = StateB.ID; UStateTreeState& StateC = StateB.AddChildState("StateC", EStateTreeStateType::State); FTestTask_PrintValue& TaskC = StateC.AddTask("StateCTask").GetNode(); AllStateHandle.C_ID = StateC.ID; UStateTreeState& StateD = StateC.AddChildState("StateD", EStateTreeStateType::State); FTestTask_PrintValue& TaskD = StateD.AddTask("StateDTask").GetNode(); StateD.SelectionBehavior = EStateTreeStateSelectionBehavior::TryEnterState; UStateTreeState& StateE = StateD.AddChildState("StateE", EStateTreeStateType::State); StateE.AddTask("StateETask"); AllStateHandle.E_ID = StateE.ID; UStateTreeState& StateF = StateE.AddChildState("StateF", EStateTreeStateType::State); StateF.AddTask("StateFTask"); UStateTreeState& StateI = StateB.AddChildState("StateI", EStateTreeStateType::State); StateI.AddTask("StateITask"); AllStateHandle.I_ID = StateI.ID; UStateTreeState& StateJ = StateI.AddChildState("StateJ", EStateTreeStateType::State); StateJ.AddTask("StateJTask"); UStateTreeState& StateX = StateA.AddChildState("StateX", EStateTreeStateType::State); StateX.AddTask("StateXTask"); AllStateHandle.X_ID = StateX.ID; UStateTreeState& StateY = StateX.AddChildState("StateY", EStateTreeStateType::State); StateY.AddTask("StateYTask"); UStateTreeState& StateO = EditorData.AddSubTree("StateO"); StateO.AddTask("StateOTask"); UStateTreeState& StateP = StateO.AddChildState("StateP", EStateTreeStateType::State); StateP.AddTask("StatePTask"); AllStateHandle.P_ID = StateP.ID; TaskB.CustomTickFunc = [&TransitionToExecute, &AllStateHandle] (FStateTreeExecutionContext& Context, const FTestTask_PrintValue* Task) { if (TransitionToExecute == ECustomFunctionToRun::TransitionB_To_I) { Context.RequestTransition(AllStateHandle.I); } if (TransitionToExecute == ECustomFunctionToRun::TransitionB_To_C) { Context.RequestTransition(AllStateHandle.C); } if (TransitionToExecute == ECustomFunctionToRun::TransitionB_To_X) { Context.RequestTransition(AllStateHandle.X); } if (TransitionToExecute == ECustomFunctionToRun::TransitionB_To_B) { Context.RequestTransition(AllStateHandle.B); } if (TransitionToExecute == ECustomFunctionToRun::TransitionB_To_P) { Context.RequestTransition(AllStateHandle.P); } if (AllStateHandle.bFinishTasksB) { Context.FinishTask(*Task, EStateTreeFinishTaskType::Succeeded); } }; TaskC.CustomTickFunc = [&TransitionToExecute, &AllStateHandle] (FStateTreeExecutionContext& Context, const FTestTask_PrintValue* Task) { if (TransitionToExecute == ECustomFunctionToRun::TransitionC_To_B) { Context.RequestTransition(AllStateHandle.B); } if (AllStateHandle.bFinishTasksC) { Context.FinishTask(*Task, EStateTreeFinishTaskType::Succeeded); } }; TaskD.CustomTickFunc = [&TransitionToExecute, &AllStateHandle] (FStateTreeExecutionContext& Context, const FTestTask_PrintValue* Task) { if (TransitionToExecute == ECustomFunctionToRun::TransitionD_To_E) { Context.RequestTransition(AllStateHandle.E); } if (AllStateHandle.bFinishTasksD) { Context.FinishTask(*Task, EStateTreeFinishTaskType::Succeeded); } }; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); AllStateHandle.B = StateTree.GetStateHandleFromId(AllStateHandle.B_ID); AllStateHandle.C = StateTree.GetStateHandleFromId(AllStateHandle.C_ID); AllStateHandle.E = StateTree.GetStateHandleFromId(AllStateHandle.E_ID); AllStateHandle.I = StateTree.GetStateHandleFromId(AllStateHandle.I_ID); AllStateHandle.X = StateTree.GetStateHandleFromId(AllStateHandle.X_ID); AllStateHandle.P = StateTree.GetStateHandleFromId(AllStateHandle.P_ID); } FStateTreeInstanceData InstanceData; { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); } constexpr int32 MaxRules = 4; auto MakeStateSelectionRule = [](int32 Index) { EStateTreeStateSelectionRules Rule = EStateTreeStateSelectionRules::None; if ((Index % 2) == 1) { Rule |= EStateTreeStateSelectionRules::CompletedTransitionStatesCreateNewStates; Rule |= EStateTreeStateSelectionRules::CompletedStateBeforeTransitionSourceFailsTransition; } if (Index >= 2) { Rule |= EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates; } return Rule; }; auto ResetInstanceData = [this, &StateTree, &InstanceData]() { { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); EStateTreeRunStatus Status = Exec.Stop(); AITEST_EQUAL(TEXT("Should stop"), Status, EStateTreeRunStatus::Stopped); } { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Start(); AITEST_EQUAL(TEXT("State should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); } return true; }; const TCHAR* EnterStateChangedStr = TEXT("EnterState=Changed"); const TCHAR* EnterStateSustainedStr = TEXT("EnterState=Sustained"); const TCHAR* ExitStateChangedStr = TEXT("ExitState=Changed");; const TCHAR* ExitStateSustainedStr = TEXT("ExitState=Sustained"); for (int32 Index = 0; Index < MaxRules; ++Index) { const EStateTreeStateSelectionRules StateSelectionRules = MakeStateSelectionRule(Index); InstanceData = FStateTreeInstanceData(); if (StateTree.GetStateSelectionRules() != StateSelectionRules) { StateTree.ResetCompiled(); UStateTreeEditorData* EditorData = CastChecked(StateTree.EditorData); UStateTreeTestSchema* Schema = CastChecked(EditorData->Schema); Schema->SetStateSelectionRules(StateSelectionRules); FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); } // Normal Start and tick. Make sure we are in ABCD before testing the transitions { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Start(); AITEST_EQUAL("Start should complete with Running", Status, EStateTreeRunStatus::Running); AITEST_TRUE("Start should enter Global tasks", Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", EnterStateChangedStr); AITEST_TRUE(TEXT("Start StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Start StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Start StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Start StateD"), LogOrder); AITEST_FALSE(TEXT("Tick StateA"), Exec.Expect("StateATask", TEXT("Tick0"))); AITEST_FALSE(TEXT("Tick StateB"), Exec.Expect("StateBTask", TEXT("Tick0"))); AITEST_FALSE(TEXT("Tick StateC"), Exec.Expect("StateCTask", TEXT("Tick0"))); AITEST_FALSE(TEXT("Tick StateD"), Exec.Expect("StateDTask", TEXT("Tick0"))); AITEST_FALSE(TEXT("Start StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Start StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Start StateC"), Exec.Expect("StateCTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Start StateD"), Exec.Expect("StateDTask", TEXT("ExitState0"))); Exec.LogClear(); } { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL("1st Tick should complete with Running", Status, EStateTreeRunStatus::Running); AITEST_TRUE("1st Tick no transition", Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateC"), Exec.Expect("StateCTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateD"), Exec.Expect("StateDTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateC"), Exec.Expect("StateCTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateD"), Exec.Expect("StateDTask", TEXT("ExitState0"))); Exec.LogClear(); } // Select new child { TransitionToExecute = ECustomFunctionToRun::TransitionD_To_E; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("2nd Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("2nd tick should be in new state"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD", "StateE", "StateF")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateETask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateE"), LogOrder); LogOrder = LogOrder.Then("StateFTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateF"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateC"), Exec.Expect("StateCTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateD"), Exec.Expect("StateDTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateC"), Exec.Expect("StateCTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateD"), Exec.Expect("StateDTask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } // Select a new child but with a completed D { TransitionToExecute = ECustomFunctionToRun::TransitionD_To_E; AllStateHandle.bFinishTasksD = true; if (!ResetInstanceData()) { return false; } const bool bUseCompletedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::CompletedTransitionStatesCreateNewStates); FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD", "StateE", "StateF")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); if (bUseCompletedRule) { LogOrder = LogOrder.Then("StateDTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); } LogOrder = LogOrder.Then("StateETask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateE"), LogOrder); LogOrder = LogOrder.Then("StateFTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateF"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateC"), Exec.Expect("StateCTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateC"), Exec.Expect("StateCTask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; AllStateHandle.bFinishTasksD = false; } // Select a new child but B is completed (before the source/target) { TransitionToExecute = ECustomFunctionToRun::TransitionD_To_E; AllStateHandle.bFinishTasksB = true; if (!ResetInstanceData()) { return false; } const bool bUseCompletedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::CompletedStateBeforeTransitionSourceFailsTransition); const bool bUseReselectedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); if (bUseCompletedRule) { AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); } else { AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD", "StateE", "StateF")); } FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); if (bUseCompletedRule) { const TCHAR* SchemaEnterStateStr = bUseReselectedRule ? EnterStateChangedStr : EnterStateSustainedStr; const TCHAR* SchemaExitStateStr = bUseReselectedRule ? ExitStateChangedStr : ExitStateSustainedStr; LogOrder = LogOrder.Then("StateDTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Enter StateB"), LogOrder); LogOrder = LogOrder.Then("StateATask", SchemaExitStateStr); AITEST_TRUE(TEXT("Enter StateA"), LogOrder); LogOrder = LogOrder.Then("StateATask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); // Only one transition LogOrder = LogOrder.Then("StateBTask", TEXT("EnterState0")); AITEST_FALSE(TEXT("Enter StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateE"), Exec.Expect("StateETask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateF"), Exec.Expect("StateFTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateE"), Exec.Expect("StateETask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateF"), Exec.Expect("StateFTask", TEXT("ExitState0"))); } else { LogOrder = LogOrder.Then("StateETask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateE"), LogOrder); LogOrder = LogOrder.Then("StateFTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateF"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateC"), Exec.Expect("StateCTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateC"), Exec.Expect("StateCTask", TEXT("ExitState0"))); } Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; AllStateHandle.bFinishTasksB = false; } // Select a sibling { TransitionToExecute = ECustomFunctionToRun::TransitionB_To_I; if (!ResetInstanceData()) { return false; } FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateI", "StateJ")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateITask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateI"), LogOrder); LogOrder = LogOrder.Then("StateJTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateJ"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } // No state completed. Reselect child. { TransitionToExecute = ECustomFunctionToRun::TransitionB_To_C; if (!ResetInstanceData()) { return false; } const bool bUseReselectedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); const TCHAR* SchemaEnterStateStr = bUseReselectedRule ? EnterStateChangedStr : EnterStateSustainedStr; const TCHAR* SchemaExitStateStr = bUseReselectedRule ? ExitStateChangedStr : ExitStateSustainedStr; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } // State completed. Reselect child. { TransitionToExecute = ECustomFunctionToRun::TransitionB_To_C; AllStateHandle.bFinishTasksC = true; if (!ResetInstanceData()) { return false; } const bool bUseCompletedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::CompletedTransitionStatesCreateNewStates); const bool bUseReselectedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); const TCHAR* SchemaEnterStateStr = bUseCompletedRule || bUseReselectedRule ? EnterStateChangedStr : EnterStateSustainedStr; const TCHAR* SchemaExitStateStr = bUseCompletedRule || bUseReselectedRule ? ExitStateChangedStr : ExitStateSustainedStr; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Enter StateB"), Exec.Expect("StateBTask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); AITEST_FALSE(TEXT("Exit StateB"), Exec.Expect("StateBTask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; AllStateHandle.bFinishTasksC = false; } // No state completed. Reselect parent. { TransitionToExecute = ECustomFunctionToRun::TransitionC_To_B; if (!ResetInstanceData()) { return false; } const bool bUseReselectedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); const TCHAR* SchemaEnterStateStr = bUseReselectedRule ? EnterStateChangedStr : EnterStateSustainedStr; const TCHAR* SchemaExitStateStr = bUseReselectedRule ? ExitStateChangedStr : ExitStateSustainedStr; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateB"), LogOrder); LogOrder = LogOrder.Then("StateBTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } // State completed. Reselect parent. { TransitionToExecute = ECustomFunctionToRun::TransitionC_To_B; AllStateHandle.bFinishTasksC = true; if (!ResetInstanceData()) { return false; } const bool bUseCompletedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::CompletedTransitionStatesCreateNewStates); const bool bUseReselectedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); const TCHAR* SchemaEnterStateStr = bUseCompletedRule || bUseReselectedRule ? EnterStateChangedStr : EnterStateSustainedStr; const TCHAR* SchemaExitStateStr = bUseCompletedRule || bUseReselectedRule ? ExitStateChangedStr : ExitStateSustainedStr; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaExitStateStr); if (bUseReselectedRule) { AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateB"), LogOrder); LogOrder = LogOrder.Then("StateBTask", EnterStateChangedStr); } else { AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", ExitStateSustainedStr); AITEST_TRUE(TEXT("Exit StateB"), LogOrder); LogOrder = LogOrder.Then("StateBTask", EnterStateSustainedStr); } AITEST_TRUE(TEXT("Enter StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); Exec.LogClear(); AllStateHandle.bFinishTasksC = false; TransitionToExecute = ECustomFunctionToRun::None; } // linked state { TransitionToExecute = ECustomFunctionToRun::TransitionB_To_X; if (!ResetInstanceData()) { return false; } FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateX", "StateY")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateB"), LogOrder); LogOrder = LogOrder.Then("StateXTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateX"), LogOrder); LogOrder = LogOrder.Then("StateYTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateY"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } // Reselection same state { TransitionToExecute = ECustomFunctionToRun::TransitionB_To_B; if (!ResetInstanceData()) { return false; } const bool bUseReselectedRule = EnumHasAllFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); const TCHAR* SchemaEnterStateStr = bUseReselectedRule ? EnterStateChangedStr : EnterStateSustainedStr; const TCHAR* SchemaExitStateStr = bUseReselectedRule ? ExitStateChangedStr : ExitStateSustainedStr; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateA", "StateB", "StateC", "StateD")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", SchemaExitStateStr); AITEST_TRUE(TEXT("Exit StateB"), LogOrder); LogOrder = LogOrder.Then("StateBTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", SchemaEnterStateStr); AITEST_TRUE(TEXT("Enter StateD"), LogOrder); AITEST_FALSE(TEXT("Enter StateA"), Exec.Expect("StateATask", TEXT("EnterState0"))); AITEST_FALSE(TEXT("Exit StateA"), Exec.Expect("StateATask", TEXT("ExitState0"))); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } // Select another root. { TransitionToExecute = ECustomFunctionToRun::TransitionB_To_P; if (!ResetInstanceData()) { return false; } FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const EStateTreeRunStatus Status = Exec.Tick(0.1f); AITEST_EQUAL(TEXT("Tick should complete with Running"), Status, EStateTreeRunStatus::Running); AITEST_TRUE(TEXT("Start should"), Exec.ExpectInActiveStates("StateO", "StateP")); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("StateATask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateA"), LogOrder); LogOrder = LogOrder.Then("StateBTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateB"), LogOrder); LogOrder = LogOrder.Then("StateCTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateC"), LogOrder); LogOrder = LogOrder.Then("StateDTask", TEXT("Tick0")); AITEST_TRUE(TEXT("Tick StateD"), LogOrder); LogOrder = LogOrder.Then("StateDTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateD"), LogOrder); LogOrder = LogOrder.Then("StateCTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateC"), LogOrder); LogOrder = LogOrder.Then("StateBTask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateB"), LogOrder); LogOrder = LogOrder.Then("StateATask", ExitStateChangedStr); AITEST_TRUE(TEXT("Exit StateA"), LogOrder); LogOrder = LogOrder.Then("StateOTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateO"), LogOrder); LogOrder = LogOrder.Then("StatePTask", EnterStateChangedStr); AITEST_TRUE(TEXT("Enter StateP"), LogOrder); Exec.LogClear(); TransitionToExecute = ECustomFunctionToRun::None; } { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); EStateTreeRunStatus Status = Exec.Stop(); AITEST_EQUAL(TEXT("Should stop"), Status, EStateTreeRunStatus::Stopped); } } return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_RequestTransition, "System.StateTree.Transition.RequestTransition"); struct FStateTreeTest_TransitionToNone : FStateTreeTestBase { virtual bool InstantTest() override { const FGameplayTag Tag = GetTestTag1(); UStateTree& StateTree = NewStateTree(); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State2 = Root.AddChildState(FName(TEXT("State2"))); FStateTreeTransition& TransitionRoot = Root.AddTransition(EStateTreeTransitionTrigger::OnEvent, EStateTreeTransitionType::GotoState); TransitionRoot.State = State1.GetLinkToState(); TransitionRoot.RequiredEvent.Tag = Tag; TStateTreeEditorNode& Task1 = State1.AddTask(FName(TEXT("Task1"))); FStateTreeTransition& TransitionState1 = State1.AddTransition(EStateTreeTransitionTrigger::OnEvent, EStateTreeTransitionType::None); TransitionState1.RequiredEvent.Tag = Tag; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); EStateTreeRunStatus Status = EStateTreeRunStatus::Unset; FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); // Start and enter state Status = Exec.Start(); AITEST_TRUE(TEXT("StateTree Task1 should enter state"), Exec.Expect(Task1.GetName(), EnterStateStr)); Exec.LogClear(); // Send event with Tag Exec.SendEvent(Tag); // Transition from Root to State2 should not be triggered Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State1")); AITEST_FALSE(TEXT("StateTree Task1 should not exit state"), Exec.Expect(Task1.GetName(), ExitStateStr)); Exec.LogClear(); Exec.Stop(); return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionToNone, "System.StateTree.Transition.ToNone"); struct FStateTreeTest_TransitionTwoEvents : FStateTreeTestBase { virtual bool InstantTest() override { UStateTree& StateTree = NewStateTree(); { UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State2 = Root.AddChildState(FName(TEXT("State2"))); UStateTreeState& State3 = State2.AddChildState(FName(TEXT("State3"))); UStateTreeState& State4 = State2.AddChildState(FName(TEXT("State4"))); State2.bHasRequiredEventToEnter = true; State2.RequiredEventToEnter.Tag = GetTestTag1(); State3.bHasRequiredEventToEnter = true; State3.RequiredEventToEnter.Tag = GetTestTag2(); FStateTreeTransition& TransitionA = State1.AddTransition(EStateTreeTransitionTrigger::OnEvent, EStateTreeTransitionType::GotoState); TransitionA.State = State2.GetLinkToState(); TransitionA.RequiredEvent.Tag = GetTestTag1(); TStateTreeEditorNode& State3Task1 = State3.AddTask(FName(TEXT("State3Task1"))); } { FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); } const TCHAR* EnterStateStr(TEXT("EnterState")); const TCHAR* ExitStateStr(TEXT("ExitState")); FStateTreeInstanceData InstanceData; TArray RecordedTransitions; { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData, {}, EStateTreeRecordTransitions::Yes); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); // Start and enter state EStateTreeRunStatus Status = Exec.Start(); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State1")); Exec.LogClear(); // Transition from State1 to State2. State3 fails (missing event), enter State4. Exec.SendEvent(GetTestTag1()); Exec.Tick(0.1f); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State2", "State4")); Exec.LogClear(); Exec.Stop(); Exec.Start(); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State1")); // Transition from State1 to State2 failed. Exec.SendEvent(GetTestTag2()); Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State1")); Exec.LogClear(); // Transition from State1 to State2. State3 succeed. Exec.SendEvent(GetTestTag2()); Exec.SendEvent(GetTestTag1()); Status = Exec.Tick(0.1f); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State2", "State3")); Exec.LogClear(); RecordedTransitions = Exec.GetRecordedTransitions(); Exec.Stop(); } { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); // Start and enter state EStateTreeRunStatus Status = Exec.Start(); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State1")); Exec.LogClear(); AITEST_TRUE(TEXT("Wrong number of transitions"), RecordedTransitions.Num() == 5); Exec.ForceTransition(RecordedTransitions[0]); // start AITEST_TRUE(TEXT("Start."), Exec.ExpectInActiveStates("Root", "State1")); Exec.ForceTransition(RecordedTransitions[1]); // state 3 doesn't have the event AITEST_TRUE(TEXT("State3 failed."), Exec.ExpectInActiveStates("Root", "State2", "State4")); Exec.ForceTransition(RecordedTransitions[2]); // start AITEST_TRUE(TEXT("Start and failed transition."), Exec.ExpectInActiveStates("Root", "State1")); Exec.ForceTransition(RecordedTransitions[3]); // failed AITEST_TRUE(TEXT("Start and failed transition."), Exec.ExpectInActiveStates("Root", "State1")); Exec.ForceTransition(RecordedTransitions[4]); // succeed AITEST_TRUE(TEXT("State3 succeed."), Exec.ExpectInActiveStates("Root", "State2", "State3")); Exec.Stop(); } return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionTwoEvents, "System.StateTree.Transition.TwoEvents"); struct FStateTreeTest_TransitionLinkedAssetFromCompletedParent : FStateTreeTestBase { virtual bool InstantTest() override { /* - Tree1 - Root1 - State1: TryEnter, Task w/ 1 frame to complete - State2 : LinkedAsset (On Complete -> State 3) - State3 - Tree2 - Root1 - State1: Task w/ 2 frames to complete */ UStateTree& StateTree1 = NewStateTree(); UStateTree& StateTree2 = NewStateTree(); // Setup linked asset { UStateTreeEditorData& EditorData = *Cast(StateTree2.EditorData); UStateTreeState& Root1 = EditorData.AddSubTree(FName("Tree2_Root1")); UStateTreeState& State1 = Root1.AddChildState(FName("Tree2_State1")); State1.AddTask("Tree2_State1_TaskPrint") .GetInstanceData().Value = 21; State1.AddTask(FName("Tree2_State1_TaskStand")) .GetNode().TicksToCompletion = 2; State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &Root1); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree2); AITEST_TRUE("StateTree should get compiled", bResult); } // Setup main asset { UStateTreeEditorData& EditorData = *Cast(StateTree1.EditorData); UStateTreeState& Root1 = EditorData.AddSubTree(FName("Tree1_Root1")); UStateTreeState& State1 = Root1.AddChildState(FName("Tree1_State1")); UStateTreeState& State2 = State1.AddChildState(FName("Tree1_State2"), EStateTreeStateType::LinkedAsset); UStateTreeState& State3 = Root1.AddChildState(FName("Tree1_State3")); { TStateTreeEditorNode& PrintTask = State1.AddTask("Tree1_State1_TaskPrint"); PrintTask.GetInstanceData().Value = 11; TStateTreeEditorNode& StandTask = State1.AddTask(FName("Tree1_State1_TaskStand")); StandTask.GetNode().TicksToCompletion = 1; StandTask.GetInstanceData().Value = 11; State1.SelectionBehavior = EStateTreeStateSelectionBehavior::TryEnterState; State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State2); } { State2.SetLinkedStateAsset(&StateTree2); TStateTreeEditorNode& PrintTask = State2.AddTask("Tree1_State2_TaskPrint"); PrintTask.GetInstanceData().Value = 12; } FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree1); AITEST_TRUE("StateTree should get compiled", bResult); } FStateTreeInstanceData InstanceData; { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); } constexpr int32 MaxRules = 4; auto MakeStateSelectionRule = [](int32 Index) { EStateTreeStateSelectionRules Rule = EStateTreeStateSelectionRules::None; if ((Index % 2) == 1) { Rule |= EStateTreeStateSelectionRules::CompletedTransitionStatesCreateNewStates; Rule |= EStateTreeStateSelectionRules::CompletedStateBeforeTransitionSourceFailsTransition; } if (Index >= 2) { Rule |= EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates; } return Rule; }; const TCHAR* EnterStateChangedStr = TEXT("EnterState=Changed"); const TCHAR* EnterStateSustainedStr = TEXT("EnterState=Sustained"); const TCHAR* ExitStateChangedStr = TEXT("ExitState=Changed"); const TCHAR* ExitStateSustainedStr = TEXT("ExitState=Sustained"); for (int32 Index = 0; Index < MaxRules; ++Index) { const EStateTreeStateSelectionRules StateSelectionRules = MakeStateSelectionRule(Index); InstanceData = FStateTreeInstanceData(); if (StateTree1.GetStateSelectionRules() != StateSelectionRules) { StateTree1.ResetCompiled(); UStateTreeEditorData* EditorData = CastChecked(StateTree1.EditorData); UStateTreeTestSchema* Schema = CastChecked(EditorData->Schema); Schema->SetStateSelectionRules(StateSelectionRules); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree1); AITEST_TRUE("StateTree should get compiled", bResult); } { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); // Start and enter state EStateTreeRunStatus Status = Exec.Start(); AITEST_TRUE(TEXT("Valid states"), Exec.ExpectInActiveStates("Tree1_Root1", "Tree1_State1")); AITEST_TRUE(TEXT("Expected amount of frames."), InstanceData.GetExecutionState()->ActiveFrames.Num() == 1); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("Tree1_State1_TaskPrint", TEXT("EnterState11")); AITEST_TRUE(TEXT("Start enters in the correct order Tree1_State1_TaskPrint"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskPrint", EnterStateChangedStr); AITEST_TRUE(TEXT("Start enters in the correct order Tree1_State1_TaskPrint"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskStand", TEXT("EnterState")); AITEST_TRUE(TEXT("Start enters in the correct order Tree1_State1_TaskStand"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskStand", EnterStateChangedStr); AITEST_TRUE(TEXT("Start enters in the correct order Tree1_State1_TaskStand"), LogOrder); Exec.LogClear(); } { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); EStateTreeRunStatus Status = Exec.Tick(0.f); AITEST_TRUE(TEXT("Active states should now include linked asset states"), Exec.ExpectInActiveStates("Tree1_Root1", "Tree1_State1", "Tree1_State2", "Tree2_Root1", "Tree2_State1")); AITEST_TRUE(TEXT("Expected amount of frames."), InstanceData.GetExecutionState()->ActiveFrames.Num() == 2); FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("Tree1_State1_TaskPrint", TEXT("Tick11")); AITEST_TRUE(TEXT("Tree1_State1_TaskPrint ticked"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskStand", TEXT("Tick")); AITEST_TRUE(TEXT("Tree1_State1_TaskStand ticked"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskStand", TEXT("StateCompleted")); AITEST_TRUE(TEXT("Completed State1 in Tree1"), LogOrder); LogOrder = LogOrder.Then("Tree1_State2_TaskPrint", TEXT("EnterState12")); AITEST_TRUE(TEXT("Entered State2 in Tree1"), LogOrder); LogOrder = LogOrder.Then("Tree1_State2_TaskPrint", EnterStateChangedStr); AITEST_TRUE(TEXT("Entered State2 in Tree1"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskPrint", TEXT("EnterState21")); AITEST_TRUE(TEXT("Entered State1 in Tree2"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskStand", TEXT("EnterState")); AITEST_TRUE(TEXT("Entered State1 in Tree2"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskStand", EnterStateChangedStr); AITEST_TRUE(TEXT("Entered State1 in Tree2"), LogOrder); Exec.LogClear(); } { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); EStateTreeRunStatus Status = Exec.Tick(0.f); AITEST_TRUE(TEXT("Active states should stay the same after Tree1_State1 completion"), Exec.ExpectInActiveStates("Tree1_Root1", "Tree1_State1", "Tree1_State2", "Tree2_Root1", "Tree2_State1")); AITEST_TRUE(TEXT("Expected amount of frames."), InstanceData.GetExecutionState()->ActiveFrames.Num() == 2); const bool bCompletedStatedCreateNewStates = EnumHasAnyFlags(StateSelectionRules, EStateTreeStateSelectionRules::CompletedTransitionStatesCreateNewStates); const bool bReselectCreateNewStates = EnumHasAnyFlags(StateSelectionRules, EStateTreeStateSelectionRules::ReselectedStateCreatesNewStates); const TCHAR* EnterStateStr = (bCompletedStatedCreateNewStates || bReselectCreateNewStates) ? EnterStateChangedStr: EnterStateSustainedStr; const TCHAR* ExitStateStr = (bCompletedStatedCreateNewStates || bReselectCreateNewStates) ? ExitStateChangedStr: ExitStateSustainedStr; FTestStateTreeExecutionContext::FLogOrder LogOrder = Exec.Expect("Tree1_State1_TaskPrint", TEXT("Tick11")); AITEST_TRUE(TEXT("Tree1_State1_TaskPrint ticked"), LogOrder); if (bCompletedStatedCreateNewStates) { LogOrder = LogOrder.Then("Tree1_State1_TaskStand", TEXT("Tick")); AITEST_TRUE(TEXT("Tree1_State1_TaskStand ticked"), LogOrder); } LogOrder = LogOrder.Then("Tree1_State2_TaskPrint", TEXT("Tick12")); AITEST_TRUE(TEXT("Tree1_State2_TaskPrint ticked"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskPrint", TEXT("Tick21")); AITEST_TRUE(TEXT("Tree2_State1_TaskPrint ticked"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskStand", TEXT("Tick")); AITEST_TRUE(TEXT("Tree2_State1_TaskStand ticked"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskStand", TEXT("StateCompleted")); AITEST_TRUE(TEXT("Completed State1 in Tree2"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskStand", TEXT("StateCompleted")); AITEST_TRUE(TEXT("Completed State1 in Tree1"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskStand", ExitStateStr); AITEST_TRUE(TEXT("Exited State1 in Tree2"), LogOrder); LogOrder = Exec.Expect("Tree2_State1_TaskPrint", TEXT("ExitState21")); AITEST_TRUE(TEXT("Exited State1 in Tree2"), LogOrder); LogOrder = Exec.Expect("Tree2_State1_TaskPrint", ExitStateStr); AITEST_TRUE(TEXT("Exited State1 in Tree2"), LogOrder); LogOrder = Exec.Expect("Tree1_State2_TaskPrint", TEXT("ExitState12")); AITEST_TRUE(TEXT("Exited State2 in Tree1"), LogOrder); LogOrder = Exec.Expect("Tree1_State2_TaskPrint", ExitStateStr); AITEST_TRUE(TEXT("Exited State2 in Tree1"), LogOrder); if (bCompletedStatedCreateNewStates) { LogOrder = LogOrder.Then("Tree1_State1_TaskStand", ExitStateStr); AITEST_TRUE(TEXT("Exited State1 in Tree1"), LogOrder); LogOrder = Exec.Expect("Tree1_State1_TaskPrint", TEXT("ExitState11")); AITEST_TRUE(TEXT("Exited State1 in Tree1"), LogOrder); LogOrder = Exec.Expect("Tree1_State1_TaskPrint", ExitStateStr); AITEST_TRUE(TEXT("Exited State1 in Tree1"), LogOrder); LogOrder = Exec.Expect("Tree1_State1_TaskPrint", TEXT("EnterState11")); AITEST_TRUE(TEXT("Entered State1 in Tree1"), LogOrder); LogOrder = LogOrder.Then("Tree1_State1_TaskStand", EnterStateStr); AITEST_TRUE(TEXT("Entered State1 in Tree1"), LogOrder); } LogOrder = Exec.Expect("Tree1_State2_TaskPrint", TEXT("EnterState12")); AITEST_TRUE(TEXT("Entered State2 in Tree1"), LogOrder); LogOrder = LogOrder.Then("Tree1_State2_TaskPrint", EnterStateStr); AITEST_TRUE(TEXT("Entered State2 in Tree1"), LogOrder); LogOrder = Exec.Expect("Tree2_State1_TaskPrint", TEXT("EnterState21")); AITEST_TRUE(TEXT("Entered State1 in Tree2"), LogOrder); LogOrder = LogOrder.Then("Tree2_State1_TaskStand", EnterStateStr); AITEST_TRUE(TEXT("Entered State1 in Tree2"), LogOrder); Exec.LogClear(); } { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); Exec.Stop(); } } return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionLinkedAssetFromCompletedParent, "System.StateTree.Transition.LinkedAssetFromCompletedParent"); struct FStateTreeTest_TransitionLinkedAssetWith2Root : FStateTreeTestBase { virtual bool InstantTest() override { /* - Tree1 - Root1 - State1 : LinkedAsset - Tree2 - Root1 - State1: Transition To Root3 - Root2 - State2 */ UStateTree& StateTree1 = NewStateTree(); UStateTree& StateTree2 = NewStateTree(); { UStateTreeEditorData& EditorData = *Cast(StateTree2.EditorData); UStateTreeState& Root1 = EditorData.AddSubTree(FName("Root1")); UStateTreeState& State1 = Root1.AddChildState(FName("State1")); UStateTreeState& Root2 = EditorData.AddSubTree(FName("Root2")); UStateTreeState& State2 = Root2.AddChildState(FName("State2")); auto& Task1 = State1.AddTask(FName("Tree2State1Task1")); Task1.GetNode().TicksToCompletion = 1; State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State2); auto& Task2 = State2.AddTask(FName(TEXT("Task1A"))); Task2.GetNode().TicksToCompletion = 1; State2.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree2); AITEST_TRUE("StateTree should get compiled", bResult); } { UStateTreeEditorData& EditorData = *Cast(StateTree1.EditorData); UStateTreeState& Root1 = EditorData.AddSubTree(FName("Root")); UStateTreeState& State1 = Root1.AddChildState(FName("State1"), EStateTreeStateType::LinkedAsset); State1.SetLinkedStateAsset(&StateTree2); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree1); AITEST_TRUE("StateTree should get compiled", bResult); } FStateTreeInstanceData InstanceData; { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); // Start and enter state EStateTreeRunStatus Status = Exec.Start(); AITEST_TRUE(TEXT("Valid states"), Exec.ExpectInActiveStates("Root", "State1", "Root1", "State1")); AITEST_TRUE(TEXT("Expected amount of frames."), InstanceData.GetExecutionState()->ActiveFrames.Num() == 2); Exec.LogClear(); } { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); EStateTreeRunStatus Status = Exec.Tick(0.f); AITEST_TRUE(TEXT("Active states should stay the same"), Exec.ExpectInActiveStates("Root", "State1", "Root2", "State2")); AITEST_TRUE(TEXT("Expected amount of frames."), InstanceData.GetExecutionState()->ActiveFrames.Num() == 2); Exec.LogClear(); } { FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); Exec.Stop(); } return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_TransitionLinkedAssetWith2Root, "System.StateTree.Transition.LinkedAssetWith2Root"); struct FStateTreeTest_MultipleTransition_TemporaryFrame_GlobalTask : FStateTreeTestBase { virtual bool InstantTest() override { /* - Tree1 - Root - State1 Transition1 -> Goto State2; Transition2 -> Goto State3 - State2 : Tree2 - State3 - Tree2 : Global Tasks - Root : Tree3 - Tree3 : Global Tasks */ UStateTree& StateTree1 = NewStateTree(); UStateTree& StateTree2 = NewStateTree(); UStateTree& StateTree3 = NewStateTree(); { UStateTreeEditorData& Tree1EditorData = *Cast(StateTree1.EditorData); UStateTreeState& Root = Tree1EditorData.AddSubTree(FName("Tree1Root")); UStateTreeState& State1 = Root.AddChildState(FName("Tree1State1")); UStateTreeState& State2 = Root.AddChildState(FName("Tree1State2")); UStateTreeState& State3 = Root.AddChildState(FName("Tree1State3")); FStateTreeTransition& LowPriorityTrans = State1.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &State2); LowPriorityTrans.Priority = EStateTreeTransitionPriority::Low; FStateTreeTransition& HighPriorityTrans = State1.AddTransition(EStateTreeTransitionTrigger::OnTick, EStateTreeTransitionType::GotoState, &State3); HighPriorityTrans.Priority = EStateTreeTransitionPriority::High; State2.Type = EStateTreeStateType::LinkedAsset; State2.SetLinkedStateAsset(&StateTree2); } { UStateTreeEditorData& Tree2EditorData = *Cast(StateTree2.EditorData); TStateTreeEditorNode& TaskEditorNode = Tree2EditorData.AddGlobalTask(FName("Tree2Stand")); UStateTreeState& Root = Tree2EditorData.AddSubTree(FName("Tree2Root")); Root.Type = EStateTreeStateType::LinkedAsset; Root.SetLinkedStateAsset(&StateTree3); } { UStateTreeEditorData& Tree3EditorData = *Cast(StateTree3.EditorData); Tree3EditorData.AddGlobalTask(FName("Tree3Stand")); UStateTreeState& Root = Tree3EditorData.AddSubTree(FName("Tree3Root")); } { FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree3); AITEST_TRUE("StateTree should get compiled", bResult); } { FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree2); AITEST_TRUE("StateTree should get compiled", bResult); } { FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree1); AITEST_TRUE("StateTree should get compiled", bResult) } { FStateTreeInstanceData InstanceData; FTestStateTreeExecutionContext Exec(StateTree1, StateTree1, InstanceData); const bool bInitSucceeded = Exec.IsValid(); AITEST_TRUE("StateTree should init", bInitSucceeded); EStateTreeRunStatus Status = Exec.Start(); AITEST_TRUE(TEXT("Valid states"), Exec.ExpectInActiveStates("Tree1Root", "Tree1State1")); Exec.LogClear(); Exec.Tick(0.1f); FTestStateTreeExecutionContext::FLogOrder LogOrder = FTestStateTreeExecutionContext::FLogOrder(Exec, 0); LogOrder = LogOrder.Then("Tree2Stand", TEXT("EnterState=Changed")).Then("Tree3Stand", TEXT("EnterState=Changed")); AITEST_TRUE(TEXT("Enter Global tasks on temporary frames correctly"), LogOrder); LogOrder = LogOrder.Then("Tree3Stand", TEXT("ExitStopped")) .Then("Tree3Stand", TEXT("ExitState=Changed")) .Then("Tree2Stand", TEXT("ExitStopped")) .Then("Tree2Stand", TEXT("ExitState=Changed")); AITEST_TRUE(TEXT("Exit Global tasks on temporary frames correctly"), LogOrder); AITEST_TRUE(TEXT("Valid states"), Exec.ExpectInActiveStates("Tree1Root", "Tree1State3")); Exec.Stop(); } return true; } }; IMPLEMENT_STATE_TREE_INSTANT_TEST(FStateTreeTest_MultipleTransition_TemporaryFrame_GlobalTask, "System.StateTree.Transition.MultipleTransition.TemporaryFrame.GlobalTask"); } // namespace UE::StateTree::Tests UE_ENABLE_OPTIMIZATION_SHIP #undef LOCTEXT_NAMESPACE