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.