How-To: Filter By Team

Goal: stop an attack hitting the people it should not - allies, the attacker, neutral wildlife - using Crimson Filter: Affiliation.

Prerequisites
Quick Start through step 4, so you have a preset and a profile. This page assumes you are NOT using any other Crimson plugin; if you own CrimsonTeams, see the callout near the end - it does all of this for you.

No team system at all yet, and not ready to build one? Skip to If you have no team system yet near the end of this page and use Crimson Filter: Actor Class instead. It needs nothing set up.
Read this before shipping
If team information cannot be resolved, the engine answers Neutral - and Crimson Filter: Affiliation allows Neutral by default. The failure mode is therefore not "nothing gets hit", it is "everything gets hit, including your allies", with no error and no warning.

That is why this page spends so long on how attitude is resolved. Getting it half-right looks exactly like getting it right until someone cleaves their own team.

How an attitude is decided

This plugin does not implement teams. It asks the engine, through FGenericTeamId::GetAttitude, and acts on the answer. That call makes three decisions in order, and any one of them can quietly return Neutral.

#What the engine doesIf it fails
1Casts the attacking actor to IGenericTeamAgentInterfaceReturns Neutral immediately. The target is never even looked at
2Calls GetTeamAttitudeTowards(Target) on it. The default implementation casts the target actor to the same interfaceReturns Neutral
3Compares the two team ids through the attitude solver. By default: same id is Friendly, different ids are Hostile-
Both actors, not just one
Step 2 is the one people miss. IGenericTeamAgentInterface on your attacker alone is not enough - the default implementation casts the target actor itself, and neither step walks to a controller or a player state.

So the interface belongs on the thing that gets hit: your Pawn or Character. It can still keep the actual team value on a controller or player state internally - see the snippet below.
Unset team ids are Hostile, not Neutral
The default solver returns Hostile for any two ids that differ, and FGenericTeamId::NoTeam is just another id. An actor that implements the interface but never sets an id is therefore hostile to everyone, including its own side. Always assign a team.

1. Make your actors team agents

Implement the interface on your Pawn or Character. Add AIModule to your module's PublicDependencyModuleNames first - that is where the interface lives.

cpp
// MyCharacter.h
#pragma once
#include "GameFramework/Character.h"
#include "GenericTeamAgentInterface.h"
#include "MyCharacter.generated.h"
UCLASS()
class AMyCharacter : public ACharacter, public IGenericTeamAgentInterface
{
GENERATED_BODY()
public:
//~ IGenericTeamAgentInterface
virtual FGenericTeamId GetGenericTeamId() const override { return FGenericTeamId(TeamIndex); }
virtual void SetGenericTeamId(const FGenericTeamId& NewTeamId) override { TeamIndex = NewTeamId.GetId(); }
//~ End IGenericTeamAgentInterface
protected:
/** 0-254 are real teams; 255 is FGenericTeamId::NoTeam. Replicated so clients can color
* nameplates - hit detection itself only ever reads it on the server. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Replicated, Category = "Teams")
uint8 TeamIndex = 0;
};
cs
// MyGame.Build.cs
PublicDependencyModuleNames.AddRange(new string[]
{
"Core", "CoreUObject", "Engine",
"AIModule", // IGenericTeamAgentInterface / FGenericTeamId
});

To keep the team on a player state or controller instead, leave the interface on the Pawn and forward to wherever the value lives:

cpp
FGenericTeamId AMyCharacter::GetGenericTeamId() const
{
// The interface must be on the actor that gets hit, but the VALUE can live anywhere.
if (const AMyPlayerState* MyState = GetPlayerState<AMyPlayerState>())
{
return FGenericTeamId(MyState->GetTeamIndex());
}
return FGenericTeamId::NoTeam;
}
Verify
Your character Blueprint shows a Team Index (or your own equivalent) in Class Defaults, and both the attacker and the things it attacks derive from it.

2. Assign the teams

Give the player one index and enemies another. Any two different indices are hostile under the default rules, so 0 for players and 1 for enemies is enough to start.

Set Team Index in each character Blueprint's Class Defaults, or at runtime on the server: Event BeginPlay -> Switch Has Authority (Authority) -> Set Generic Team Id (Team Id = your index).

Verify
Two enemies share an index, and the player has a different one. Nothing is left on FGenericTeamId::NoTeam (255).

3. Add the filter to your preset

Add Crimson Filter: Affiliation to the Targeting Preset, after the selection task. Its four toggles are checked against the resolved attitude.

SettingDefaultMeans
Allow HostileOnEnemies. Leave on for damage
Allow NeutralOnAnything with no relationship - and anything whose team could not be resolved
Allow FriendlyOffAllies. Turn on for buffs, heals and revives, or a friendly-fire game
Allow SelfOffThe attacker. Resolved through the same owner walk, so a projectile's "self" is the pawn that fired it
Verify
Attack an ally with Crimson.HitDetection.Debug 1 on. The ally should draw red - the shape reached them and the filter discarded them. If they draw green, attitude is not resolving; see Troubleshooting.
Turn off Allow Neutral while you are testing
Allow Neutral on is what turns an unresolved team into a friendly-fire incident. Switching it off during development makes the failure loud - unresolved actors stop being hit at all, which you will notice immediately. Turn it back on when you actually want neutral targets.

How the attacking actor is resolved

The engine casts whatever actor it is handed and gives up if that fails. A hit window's source actor is often not the thing with the team: a projectile's source is the projectile, a weapon's is the weapon.

So before asking the engine, this filter walks the attacking actor to something that actually is a team agent, in this order:

  1. The source actor itself.
  2. Its Instigator - for a projectile, the pawn that fired it.
  3. Its Owner - for a pawn, its possessing controller, which is how AAIController gets found.
This is only the attacker side
There is no equivalent walk for targets, because the engine's default GetTeamAttitudeTowards casts the target actor directly and this plugin does not override it. Targets must be agents themselves.

Changing what friendly and hostile mean

Numeric ids with same-is-friendly are only the default. Two hooks change it, and both are C++.

HookScopeUse for
FGenericTeamId::SetAttitudeSolverThe whole gameA different meaning for the ids - factions with a reputation matrix, free-for-all where every id is hostile including its own
Override GetTeamAttitudeTowards on your actorThat actorPer-actor rules - a charmed enemy, a duel where one pair is hostile inside an otherwise allied team
cpp
// A free-for-all: everyone is hostile to everyone, including their own id.
// Call once at startup, e.g. from your game mode's BeginPlay on the server.
FGenericTeamId::SetAttitudeSolver([](FGenericTeamId A, FGenericTeamId B)
{
return ETeamAttitude::Hostile;
});
// Restore the engine default (same id friendly, different id hostile).
FGenericTeamId::ResetAttitudeSolver();
The filter follows automatically
Both hooks sit underneath FGenericTeamId::GetAttitude, which is what the filter calls. Change either and every hit query in your game changes with it - there is nothing to update here.

If you have no team system yet

Crimson Filter: Actor Class keeps candidates by what they are rather than whose side they are on. Add it to the preset after the selection task, fill in Allowed Classes, and the attack connects with those classes and nothing else. Nothing to implement, no interface, no ids.

PropertyWhat it does
Allowed ClassesThe classes this filter is about. Subclasses count, so naming a base covers everything beneath it. List as many as you need - several unrelated bases is the normal state of a project that has not unified its enemies yet
InvertTurns the list into a blocklist: everything is kept EXCEPT those classes
Allow SelfOff by default, and leave it off. Your player almost certainly shares a base class with your enemies, so a class list tends to match the attacker too. Swept weapons already ignore their owner; an area query centered on the caster does not
An empty list keeps everything
In both directions. A blocklist naming nothing blocks nothing, and an allowlist naming nothing is an unconfigured filter - it must not silently delete every target while you are still filling it in. Empty rows are skipped for the same reason.

So a freshly added task changes nothing until you name a class. If the attack still hits everything, that is the first thing to check.
Which way it fails is not symmetric
An allowlist that misses a class quietly makes that enemy immune. A blocklist that misses one quietly makes it hittable. Neither logs anything - both look like a working filter.

Pick the mode whose mistake your game can survive, and prefer whichever list is shorter and more stable. Then set Crimson.HitDetection.Debug 1 and confirm rejected candidates draw red rather than never appearing at all.
Blueprint can vary the list per attack; C++ gets the authored one
Allowed Classes is marked ExposeOnTargeting, so it appears as an array pin on Crimson Hit Window and Crimson Hit Query and one preset can serve attacks that target different things. That is a Blueprint path only - the pins are written by the node at compile time, and a C++ caller gets whatever the asset was authored with. See How-To: Vary An Attack's Values.
Move to attitude when you can
A class list does not notice faction changes, charm, mind control, or a new enemy type nobody remembered to add to it. Crimson Filter: Affiliation keeps working through all of those because it asks a question about the relationship rather than about the type.

This filter is the right call to get combat working today. When teams exist, swap the task and delete nothing else - the rest of the preset is unchanged.

If you own CrimsonTeams

CrimsonTeams does all of the above for you
Nothing on this page is needed if your characters come from CrimsonCore. ACrimsonCharacter implements the interface and overrides GetTeamAttitudeTowards to call UCrimsonTeamStatics::GetTeamAttitudeBetween, which resolves attitude from gameplay tags - explicit hostility tags first, then shared team tags, then neutral - instead of numeric ids.

That works through this filter with no configuration and no dependency: the plugin only ever calls the engine interface, and your override is what answers. Tag-based teams, faction reputation and runtime team changes all just work.

It also relaxes step 2 above. Because the override resolves BOTH sides through UCrimsonTeamAgentComponent, only the attacking actor needs the interface - the actors being hit need nothing but the component. If you own CrimsonTeams but not CrimsonCore, that one forward is the only C++ you need: see CrimsonTeams -> How-To: Make an Actor a Team Agent.

Troubleshooting

SymptomCauseFix
Allies are hitAttitude resolved as Neutral and Allow Neutral is onConfirm the TARGET actor implements the interface, not just its controller or player state
Nothing is ever hitBoth actors are agents but share an id, and Allow Friendly is offGive the enemies a different index
Allies of allies are hitOne side never set an id, so it sits on NoTeam and reads as hostileAssign every team agent an index
A projectile hits allies but the melee attack does notThe projectile's owner or instigator was not set at spawnSpawn it with SpawnParams.Instigator; the launch ability task already does
Works in single player, hits allies on a dedicated serverTeam data lives on a client-only objectDetection is server-only - the value must exist on the server, so replicate it or keep it server-side
Fastest diagnosis
Crimson.HitDetection.Debug 1. A candidate drawn red was reached and rejected by a filter, which means attitude resolved. Drawn green when it should have been rejected means attitude came back Neutral - start at step 1 above.

See also

  • Quick Start
  • Concept: Windows, Shapes and Filters
  • API Reference