How do I use the AI Perception Teams?

I’m trying to implement AI Perception in C++ and I got to the point I always get stuck with and do workaround, the teams or affiliation (friendly, neutral, enemy). How do I assign different teams to different controllers? I know this was asked before, but I can’t really understand how to do it, and some stuff is outdated. The AI Controller already implements the GenericTeamAgentInterface, so what would be my next step from here? Sorry if it’s super clear and I’m missing it, I come from Blueprints and all I know of C++ is from experimenting in the engine. Thanks for your time <3

2 Likes

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.

10 Likes

Onto the thankfully less meaty AI Controllers and Pawns.

There’s two key things to learn here:

A PerceptionComponent must be on an AIController to work. AIController already inherits from IGenericTeamAgentInterface, but the value it returns is private, both in Blueprint and C++, AND isn’t recognized in the Reflection System. Huh…

An **AIPerceptionStimuliSource **needs to be on the actual Source Actor, which must implement the IGenericTeamAgentInterface in order for the Attitude Solver to return an Attitude that isn’t just Neutral.

So to get this working fully, we’ll want to Implement the **IGenericTeamAgentInterface **on an **AAIController **child class, and on any AActor that will have an **AIPerceptionStimuliSource, **returning what we want instead:



UCLASS()
class AMyAIController : public AAIController
{
    GENERATED_BODY()
public:

    UPROPERTY(Category = "Artificial Intelligence", BlueprintReadWrite, EditAnywhere)
        EGameTeam AITeamID;

public:/**IGenericTeamAgentInterface*/

    virtual void SetGenericTeamId(const FGenericTeamId& InTeamID) override;
    virtual FGenericTeamId GetGenericTeamId() const override;
};




void AMyAIController::SetGenericTeamId(const FGenericTeamId & InTeamID)
{
    AITeamID = (EGameTeam)InTeamID.GetId();
}

FGenericTeamId AMyAIController::GetGenericTeamId() const
{
    return uint8(AITeamID);
}


The only change to the above code for a Regular **AActor **class, would be to add in “, public IGenericTeamAgentInterface” after “public AActor

Now, we should be done. I’ve probably forgotten something, so let me know how you go!

5 Likes

Thanks for the answer, I’ll try to implement everything on my side and come back here if I run into any other problems

I may be missing something here, where would the UENUM and USTRUCT go? Should I use an empty class to hold them?

Update: I used an empty class, though the GetAttitude function is being a bit problematic on compile:


Settings.cpp(23) : error C2440: 'initializing': cannot convert from 'const TArray<FTeamAttitude,FDefaultAllocator>' to 'TArray<FTeamAttitude,FDefaultAllocator> &'
Settings.cpp(23): note: Conversion loses qualifiers
Settings.cpp(29) : error C2440: 'initializing': cannot convert from 'TArray<TEnumAsByte<ETeamAttitude::Type>,FDefaultAllocator>' to 'FTeamAttitude &'
Settings.cpp(30) : error C2039: 'IsValidIndex': is not a member of 'FTeamAttitude'
Custom.h(34): note: see declaration of 'FTeamAttitude'
Settings.cpp(32) : error C2676: binary '': 'FTeamAttitude' does not define this operator or a conversion to a type acceptable to the predefined operator


If I change


TArray<FTeamAttitude> & teamAttitudes = UPsychiatrixSettings::Get()->TeamAttitudes;

to


TArray<FTeamAttitude> teamAttitudes = UPsychiatrixSettings::Get()->TeamAttitudes;

I get:


Settings.cpp(29) : error C2440: 'initializing': cannot convert from 'TArray<TEnumAsByte<ETeamAttitude::Type>,FDefaultAllocator>' to 'FTeamAttitude &'
Settings.cpp(30) : error C2039: 'IsValidIndex': is not a member of 'FTeamAttitude'
Custom.h(34): note: see declaration of 'FTeamAttitude'
Settings.cpp(32) : error C2676: binary '': 'FTeamAttitude' does not define this operator or a conversion to a type acceptable to the predefined operator

I have a feeling I’m missing something here.

Ahh yes sorry I goofed a little during my cleanup. Add a const qualifier in front:


const TArray<FTeamAttitude> & teamAttitudes = UPsychiatrixSettings::Get()->TeamAttitudes;

2 Likes

That one’s fixed now. The remaining errors are:



Settings.cpp(29) : error C2440: 'initializing': cannot convert from 'TArray<TEnumAsByte<ETeamAttitude::Type>,FDefaultAllocator>' to 'FTeamAttitude &'
Settings.cpp(30) : error C2039: 'IsValidIndex': is not a member of 'FTeamAttitude'
Custom.h(34): note: see declaration of 'FTeamAttitude'
Settings.cpp(32) : error C2676: binary '': 'FTeamAttitude' does not define this operator or a conversion to a type acceptable to the predefined operator 

The offending lines are on GetAttitude and are this ones:


const FTeamAttitude& attitudes = teamAttitudes[Of.GetId()].Attitude;
        if (attitudes.IsValidIndex(Towards.GetId()))
        {
            return attitudes[Towards.GetId()];
        }

Thanks for your help so far, I really appreciate it!

I have perception component in character class, and everything works fine for me. And it is easier to update character states instead of passing them from controller.
Can you explain the difference between using perception component in AIController and Character?

Soo, any ideas, anyone?

Well, I did some changes based on what my brain is being able to process:



ETeamAttitude::Type UPsychiatrixSettings::GetAttitude(FGenericTeamId Of, FGenericTeamId Towards)
{
    const TArray<FTeamAttitude> & teamAttitudes = Get()->TeamAttitudes;

    if (teamAttitudes.IsValidIndex(Of.GetId()) && teamAttitudes.IsValidIndex(Towards.GetId()))
    {
        const TArray<ETeamAttitude::Type> & attitudes = teamAttitudes[Of.GetId()].Attitude;
        if (attitudes.IsValidIndex(Towards.GetId()))
        {
            return attitudes[Towards.GetId()];
        }
    }
    return ETeamAttitude::Neutral;
}

With those changes I managed to reduce the error count to 1:


PsychiatrixSettings.cpp(27) : error C2440: 'initializing': cannot convert from 'const TArray<TEnumAsByte<ETeamAttitude::Type>,FDefaultAllocator>' to 'const TArray<ETeamAttitude::Type,FDefaultAllocator> &'

The problem seems to be related to the “array of arrays” in the USTRUCT(), but I can’t figure out how to make it work unfortunately. Any ideas anyone?

Hi Rainbow!

This line:


const TArray<ETeamAttitude::Type> & attitudes = teamAttitudes[Of.GetId()].Attitude;

Needs to match the actual type of the array, which the result would be:


const TArray<TEnumAsByte<ETeamAttitude::Type>> & attitudes = teamAttitudes[Of.GetId()].Attitude;

I know I didn’t put them in the example but in my code, I use the auto keyword a fair bit where types might be a little verbose, in which case it would look like this:


const auto & attitudes = teamAttitudes[Of.GetId()].Attitude;

The & means we are only referencing, not making a copy, of the array.
The const says that we are not going to change that array.

Sure!

In the AIPerceptionComponent, when it is Registered, it casts the result of GetOwner*()* to AIController and sets it to the AIOwner property on the Component.

*AIOwner *is the target it uses to get FGenericTeamId for the **AIPerceptionComponent. **Without that reference, the component will always return FGenericTeamId::NoTeam.



FGenericTeamId UAIPerceptionComponent::GetTeamIdentifier() const
{
    return AIOwner ? FGenericTeamId::GetTeamIdentifier(AIOwner) : FGenericTeamId::NoTeam;
}


It is also used to set the IsHostile property on Structures passed up to blueprint:



PerceptualInfo->bIsHostile = AIOwner != NULL && FGenericTeamId::GetAttitude(AIOwner, SourcedStimulus->Source) == ETeamAttitude::Hostile;


So, by attaching the AIPerceptionComponent to a pawn, it will always be FGenericTeamId::NoTeam, and will aways have Neutral Attitude. It is however still useful for detecting sources.

2 Likes

Man, I really needed to hear that sweet “Compile Complete!” sound. Thanks for your help so far!

1 Like

Excellent! I’m glad you got it going :smiley:

1 Like

@Gossy

you mean when it created dynamically in runtime? But in my case it created in pawn class constructor.

@Gossy
Oh, I think I get it. Thank you!

Wow, this is an excellent write up Gossy. I had written a team agent interface as an attached component that was only partially working with the AI system, but with this change it is now working with functions like GetHostileActors. Thank you for taking the time to explain this!

I’ve just published a tutorial about this topic:

1 Like

Hi , years later, could you post a sample of your blank class that you created following this thread’s advice?

Cheers