How-To: Add Holds, Channels & a Progress Prompt

Goal: extend the Quick Start ability with hold-to-confirm interactions, timed channels (mining, reviving), and an on-screen prompt - still using only CrimsonInteraction + CrimsonCommon.

Prerequisites
Quick Start completed: you have GA_Interact with CurrentOptions, the Input.Interact.Pressed / Input.Interact.Released events, and a working instant press.

1. Hold support

Hold timing lives in ability timers, never in an Enhanced Input Hold trigger (per-option durations are data, and the input layer cannot know what you are aiming at). A repeating timer pushes progress to the target; a one-shot timer fires the interaction when the duration elapses; the release event cancels. Add the members PendingHoldOption, bHoldPending, HoldElapsed, HoldTickHandle, HoldDoneHandle to GA_Interact, branch on InputType in the press handler, and bind OnReleased to the release event from Quick Start step 6.

Press path branches on the option's Input Type: Hold -> Set Timer by Event (0.05, looping) -> TickHold event (Elapsed/Duration -> Notify Interaction Progress on the option's Interactable Target) + Set Timer by Event (Input Hold Duration, one-shot) -> CompleteHold (clear timers, Notify 1.0, Trigger Interaction Option). Wait Gameplay Event (Input.Interact.Released) -> clear both timers, Notify 0.0.
Verify
Set an option to InputType = Hold, InputHoldDuration = 1.5. Holding fills the target's progress (override Notify Interaction Progress on the interactable to see it); releasing early resets it to 0; completing fires the handler.

2. A channelled action phase

For mining / reviving style channels, set the option's InteractionActionAbility to your own channel ability. The grant task pre-grants it automatically (it grants both ability fields), so you launch it the same handle-based way. Give the channel ability its own gameplay-event trigger tag (e.g. Ability.Interaction.Duration.Start - add it like the Quick Start step-2 tags), then route options that carry an InteractionActionAbility to a launcher like this instead of the instant fire:

cpp
void UGA_Interact::StartChannel(const FCrimsonInteractionOption& Option)
{
UAbilitySystemComponent* ASC = GetAbilitySystemComponentFromActorInfo();
const FGameplayAbilitySpec* Spec = ASC ? ASC->FindAbilitySpecFromClass(Option.InteractionActionAbility) : nullptr;
if (!Spec)
{
return; // grant hasn't replicated yet - ignore the press
}
FGameplayEventData EventData;
EventData.EventTag = FGameplayTag::RequestGameplayTag(FName("Ability.Interaction.Duration.Start"));
EventData.Instigator = GetAvatarActorFromActorInfo();
EventData.Target = UCrimsonInteractionStatics::GetActorFromInteractableTarget(Option.InteractableTarget);
// Pass CLASSES / net-addressable objects, never spec handles - handles are machine-local.
EventData.OptionalObject = Option.TargetAbilitySystem;
EventData.OptionalObject2 = Option.InteractionAbilityToGrant.Get();
ASC->TriggerAbilityFromGameplayEvent(Spec->Handle, GetCurrentActorInfo(),
EventData.EventTag, &EventData, *ASC);
}

The channel ability itself is an event-triggered ability (constructor identical to the Quick Start handler but with your duration tag) that ticks a timer, pushes progress, and notifies the target on completion. The plugin's UCrimsonInteractionDurationContext gives its widget a ready-made progress channel, and FCrimsonInteractionProgressMessage (CrimsonCommon) broadcasts progress to any HUD:

cpp
// GA_Mine.cpp - members: float DurationSeconds = 3.f; float Elapsed = 0.f;
// TObjectPtr<AActor> Target; TObjectPtr<UCrimsonInteractionDurationContext> DurationContext;
// TSubclassOf<UUserWidget> ProgressWidgetClass; FTimerHandle TickHandle;
#include "CrimsonInteractionDurationContext.h" // CrimsonInteraction
#include "Interaction/CrimsonInteractionProgressMessage.h" // CrimsonCommon
#include "Interaction/ICrimsonInteractableTarget.h" // CrimsonCommon
#include "Messaging/CrimsonMessageSubsystem.h" // CrimsonCommon
#include "CrimsonCommonTags.h" // CrimsonCommon
void UGA_Mine::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
Target = TriggerEventData ? const_cast<AActor*>(ToRawPtr(TriggerEventData->Target)) : nullptr;
// Optional progress widget via the UIExtension system (CrimsonCommon) + the plugin's payload:
if (ActorInfo->IsLocallyControlled() && ProgressWidgetClass)
{
DurationContext = NewObject<UCrimsonInteractionDurationContext>(this);
DurationContext->Construct(ProgressWidgetClass, FText::FromString(TEXT("Mining...")), DurationSeconds);
// Register at your HUD extension point: UCrimsonUIExtensionSubsystem RegisterExtensionAsData(SlotTag, LocalPlayer, DurationContext, -1)
}
GetWorld()->GetTimerManager().SetTimer(TickHandle, this, &UGA_Mine::TickChannel, 0.05f, true);
}
void UGA_Mine::TickChannel()
{
Elapsed += 0.05f;
if (DurationContext) { DurationContext->UpdateProgress(Elapsed, DurationSeconds); }
FCrimsonInteractionProgressMessage Msg;
Msg.Instigator = GetAvatarActorFromActorInfo();
Msg.Target = Target;
Msg.Elapsed = Elapsed;
Msg.Duration = DurationSeconds;
Msg.Progress01 = FMath::Clamp(Elapsed / DurationSeconds, 0.f, 1.f);
Msg.bComplete = Elapsed >= DurationSeconds;
UCrimsonMessageSubsystem::Get(this).BroadcastMessage(CrimsonGameplayTags::Message_Interaction_Progress, Msg);
if (Elapsed >= DurationSeconds)
{
GetWorld()->GetTimerManager().ClearTimer(TickHandle);
if (Target && Target->Implements<UCrimsonInteractableTarget>())
{
ICrimsonInteractableTarget::Execute_NotifyInteractionSuccess(Target, GetAvatarActorFromActorInfo());
}
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
}
Channel launch in Blueprint
The handle-precise channel launch is C++ (spec lookup + TriggerAbilityFromGameplayEvent). Blueprint alternative: give each channel ability a unique gameplay-event trigger tag and launch it with Send Gameplay Event to Actor (Actor = avatar, payload built with Make GameplayEventData, Target = the interactable). The tag must be unique per channel ability - a shared tag would activate every listening ability at once. The channel ability body itself (timers, Update Progress on the duration context, Broadcast Message, Notify Interaction Success) is fully Blueprint-authorable.

3. A minimal prompt

You already have both signals: OnOptionsChanged tells you what to show (CurrentOptions[0].Text, or hide when empty), and FCrimsonInteractionProgressMessage on Message.Interaction.Progress carries hold/channel progress. Push the text into any widget you like from the options handler; for progress, listen on the message bus (Blueprint: the Listen For Crimson Messages async node; C++ below). Filter on Instigator so a listen server does not show other players' progress.

cpp
// In your HUD widget - members: FCrimsonMessageListenerHandle ListenerHandle;
#include "Messaging/CrimsonMessageSubsystem.h" // CrimsonCommon
#include "CrimsonCommonTags.h"
void UMyInteractHUD::NativeConstruct()
{
Super::NativeConstruct();
ListenerHandle = UCrimsonMessageSubsystem::Get(this).RegisterListener<FCrimsonInteractionProgressMessage>(
CrimsonGameplayTags::Message_Interaction_Progress,
[this](FGameplayTag, const FCrimsonInteractionProgressMessage& M)
{
if (M.Instigator == GetOwningPlayerPawn())
{
SetBarPercent(M.Progress01);
if (M.bComplete) { HideBar(); }
}
});
}
void UMyInteractHUD::NativeDestruct()
{
ListenerHandle.Unregister();
Super::NativeDestruct();
}
Verify
PIE as client: prompt appears near the chest, instant press opens it, hold options fill and cancel correctly, a channel ticks its widget, and a second client sees the chest react (server-authoritative).

See also

  • How-To: Instant, Hold & Duration Interactions - choosing the right shape per option.
  • Concept: Two Activation Methods - the replication rules behind the handle-based launches.
  • How-To: World Indicators & Hold-Progress Feedback - richer feedback options.