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 [FONT=Courier New]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 [FONT=Courier New]UPawnSensingComponent is somewhat sparse, but the purpose of the component is pretty obvious from the API calls and class properties: [FONT=Courier New]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”.

[FONT=Courier New]UPawnSensingComponent works very much like other components. You declare it in your header as a [FONT=Courier New]TSubobjectPtr, and then create the actual subobject in your constructor using [FONT=Courier New]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. [FONT=Courier New]UPawnSensingComponent uses two delegates to let you take action when the pawn hears or sees something.

Using [FONT=Courier New]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. [FONT=Courier New]UPawnSensingComponent’s default value of [FONT=Courier New]0.5f does detection checks every half second and it’s a good general default value.

If you need more sophisticated detection than [FONT=Courier New]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 [FONT=Courier New]CouldSeePawn() and [FONT=Courier New]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 [FONT=Courier New]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 [FONT=Courier New]UPawnSensingComponent to our character or pawn. To do that, we add a declaration in our header file somewhere after the [FONT=Courier New]GENERATED_UCLASS_BODY() macro, like this:


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.


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 [FONT=Courier New]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 [FONT=Courier New]Perception/PawnSensingComponent.h. In there, we see the two delegates are declared like so:


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 [FONT=Courier New]OnSeePawn() function must take a single parameter: a pointer to an [FONT=Courier New]APawn representing the pawn or character that has been seen. It also tells us that our [FONT=Courier New]OnHearNoise() function must take three parameters: a pointer to an [FONT=Courier New]APawn representing the character or pawn who instigated the sound, an [FONT=Courier New]FVector passed by reference that tells us where the sound happened, and a [FONT=Courier New]float representing the volume of the sound.

These delegate functions must be [FONT=Courier New]UFUNCTIONs, so in our header file, we need to add them like this:


//////////////////////////////////////////////////////////////////////////
// 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 [FONT=Courier New]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:


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 [FONT=Courier New]UPawnSensorComponent, we need to override [FONT=Courier New]PostInitializeComponents() if we aren’t already, and register our functions. That’s done like so:


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 [FONT=Courier New]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 [FONT=Courier New]UPawnSensingComponent, an actor needs two things. They need a [FONT=Courier New]UPawnNoiseEmitterComponent, and they need to make calls to a function called [FONT=Courier New]MakeNoise() whenever they do something that generates detectable sounds. [FONT=Courier New]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 [FONT=Courier New]UPawnNoiseEmitterComponent. In the character’s header file, declare it like so:


    UPROPERTY(visibleAnywhere, BlueprintReadOnly, Category=Noise)
    TSubobjectPtr<class UPawnNoiseEmitterComponent> NoiseEmitter;

We then need to initialize the component in our constructor:


NoiseEmitter = PCIP.CreateDefaultSubobject<UPawnNoiseEmitterComponent>(this, TEXT("Noise Emitter"));

And then, we need to call [FONT=Courier New]MakeNoise(). Here’s a very rudimentary footstep noise generation done in [FONT=Courier New]Tick() to show how you generate noise. It generates a footstep every second if the character is moving and isn’t crouched:


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

[FONT=Courier New]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.

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

Thanks for the kind words.

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.

Excellent explanation, thank you!
This site has also some good info about the sensors: link removed

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

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?

[FONT=Courier New]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:



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


I probably should, I just never seem to find time to do it. :-/

I honestly don’t know the implementation details, sorry. :frowning:

I’m happy to do it my self if you don’t mind. I’ll credit it to you of course :slight_smile:

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.

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.

By all means.

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 [FONT=Courier New]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.

That would be awesome. I love it when somebody else does the hard work. :slight_smile:

I’m writing :


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… ).

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

Try this instead:



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


Tried that too and it didn’t work :\

Ok just added:


#include "Runtime/AIModule/Classes/Perception/PawnSensingComponent.h" 

at the top of my header and cpp files and now it works with UPawnSensingComponent* PawnSensor .

UPDATE 4.22!!!

hI! I would like to add my contribution here!! Here’s how I did my enemy character with a pawn sensing

in my enemycharacter.h :


//the BlueprintCallable is crucial!! -- well for my case, yes.
    UFUNCTION(BlueprintCallable)
        void OnSeePawn(APawn* SeenPawn);



enemycharacter.cpp:


void AEnemyCharacter::BeginPlay()
{
    Super::BeginPlay();

    if (GEngine)
        GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Orange, TEXT("EnemyCharacter"));

    FScriptDelegate fScriptDelegate;
    fScriptDelegate.BindUFunction(this, "OnSeePawn");
    pawnSensor->OnSeePawn.AddUnique(fScriptDelegate);
}

Since adddynamic was removed, I have been using FScriptDelegate to bind the function. It acts like a glue between the event and your code, it’s the same method as dynamic event.


void AEnemyCharacter::OnSeePawn(APawn* OtherPawn)
{
    if (GEngine)
        GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Blue, TEXT("PawnSensingfunction"));

    auto playerPawn = UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

    if (OtherPawn == playerPawn)
    {
        FString message = TEXT("Saw Actor ") + OtherPawn->GetName();
        GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, message);
    }
}

Enjoy!!

1 Like