UE Mass AI 随机移动案例

案例1

https://github.com/Q1143316492/LochMassAITest

{
    "FileVersion": 3,
    "EngineAssociation": "5.6",
    "Category": "",
    "Description": "",
    "Modules": [
        {
            "Name": "LochMassAI",
            "Type": "Runtime",
            "LoadingPhase": "Default"
        }
    ],
    "Plugins": [
        {
            "Name": "ModelingToolsEditorMode",
            "Enabled": true,
            "TargetAllowList": [
                "Editor"
            ]
        },
        {
            "Name": "MassAI",
            "Enabled": true
        },
        {
            "Name": "MassCrowd",
            "Enabled": true
        },
        {
            "Name": "MassGameplay",
            "Enabled": true
        },
        {
            "Name": "StateTree",
            "Enabled": true
        }
    ]
}

首先 build 点 c s 里面要把需要用到的模块先加上

// Copyright Epic Games, Inc. All Rights Reserved.

using UnrealBuildTool;

public class LochMassAI : ModuleRules
{
    public LochMassAI(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] {
            "Core",
            "CoreUObject",
            "Engine",
            "InputCore",
            "EnhancedInput",

            "MassEntity",
            "MassCommon",
            "MassActors",
            "MassSignals",
            "MassAIBehavior",
            "StateTreeModule",
            "MassMovement",
            "MassNavigation",
            "NavigationSystem",
            "MassRepresentation"
        });

    }
}

然后我们找一个空文件,把我们的片段定义一下。

USTRUCT(BlueprintType)
struct FLchTransformBaseFragment : public FMassFragment
{
    GENERATED_BODY()

    UPROPERTY()
    FTransform BaseTransform;
};

USTRUCT(BlueprintType)
struct FLchBlendTransformFragment : public FMassFragment
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere)
    FTransform StartTransform;

    UPROPERTY(EditAnywhere)
    FTransform EndTransform;

    UPROPERTY(EditAnywhere)
    float MaxTime = 1.f;

    UPROPERTY(EditAnywhere)
    float CurrentTime = 0.f;
};

Mass Moveable visualization

在这里可以配置我们 spawn 中的 mass 的静态网格,还有对应的 actor,根据不同的 LOD 选择不同的生成方式

这里我暂时全部选成静态网格

Assorted Fragments 是片段特征。

把我们自己新建的加进去后。点击验证,它会不断的提示,让你加新的片段,黄色的框里面的就是。

Mass Spawn

这些东西和人群的一样,注意 Generator决定从哪里出来,暂时先用Zone Graph

创建一个Processor

#pragma once

#include "CoreMinimal.h"
#include "MassProcessor.h"
#include "LchBlendTransformProcessor.generated.h"

UCLASS()
class LOCHMASSAI_API ULchBlendTransformProcessor : public UMassProcessor
{
    GENERATED_BODY()

public:
    ULchBlendTransformProcessor();

protected:
    virtual void ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager) override;
    virtual void Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) override;

    FMassEntityQuery EntityQuery;
};

cpp

关键点是构造函数 : EntityQuery(*this)

ConfigureQueries 中绑定对应片段,同时拥有这些片段的 Entity会执行到这个Processor

然后在 Execute 中做具体逻辑

#include "LchBlendTransformProcessor.h"
#include "MassCommonFragments.h"
#include "MassCommonTypes.h"
#include "MassExecutionContext.h"
#include "MassMovementFragments.h"
#include "MassSignalSubsystem.h"
#include "RandomMoveType.h"

ULchBlendTransformProcessor::ULchBlendTransformProcessor()
    : EntityQuery(*this)
{
    ExecutionFlags = static_cast<int32>(EProcessorExecutionFlags::All);
    ExecutionOrder.ExecuteInGroup = UE::Mass::ProcessorGroupNames::Movement;
}

void ULchBlendTransformProcessor::ConfigureQueries(const TSharedRef<FMassEntityManager>& EntityManager)
{
    EntityQuery.AddRequirement<FLchBlendTransformFragment>(EMassFragmentAccess::ReadWrite);
    EntityQuery.AddRequirement<FLchTransformBaseFragment>(EMassFragmentAccess::ReadWrite);
    EntityQuery.AddRequirement<FTransformFragment>(EMassFragmentAccess::ReadWrite);
    EntityQuery.RegisterWithProcessor(*this);
}

void ULchBlendTransformProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) 
{
    EntityQuery.ForEachEntityChunk(Context, [&](FMassExecutionContext& Context)
    {
        const int32 NumEntities = Context.GetNumEntities();

        TArrayView<FLchBlendTransformFragment> BlendTransformList = Context.GetMutableFragmentView<FLchBlendTransformFragment>();
        TArrayView<FLchTransformBaseFragment> TransformBaseList = Context.GetMutableFragmentView<FLchTransformBaseFragment>();
        TArrayView<FTransformFragment> TransformList = Context.GetMutableFragmentView<FTransformFragment>();

        for (int32 i = 0; i < NumEntities; ++i)
        {
            FLchBlendTransformFragment& BlendTransform = BlendTransformList[i];
            FLchTransformBaseFragment& TransformBase = TransformBaseList[i];
            FTransformFragment& Transform = TransformList[i];

            // do something
        }
    });

}

就是一个魔杖static mesh 乱飞的示例

void ULchBlendTransformProcessor::Execute(FMassEntityManager& EntityManager, FMassExecutionContext& Context) 
{
    EntityQuery.ForEachEntityChunk(Context, [&](FMassExecutionContext& Context)
    {
        const int32 NumEntities = Context.GetNumEntities();

        TArrayView<FLchBlendTransformFragment> BlendTransformList = Context.GetMutableFragmentView<FLchBlendTransformFragment>();
        TArrayView<FLchTransformBaseFragment> TransformBaseList = Context.GetMutableFragmentView<FLchTransformBaseFragment>();
        TArrayView<FTransformFragment> TransformList = Context.GetMutableFragmentView<FTransformFragment>();

        for (int32 i = 0; i < NumEntities; ++i)
        {
            FLchBlendTransformFragment& BlendTransform = BlendTransformList[i];
            FLchTransformBaseFragment& TransformBase = TransformBaseList[i];
            FTransformFragment& Transform = TransformList[i];

            if (BlendTransform.CurrentTime < BlendTransform.MaxTime)
            {
                // 正常的混合插值
                BlendTransform.CurrentTime += Context.GetDeltaTimeSeconds();
                float Alpha = FMath::Clamp(BlendTransform.CurrentTime / BlendTransform.MaxTime, 0.f, 1.f);
                FTransform NewTransform = FTransform::Identity;
                NewTransform.Blend(BlendTransform.StartTransform, BlendTransform.EndTransform, Alpha);
                TransformBase.BaseTransform = NewTransform;
                Transform.SetTransform(NewTransform);
            }
            else
            {
                // 到达终点后,交换起点和终点,生成新的随机目标位置,实现循环往复运动
                FTransform OldStart = BlendTransform.StartTransform;
                FTransform OldEnd = BlendTransform.EndTransform;

                // 当前位置作为新的起点
                BlendTransform.StartTransform = OldEnd;

                // 生成随机的新终点(在当前位置附近的随机偏移)
                FVector RandomOffset = FVector(
                    FMath::RandRange(-500.f, 500.f),  // X 轴随机偏移
                    FMath::RandRange(-500.f, 500.f),  // Y 轴随机偏移
                    FMath::RandRange(0.f, 100.f)   // Z 轴随机偏移(较小的垂直变化)
                );

                FTransform NewEndTransform = OldEnd;
                NewEndTransform.AddToTranslation(RandomOffset);
                BlendTransform.EndTransform = NewEndTransform;

                // 重置时间,开始新的循环
                BlendTransform.CurrentTime = 0.f;

                // 可选:随机化每次循环的持续时间
                BlendTransform.MaxTime = FMath::RandRange(2.f, 5.f);
            }
        }
    });

}

另外,东西看不见?写一个这个东西注册一下。

#pragma once

#include "CoreMinimal.h"
#include "MassVisualizationLODProcessor.h"
#include "MassRepresentationProcessor.h"
#include "LchMassAIVisualizationProcessor.generated.h"

UCLASS()
class ULchMassAIVisualizationProcessor : public UMassVisualizationProcessor
{
    GENERATED_BODY()

public:
    ULchMassAIVisualizationProcessor()
    {
        bAutoRegisterWithProcessingPhases = true;
    }
};

效果就是魔杖乱飞

案例2: 加上状态树

新建场景,Mass Spawn,但是这次我们选择 EQS

EQS里面简单写个Grid,基本就是在spawn周围一点范围的格子。具体后面张开

EQS是,environment query

配置DA

一直点验证,最后加出了这么多东西

在可视化里面还要配置一下mesh

另外就是片段中加上了我们自己的片段

USTRUCT(BlueprintType)
struct FLchRandomMoveSTFragment : public FMassFragment
{
    GENERATED_BODY()

    UPROPERTY()
    FVector TargetLocation;
};

点到没有报错的时候,还有几个片段,是个什么的(红框)

最重要的是状态树的关联

摆一个 Nav Mesh Bounds Volume,按P显示区域

然后就动起来了

具体可以看看这两个Task的代码

。。。

自定义信号

#pragma once

#include "CoreMinimal.h"
#include "MassSignalProcessorBase.h"
#include "LchCustomSignalProcessor.generated.h"

namespace UE::Mass::Signals
{
    const FName LchCustomSignalTest = FName(TEXT("LchCustomSignalTest"));
};

UCLASS()
class ULchCustomSignalProcessor : public UMassSignalProcessorBase
{
    GENERATED_BODY()

public:
    ULchCustomSignalProcessor(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());

protected:
    virtual void InitializeInternal(UObject& Owner, const TSharedRef<FMassEntityManager>& InEntityManager) override;
    virtual void SignalEntities(FMassEntityManager& EntityManager, FMassExecutionContext& Context, FMassSignalNameLookup& EntitySignals) override;
};

定义信号类,信号是一个 FName

一个处理器,继承 UMassSignalProcessorBase

InitializeInternal中注册型号,SignalEntities 处理型号回调

#include "LchCustomSignalProcessor.h"
#include "MassSignalSubsystem.h"
#include "MassExecutionContext.h"
#include "RandomMoveSTType.h"

ULchCustomSignalProcessor::ULchCustomSignalProcessor(const FObjectInitializer& ObjectInitializer)
    : Super(ObjectInitializer)
{
    bRequiresGameThreadExecution = true;
}

void ULchCustomSignalProcessor::InitializeInternal(UObject& Owner, const TSharedRef<FMassEntityManager>& InEntityManager)
{
    Super::InitializeInternal(Owner, InEntityManager);

    UMassSignalSubsystem* SignalSubsystem = UWorld::GetSubsystem<UMassSignalSubsystem>(Owner.GetWorld());
    SubscribeToSignal(*SignalSubsystem, UE::Mass::Signals::LchCustomSignalTest);

    EntityQuery.AddRequirement<FLchRandomMoveSTFragment>(EMassFragmentAccess::ReadOnly);
    ProcessorRequirements.AddSubsystemRequirement<UMassSignalSubsystem>(EMassFragmentAccess::ReadWrite);
}

void ULchCustomSignalProcessor::SignalEntities(FMassEntityManager& EntityManager, FMassExecutionContext& Context, FMassSignalNameLookup& EntitySignals)
{
    EntityQuery.ForEachEntityChunk(Context, [](FMassExecutionContext& Context)
    {
        const int32 NumEntities = Context.GetNumEntities();
        // UE_LOG(LogTemp, Warning, TEXT("LchCustomSignalProcessor received signal for %d entities"), NumEntities);
    });
}

bRequiresGameThreadExecution 会让信号的逻辑在主线程中被处理

InitializeInternal 中注册这个处理西要处理这个型号

关联FLchRandomMoveSTFragment片段只是为了让这个处理器有条件跑起来,

然后在型号发出的时候,SignalEntities 就会被调用。

if (!EntitiesToSignal.IsEmpty())
{
    SignalSubsystem.SignalEntities(UE::Mass::Signals::NewStateTreeTaskRequired, EntitiesToSignal);
    SignalSubsystem.SignalEntities(UE::Mass::Signals::LchCustomSignalTest, EntitiesToSignal);
}

调用的地方就写在前面的状态树下面。

求值器

状态树的这个地方可以加一个求值器。

这个地方可以从其他像角色身上获取一些数值,然后修改一些变量,然后这个变量就可以在状态树的其他节点中使用。

#pragma once

#include "MassComponentHitSubsystem.h"
#include "MassStateTreeTypes.h"
#include "RandomMoveSTType.h"
#include "LchMassTestSubsystem.h"
#include "LchMassTestRvaluator.generated.h"

namespace UE::MassBehavior
{
    struct FStateTreeDependencyBuilder;
}

USTRUCT(BlueprintType)
struct FLchMassTestEvaluatorInstanceData
{
    GENERATED_BODY()

    UPROPERTY(VisibleAnywhere, Category = "Output")
    bool bGotHit = false;

    UPROPERTY(VisibleAnywhere, Category = "Output")
    FMassEntityHandle HitEntity;
};

USTRUCT(meta = (DisplayName = "Lch Mass Test Evaluator"))
struct FLchMassTestEvaluator : public FMassStateTreeEvaluatorBase
{
    GENERATED_BODY()

protected:
    virtual bool Link(FStateTreeLinker& Linker) override;

    virtual const UStruct* GetInstanceDataType() const override { return FLchMassTestEvaluatorInstanceData::StaticStruct(); }

    virtual void Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const override;

    virtual void GetDependencies(UE::MassBehavior::FStateTreeDependencyBuilder& Builder) const override;

    TStateTreeExternalDataHandle<ULchMassTestSubsystem> TestSubsystemHandle;

    TStateTreeExternalDataHandle<FLchRandomMoveSTFragment> RandomMoveFragmentHandle;
};

通过继承 FMassStateTreeEvaluatorBase 实现一个求值器

#include "LchMassTestRvaluator.h"
#include "StateTreeLinker.h"
#include "MassStateTreeExecutionContext.h"
#include "MassStateTreeDependency.h"

UE_DISABLE_OPTIMIZATION

bool FLchMassTestEvaluator::Link(FStateTreeLinker& Linker)
{
    Linker.LinkExternalData(TestSubsystemHandle);
    Linker.LinkExternalData(RandomMoveFragmentHandle);
    return true;
}

void FLchMassTestEvaluator::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const
{
    const ULchMassTestSubsystem& TestSubsystem = Context.GetExternalData(TestSubsystemHandle);
    const FLchRandomMoveSTFragment& RandomMoveFragment = Context.GetExternalData(RandomMoveFragmentHandle);
}

void FLchMassTestEvaluator::GetDependencies(UE::MassBehavior::FStateTreeDependencyBuilder& Builder) const
{
    Builder.AddReadOnly(TestSubsystemHandle);
    Builder.AddReadOnly(RandomMoveFragmentHandle);
}

UE_ENABLE_OPTIMIZATION

我们可以在 tick 中获取一些 sub system 和片段来进行一些自己的逻辑处理

如何定义sub system呢?

#pragma once

#include "MassEntityTypes.h"
#include "MassComponentHitTypes.h"
#include "MassSubsystemBase.h"
#include "LchMassTestSubsystem.generated.h"

class UMassSignalSubsystem;
class UMassAgentSubsystem;

UCLASS(MinimalAPI)
class ULchMassTestSubsystem : public UMassTickableSubsystemBase
{
    GENERATED_BODY()

protected:
    virtual void Initialize(FSubsystemCollectionBase& Collection) override;
    virtual void Deinitialize() override;
    virtual void Tick(float DeltaTime) override;
    virtual TStatId GetStatId() const override;

    UPROPERTY()
    TObjectPtr<UMassSignalSubsystem> SignalSubsystem;

    UPROPERTY()
    TObjectPtr<UMassAgentSubsystem> AgentSubsystem;
};
#include "LchMassTestSubsystem.h"
#include "MassAgentComponent.h"
#include "MassAgentSubsystem.h"
#include "MassSignalSubsystem.h"
#include "MassSimulationSubsystem.h"

UE_DISABLE_OPTIMIZATION
void ULchMassTestSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);

    SignalSubsystem = Collection.InitializeDependency<UMassSignalSubsystem>();
    AgentSubsystem = Collection.InitializeDependency<UMassAgentSubsystem>();
}

void ULchMassTestSubsystem::Deinitialize()
{
    Super::Deinitialize();
}

void ULchMassTestSubsystem::Tick(float DeltaTime)
{
}

TStatId ULchMassTestSubsystem::GetStatId() const
{
    RETURN_QUICK_DECLARE_CYCLE_STAT(ULchMassTestSubsystem, STATGROUP_Tickables);
}

UE_ENABLE_OPTIMIZATION

状态树的其他内容

USTRUCT(BlueprintType)
struct FLchMeshPathFollowTaskInstanceData
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category = "Input")
    FVector TargetLocation;

    UPROPERTY(EditAnywhere, Category = "Parameter")
    FMassMovementStyleRef MovementStyle;

    UPROPERTY(EditAnywhere, Category = "Parameter")
    float SpeedScale = 1.0f;
};

这个分类的 Input,Output, Parameter

相当于

void Test(int & Out, int Input, int Parm = 100) {}

Try Enter 只执行这个节点,子节点不执行了

Try Select Children In Order 执行,并且顺序执行子节点

Try Select Children At Random 执行,然后随机【一个】子节点

。。。。 With Highest 得分最高的执行,具体的折腾一下这里

上一篇
下一篇