Announcement

Collapse
No announcement yet.

Mini-Tutorial: Using UPawnSensingComponent in C++

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

    Mini-Tutorial: Using UPawnSensingComponent in C++

    Using UPawnSensingComponent in C++

    After spending many days writing code to let my enemy characters see and hear the player, I stumbled across a built-in component called UPawnSensingComponent. There wasn’t a lot of documentation on it, and no tutorials that I could find (at least using C++) but it looked like it provided built-in functionality to do what I was busy working on.

    Being a firm believer in the motto “the best code is good code I don’t have to write or maintain”, I threw out what I’d been working on and switched over to using this built-in functionality. It took a little trial and error and some help from the AnswerHub and these Forums, but I got it working. There were a few gotchas along the way, so I thought it was worth writing up the basic process for others who might be looking to use it in their games.

    Meet UPawnSensingComponent

    The documentation for UPawnSensingComponent is somewhat sparse, but the purpose of the component is pretty obvious from the API calls and class properties: UPawnSensingComponent allows pawns to see other pawns/characters and to hear noises created by them. These are the two bits of “world awareness” that many game AI system are likely to use, so Epic has nicely provide a basic version for us to use “out of the box”.

    UPawnSensingComponent works very much like other components. You declare it in your header as a TSubobjectPtr, and then create the actual subobject in your constructor using CreateDefaultSubobject. There are a number of properties you can set to indicate things like the pawn's field of vision, how far they can see, and how good their hearing is. UPawnSensingComponent uses two delegates to let you take action when the pawn hears or sees something.

    Using UPawnSensingComponent has some advantages over rolling your own awareness code besides the obvious "not having to write awareness code". It has support for configurable sensing intervals, meaning you can do sight and sound detection every half second, quarter second, tenth of a second, or any other interval you want. Doing line of sight and distance calculations are not, perhaps, super processor intensive for modern hardware, but there’s also no need to be doing those calculations every tick. In fact, a low interval value gives more believable results than sensing every tick because real people don’t react instantaneously the way a game character checking every thirty or sixty times per second would. UPawnSensingComponent’s default value of 0.5f does detection checks every half second and it’s a good general default value.

    If you need more sophisticated detection than UPawnSensingComponent offers out of the box, it’s still worth using. You can always subclass it to add or change behavior. You might, for example, want to override CouldSeePawn() and HasLineOfSightTo() in order to take the current angle of rotation of your character’s head bone into account in those calculations. You can make that change by overriding just a couple of functions while still leveraging the rest of the built-in “for free” functionality that UPawnSensingComponent offers.

    Let’s look at setting up the most basic pawn sensing functionality. For illustrative purposes, we’ll just do a debug print statement when we see or hear something.

    Adding the Component

    The first thing we have to do is to add a UPawnSensingComponent to our character or pawn. To do that, we add a declaration in our header file somewhere after the GENERATED_UCLASS_BODY() macro, like this:

    Code:
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=Awareness)
    TSubobjectPtr<class UPawnSensingComponent> PawnSensor;
    Next, in the constructor for our pawn or character, we have to create the actual subobject. This is also a good place to configure any parameters we might want to change from their default values. The actual values you use will depend on the needs of your specific game, of course, but this code sets a few values to show the process.

    Code:
    PawnSensor = PCIP.CreateDefaultSubobject<UPawnSensingComponent>(this, TEXT("Pawn Sensor"));
    PawnSensor->SensingInterval = .25f; // 4 times per second
    PawnSensor->bOnlySensePlayers = false;
    PawnSensor->SetPeripheralVisionAngle(85.f);
    Creating the Delegate Functions
    The UPawnSensingComponent has two delegates: OnHearNoise and OnSeePawn. In order to be notified that a noise has been heard or another pawn has been seen, we need to add functions to our pawn or character matching the delegate function signatures. Strangely, the API documentation doesn’t tell you the function signature you have to use for delegates. To figure that out, you have to go into the Unreal Engine header files and see how the delegates are declared.

    In this case, we need to open up Perception/PawnSensingComponent.h. In there, we see the two delegates are declared like so:

    Code:
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam( FSeePawnDelegate, APawn*, Pawn );
    DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams( FHearNoiseDelegate, APawn*, Instigator, const FVector&, Location, float, Volume);
    This tells us that our OnSeePawn() function must take a single parameter: a pointer to an APawn representing the pawn or character that has been seen. It also tells us that our OnHearNoise() function must take three parameters: a pointer to an APawn representing the character or pawn who instigated the sound, an FVector passed by reference that tells us where the sound happened, and a float representing the volume of the sound.

    These delegate functions must be UFUNCTIONs, so in our header file, we need to add them like this:

    Code:
    //////////////////////////////////////////////////////////////////////////
    // UPawnSensingComponent Delegates
    
    UFUNCTION()
    void OnHearNoise(APawn *OtherActor, const FVector &Location, float Volume);
    
    UFUNCTION()
    void OnSeePawn(APawn *OtherPawn);
    Next, we have to write the actual implementations for these two functions. For illustrative purposes, we’ll just debug print the parameters received. One thing to note is that the sound volume that comes in the OnHearNoise delegate is not adjusted for distance. You’re given both the location and the originating volume and have to account for sound falloff yourself. Most games won’t need to actually bother with a full inverse square falloff calculation, but Epic leaves it to us developers to decide how our game will account for distance and sound falloff.

    In the .cpp file, we can add our function implementations like this:

    Code:
    void AEnemyCharacter::OnHearNoise(APawn *OtherActor, const FVector &Location, float Volume)
    {
    
        const FString VolumeDesc = FString::Printf(TEXT(" at volume %f"), Volume);    
        FString message = TEXT("Heard Actor ") + OtherActor->GetName() + VolumeDesc;
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, message);
    
        // TODO: game-specific logic    
    }
    
    void AEnemyCharacter::OnSeePawn(APawn *OtherPawn)
    {
        FString message = TEXT("Saw Actor ") + OtherPawn->GetName();
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, message);
    
        // TODO: game-specific logic
    }
    Of course, in a real game, these delegate functions would actually do something with the inputs.

    Registering our Delegate Functions

    In order for these functions to actually get called by the UPawnSensorComponent, we need to override PostInitializeComponents() if we aren’t already, and register our functions. That’s done like so:

    Code:
    void AEnemyCharacter::PostInitializeComponents()
    {
        Super::PostInitializeComponents();
        PawnSensor->OnSeePawn.AddDynamic(this, &AEnemyCharacter::OnSeePawn);
        PawnSensor->OnHearNoise.AddDynamic(this, &AEnemyCharacter::OnHearNoise);
    }
    At this point, if you place an instance of this pawn in a level, you will get onscreen messages whenever it sees another pawn. You won’t, however, get any notifications to your OnHearNoise() delegate function because pawns and characters don’t make any noise out of the box. That’s true even if they trigger the playing of sound files. In order to make a noise that can be detected by UPawnSensingComponent, an actor needs two things. They need a UPawnNoiseEmitterComponent, and they need to make calls to a function called MakeNoise() whenever they do something that generates detectable sounds. MakeNoise() doesn’t play a sound file, though. It just creates a virtual nose that can be detected by another pawn. The actual sound that the player will hear has to be handled separately.

    Making Noise

    To set up a character to make noise, we first have to add a UPawnNoiseEmitterComponent. In the character’s header file, declare it like so:

    Code:
        UPROPERTY(visibleAnywhere, BlueprintReadOnly, Category=Noise)
        TSubobjectPtr<class UPawnNoiseEmitterComponent> NoiseEmitter;
    We then need to initialize the component in our constructor:

    Code:
    NoiseEmitter = PCIP.CreateDefaultSubobject<UPawnNoiseEmitterComponent>(this, TEXT("Noise Emitter"));
    And then, we need to call MakeNoise(). Here’s a very rudimentary footstep noise generation done in Tick() to show how you generate noise. It generates a footstep every second if the character is moving and isn’t crouched:

    Code:
    void MyCharacter::Tick(float DelaySeconds)
    {
        Super::Tick(DelaySeconds);
    
        static float LastFootstep = 0.f;
    
    
        FVector VelocityVector = CharacterMovement->Velocity;
        float VectorMagnitude = VelocityVector.Size();
    
        float Now = GetWorld()->GetTimeSeconds();
    
        if (Now > LastFootstep + 1.0f && VectorMagnitude > 0.f && !CharacterMovement->IsCrouching())
        {
            MakeNoise(1.0f, this, FVector::ZeroVector);
            LastFootstep = Now;
        }   
    
    }
    Conclusion

    UPawnSensingComponent provides a good baseline functionality for detecting other characters and sounds to drive your game’s character AI, and it’s easy to set up and easy to extend.

    I hope this has helped get some people pointed in the right direction. A lot of this was figured out through trial and error, so if anyone sees problems with the code in this tutorial, or if you have suggestions for improvements to the code, please let me know and I'll gladly incorporate your suggestions.

    Jeff

    #2
    Right on. There are a lot of people here in the forums that are great at explaining things and turning over tidbits I would have never known. I'm not a programmer, but I, well, I understood this more than I usually do when trying to follow code talk. Very excellent explanation. Thank You

    Comment


      #3
      Thanks for the kind words.

      Comment


        #4
        Really helpful guide and well written. I was learning AI these past few days. I thought of starting with UPawnSensingComponent in a few days but thanks to you I wouldn't have to spend much time on it.

        Comment


          #5
          Excellent explanation, thank you!
          This site has also some good info about the sensors: *link removed*
          Last edited by Xenome; 03-28-2017, 03:32 AM. Reason: Dodgy link removed

          Comment


            #6
            I dont see the option AddDynamic
            I see the following options
            __Internal_AddDynamic
            add
            addUnique

            I cannot make any of these function work, please help

            Comment


              #7
              Thanks heaps for writing this up! I remember coming across this once while I was playing around with blueprints and being thoroughly confused! Might it be worth throwing a copy of this up on the wiki so it doesn't get lost as it falls to the bottom of the forum?

              Do you know if the sound component takes into account level geometry? Is it just checking it the distance to the sound using Pythagoras or is it using the pathfinding system to get the real distance the sound or something more complex?
              - Dev Blog - Twitter - Facebook - Google+

              Comment


                #8
                Originally posted by DanielGarcia View Post
                I cannot make any of these function work, please help
                AddDynamic is defined in Delegate.h. Where, exactly, are you looking for it? It's a macro, not a define member Function, so that may be why it's not coming up in autocomplete or CodeSense.

                There have been some changes to the APIs since I wrote this, but I just checked the code in my project, and it looks pretty much like it did when I wrote the tutorial:

                Code:
                void AEnemyCharacter::PostInitializeComponents()
                {
                	Super::PostInitializeComponents();
                	
                	if (PawnSensor && PawnSensor->IsValidLowLevel())
                	{
                		PawnSensor->OnSeePawn.AddDynamic(this, &AEnemyCharacter::OnSeePawn);
                		PawnSensor->OnHearNoise.AddDynamic(this, &AEnemyCharacter::OnHearNoise);
                	}
                }


                Originally posted by karltheawesome View Post
                Might it be worth throwing a copy of this up on the wiki so it doesn't get lost as it falls to the bottom of the forum?
                I probably should, I just never seem to find time to do it. :-/


                Originally posted by karltheawesome View Post
                Do you know if the sound component takes into account level geometry? Is it just checking it the distance to the sound using Pythagoras or is it using the pathfinding system to get the real distance the sound or something more complex?

                I honestly don't know the implementation details, sorry.

                Comment


                  #9
                  Originally posted by jeff_lamarche View Post
                  I probably should, I just never seem to find time to do it. :-/
                  I'm happy to do it my self if you don't mind. I'll credit it to you of course

                  Originally posted by jeff_lamarche View Post
                  I honestly don't know the implementation details, sorry.
                  I had a dig around the source. It looks like by default it just does a distance and line of sight check. This wouldn't be very good in an indoor environment as they can't hear around corners. Lucky CanHear() is virtual so we can just override it.
                  - Dev Blog - Twitter - Facebook - Google+

                  Comment


                    #10
                    By the way, PawnSensingComponent is NOT the way to do senses going forward. The newer AIPerceptionComponent is far faaaaaar better, or rather it will be from 4.8 onwards.

                    The reason being that you will have a lot more ways to control the perception component for different senses and it actually expires them for you etc! Buuuut, currently its not quite ready for prime time in that there's a few things not exposed to blueprint yet, which should be fixed by 4.8 (a few really key things you need are still missing but I've discussed them a bit with Mieszko and I should hope those will be fixed in 4.8).

                    I'm going to do a series of tutorials around this once it ships (it really is a big improvement on the pawnsensing stuff) and might do a preview this week for people who want to get started with it, but wanted to give a heads up in this thread too, in case anyone is interested.

                    Comment


                      #11
                      Originally posted by karltheawesome View Post
                      I'm happy to do it my self if you don't mind. I'll credit it to you of course
                      By all means.

                      Originally posted by zoombapup View Post
                      By the way, PawnSensingComponent is NOT the way to do senses going forward. The newer AIPerceptionComponent is far faaaaaar better, or rather it will be from 4.8 onwards.
                      Understood, but I can't wait for 4.8 to have functioning AI. I've attempted to convert my code to use Mieszko's new shiny, because it looks amazing, but the lack of documentation makes it hard to implement, and it just doesn't feel like it's 100% done. So, for now, I live with UPawnSensingComponent, and honestly, it's good enough for our game at this point. When 4.8 comes out, I'll look again and decide whether to convert over.


                      Originally posted by zoombapup View Post
                      I'm going to do a series of tutorials around this once it ships (it really is a big improvement on the pawnsensing stuff) and might do a preview this week for people who want to get started with it, but wanted to give a heads up in this thread too, in case anyone is interested.
                      That would be awesome. I love it when somebody else does the hard work.

                      Comment


                        #12
                        I'm writing :

                        Code:
                        TSubobjectPtr<class UPawnSensingComponent> aaa = ObjectInitializer.CreateDefaultSubobject<UPawnSensingComponent>(this, TEXT("pawn_sensor"));
                        And it gives me:

                        Error 5 error C2440: 'initializing' : cannot convert from 'UPawnSensingComponent *' to 'TSubobjectPtrDeprecated<UObject>' C:\OldSchoolNightmare\Source\OldSchoolNightmare\Enemy_Character.cpp 15 1 OldSchoolNightmare


                        And any other component that i try to create that way works ( tried UActorComponent and some other one... ).
                        Check out my game OldSchool Nightmare : http://www.indiedb.com/games/oldschool-nightmare

                        Comment


                          #13
                          You're probably using 4.7. There were a lot of changes that happened in Unreal C++

                          Try this instead:

                          Code:
                          UPawnSensingComponent* PawnSensor = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("Pawn Sensor"));

                          Comment


                            #14
                            Originally posted by jeff_lamarche View Post
                            You're probably using 4.7. There were a lot of changes that happened in Unreal C++

                            Try this instead:

                            Code:
                            UPawnSensingComponent* PawnSensor = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("Pawn Sensor"));
                            Tried that too and it didn't work :\
                            Check out my game OldSchool Nightmare : http://www.indiedb.com/games/oldschool-nightmare

                            Comment


                              #15
                              Ok just added:

                              Code:
                              #include "Runtime/AIModule/Classes/Perception/PawnSensingComponent.h"
                              at the top of my header and cpp files and now it works with UPawnSensingComponent* PawnSensor .
                              Check out my game OldSchool Nightmare : http://www.indiedb.com/games/oldschool-nightmare

                              Comment

                              Working...
                              X