How can I make enemy detection in a certain area while looking through ally/neutral entities?

Hello, I’m creating a tower defense game, and I’m struggling with detection system.

First of all I have to clarify something: the game I’m creating doesn’t have just some path for enemies that they follow up to the end, the enemies can attack towers or players, i.e. abandon their path if something has attracted their attention. This brings us from “just fire the closest to the end, but in fire range, enemy”, to something more complicated.

I tried to implement this system using UAISense_Sight, and at first glance it worked perfectly - tower could easily spot an enemy and attack it, but by adding a few more towers in same place they started covering each others vision, so that the ones behind won’t be able to see the enemies in front of it, and like that they just stay AFK. What I want is that a tower can see through other towers, players and other friendly entities.
By the way I also have different collision channels for allies and enemies, so towers and players are in the former layer, while the enemies are in the latter one.

To clarify what exactly I want to scan:

The circle at the bottom is the tower, while the triangle is the tower vision. The picture represent both XY and XZ axis views. The triangle angle nearby the tower and the distance from it may vary depending on parameters. At the end the area that I mean is just a piece of a circle, just as a camera viewport.

Everything that comes to my mind seems odd, I thought about storing every enemy in some container that is accessible for towers, and that they can look for all enemies that are in range, and then check whether LineTraceSingleByChannel using enemy collision channel returns true or not, but it seems non-performant at all.

So how can look for enemies that are in a unique collision layer in a certain area of that shape?

Thanks for reading.

Hi, there are two things you could consider:

(-) Sight sense uses the Visibility collision channel by default, but you can set it to any collision channel you want (in the project settings → Engine - AI System → PerceptionSystem → Default Sight Collision Channel). So you could use or make a collision channel that only the enemies block, and not the towers and the player and then use that collision channel for the sight sense.

(-) If you need more control, then you could implement the CanBeSeenFrom function of the IAISightTargetInterface in your enemies (in c++). By default the sight sense will just do a single linetrace using the collision channel specified in the project settings, and if it can hit the actor then it can see it. But if the actor implements the IAISightTargetInterface, it will use that instead. There you can then have an arbitrary logic to check whether the tower can see the enemy. In your case I guess that would just be to do a single linetrace with a collision channel that ignores towers and the players, but is blocked by enemies.

1 Like

Thanks for an explained reply.

I thought about the first solution, but it looked restrictive, in sense that the sight component will not be usable for detecting anything else except enemies.

The second solution looks better for me actually though. So, I need to:

  • use UAISense_Sight in my towers controller;
  • use UAIPerceptionStimuliSourceComponent in my enemies controller (I find managing the perception registration manually better over the automatic one);
  • inherit IAISightTargetInterfacein my enemy class (or could controller be a better place?) and implement CanBeSeenFrom method. The method will be called by sight sense anytime it tries to check whether the enemy can be seen or not, and all I need to write in its implementation is just a single call of UWorld::LineTraceSignelByChannel using the enemy collision layer, and return its result.

Is everything correct?

Yes, looks correct.

inherit IAISightTargetInterfacein my enemy class (or could controller be a better place?)

I had that implement in the enemy actor, don’t know whether it will also work in the controller, but I guess not from looking at the code of the ai sense sight.

and all I need to write in its implementation is just a single call of UWorld::LineTraceSignelByChannel using the enemy collision layer, and return its result.

Yes the return value specifies whether it can be seen or not (returning true means it can be seen, false means it can’t be seen). You would also need to set the OutSeenLocation (e.g. just the location of your enemy actor or where the linetrace hits it), optionally also the StimulusStrength (if you do not set anything there it should be 1.0). And I would also increase NumberOfLoSChecksPerformed by the number of linetraces you do in the CanBeSeenFrom function, since the sight sense does a fixed amount of traces per tick for performance reasons.

And yes, in the CanBeSeenFrom function you can then just do a linetrace with your enemy collision channel from the ObserverLocation to your enemy location (e.g. GetActorLocation).

If I register actors into perception system myself by using UAIPerceptionStimuliSourceComponent::RegisterForSense() then I can register only controllers there, so that only they will be seen by the sight sense instead, not the actual enemy actors.
A good reason using controllers instead is the fact that AAIController inherit IGenericTeamAgentInterface, which allows you to assign team membership in order to use DetectedByAffiliation in the sight sense.
Actually though it doesn’t really matter for my case at the moment, but it can be useful in the future.

If I do only one line trace, then I need to increase it by one, right?

If I register actors into perception system myself by using UAIPerceptionStimuliSourceComponent::RegisterForSense() then I can register only controllers there, so that only they will be seen by the sight sense instead, not the actual enemy actors.

Ah ok, if the sight sense also works with controller then I guess you can also implement the interface there. Yes for the team, I also had that the IGenericTeamAgentInterface implemented in the pawn then :slight_smile:

If I do only one line trace, then I need to increase it by one, right?

Yes.

EDIT: And for the team affiliation it is useful for performance because the system will still do linetraces to all friendlies and neutrals in range, unless you disable looking for friendlies and neutrals (and the system also prioritizes its linetraces by distance and I guess that the friendlies will usually be closer than the enemies).

I forgot asking about OutSightStrength, how would I calculate it if I needed, and how does it work? What are the use cases? Just some curiosity, I don’t really need it in my case :smile:

For the OutSightStrength, that is what you get in the Strength variable in the Stimulus in the perception component (e.g. in OnTargetPerceptionUpdated). You can calculate and use it how you like (if you would not implement the CanBeSeenFrom function, then it would always be 1.0).

E.g. for me I used it for things like when I have foliage or smoke in between the AI and the player, then I set the OutSightStrength based on that. Then I had different alerted states in the AI, based on which I chose the minimus StimulusStrength for the AI to react to.

Gotcha, thanks for all the explanations! I’ll try to implement everything as said before, and tell you if it works later on

Deleted last message because I made many mistakes by not testing everything I could before writing it.

So, I’ve implemented everything, the tower controller now can use sight sense towards the enemies by using their CanBeSeenFrom function, but the problem is still not resolved.

First of all, whenever I call LineTraceSingleByChannel using the enemy collision layer, for some reason it still hits everything it can along the way, i.e. other towers, players and walls. The call looks like this:

GetWorld()->LineTraceSingleByChannel(EnemyHitResult, ObserverLocation, PawnLocation, EnemyCollisionChannel, FCollisionQueryParams(TEXT(""), false, IgnoreActor));

EnemyCollisionChannel is ECollisionChannel::ECC_GameTraceChannel2 (in Project Settings enemies object channel is the second one).

The reason, I guess, is that towers, player and walls collision presets are set to block this channel, so that’s the reason why the check doesn’t hit the enemy.

I tried to create a new collision channel that will be ignored by everything by default, but not by the enemies, they’ll block it instead, to test if it works, and it does.
So is it a good way of doing this thing?

Another problem that I just realized is that if towers can look through everything, then they’ll also be able to see through walls, so they’ll fire at walls whenever there’s an enemy behind it. To fix this issue I thought about adding another line trace check after the first one, the one to check whether there’s a wall or some other level object.

As I understand, to make this check work properly I need once again add a new collision channel, that will be reserved just for my static objects, i.e. the objects that will be placed on the level before start playing.
Is it the way to go?

Creating a new collision channel is also what I would do, but I don’t see why you need more than one. You can set the enemies and e.g. your static objects to block it, and the towers and player to ignore it.

You could:

(-) If you only want the tower and player to be ignored, then you could set the collision channel to block all by default and then e.g. in the player and tower set the collision presets to Custom and then set them to ignore your collision channel

(-) I guess it should also work if you make a new collision preset (project settings → Engine → collision) for your towers and player, and set that preset to ignore your collision channel. Then set your player and tower blueprints / meshes to use that collision preset.

Yeah, you’re right. By playing a little bit more and reading about the topic, I realized that I can create only one trace collision type, not object one, because it makes more sense for my point.
By default its response is set to ignore, but I modified some collision profiles, and I set to block it on BlockEverything (e.g. walls) and enemy type themselves, so that enemies will be visible through towers, players and projectiles, I guess that’s the only object types I will have in my game.

Thanks for helping me once again.

It’s me, once again :stuck_out_tongue:

When I wrote the post originally I didn’t have any AI going on, so enemies that have to be fired didn’t move, and everything seemed working perfectly. Another weird problem arose after I added some AI to the enemies, and so they started to move.

The problem, as I understand, is that the tower (actually its controller, since it has the sight sense instead) does not update its sense, it always remember that the enemy was at the point the tower first saw it at. By using the AI debugging thing I can see the following:

2022-10-01_11-18

Just to explain the picture a bit better: The green point the tower is looking at is the enemy spawn position, while the enemy just walked away to the top.

Whenever I use UAIPerceptionComponent:GetCurrentlyPerceivedActors() it always returns a container containing that actor regardless its position.

I tried to look for other people’s IAISightTargetInterface implementation and the way they use it, but I didn’t notice any fundamental difference.

I also noticed that enemy controller’s CanBeSeenFrom() is called twice at the very beginning of the game, but it doesn’t get called later on. I’m not sure if it’s the behavior it should have or not, nevertheless the whole thing doesn’t work as supposed when the controller moves in space (by the way the controller is attached to the pawn it’s controlling).

So, do @chrudimer or someone else have any idea how I can make the sight sense notice that something that generates sight stimuli changed over time?

Thanks for reading.

I don’t know. By default the controller does not seem to change its location, so out of the box it will always return the default location and will not work as expected with the sight sense. If you attached it to the pawn and its changing its location with it, then that may not be the issue.

I never used controllers as perception sources, so I don’t know what else you would need to setup to get it to work with controllers instead of pawns.


The sight sense is updating constantly (on tick I think), so that should also not be the problem. The CanBeSeenFrom function should then also be called constantly, as long as the source is in location and dot for the sight sense, so something is not going as it should there (because from the debugger it shows that it can be seen, which means it should call CanBeSeenFrom). Did you check your AutoSuccess range in the sight sense config (that it is set to -1.0)?

You could set some breakpoints in the Update function of the sight sense, and see what it does there, why it is not calling the CanBeSeenFrom function.

It’s called, but after testing a little bit more I noticed that it’s called after enemy enters the sight, and some time after, it isn’t consistent, apparently it’s based on distance between point and the new location, or something like that. I probably can explain it better with a video instead.

I didn’t change its value, so it should’ve been the default one, but just in case I set it to -1.0.

Whenever I try to debug any engine related code, it doesn’t enter there: I’m using Rider, and instead of showing the red dot at the left hand side, it shows this :no_entry_sign: sign , but gray. Just in case: I do use DebugGame Editor build.

I tried to move stimuli and team related code to the enemy instead, and it worked the same way it did in the controller. By looking at stimuli system functions, I found RequestStimuliListenerUpdate(), and if I call it whenever I need to look for enemies, then the circle does move, and the CanBeSeenFrom() is called as well.

The thing is that some time ago I was told that I don’t have to call the request function every time I need to process stimuli because it’s a rather expensive call, and only should be done when changing sense config or team, so I probably shouldn’t call it that way.

So, at the end: by moving stimuli and generic team related code to the enemy class instead, it doesn’t change anything, the debug circle is still at the first location where the actor entered the sight from, but it does move whenever I call RequestStimuliListenerUpdate().

Any ideas?

Whenever I try to debug any engine related code, it doesn’t enter there: I’m using Rider, and instead of showing the red dot at the left hand side, it shows this :no_entry_sign: sign , but gray. Just in case: I do use DebugGame Editor build.

As far as I know you would at least need to install the editor symbols for debugging, I do not use rider so I don’t know whether you need something additional.


This is a minimalistic example of implementing the IAISightTargetInterface:

	//////// Begin IAISightTargetInterface implementation
	virtual bool CanBeSeenFrom(const FVector& ObserverLocation, FVector& OutSeenLocation, int32& NumberOfLoSChecksPerformed, float& OutSightStrength, const AActor* IgnoreActor = nullptr, const bool* bWasVisible = nullptr, int32* UserData = nullptr) const override
	{
		NumberOfLoSChecksPerformed = 0;
		OutSightStrength = 1.0;
		OutSeenLocation = GetActorLocation();
		return true;
	}
	//////// End IAISightTargetInterface implementation

I created a new blank third person project and added the code above to the header file of the character I want to be seen (and set the perception component to detect neutrals). The debug circle / the stimuli location does move with the character and the CanBeSeenFrom function is called constantly, so it is working there.


Any ideas?

What I usually do in such cases is:

(-) Try to debug the code, going with the debugger through my code and the engine code and try to figure out where it is not working how I would expect, or generally how the engine code there is working. If I cannot spot the problem there after some time trying, then I do:

(-) Get a minimalistic example to work (like the one above), and then step by step add more things to it until I arrive where I want to be or where it stops working in one step. Then what I added in that step made it stop working, and I can change that code.

That’s sweet, thanks.

I tried to follow your advice, and so I created an empty project, and implemented things I needed, and they did work. Then I moved them to my project, and they did work as well. After that I started trying to make my original classes to look like the new ones, and the enemy class (the controller) did work as supposed, however the tower controller was the actual issue.

The thing is that I thought that a tower may vary some sight parameters, such as vision range, so I added some methods to modify the config, and that’s was the issue. If I remove everything that varies the config code at runtime (i.e. after all the initialization), everything works perfectly – the green circle does follow the enemy, and the CanBeSeenFrom function gets called on every update call.

Now the question is: How can I reconfigure the sense on runtime without breaking it?

After looking for some answers I found that I have to call UAIPerceptionSystem::UpdateListener() with the sense I want to configure, or just call RequestStimuliListenerUpdate() on it instead. However, for some reason the problem doesn’t get resolved, so apparently I have to step through the engine function calls, and see what’s wrong with the sight sense update.

I’m still downloading the editor symbols, so once I got them, and went through the function calls, I’ll tell what I’ve found eventually.

Thanks for the explained answer once again.

I finally got everything working as supposed.

In my tower controller I have UAISenseConfig_Sight and a bunch of variables that are required to initialize it because by editing the sight config asset (not the sense directly) in the editor didn’t work for me, it just didn’t save things after restarting the editor or something like that.

In the constructor I create the sight config, the perception component, and then assign all the variables to the config, and call ConfigureSense on the perception component. However, apparently it was the issue, but only in some cases, I’m still not sure what exactly went wrong, it has something to do with compilation using Live Coding.

After first Live Coding usage the variables were ignored, and the default configuration (in the perception system) was used, so no one was a target.

TL;DR Don’t initialize the config in the constructor, but somewhere else. I do it in OnPosses(), and after the initialization I call ConfigureSense(). If I do it on runtime, I call RequestStimuliListenerUpdate() afterwards.

1 Like