< Return to Engineering Blog
Engine ArchitectureOCT 14, 2026 // 4 MIN READ

Souls-Like Parry Windows in Unreal Engine C++: An Architecture

Learn how to build a sub-frame accurate souls-like parry window in Unreal Engine C++ using timestamp validation and dot-product vector math.

The Architectural Flaw of Montage-Driven Parries

When building hardcore action combat systems—such as our work on Project Ronin here at Outlore Interactives—combat feel hinges on millisecond-accurate arbitration rather than loose montage booleans. The standard blueprint-centric approach to parrying relies heavily on Animation Notifies (AnimNotifies) to toggle boolean states like IsParrying on or off. In practice, this architecture is fundamentally brittle. If frame rates dip, or if network jitter introduces latency in multiplayer environments, AnimNotifies can easily fire late, skip frames entirely, or misalign with the actual incoming hit trace. Read technical details on outlore game studio project ronin. Reference: Why UE5 Live Coding Crashes When You Edit Header Files.

DIAGRAM // ARCHITECTURE
CLIENT_INPUT Sub-frame Buffering SIMULATION_CORE Hit-Stop & Physics GPU_PIPELINE Zero-Stutter Presentation
// Figure 1: Architectural pipeline and execution flow for how to implement souls-like parry window in unreal engine c++.

At Outlore, we reject solutions that bind authoritative combat resolution directly to rendering pipelines. When you tie your defensive window to an animation montage, a stuttering frame rate means your parry window literally shifts in time. Instead, we frame the parry window as an explicit, authoritative timestamp state machine housed within a dedicated C++ ActorComponent. This decouples the visual feedback of the animation from the mathematical reality of the combat arbitration. Read technical details on project ronin action combat souls-like.

Designing the Authoritative Parry Component

To achieve sub-frame accuracy without bloating your system with unnecessary overhead, you need a lightweight C++ component that tracks activation time using engine timestamps. Rather than relying on fragile tick-based decrementing flags, we record the exact floating-point timestamp when the parry is initiated via GetWorld()->GetTimeSeconds(). Read technical details on unity c++ engine garbage collection custom engine.

cpp
UCLASS(ClassGroup=(Combat), meta=(BlueprintSpawnableComponent))
class UOutloreCombatComponent : public UActorComponent
{
    GENERATED_BODY()

public:
    UOutloreCombatComponent();

    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Combat|Parry")
    float ParryWindowDuration = 0.15f;

    UFUNCTION(BlueprintCallable, Category = "Combat|Parry")
    void TriggerParry();

    UFUNCTION(BlueprintPure, Category = "Combat|Parry")
    bool IsInParryWindow() const;

    bool EvaluateIncomingAttack(AActor* Attacker, const FVector& HitLocation);

private:
    double ParryStartTime = -1.0;
};

By storing ParryStartTime as a double-precision float, we avoid floating-point drift over long play sessions. Checking whether the defender is actively parrying becomes a simple, deterministic math operation comparing the current world time against the activation threshold. Read technical details on parry mechanics hit-stop input buffering combat design.

cpp
bool UOutloreCombatComponent::IsInParryWindow() const
{
    if (ParryStartTime < 0.0)
    {
        return false;
    }
    
    const double CurrentTime = GetWorld()->GetTimeSeconds();
    return (CurrentTime - ParryStartTime) <= static_cast<double>(ParryWindowDuration);
}

A robust souls-like parry system cannot accept attacks from any angle. If a player is facing north, they shouldn't parry an enemy striking them from behind. We enforce spatial validity by calculating the dot product between the defender's normalized forward vector and the directional vector pointing from the defender to the attacker.

cpp
bool UOutloreCombatComponent::EvaluateIncomingAttack(AActor* Attacker, const FVector& HitLocation)
{
    if (!IsInParryWindow() || !Attacker)
    {
        return false;
    }

    AActor* OwnerActor = GetOwner();
    if (!OwnerActor)
    {
        return false;
    }

    // Calculate normalized direction vector from defender to attacker
    FVector DefenderForward = OwnerActor->GetActorForwardVector().GetSafeNormal();
    FVector ToAttacker = (Attacker->GetActorLocation() - OwnerActor->GetActorLocation()).GetSafeNormal();

    // Dot product: 1.0 = direct hit front, 0.0 = perpendicular, -1.0 = direct rear
    float DirectionDot = FVector::DotProduct(DefenderForward, ToAttacker);

    // Threshold of 0.5 roughly corresponds to a 60-degree frontal cone
    constexpr float FrontalParryThreshold = 0.5f;
    if (DirectionDot < FrontalParryThreshold)
    {
        // Attack is outside the parry cone; process as standard block or hit
        return false;
    }

    // Parry successful: Trigger riposte state or stun attacker
    ParryStartTime = -1.0; // Consume the parry window
    return true;
}

Handling Time Dilation and Hit-Stop Robustness

Hardcore action games rely heavily on hit-stop (brief pauses in time on impact) and custom time dilation to sell the weight of heavy weapon clashes. If your parry component relies on regular tick intervals or standard delta times, hit-stop can cause timers to desynchronize or expire prematurely.

Because GetWorld()->GetTimeSeconds() returns unscaled real-world relative time depending on how you configure your world settings, high-performance systems should evaluate windows using game-thread execution counts or custom delta times scaled via CustomTimeDilation. Keeping your timestamp checks independent of visual animation ticks ensures that even when the game engine enters a heavy hit-stop frame freeze, your parry arbitration remains mathematically sound.

Decoupling Animation from Combat Arbitration

The golden rule of building responsive melee combat in Unreal Engine is that code commands data, and data commands animation—never the reverse. When a player presses the parry input, the input handler immediately calls TriggerParry() on the C++ combat component. Only after the C++ state transitions do we fire an event to play the corresponding animation montage.

If the animation fails to load, stutters, or gets interrupted by network prediction corrections, the underlying combat state machine remains completely intact. This architecture guarantees that player inputs are never swallowed by rendering bottlenecks.

KEY ARCHITECTURAL TAKEAWAYS

  • 01.Never tie combat arbitration states like parry windows to Animation Notifies, which can desync or drop during frame rate dips.
  • 02.Use double-precision floating-point timestamps (GetWorld()->GetTimeSeconds()) to track parry durations instead of tick-based decrementing flags.
  • 03.Validate incoming attack angles using dot products between the defender's forward vector and the attacker's relative position vector to reject rear strikes.
  • 04.Decouple combat logic entirely from rendering pipelines so that input responsiveness remains absolute even under heavy GPU loads.

FREQUENTLY ASKED QUESTIONS

Why use C++ instead of Gameplay Ability System (GAS) for a parry window?

While GAS is powerful, it introduces significant boilerplate and memory overhead via Gameplay Effects and Tags for sub-millisecond mechanics. For tight, deterministic combat frames like those in Project Ronin, a lightweight C++ ActorComponent offers direct memory access, zero interpretive lag, and absolute control over execution order.

How do I prevent double-parrying during a single active window?

Reset your parry tracking variable immediately upon a successful arbitration check (as shown in the EvaluateIncomingAttack function by setting ParryStartTime = -1.0). This consumes the window instantly, preventing subsequent incoming hits within the same timeframe from triggering multiple ripostes.

How can I network this parry system for multiplayer action games?

To prevent cheating and desync, execute the parry input via a Server RPC that validates the timestamp against the server's authoritative GetWorld()->GetTimeSeconds(), accounting for a reasonable ping window. Send multicast updates only for the resulting visual feedback.

// CONNECTED_ENGINEERING_DOSSIERS

All Articles >