// Copyright Epic Games, Inc. All Rights Reserved. #include "StateTreeTest.h" #include "AITestsCommon.h" #include "StateTreeEditorData.h" #include "StateTreeState.h" #include "StateTreeCompiler.h" #include "Conditions/StateTreeCommonConditions.h" #include "StateTree.h" #include "StateTreeTestTypes.h" #include "StateTreeExecutionContext.h" #include "Engine/World.h" #include "Async/ParallelFor.h" #include "UObject/Package.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(StateTreeTest) #define LOCTEXT_NAMESPACE "AITestSuite_StateTreeTest" std::atomic FStateTreeTestConditionInstanceData::GlobalCounter = 0; namespace UE::StateTree::Tests { UStateTree& NewStateTree(UObject* Outer = GetTransientPackage()) { UStateTree* StateTree = NewObject(Outer); check(StateTree); UStateTreeEditorData* EditorData = NewObject(StateTree); check(EditorData); StateTree->EditorData = EditorData; EditorData->Schema = NewObject(); return *StateTree; } } struct FStateTreeTest_MakeAndBakeStateTree : FAITestBase { virtual bool InstantTest() override { UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld()); 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"))); // Root auto& EvalA = EditorData.AddEvaluator(); // State A auto& TaskB1 = StateA.AddTask(); EditorData.AddPropertyBinding(FStateTreeEditorPropertyPath(EvalA.ID, TEXT("IntA")), FStateTreeEditorPropertyPath(TaskB1.ID, TEXT("IntB"))); auto& IntCond = StateA.AddEnterCondition(EGenericAICheck::Less); IntCond.GetInstanceData().Right = 2; EditorData.AddPropertyBinding(FStateTreeEditorPropertyPath(EvalA.ID, TEXT("IntA")), FStateTreeEditorPropertyPath(IntCond.ID, TEXT("Left"))); StateA.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &StateB); // State B auto& TaskB2 = StateB.AddTask(); EditorData.AddPropertyBinding(FStateTreeEditorPropertyPath(EvalA.ID, TEXT("bBoolA")), FStateTreeEditorPropertyPath(TaskB2.ID, TEXT("bBoolB"))); FStateTreeTransition& Trans = StateB.AddTransition({}, EStateTreeTransitionType::GotoState, &Root); auto& TransFloatCond = Trans.AddCondition(EGenericAICheck::Less); TransFloatCond.GetInstanceData().Right = 13.0f; EditorData.AddPropertyBinding(FStateTreeEditorPropertyPath(EvalA.ID, TEXT("FloatA")), FStateTreeEditorPropertyPath(TransFloatCond.ID, TEXT("Left"))); StateB.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded); FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_MakeAndBakeStateTree, "System.StateTree.MakeAndBakeStateTree"); struct FStateTreeTest_Sequence : FAITestBase { virtual bool InstantTest() override { UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld()); 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"))); auto& Task1 = State1.AddTask(FName(TEXT("Task1"))); State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState); auto& Task2 = State2.AddTask(FName(TEXT("Task2"))); State2.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 FString TickStr(TEXT("Tick")); const FString EnterStateStr(TEXT("EnterState")); const FString ExitStateStr(TEXT("ExitState")); Status = Exec.Start(); AITEST_TRUE("StateTree Task1 should enter state", Exec.Expect(Task1.GetName(), EnterStateStr)); AITEST_FALSE("StateTree Task1 should not tick", Exec.Expect(Task1.GetName(), TickStr)); Exec.LogClear(); Status = Exec.Tick(0.1f); AITEST_TRUE("StateTree Task1 should tick, and exit state", Exec.Expect(Task1.GetName(), TickStr).Then(Task1.GetName(), ExitStateStr)); AITEST_TRUE("StateTree Task2 should enter state", Exec.Expect(Task2.GetName(), EnterStateStr)); AITEST_FALSE("StateTree Task2 should not tick", Exec.Expect(Task2.GetName(), TickStr)); AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running); Exec.LogClear(); Status = Exec.Tick(0.1f); AITEST_TRUE("StateTree Task2 should tick, and exit state", Exec.Expect(Task2.GetName(), TickStr).Then(Task2.GetName(), ExitStateStr)); AITEST_FALSE("StateTree Task1 should not tick", Exec.Expect(Task1.GetName(), TickStr)); AITEST_TRUE("StateTree should be completed", Status == EStateTreeRunStatus::Succeeded); Exec.LogClear(); Status = Exec.Tick(0.1f); AITEST_FALSE("StateTree Task1 should not tick", Exec.Expect(Task1.GetName(), TickStr)); AITEST_FALSE("StateTree Task2 should not tick", Exec.Expect(Task2.GetName(), TickStr)); Exec.LogClear(); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_Sequence, "System.StateTree.Sequence"); struct FStateTreeTest_Select : FAITestBase { virtual bool InstantTest() override { UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld()); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State1A = State1.AddChildState(FName(TEXT("State1A"))); auto& TaskRoot = Root.AddTask(FName(TEXT("TaskRoot"))); TaskRoot.GetNode().TicksToCompletion = 2; auto& Task1 = State1.AddTask(FName(TEXT("Task1"))); Task1.GetNode().TicksToCompletion = 2; auto& Task1A = State1A.AddTask(FName(TEXT("Task1A"))); Task1A.GetNode().TicksToCompletion = 2; State1A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State1); 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 FString TickStr(TEXT("Tick")); const FString EnterStateStr(TEXT("EnterState")); const FString ExitStateStr(TEXT("ExitState")); // Start and enter state Status = Exec.Start(); AITEST_TRUE("StateTree TaskRoot should enter state", Exec.Expect(TaskRoot.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task1 should enter state", Exec.Expect(Task1.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task1A should enter state", Exec.Expect(Task1A.GetName(), EnterStateStr)); AITEST_FALSE("StateTree TaskRoot should not tick", Exec.Expect(TaskRoot.GetName(), TickStr)); AITEST_FALSE("StateTree Task1 should not tick", Exec.Expect(Task1.GetName(), TickStr)); AITEST_FALSE("StateTree Task1A should not tick", Exec.Expect(Task1A.GetName(), TickStr)); AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running); Exec.LogClear(); // Regular tick Status = Exec.Tick(0.1f); AITEST_TRUE("StateTree tasks should update in order", Exec.Expect(TaskRoot.GetName(), TickStr).Then(Task1.GetName(), TickStr).Then(Task1A.GetName(), TickStr)); AITEST_FALSE("StateTree TaskRoot should not EnterState", Exec.Expect(TaskRoot.GetName(), EnterStateStr)); AITEST_FALSE("StateTree Task1 should not EnterState", Exec.Expect(Task1.GetName(), EnterStateStr)); AITEST_FALSE("StateTree Task1A should not EnterState", Exec.Expect(Task1A.GetName(), EnterStateStr)); AITEST_FALSE("StateTree TaskRoot should not ExitState", Exec.Expect(TaskRoot.GetName(), ExitStateStr)); AITEST_FALSE("StateTree Task1 should not ExitState", Exec.Expect(Task1.GetName(), ExitStateStr)); AITEST_FALSE("StateTree Task1A should not ExitState", Exec.Expect(Task1A.GetName(), ExitStateStr)); AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running); Exec.LogClear(); // Partial reselect, Root should not get EnterState Status = Exec.Tick(0.1f); AITEST_FALSE("StateTree TaskRoot should not enter state", Exec.Expect(TaskRoot.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task1 should tick, exit state, and enter state", Exec.Expect(Task1.GetName(), TickStr).Then(Task1.GetName(), ExitStateStr).Then(Task1.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task1A should tick, exit state, and enter state", Exec.Expect(Task1A.GetName(), TickStr).Then(Task1A.GetName(), ExitStateStr).Then(Task1A.GetName(), EnterStateStr)); AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running); Exec.LogClear(); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_Select, "System.StateTree.Select"); struct FStateTreeTest_FailEnterState : FAITestBase { virtual bool InstantTest() override { UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld()); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1"))); UStateTreeState& State1A = State1.AddChildState(FName(TEXT("State1A"))); auto& TaskRoot = Root.AddTask(FName(TEXT("TaskRoot"))); auto& Task1 = State1.AddTask(FName(TEXT("Task1"))); auto& Task2 = State1.AddTask(FName(TEXT("Task2"))); Task2.GetNode().EnterStateResult = EStateTreeRunStatus::Failed; auto& Task3 = State1.AddTask(FName(TEXT("Task3"))); auto& Task1A = State1A.AddTask(FName(TEXT("Task1A"))); State1A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State1); 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 FString TickStr(TEXT("Tick")); const FString EnterStateStr(TEXT("EnterState")); const FString ExitStateStr(TEXT("ExitState")); const FString StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Status = Exec.Start(); AITEST_TRUE("StateTree TaskRoot should enter state", Exec.Expect(TaskRoot.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task1 should enter state", Exec.Expect(Task1.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task2 should enter state", Exec.Expect(Task2.GetName(), EnterStateStr)); AITEST_FALSE("StateTree Task3 should not enter state", Exec.Expect(Task3.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Should execute StateCompleted in reverse order", Exec.Expect(Task2.GetName(), StateCompletedStr).Then(Task1.GetName(), StateCompletedStr).Then(TaskRoot.GetName(), StateCompletedStr)); AITEST_FALSE("StateTree Task3 should not state complete", Exec.Expect(Task3.GetName(), StateCompletedStr)); AITEST_TRUE("StateTree exec status should be failed", Exec.GetLastTickStatus() == EStateTreeRunStatus::Failed); Exec.LogClear(); // Stop and exit state Status = Exec.Stop(); AITEST_TRUE("StateTree TaskRoot should exit state", Exec.Expect(TaskRoot.GetName(), ExitStateStr)); AITEST_TRUE("StateTree Task1 should exit state", Exec.Expect(Task1.GetName(), ExitStateStr)); AITEST_TRUE("StateTree Task2 should exit state", Exec.Expect(Task2.GetName(), ExitStateStr)); AITEST_FALSE("StateTree Task3 should not exit state", Exec.Expect(Task3.GetName(), ExitStateStr)); Exec.LogClear(); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_FailEnterState, "System.StateTree.FailEnterState"); struct FStateTreeTest_SubTree : FAITestBase { virtual bool InstantTest() override { UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld()); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); UStateTreeState& State1 = Root.AddChildState(FName(TEXT("State1")), EStateTreeStateType::Linked); UStateTreeState& State2 = Root.AddChildState(FName(TEXT("State2"))); UStateTreeState& State3 = Root.AddChildState(FName(TEXT("State3")), EStateTreeStateType::Subtree); UStateTreeState& State3A = State3.AddChildState(FName(TEXT("State3A"))); UStateTreeState& State3B = State3.AddChildState(FName(TEXT("State3B"))); State1.LinkedSubtree.Set(&State3); auto& Task2 = State2.AddTask(FName(TEXT("Task2"))); auto& Task3A = State3A.AddTask(FName(TEXT("Task3A"))); State3A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State3B); auto& Task3B = State3B.AddTask(FName(TEXT("Task3B"))); 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 FString TickStr(TEXT("Tick")); const FString EnterStateStr(TEXT("EnterState")); const FString ExitStateStr(TEXT("ExitState")); const FString StateCompletedStr(TEXT("StateCompleted")); // Start and enter state Status = Exec.Start(); AITEST_TRUE("StateTree Active States should be in Root/State1/State3/State3A", Exec.ExpectInActiveStates(Root.Name, State1.Name, State3.Name, State3A.Name)); AITEST_TRUE("StateTree Task2 should enter state", !Exec.Expect(Task2.GetName(), EnterStateStr)); AITEST_TRUE("StateTree Task3A should enter state", Exec.Expect(Task3A.GetName(), EnterStateStr)); AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running); Exec.LogClear(); Status = Exec.Tick(0.1f); AITEST_TRUE("StateTree Active States should be in Root/State1/State3/State3B", Exec.ExpectInActiveStates(Root.Name, State1.Name, State3.Name, State3B.Name)); AITEST_TRUE("StateTree Task3B should enter state", Exec.Expect(Task3B.GetName(), EnterStateStr)); AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_SubTree, "System.StateTree.SubTree"); struct FStateTreeTest_SharedInstanceData : FAITestBase { virtual bool InstantTest() override { UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld()); UStateTreeEditorData& EditorData = *Cast(StateTree.EditorData); UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root"))); auto& IntCond = Root.AddEnterCondition(); IntCond.GetInstanceData().Count = 1; auto& Task = Root.AddTask(FName(TEXT("Task"))); Task.GetNode().TicksToCompletion = 2; FStateTreeCompilerLog Log; FStateTreeCompiler Compiler(Log); const bool bResult = Compiler.Compile(StateTree); AITEST_TRUE("StateTree should get compiled", bResult); // Init, nothing should access the shared data. constexpr int32 NumConcurrent = 100; FStateTreeTestConditionInstanceData::GlobalCounter = 0; bool bInitSucceeded = true; TArray InstanceDatas; InstanceDatas.SetNum(NumConcurrent); for (int32 Index = 0; Index < NumConcurrent; Index++) { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceDatas[Index]); bInitSucceeded &= Exec.IsValid(); } AITEST_TRUE("All StateTree contexts should init", bInitSucceeded); AITEST_EQUAL("Test condition global counter should be 0", (int32)FStateTreeTestConditionInstanceData::GlobalCounter, 0); // Start in parallel // This should create shared data per thread. // We expect that ParallelForWithTaskContext() creates a context per thread. TArray RunContexts; ParallelForWithTaskContext( RunContexts, InstanceDatas.Num(), [&InstanceDatas, &StateTree](FStateTreeTestRunContext& RunContext, int32 Index) { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceDatas[Index]); const EStateTreeRunStatus Status = Exec.Start(); if (Status == EStateTreeRunStatus::Running) { RunContext.Count++; } } ); int32 StartTotalRunning = 0; for (FStateTreeTestRunContext RunContext : RunContexts) { StartTotalRunning += RunContext.Count; } AITEST_EQUAL("All StateTree contexts should be running after Start", StartTotalRunning, NumConcurrent); AITEST_EQUAL("Test condition global counter should equal context count after Start", (int32)FStateTreeTestConditionInstanceData::GlobalCounter, InstanceDatas.Num()); // Tick in parallel // This should not recreate the data, so FStateTreeTestConditionInstanceData::GlobalCounter should stay as is. for (FStateTreeTestRunContext RunContext : RunContexts) { RunContext.Count = 0; } ParallelForWithTaskContext( RunContexts, InstanceDatas.Num(), [&InstanceDatas, &StateTree](FStateTreeTestRunContext& RunContext, int32 Index) { FTestStateTreeExecutionContext Exec(StateTree, StateTree, InstanceDatas[Index]); const EStateTreeRunStatus Status = Exec.Tick(0.1f); if (Status == EStateTreeRunStatus::Running) { RunContext.Count++; } } ); int32 TickTotalRunning = 0; for (FStateTreeTestRunContext RunContext : RunContexts) { TickTotalRunning += RunContext.Count; } AITEST_EQUAL("All StateTree contexts should be running after Tick", TickTotalRunning, NumConcurrent); AITEST_EQUAL("Test condition global counter should equal context count after Tick", (int32)FStateTreeTestConditionInstanceData::GlobalCounter, InstanceDatas.Num()); return true; } }; IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_SharedInstanceData, "System.StateTree.SharedInstanceData"); #undef LOCTEXT_NAMESPACE