Modify AI perception sense range during runtime

I have been looking around quite a lot to try and find an answer to this question, but as I am fairly new to using UE4 and still not great at c++ I was hoping someone could point me in the right direction.

What I want to be able to do is to modify the range of senses during runtime for my AI. This is to be able to make the range smaller if the AI is chasing after a sound or to make the sight radius bigger if the player is using a flashlight.

After finding this link and messing around in the BP editor I assume that the functionality was never added to blueprints, and as such I need to be able to do it using c++, but I am not sure how to use the provided code to expose the sense range to the BP editor.

I also have this same question. There is an old response that has some code on it, but for the life of me I could not get it to work. Maybe I just don’t understand the AI Perception well enough.

Modify AIPerception Sight Range at runtime [4.7.6] - Programming & Scripting - Unreal Engine Forums

That’s the link I found that apparently solved the problem for this person. I personally couldn’t implement it, but my coding skills are still at the beginner level so I don’t really even know where you would add that to make it work.

I created example project for my old code (SetSightRange function inside Util.h/cpp)

[download MyProject2.7z][1]

You can see the result using the gameplay debugger

1 Like

Relevant code if anyone is interested. Compiles on 4.19.2 and seems to be working.

bool UYourFunctionLibrary::SetSightRange(AAIController* Controller, float NewSightRange)
{
	if (Controller == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("Controller == nullptr"));
		return false;
	}

	FAISenseID Id = UAISense::GetSenseID(UAISense_Sight::StaticClass());
	if (!Id.IsValid())
	{
		UE_LOG(LogTemp, Error, TEXT("Wrong Sense ID"));
		return false;
	}

	auto Perception = Controller->GetAIPerceptionComponent();
	if (Perception == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("Perception == nullptr"));
		return false;
	}

	auto Config = Perception->GetSenseConfig(Id);
	if (Config == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("Config == nullptr"));
		return false;
	}

	auto ConfigSight = Cast<UAISenseConfig_Sight>(Config);

	// Save original lose range
	float LoseRange = ConfigSight->LoseSightRadius - ConfigSight->SightRadius;

	ConfigSight->SightRadius = NewSightRange;

	// Apply lose range to new radius of the sight
	ConfigSight->LoseSightRadius = ConfigSight->SightRadius + LoseRange;

	Perception->RequestStimuliListenerUpdate();

	return true;
}
2 Likes

I took the code from v.s.'s post and split it into two functions, so that I can easily add the same thing for hearing or other senses. Something that may need explanation is how the LoseSightRadius value gets automatically set, it takes the difference of the current SightRadius from LoseSightRadius and adds that to NewSightRadius. In other words it keeps the same distance between SightRadius and LoseSightRadius and uses that with your NewSightRadius.

Header File

static UAISenseConfig* GetPerceptionSenseConfig(AAIController *Controller, TSubclassOf<UAISense> SenseClass);

UFUNCTION(BlueprintCallable)
		static bool SetSightRange(AAIController* Controller, float SightRange);

CPP File

UAISenseConfig* UYourFunctionLibrary::GetPerceptionSenseConfig(AAIController* Controller, TSubclassOf<UAISense> SenseClass)
{
	UAISenseConfig * result = nullptr;

	FAISenseID Id = UAISense::GetSenseID(SenseClass);
	if (!Id.IsValid())
	{
		UE_LOG(LogTemp, Error, TEXT("GetPerceptionSenseConfig: Wrong Sense ID"));
	}
	else if (Controller == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("GetPerceptionSenseConfig: Controller == nullptr"));
	}
	else
	{
		UAIPerceptionComponent *Perception = Controller->GetAIPerceptionComponent();
		if (Perception == nullptr)
		{
			UE_LOG(LogTemp, Error, TEXT("GetPerceptionSenseConfig: Perception == nullptr"));
		}
		else
		{
			result = Perception->GetSenseConfig(Id);
		}
	}

	return result;
}

bool UYourFunctionLibrary::SetSightRange(AAIController* Controller, float SightRange)
{
	UAISenseConfig* Config = GetPerceptionSenseConfig(Controller, UAISense_Sight::StaticClass());
	if (Config == nullptr)
	{
		UE_LOG(LogTemp, Error, TEXT("SetSightRange: Config == nullptr"));
		return false;
	}
	else
	{
		UAISenseConfig_Sight *ConfigSight = Cast<UAISenseConfig_Sight>(Config);

		UE_LOG(LogTemp, Verbose, TEXT("SetSightRange was %f %f, setting to %f %f")
			, ConfigSight->SightRadius, ConfigSight->LoseSightRadius, SightRange, (ConfigSight->LoseSightRadius - ConfigSight->SightRadius + SightRange));

		// Save original lose range
		float LoseRange = ConfigSight->LoseSightRadius - ConfigSight->SightRadius;
		ConfigSight->SightRadius = SightRange;
		// Apply lose range to new radius of the sight
		ConfigSight->LoseSightRadius = ConfigSight->SightRadius + LoseRange;
		UAIPerceptionComponent *Perception = Controller->GetAIPerceptionComponent();
		Perception->RequestStimuliListenerUpdate();
	}

	return true;
}
2 Likes

Thank you this code helped me a lot!

Any ideas how to do this without having to pass an AIController, I am using the perception system on my player character to work out when NPC’s are in range, so I need a blueprint that takes the perception component and not the AIController - any ideas?

I’m not positive but I think the perception component might be a child of AIcontroller on mine.

Does anyone still have the files or the entire code for this? I can’t seem to get this to work and I don’t know if it’s UE4.26 or my own incompetence in c++

This is what I added to my function library files:

Header:
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "CPP_SetPerceptionRange.generated.h"

UCLASS()
class GAME_API UCPP_SetPerceptionRange : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()


		static UAISenseConfig* GetPerceptionSenseConfig(AAIController* Controller, TSubclassOf<UAISense> SenseClass);

	UFUNCTION(BlueprintCallable)
		static bool SetSightRange(AAIController* Controller, float SightRange);

};

C++ File:

#include "CPP_SetPerceptionRange.h"


UAISenseConfig* UCPP_SetPerceptionRange::GetPerceptionSenseConfig(AAIController* Controller, TSubclassOf<UAISense> SenseClass)
{
    UAISenseConfig* result = nullptr;

    FAISenseID Id = UAISense::GetSenseID(SenseClass);
    if (!Id.IsValid())
    {

… with the rest of Modeus Code in the c++ file, I cant paste it all cause of the Message length restriction.

it just won’t compile, what am I doing wrong here?

ok so mordeus solution works perfectly, all I had to do was add this to my .h file:

#include "AIController.h"
#include "Perception/AISenseConfig.h"
#include "Perception/AISense.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig.h"

I hope this helps anyone who is as stupid as I am!

1 Like

Try to add “AIModule” in your project .Build.cs file

PublicDependencyModuleNames.AddRange(new string[] { 
			"Core",
			"CoreUObject", 
			"Engine", 
			"InputCore", 
			"Sockets", 
			"Networking",
			"AIModule"
		});

Thank you! Just what I was looking for.

I’m not sure if i’m alone in this here but i have tried to make a child component that does just that–the functions i have created seems to work, but every time i load the project with the plugin set to the default phase, it breaks all references in blueprints, but setting it to pre-default breaks the perception component by forcing it to register too early. Since this thread is full of people who already attempted to do what I am doing, it seems like some of you got it working? Is this an issue that is specific to 5.5 or did none of you who got it to work try and put it in a plugin?

Have anyone tried this with unreal 5.7?

this will always be invalid so it never runs
``FAISenseID Id = UAISense::GetSenseID(UAISense_Sight::StaticClass());``

Ok, so for me, the only way I found is to have the AI Perception component in the controller, set the dominant sense, but not add any sense config in the blueprint.

Then in code I register the sense with the cofiguration already correct.
That way it is always correct and doesn’t need to be changed.

In my AI Controller, I control how to call this with an extra validation.
Add a counter to the controller, set to 0.
Create your custom event and check if the counter is equal 2, if not just ignores, if it is, call your c++ register function;

On begin play, you add 1.
On possess you add 1.

In this case, I’m setting the radius before starting my State tree, so that is why the function is named Init state tree, but doesn’t metter, as long you call your c++ register where you register your sense with the correct configuration already.

This guarantee that both controller and pawn will exists already, and you can read your pawn properties to get what attributes you need to calculate the sense.

the only annoying part as changing in runtine means, removing the AI Perception, and adding a new one, then calling the reigster again. I could not get it to update at runtime without this. If you go this route, remember to re-bind the events as it will get lost

UCLASS()
class YourClass_API ABaseDetourAiController : public ADetourCrowdAIController{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable)
    void setSightRange(float seeRange, float loseRange);

}

void ABaseDetourAiController::setSightRange(float seeRange, float loseRange) {
    if (UAIPerceptionComponent* Perception = GetAIPerceptionComponent()) {
        UAISenseConfig_Sight* SightConfig = NewObject<UAISenseConfig_Sight>(this);
        SightConfig->PeripheralVisionAngleDegrees = 180;
        SightConfig->SightRadius = seeRange;
        SightConfig->LoseSightRadius = loseRange;
        SightConfig->DetectionByAffiliation.bDetectEnemies = true;
        SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
        SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
        SightConfig->SetStartsEnabled(true);
        Perception->ConfigureSense(*SightConfig);

        UAIPerceptionSystem::GetCurrent(this)->RegisterSenseClass(UAISense_Sight::StaticClass());
        UKismetSystemLibrary::PrintString(GetWorld(), FString("Sight sense registered"), true, true, FColor::Green, 10.f);
        Perception->RequestStimuliListenerUpdate();
        return;
}

    UKismetSystemLibrary::PrintString(GetWorld(), FString("Sight sense failed to register"), true, true, FColor::Red, 10.f);

}

I’m passing the parameters here, but you could as well just read from the pawn directly or anywhere you have your paramters;