Announcement

Collapse
No announcement yet.

How do I use the AI Perception Teams?

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

    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
    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:

    Code:
    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.

    Code:
    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:
    Code:
    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:
    Code:
    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:

    Code:
    #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:

    Code:
    #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.

    Code:
    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.
    Last edited by Gossy; 01-16-2019, 06:49 AM. Reason: fixed some typos in the code, see below

    Comment


      #3
      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:

      Code:
      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;
      };
      Code:
      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!

      Comment


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

        Comment


          #5
          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:

          Code:
          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
          Code:
          TArray<FTeamAttitude> & teamAttitudes = UPsychiatrixSettings::Get()->TeamAttitudes;
          to
          Code:
          TArray<FTeamAttitude> teamAttitudes = UPsychiatrixSettings::Get()->TeamAttitudes;
          I get:

          Code:
          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.
          Last edited by RainbowCookie32; 01-11-2019, 11:26 PM.

          Comment


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

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

            Comment


              #7
              That one's fixed now. The remaining errors are:
              Code:
              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:
              Code:
              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!

              Comment


                #8
                Originally posted by Gossy View Post
                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:

                Code:
                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;
                };
                Code:
                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!
                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?
                Rocketeer

                my portfolio
                my youtube

                Camera Volumes
                Procedurally Instanced Meshes
                Simple Portals
                Water Flow For UDK
                Setup Swarm

                Comment


                  #9
                  Originally posted by RainbowCookie32 View Post
                  That one's fixed now. The remaining errors are:
                  Code:
                  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:
                  Code:
                  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!
                  Soo, any ideas, anyone?

                  Comment


                    #10
                    Well, I did some changes based on what my brain is being able to process:
                    Code:
                    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:
                    Code:
                    PsychiatrixSettings.cpp(27) : error C2440: 'initializing': cannot convert from 'const TArray<TEnumAsByte<ETeamAttitude::Type>,FDefaultAllocator>' to 'const TArray<ETeamAttitude::Type,FDefaultAllocator> &'

                    Comment


                      #11
                      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?
                      Last edited by RainbowCookie32; 01-14-2019, 02:14 AM.

                      Comment


                        #12
                        Hi Rainbow!

                        This line:

                        Code:
                        const TArray<ETeamAttitude::Type> & attitudes = teamAttitudes[Of.GetId()].Attitude;
                        Needs to match the actual type of the array, which the result would be:

                        Code:
                        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:

                        Code:
                        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.
                        Last edited by Gossy; 01-16-2019, 06:28 AM. Reason: few more hints

                        Comment


                          #13
                          Originally posted by redbox View Post
                          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?
                          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.

                          Code:
                          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:

                          Code:
                          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.

                          Comment


                            #14
                            Originally posted by Gossy View Post
                            Hi Rainbow!

                            This line:

                            Code:
                            const TArray & attitudes = teamAttitudes[Of.GetId()].Attitude;
                            Needs to match the actual type of the array, which the result would be:

                            Code:
                            const TArray & 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:

                            Code:
                            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.
                            Man, I really needed to hear that sweet "Compile Complete!" sound. Thanks for your help so far!

                            Comment


                              #15
                              Originally posted by RainbowCookie32 View Post
                              Man, I really needed to hear that sweet "Compile Complete!" sound. Thanks for your help so far!
                              Excellent! I'm glad you got it going

                              Comment

                              Working...
                              X