Mover - ending/canceling a LayeredMove early from outside

Hey there!

I have recently started working with LayeredMoves and have noticed a behaviour I’m unsure how to overcome without some hacky solutions, so I might as well ask how to approach this before I do that.

We use an ActionSystem that handles priorities and actions across different scopes (e.g. movement). Put simply, if we receive an action with a higher priority, we drop the current action (and properly clean up) and install the higher one (e.g. a “HitReaction” action).

In our system, I’m trying to implement a “DashAction”, that is basically just a wrapper around `FLayeredMove_Dash` (it derives from `FLayeredMove_LinearVelocity` with `Clone()`, `GetScriptStruct()`, `ToSimpleString()` and `AddReferencedObjects(..)` overriden).

What I want to achieve - when `DashAction` installs itself, we `QueueLayeredMove(…)` and when a higher priority action interrupts us - I want to instantly cancel an ongoing LayeredMove that still has some remaining time.

At first, I was planning on queueing a TSharedPtr<>, but it turned out that the Mover creates a copy of the LayeredMove, and the shared pointer I’m holding onto is basically useless. It seems like the Mover expects to receive a LayeredMove, but won’t allow you to edit/cancel it from the outside once queued/started.

What I tried at first:

(`EnterImpl(…)`, `ExitImpl()`, `UpdateImpl(…)` are our own ActionSystem virtual functions)

bool FDashAction::EnterImpl(const ActionSystem::FActionEnterContext& Context)
{
    // "TSharedPtr<FLayeredMove_Dash> LayeredMove" is a member variable 
    LayeredMove = MakeShared<FLayeredMove_Dash>();
    LayeredMove->Velocity = MoverComponent.GetOwner()->GetActorForwardVector() * 5000.0f;
    LayeredMove->MixMode = EMoveMixMode::OverrideVelocity; 
    LayeredMove->DurationMs = 250.0f;
    MoverComponent.QueueLayeredMove(LayeredMove);
    
    return true;
}

And once we get interrupted:

void FDashAction::ExitImpl(const ActionSystem::FActionExitContext& Context)
{
    if (Context.ExitType == ActionSystem::EActionExitType::Interrupt)
    {
       // A higher priority action interrupted us, let's cancel the ongoing LayeredMove
    }
    Super::ExitImpl(Context);
}

Ending my wrapper action when the LayeredMove naturally finishes:

ActionSystem::EActionUpdateResult FDashAction::UpdateImpl(float Dt)
{
    auto* ActiveLayeredMove = MoverComponent.FindActiveLayeredMoveByType<FLayeredMove_Dash>();
    return ActiveLayeredMove ? ActionSystem::EActionUpdateResult::Continue : ActionSystem::EActionUpdateResult::Done;
}

Therefore, the wrapper DashAction can end for 2 reasons: LayeredMove finished naturally (checking this inside the `UpdateImpl(…)`), or a higher priority arrived (we want to cancel the LayeredMove from within the `ExitImpl(..)` <- unsure how to properly do).

I’ve also tried finding it manually by its type and trying to edit it, but the function is const:

MoverComponent.FindActiveLayeredMoveByType<FLayeredMove_Dash>();
 
// definition
template <typename MoveT = FLayeredMoveBase UE_REQUIRES(std::is_base_of_v<FLayeredMoveBase, MoveT>)>
const MoveT* FindActiveLayeredMoveByType() const { return static_cast<const MoveT*>(FindActiveLayeredMoveByType(MoveT::StaticStruct())); }

Even when trying to `const_cast` it looks like the Mover SyncState has 2 copies of this struct and uses a double buffer to swap them each frame.

Therefore, I was thinking about some possible hacky solutions, like my derived DashLayeredMove receiving a pointer on creation to the wrapper DashAction and asking `DashAction->ShouldTerminate(..)` inside the `GenerateMove(…)` step, and by that, modifying `DurationMs=-1.0f` from within the LayeredMove, but that seemed pretty disgusting :D.

Thanks for your help!

Hey there,

We’ve had a few questions for this over the past little while and you’re correct this currently isn’t handled. This path you’re taking could work, but you’ve found some of the pitfalls. In 5.7 we added MOVER_API void CancelFeaturesWithTag. With it, if you implement a FLayeredMove_RootMotionAttribute that implements a HasGameplayTag, you can cancel it using the same tag. This is probably the pathway that we’ll be taking as well, but that isn’t quite clear to us yet.

Dustin

Thanks for the reply. I will take a look and try to implement it using the gameplay tags.