Files
UnrealEngineUWP/Engine/Plugins/Runtime/StateTree/Source/StateTreeTestSuite/Private/StateTreeTest.cpp
joe pribele 0926a226cf removed PRAGMA_DISABLE_OPTIMZATIONS that appear to be left in by accident
#rb zousar.shaker
#preflight 636c1c6f7c2b50519028f593

[CL 23063526 by joe pribele in ue5-main branch]
2022-11-09 16:47:19 -05:00

438 lines
19 KiB
C++

// 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<int32> FStateTreeTestConditionInstanceData::GlobalCounter = 0;
namespace UE::StateTree::Tests
{
UStateTree& NewStateTree(UObject* Outer = GetTransientPackage())
{
UStateTree* StateTree = NewObject<UStateTree>(Outer);
check(StateTree);
UStateTreeEditorData* EditorData = NewObject<UStateTreeEditorData>(StateTree);
check(EditorData);
StateTree->EditorData = EditorData;
EditorData->Schema = NewObject<UStateTreeTestSchema>();
return *StateTree;
}
}
struct FStateTreeTest_MakeAndBakeStateTree : FAITestBase
{
virtual bool InstantTest() override
{
UStateTree& StateTree = UE::StateTree::Tests::NewStateTree(&GetWorld());
UStateTreeEditorData& EditorData = *Cast<UStateTreeEditorData>(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<FTestEval_A>();
// State A
auto& TaskB1 = StateA.AddTask<FTestTask_B>();
EditorData.AddPropertyBinding(FStateTreeEditorPropertyPath(EvalA.ID, TEXT("IntA")), FStateTreeEditorPropertyPath(TaskB1.ID, TEXT("IntB")));
auto& IntCond = StateA.AddEnterCondition<FStateTreeCompareIntCondition>(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<FTestTask_B>();
EditorData.AddPropertyBinding(FStateTreeEditorPropertyPath(EvalA.ID, TEXT("bBoolA")), FStateTreeEditorPropertyPath(TaskB2.ID, TEXT("bBoolB")));
FStateTreeTransition& Trans = StateB.AddTransition({}, EStateTreeTransitionType::GotoState, &Root);
auto& TransFloatCond = Trans.AddCondition<FStateTreeCompareFloatCondition>(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<UStateTreeEditorData>(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<FTestTask_Stand>(FName(TEXT("Task1")));
State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState);
auto& Task2 = State2.AddTask<FTestTask_Stand>(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<UStateTreeEditorData>(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<FTestTask_Stand>(FName(TEXT("TaskRoot")));
TaskRoot.GetNode().TicksToCompletion = 2;
auto& Task1 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task1")));
Task1.GetNode().TicksToCompletion = 2;
auto& Task1A = State1A.AddTask<FTestTask_Stand>(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<UStateTreeEditorData>(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<FTestTask_Stand>(FName(TEXT("TaskRoot")));
auto& Task1 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task1")));
auto& Task2 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task2")));
Task2.GetNode().EnterStateResult = EStateTreeRunStatus::Failed;
auto& Task3 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task3")));
auto& Task1A = State1A.AddTask<FTestTask_Stand>(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<UStateTreeEditorData>(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<FTestTask_Stand>(FName(TEXT("Task2")));
auto& Task3A = State3A.AddTask<FTestTask_Stand>(FName(TEXT("Task3A")));
State3A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State3B);
auto& Task3B = State3B.AddTask<FTestTask_Stand>(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<UStateTreeEditorData>(StateTree.EditorData);
UStateTreeState& Root = EditorData.AddSubTree(FName(TEXT("Root")));
auto& IntCond = Root.AddEnterCondition<FStateTreeTestCondition>();
IntCond.GetInstanceData().Count = 1;
auto& Task = Root.AddTask<FTestTask_Stand>(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<FStateTreeInstanceData> 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<FStateTreeTestRunContext> 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