How do I use the AI Perception Teams?

Hi Rainbow!

Don’t feel bad about been confused by this system, it’s a bit of a learning cliff. I had to read through quite a lot of code in order to figure it out.

The ordering of this might be a little off, because I’m working on memory that’s a little old.

First up, we need to define the Teams. I found the best way was to create a new UENUM:



UENUM(BlueprintType)
enum class EGameTeam : uint8
{
    Neutral,//Everyone ignores this team
    Team1,
    Team2,
    Team3,
    Team4 //etc...
};

Second, we need to define the Attitude of each team, towards each other team,

I made a Struct that contains an Array of ETeamAttitudes. Putting this struct as the Element of another Array gives us an entry for each team’s attitude towards all the other teams.

Important: Unreal’s Serializer does not understand “Array of Arrays”. Hence, putting it in the structure.



USTRUCT(BlueprintType)
struct FTeamAttitude
{
    GENERATED_BODY()
public:
    UPROPERTY(BlueprintReadWrite, EditAnywhere)
        TArray<TEnumAsByte<ETeamAttitude::Type>> Attitude;

    FTeamAttitude() {};

    FTeamAttitude(std::initializer_list<TEnumAsByte<ETeamAttitude::Type>> attitudes):
        Attitude(std::move(attitudes))
    { };
};

Now, here’s the tricky bit: Where do we put this Array? AND, how do we tell the Perception System to use it?

We need to tell it to use a custom AttitudeSolver, which tells it the **ETeamAttitude **of each of your Teams towards the others. We tell it with this function:


void FGenericTeamId::SetAttitudeSolver(FGenericTeamId::FAttitudeSolverFunction* Solver)

Note that this function is static and doesn’t accept a delegate, it expects a raw function pointer. That function signature looks like this:


ETeamAttitude::Type AnAttitudeSolver(FGenericTeamId A, FGenericTeamId B)

So, we need a place to store our Attitude Data (in Serializer friendly way) that can be accessed globally. And of course in Editor. Yuck.

The feature I found that is a good solution to this is extending the **UDeveloperSettings **class. Correctly setup, this will give your game it’s own settings menu in the Project Settings. I suggest something along the lines of this as a starting point:



#pragma once

#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "TeamAttitude.h" //Our creation
#include "GenericTeamAgentInterface.h"
#include "MyGameSettings.generated.h"

UCLASS(Config = Game, DefaultConfig)
class UMyGameSettings : public UDeveloperSettings
{
    GENERATED_BODY()
public:

    UPROPERTY(Category = "Artificial Intelligence", EditAnywhere, BlueprintReadOnly, Config)
        TArray<FTeamAttitude> TeamAttitudes;

public:

    UMyGameSettings(const FObjectInitializer& ObjectInitializer);

    static const UMyGameSettings* Get();

    UFUNCTION(Category = "Artificial Intelligence", BlueprintPure)
        static ETeamAttitude::Type GetAttitude(FGenericTeamId Of, FGenericTeamId Towards);
};


This is where the magic happens. Notice those static function declarations? Also an important note here is that the **UMyGameSettings **class is setup as a Config. When you modify these later on, they will be stored in your DefaultGame.ini file.

It’s implementation is going to look something like this:



#include "MyGameSettings.h"

UMyGameSettings::UMyGameSettings(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    typedef ETeamAttitude::Type EA;
    TeamAttitudes = {
        {EA::Friendly, EA::Neutral,  EA::Neutral,  EA::Neutral,  EA::Neutral },//Neutral
        {EA::Neutral, EA::Friendly, EA::Hostile,  EA::Friendly, EA::Hostile},//Team1
        {EA::Neutral, EA::Hostile,  EA::Friendly, EA::Hostile,  EA::Hostile},//Team2
        {EA::Neutral, EA::Friendly, EA::Hostile,  EA::Friendly, EA::Friendly },//Team3
        {EA::Neutral, EA::Hostile,  EA::Hostile,  EA::Friendly, EA::Friendly }//Team4
    };
}

const UMyGameSettings* UMyGameSettings::Get()
{
    return GetDefault<UMyGameSettings>();
}

ETeamAttitude::Type UMyGameSettings::GetAttitude(FGenericTeamId Of, FGenericTeamId Towards)
{
    auto & teamAttitudes = UMyGameSettings::Get()->TeamAttitudes;
    bool ofValid = teamAttitudes.IsValidIndex(Of.GetId());
    bool towardsValid = teamAttitudes.IsValidIndex(Towards.GetId());

    if (ofValid && towardsValid)
    {
        auto & attitudes = teamAttitudes[Of.GetId()].Attitude;
        if (attitudes.IsValidIndex(Towards.GetId()))
        {
            return attitudes[Towards.GetId()];
        }
    }
    return ETeamAttitude::Neutral;
}


Now, because of the static declarations, we can access UMyGameSettings::Get and UMyGameSettings::GetAttitude from anywhere! The **UMyGameSettings **class uses a feature in Unreal, where all classes have a “default” constructed for them at load time. This default object is loaded up with our configured Team Attitudes, which we can also tinker with in the Project Settings. Nice!

Now we tell the Perception System to start using it! The logical place I found to do this is to override either AGameModeBase::StartPlay or AGameModeBase::InitGame.



void AMyGameMode::StartPlay()
{
    //Setup our teams detection
    FGenericTeamId::SetAttitudeSolver(&UMyGameSettings::GetAttitude);
    OnStartPlay();
}


Here, we are giving that pesky static function our statically defined GetAttitude function. (Which we made blueprint accessible, because we aren’t monsters like they who designed this).

Phew! That’s a lot of text. I’m sorry if I’ve made mistakes in the code, I had to strip some stuff out of it.

I’ll post up what you need to do to your Actors / Controllers in another post. After I’ve grabbed a cuppa.

6 Likes