Adaptive Stalking AI

An FSM-based AI system that dynamically adjusts aggression based on player velocity and navigates a segmented maze environment.

Engine: Unreal Engine (C++)
System: Finite State Machine–driven enemy AI with velocity-based aggression and light-based avoidance
Architecture: Modular FSM with navigation memory, sensory checks, and behavior transitions evaluated per-tick for adaptive horror gameplay

Decision Logic

The AI evaluates Player Speed (for aggression) and Light Exposure (for fleeing), while maintaining pathing through complex room segments.

FSM States:
• Stalking (default pursuit state)
• HighAggression (anti-camping pressure)
• Fleeing (light avoidance behavior)

stateDiagram-v2 [*] --> Stalking Stalking --> HighAggression: Player Stops HighAggression --> Stalking: Player Moves Stalking --> Fleeing: Light Detected Fleeing --> Stalking: Safe Distance

1. The Navigation Challenge: Segmented Rooms

The environment consists of distinct rectangular rooms connected by narrow door frames. A major technical hurdle was the AI losing the player when traversing between these segments.

The Problem: When the player passed through a door, the MoveTo request would sometimes fail or cancel because the direct path was broken by door frame geometry, causing the Ghost to stop “flowing” after the player.

The Solution: I optimized NavMesh Query settings to increase tile resolution around doorframes. I also implemented a Last Known Location memory so the AI continues to the doorway if line-of-sight is temporarily lost.

2. Dynamic Gameplay: Velocity Aggression

To prevent players from bypassing tension by camping in safe corners, the AI’s aggression is directly driven by the Player’s Velocity Vector.


// Ramping aggression if player is camping
FVector PlayerVel = TargetPlayer->GetVelocity();

if (PlayerVel.Size() < 10.f)
{
    TimeSinceStopped += DeltaTime;

    if (TimeSinceStopped > 2.0f)
    {
        // Enrage Mode
        GetCharacterMovement()->MaxWalkSpeed = 600.f;
    }
}
else
{
    TimeSinceStopped = 0.f;
    GetCharacterMovement()->MaxWalkSpeed = DefaultSpeed;
}
        

3. Sensory System: Light Detection

The Ghost’s primary weakness is light. A vector math check determines whether the Ghost is inside the player’s flashlight cone.


// Check if Ghost is inside flashlight cone
FVector ToGhost = (GetActorLocation() - Player->GetActorLocation()).GetSafeNormal();
FVector LightForward = Player->GetFirstPersonCamera()->GetForwardVector();

float LightDot = FVector::DotProduct(LightForward, ToGhost);

if (LightDot > 0.7f && Player->IsFlashlightOn())
{
    EnterState(EAIState::Fleeing);
}
        

Gameplay Impact

The result is an AI system that maintains tension organically by responding directly to player intent rather than scripted triggers.

Passive play is punished through escalating aggression, rapid movement increases pressure, and light becomes a tactical tool instead of a guaranteed escape. This creates a feedback loop where the player’s own behavior drives the intensity of the encounter.

By combining simple sensory checks, navigation memory, and FSM-driven decision-making, the system produces emergent horror without relying on jump scares or hard-coded events.