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.
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.
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:
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:
// 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" // CrimsonCommonvoid 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);}}
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.
// 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();}
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.