Files
UnrealEngineUWP/Engine/Plugins/Runtime/StateTree/Source/StateTreeTestSuite/Private/StateTreeTest.cpp
mikko mononen 69fc0b5edc StateTree: Sort property bindings based on layout.
- the bindings sent to the compiler are in order they were created
- sort the bindings so that e.g. copying and array and a speciific array item will first copy the earliest property (array) and then the rest (specific item)

#rb Mieszko.Zielinski
#preflight 63ef2b42500c05a624aff988

[CL 24278616 by mikko mononen in ue5-main branch]
2023-02-17 04:00:32 -05:00

1116 lines
49 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "StateTreeTest.h"
#include "AITestsCommon.h"
#include "StateTreeCompilerLog.h"
#include "StateTreeEditorData.h"
#include "StateTreeCompiler.h"
#include "Conditions/StateTreeCommonConditions.h"
#include "StateTreeTestTypes.h"
#include "Engine/World.h"
#include "Async/ParallelFor.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(StateTreeTest)
#define LOCTEXT_NAMESPACE "AITestSuite_StateTreeTest"
UE_DISABLE_OPTIMIZATION_SHIP
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(EvalA, TEXT("IntA"), TaskB1, TEXT("IntB"));
auto& IntCond = StateA.AddEnterCondition<FStateTreeCompareIntCondition>(EGenericAICheck::Less);
IntCond.GetInstanceData().Right = 2;
EditorData.AddPropertyBinding(EvalA, TEXT("IntA"), IntCond, TEXT("Left"));
StateA.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &StateB);
// State B
auto& TaskB2 = StateB.AddTask<FTestTask_B>();
EditorData.AddPropertyBinding(EvalA, TEXT("bBoolA"), TaskB2, TEXT("bBoolB"));
FStateTreeTransition& Trans = StateB.AddTransition({}, EStateTreeTransitionType::GotoState, &Root);
auto& TransFloatCond = Trans.AddCondition<FStateTreeCompareFloatCondition>(EGenericAICheck::Less);
TransFloatCond.GetInstanceData().Right = 13.0f;
EditorData.AddPropertyBinding(EvalA, TEXT("FloatA"), TransFloatCond, 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 = 3; // let Task1A to complete first
auto& Task1 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task1")));
Task1.GetNode().TicksToCompletion = 3; // let Task1A to complete first
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_Restart : 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")));
auto& Task1 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task1")));
Task1.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 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 Task1 should enter state", Exec.Expect(Task1.GetName(), EnterStateStr));
AITEST_TRUE("StateTree exec status should be running", Exec.GetLastTickStatus() == EStateTreeRunStatus::Running);
Exec.LogClear();
// Tick
Status = Exec.Tick(0.1f);
AITEST_TRUE("StateTree exec status should be running", Exec.GetLastTickStatus() == EStateTreeRunStatus::Running);
Exec.LogClear();
// Call Start again, should stop and start the tree.
Status = Exec.Start();
AITEST_TRUE("StateTree Task1 should exit state", Exec.Expect(Task1.GetName(), ExitStateStr));
AITEST_TRUE("StateTree Task1 should enter state", Exec.Expect(Task1.GetName(), EnterStateStr));
AITEST_TRUE("StateTree exec status should be running", Exec.GetLastTickStatus() == EStateTreeRunStatus::Running);
Exec.LogClear();
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_Restart, "System.StateTree.Restart");
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 = State3.GetLinkToState();
State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State2);
auto& Task2 = State2.AddTask<FTestTask_Stand>(FName(TEXT("Task2")));
State2.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded);
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")));
State3B.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"));
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();
// Transition within subtree
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);
Exec.LogClear();
// Complete subtree
Status = Exec.Tick(0.1f);
AITEST_TRUE("StateTree Active States should be in Root/State2", Exec.ExpectInActiveStates(Root.Name, State2.Name));
AITEST_TRUE("StateTree Task2 should enter state", Exec.Expect(Task2.GetName(), EnterStateStr));
AITEST_TRUE("StateTree should be running", Status == EStateTreeRunStatus::Running);
Exec.LogClear();
// Complete the whole tree
Status = Exec.Tick(0.1f);
AITEST_TRUE("StateTree should complete in succeeded", Status == EStateTreeRunStatus::Succeeded);
Exec.LogClear();
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");
struct FStateTreeTest_TransitionPriority : 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")));
UStateTreeState& State1B = State1.AddChildState(FName(TEXT("State1B")));
UStateTreeState& State1C = State1.AddChildState(FName(TEXT("State1C")));
auto& Task1 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task1")));
Task1.GetNode().TicksToCompletion = 2;
State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded);
auto& Task1A = State1A.AddTask<FTestTask_Stand>(FName(TEXT("Task1A")));
Task1A.GetNode().TicksToCompletion = 1;
State1A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState);
auto& Task1B = State1B.AddTask<FTestTask_Stand>(FName(TEXT("Task1B")));
Task1B.GetNode().TicksToCompletion = 2;
State1B.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::NextState);
auto& Task1C = State1C.AddTask<FTestTask_Stand>(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 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 Task1 should enter state", Exec.Expect(Task1.GetName(), EnterStateStr));
AITEST_TRUE("StateTree Task1A should enter state", Exec.Expect(Task1A.GetName(), EnterStateStr));
Exec.LogClear();
// Transition from Task1A to Task1B
Status = Exec.Tick(0.1f);
AITEST_TRUE("StateTree Task1A should complete", Exec.Expect(Task1A.GetName(), StateCompletedStr));
AITEST_TRUE("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("Tree execution should stop on success", Status, EStateTreeRunStatus::Succeeded);
Exec.LogClear();
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_TransitionPriority, "System.StateTree.Transition.Priority");
struct FStateTreeTest_TransitionPriorityEnterState : 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& 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<FTestTask_Stand>(FName(TEXT("Task0")));
State0.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State1);
auto& Task1 = State1.AddTask<FTestTask_Stand>(FName(TEXT("Task1")));
Task1.GetNode().EnterStateResult = EStateTreeRunStatus::Failed;
State1.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State2);
auto& Task1A = State1A.AddTask<FTestTask_Stand>(FName(TEXT("Task1A")));
State1A.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &State3);
auto& Task2 = State2.AddTask<FTestTask_Stand>(FName(TEXT("Task2")));
State2.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::Succeeded);
auto& Task3 = State3.AddTask<FTestTask_Stand>(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 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 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("StateTree Task0 should complete", Exec.Expect(Task0.GetName(), StateCompletedStr));
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));
Exec.LogClear();
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_TransitionPriorityEnterState, "System.StateTree.Transition.PriorityEnterState");
struct FStateTreeTest_TransitionGlobalDataView : FAITestBase
{
// Tests that the global eval and task dataviews are kept up to date when transitioning from
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")));
auto& EvalA = EditorData.AddEvaluator<FTestEval_A>(FName(TEXT("Eval")));
EvalA.GetInstanceData().IntA = 42;
auto& GlobalTask = EditorData.AddGlobalTask<FTestTask_PrintValue>(FName(TEXT("Global")));
GlobalTask.GetInstanceData().Value = 123;
// State A
auto& Task0 = StateA.AddTask<FTestTask_Stand>(FName(TEXT("Task0")));
StateA.AddTransition(EStateTreeTransitionTrigger::OnStateCompleted, EStateTreeTransitionType::GotoState, &StateB);
// State B
auto& Task1 = StateB.AddTask<FTestTask_PrintValue>(FName(TEXT("Task1")));
EditorData.AddPropertyBinding(EvalA, TEXT("IntA"), Task1, TEXT("Value"));
auto& Task2 = StateB.AddTask<FTestTask_PrintValue>(FName(TEXT("Task2")));
EditorData.AddPropertyBinding(GlobalTask, TEXT("Value"), Task2, TEXT("Value"));
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 EnterStateStr(TEXT("EnterState"));
const FString EnterState42Str(TEXT("EnterState42"));
const FString EnterState123Str(TEXT("EnterState123"));
// Start and enter state
Status = Exec.Start();
AITEST_TRUE("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("StateTree Task0 should enter state with value 42", Exec.Expect(Task1.GetName(), EnterState42Str));
AITEST_TRUE("StateTree Task1 should enter state with value 123", Exec.Expect(Task2.GetName(), EnterState123Str));
Exec.LogClear();
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_TransitionGlobalDataView, "System.StateTree.Transition.GlobalDataView");
struct FStateTreeTest_PropertyPathOffset : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("StructB.B"));
AITEST_TRUE("Parsing path should succeeed", bParseResult);
AITEST_EQUAL("Should have 2 path segments", Path.NumSegments(), 2);
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirections(FStateTreeTest_PropertyStruct::StaticStruct(), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path should succeeed", bResolveResult);
AITEST_EQUAL("Should have no resolve errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 2 indirections", Indirections.Num(), 2);
AITEST_EQUAL("Indirection 0 should be Offset type", Indirections[0].GetAccessType(), EStateTreePropertyAccessType::Offset);
AITEST_EQUAL("Indirection 1 should be Offset type", Indirections[1].GetAccessType(), EStateTreePropertyAccessType::Offset);
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathOffset, "System.StateTree.PropertyPath.Offset");
struct FStateTreeTest_PropertyPathParseFail : FAITestBase
{
virtual bool InstantTest() override
{
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("")); // empty is valid.
AITEST_TRUE("Parsing path should succeed", bParseResult);
}
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("StructB.[0]B"));
AITEST_FALSE("Parsing path should fail", bParseResult);
}
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("StructB..NoThere"));
AITEST_FALSE("Parsing path should fail", bParseResult);
}
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("."));
AITEST_FALSE("Parsing path should fail", bParseResult);
}
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("StructB..B"));
AITEST_FALSE("Parsing path should fail", bParseResult);
}
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathParseFail, "System.StateTree.PropertyPath.ParseFail");
struct FStateTreeTest_PropertyPathOffsetFail : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("StructB.Q"));
AITEST_TRUE("Parsing path should succeeed", bParseResult);
AITEST_EQUAL("Should have 2 path segments", Path.NumSegments(), 2);
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirections(FStateTreeTest_PropertyStruct::StaticStruct(), Indirections, &ResolveErrors);
AITEST_FALSE("Resolve path should not succeeed", bResolveResult);
AITEST_NOT_EQUAL("Should have errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 0 indirections", Indirections.Num(), 0);
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathOffsetFail, "System.StateTree.PropertyPath.OffsetFail");
struct FStateTreeTest_PropertyPathObject : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("InstancedObject.A"));
AITEST_TRUE("Parsing path should succeeed", bParseResult);
AITEST_EQUAL("Should have 2 path segments", Path.NumSegments(), 2);
UStateTreeTest_PropertyObject* Object = NewObject<UStateTreeTest_PropertyObject>();
Object->InstancedObject = NewObject<UStateTreeTest_PropertyObjectInstanced>();
const bool bUpdateResult = Path.UpdateInstanceStructsFromValue(FStateTreeDataView(Object));
AITEST_TRUE("Update instance types should succeeed", bUpdateResult);
AITEST_TRUE("Path segment 0 instance type should be UStateTreeTest_PropertyObjectInstanced", Path.GetSegment(0).GetInstanceStruct() == UStateTreeTest_PropertyObjectInstanced::StaticClass());
AITEST_TRUE("Path segment 1 instance type should be nullptr", Path.GetSegment(1).GetInstanceStruct() == nullptr);
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathObject, "System.StateTree.PropertyPath.Object");
struct FStateTreeTest_PropertyPathWrongObject : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("InstancedObject.B"));
AITEST_TRUE("Parsing path should succeeed", bParseResult);
AITEST_EQUAL("Should have 2 path segments", Path.NumSegments(), 2);
UStateTreeTest_PropertyObject* Object = NewObject<UStateTreeTest_PropertyObject>();
Object->InstancedObject = NewObject<UStateTreeTest_PropertyObjectInstancedWithB>();
{
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path should succeeed", bResolveResult);
AITEST_EQUAL("Should have 2 indirections", Indirections.Num(), 2);
AITEST_TRUE("Object ", Indirections[0].GetAccessType() == EStateTreePropertyAccessType::ObjectInstance);
AITEST_TRUE("Object ", Indirections[0].GetContainerStruct() == Object->GetClass());
AITEST_TRUE("Object ", Indirections[0].GetInstanceStruct() == UStateTreeTest_PropertyObjectInstancedWithB::StaticClass());
AITEST_EQUAL("Should not have error", ResolveErrors.Len(), 0);
}
Object->InstancedObject = NewObject<UStateTreeTest_PropertyObjectInstanced>();
{
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_FALSE("Resolve path should fail", bResolveResult);
AITEST_EQUAL("Should have 0 indirections", Indirections.Num(), 0);
AITEST_NOT_EQUAL("Should have error", ResolveErrors.Len(), 0);
}
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathWrongObject, "System.StateTree.PropertyPath.WrongObject");
struct FStateTreeTest_PropertyPathArray : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("ArrayOfInts[1]"));
AITEST_TRUE("Parsing path should succeeed", bParseResult);
AITEST_EQUAL("Should have 1 path segments", Path.NumSegments(), 1);
UStateTreeTest_PropertyObject* Object = NewObject<UStateTreeTest_PropertyObject>();
Object->ArrayOfInts.Add(42);
Object->ArrayOfInts.Add(123);
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path should succeeed", bResolveResult);
AITEST_EQUAL("Should have no resolve errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 2 indirections", Indirections.Num(), 2);
AITEST_EQUAL("Indirection 0 should be IndexArray type", Indirections[0].GetAccessType(), EStateTreePropertyAccessType::IndexArray);
AITEST_EQUAL("Indirection 1 should be Offset type", Indirections[1].GetAccessType(), EStateTreePropertyAccessType::Offset);
const int32 Value = *reinterpret_cast<const int32*>(Indirections[1].GetPropertyAddress());
AITEST_EQUAL("Value should be 123", Value, 123);
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathArray, "System.StateTree.PropertyPath.Array");
struct FStateTreeTest_PropertyPathArrayInvalidIndex : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
const bool bParseResult = Path.FromString(TEXT("ArrayOfInts[123]"));
AITEST_TRUE("Parsing path should succeeed", bParseResult);
AITEST_EQUAL("Should have 1 path segments", Path.NumSegments(), 1);
UStateTreeTest_PropertyObject* Object = NewObject<UStateTreeTest_PropertyObject>();
Object->ArrayOfInts.Add(42);
Object->ArrayOfInts.Add(123);
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_FALSE("Resolve path should fail", bResolveResult);
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathArrayInvalidIndex, "System.StateTree.PropertyPath.ArrayInvalidIndex");
struct FStateTreeTest_PropertyPathArrayOfStructs : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path1;
Path1.FromString(TEXT("ArrayOfStruct[0].B"));
FStateTreePropertyPath Path2;
Path2.FromString(TEXT("ArrayOfStruct[2].StructB.B"));
UStateTreeTest_PropertyObject* Object = NewObject<UStateTreeTest_PropertyObject>();
Object->ArrayOfStruct.AddDefaulted_GetRef().B = 3;
Object->ArrayOfStruct.AddDefaulted();
Object->ArrayOfStruct.AddDefaulted_GetRef().StructB.B = 42;
{
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path1.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path1 should succeeed", bResolveResult);
AITEST_EQUAL("Should have no resolve errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 3 indirections", Indirections.Num(), 3);
AITEST_EQUAL("Indirection 0 should be ArrayIndex type", Indirections[0].GetAccessType(), EStateTreePropertyAccessType::IndexArray);
AITEST_EQUAL("Indirection 1 should be Offset type", Indirections[1].GetAccessType(), EStateTreePropertyAccessType::Offset);
AITEST_EQUAL("Indirection 2 should be Offset type", Indirections[2].GetAccessType(), EStateTreePropertyAccessType::Offset);
const int32 Value = *reinterpret_cast<const int32*>(Indirections[2].GetPropertyAddress());
AITEST_EQUAL("Value should be 3", Value, 3);
}
{
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path2.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path2 should succeeed", bResolveResult);
AITEST_EQUAL("Should have no resolve errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 4 indirections", Indirections.Num(), 4);
AITEST_EQUAL("Indirection 0 should be ArrayIndex type", Indirections[0].GetAccessType(), EStateTreePropertyAccessType::IndexArray);
AITEST_EQUAL("Indirection 1 should be Offset type", Indirections[1].GetAccessType(), EStateTreePropertyAccessType::Offset);
AITEST_EQUAL("Indirection 2 should be Offset type", Indirections[2].GetAccessType(), EStateTreePropertyAccessType::Offset);
AITEST_EQUAL("Indirection 3 should be Offset type", Indirections[3].GetAccessType(), EStateTreePropertyAccessType::Offset);
const int32 Value = *reinterpret_cast<const int32*>(Indirections[3].GetPropertyAddress());
AITEST_EQUAL("Value should be 42", Value, 42);
}
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathArrayOfStructs, "System.StateTree.PropertyPath.ArrayOfStructs");
struct FStateTreeTest_PropertyPathArrayOfInstancedObjects : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreePropertyPath Path;
Path.FromString(TEXT("ArrayOfInstancedStructs[0].B"));
FStateTreeTest_PropertyStruct Struct;
Struct.B = 123;
UStateTreeTest_PropertyObject* Object = NewObject<UStateTreeTest_PropertyObject>();
Object->ArrayOfInstancedStructs.Emplace(FConstStructView::Make(Struct));
const bool bUpdateResult = Path.UpdateInstanceStructsFromValue(FStateTreeDataView(Object));
AITEST_TRUE("Update instance types should succeeed", bUpdateResult);
AITEST_EQUAL("Should have 2 path segments", Path.NumSegments(), 2);
AITEST_TRUE("Path segment 0 instance type should be FStateTreeTest_PropertyStruct", Path.GetSegment(0).GetInstanceStruct() == FStateTreeTest_PropertyStruct::StaticStruct());
AITEST_TRUE("Path segment 1 instance type should be nullptr", Path.GetSegment(1).GetInstanceStruct() == nullptr);
{
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirections(UStateTreeTest_PropertyObject::StaticClass(), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path should succeeed", bResolveResult);
AITEST_EQUAL("Should have no resolve errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 3 indirections", Indirections.Num(), 3);
AITEST_EQUAL("Indirection 0 should be ArrayIndex type", Indirections[0].GetAccessType(), EStateTreePropertyAccessType::IndexArray);
AITEST_EQUAL("Indirection 1 should be StructInstance type", Indirections[1].GetAccessType(), EStateTreePropertyAccessType::StructInstance);
AITEST_EQUAL("Indirection 2 should be Offset type", Indirections[2].GetAccessType(), EStateTreePropertyAccessType::Offset);
}
{
FString ResolveErrors;
TArray<FStateTreePropertyPathIndirection> Indirections;
const bool bResolveResult = Path.ResolveIndirectionsWithValue(FStateTreeDataView(Object), Indirections, &ResolveErrors);
AITEST_TRUE("Resolve path should succeeed", bResolveResult);
AITEST_EQUAL("Should have no resolve errors", ResolveErrors.Len(), 0);
AITEST_EQUAL("Should have 3 indirections", Indirections.Num(), 3);
AITEST_EQUAL("Indirection 0 should be ArrayIndex type", Indirections[0].GetAccessType(), EStateTreePropertyAccessType::IndexArray);
AITEST_EQUAL("Indirection 1 should be StructInstance type", Indirections[1].GetAccessType(), EStateTreePropertyAccessType::StructInstance);
AITEST_EQUAL("Indirection 2 should be Offset type", Indirections[2].GetAccessType(), EStateTreePropertyAccessType::Offset);
const int32 Value = *reinterpret_cast<const int32*>(Indirections[2].GetPropertyAddress());
AITEST_EQUAL("Value should be 123", Value, 123);
}
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_PropertyPathArrayOfInstancedObjects, "System.StateTree.PropertyPath.ArrayOfInstancedObjects");
struct FStateTreeTest_BindingsCompiler : FAITestBase
{
virtual bool InstantTest() override
{
FStateTreeCompilerLog Log;
FStateTreePropertyBindings Bindings;
FStateTreePropertyBindingCompiler BindingCompiler;
const bool bInitResult = BindingCompiler.Init(Bindings, Log);
AITEST_TRUE("Expect init to succeed", bInitResult);
FStateTreeBindableStructDesc SourceADesc;
SourceADesc.Name = FName(TEXT("SourceA"));
SourceADesc.Struct = TBaseStructure<FStateTreeTest_PropertyCopy>::Get();
SourceADesc.DataSource = EStateTreeBindableStructSource::Parameter;
SourceADesc.ID = FGuid::NewGuid();
FStateTreeBindableStructDesc SourceBDesc;
SourceBDesc.Name = FName(TEXT("SourceB"));
SourceBDesc.Struct = TBaseStructure<FStateTreeTest_PropertyCopy>::Get();
SourceBDesc.DataSource = EStateTreeBindableStructSource::Parameter;
SourceBDesc.ID = FGuid::NewGuid();
FStateTreeBindableStructDesc TargetDesc;
TargetDesc.Name = FName(TEXT("Target"));
TargetDesc.Struct = TBaseStructure<FStateTreeTest_PropertyCopy>::Get();
TargetDesc.DataSource = EStateTreeBindableStructSource::Parameter;
TargetDesc.ID = FGuid::NewGuid();
const int32 SourceAIndex = BindingCompiler.AddSourceStruct(SourceADesc);
const int32 SourceBIndex = BindingCompiler.AddSourceStruct(SourceBDesc);
auto MakeBinding = [](const FGuid& SourceID, const FString& Source, const FGuid& TargetID, const FString& Target)
{
FStateTreePropertyPath SourcePath;
SourcePath.FromString(Source);
SourcePath.SetStructID(SourceID);
FStateTreePropertyPath TargetPath;
TargetPath.FromString(Target);
TargetPath.SetStructID(TargetID);
return FStateTreePropertyPathBinding(SourcePath, TargetPath);
};
TArray<FStateTreePropertyPathBinding> PropertyBindings;
PropertyBindings.Add(MakeBinding(SourceBDesc.ID, TEXT("Item"), TargetDesc.ID, TEXT("Array[1]")));
PropertyBindings.Add(MakeBinding(SourceADesc.ID, TEXT("Item.B"), TargetDesc.ID, TEXT("Array[1].B")));
PropertyBindings.Add(MakeBinding(SourceADesc.ID, TEXT("Array"), TargetDesc.ID, TEXT("Array")));
int32 CopyBatchIndex = INDEX_NONE;
const bool bCompileBatchResult = BindingCompiler.CompileBatch(TargetDesc, PropertyBindings, CopyBatchIndex);
AITEST_TRUE("CompileBatch should succeed", bCompileBatchResult);
AITEST_NOT_EQUAL("CopyBatchIndex should not be INDEX_NONE", CopyBatchIndex, (int32)INDEX_NONE);
BindingCompiler.Finalize();
const bool bResolveResult = Bindings.ResolvePaths();
AITEST_TRUE("ResolvePaths should succeed", bResolveResult);
FStateTreeTest_PropertyCopy SourceA;
SourceA.Item.B = 123;
SourceA.Array.AddDefaulted_GetRef().A = 1;
SourceA.Array.AddDefaulted_GetRef().B = 2;
FStateTreeTest_PropertyCopy SourceB;
SourceB.Item.A = 41;
SourceB.Item.B = 42;
FStateTreeTest_PropertyCopy Target;
AITEST_TRUE("SourceAIndex should be less than max number of source structs.", SourceAIndex < Bindings.GetSourceStructNum());
AITEST_TRUE("SourceBIndex should be less than max number of source structs.", SourceBIndex < Bindings.GetSourceStructNum());
TArray<FStateTreeDataView> SourceViews;
SourceViews.SetNum(Bindings.GetSourceStructNum());
SourceViews[SourceAIndex] = FStateTreeDataView(TBaseStructure<FStateTreeTest_PropertyCopy>::Get(), (uint8*)&SourceA);
SourceViews[SourceBIndex] = FStateTreeDataView(TBaseStructure<FStateTreeTest_PropertyCopy>::Get(), (uint8*)&SourceB);
FStateTreeDataView TargetView(TBaseStructure<FStateTreeTest_PropertyCopy>::Get(), (uint8*)&Target);
const bool bCopyResult = Bindings.CopyTo(SourceViews, FStateTreeIndex16(CopyBatchIndex), TargetView);
AITEST_TRUE("CopyTo should succeed", bCopyResult);
// Due to binding sorting, we expect them to executed in this order (sorted based on target access, earliest to latest)
// SourceA.Array -> Target.Array
// SourceB.Item -> Target.Array[1]
// SourceA.Item.B -> Target.Array[1].B
AITEST_EQUAL("Expect TargetArray to be copied from SourceA", Target.Array.Num(), SourceA.Array.Num());
AITEST_EQUAL("Expect Target.Array[0].A copied from SourceA.Array[0].A", Target.Array[0].A, SourceA.Array[0].A);
AITEST_EQUAL("Expect Target.Array[0].B copied from SourceA.Array[0].B", Target.Array[0].B, SourceA.Array[0].B);
AITEST_EQUAL("Expect Target.Array[1].A copied from SourceB.Item.A", Target.Array[1].A, SourceB.Item.A);
AITEST_EQUAL("Expect Target.Array[1].B copied from SourceA.Item.B", Target.Array[1].B, SourceA.Item.B);
return true;
}
};
IMPLEMENT_AI_INSTANT_TEST(FStateTreeTest_BindingsCompiler, "System.StateTree.BindingsCompiler");
UE_ENABLE_OPTIMIZATION_SHIP
#undef LOCTEXT_NAMESPACE