Skip to main content

What Problem Does This Solve?

Delegates let you react to LCK events in real-time:
  • Recording started/stopped
  • Save progress updates
  • Button interactions
  • Camera mode changes
  • Audio callbacks
Instead of polling state every frame, you bind handlers that fire when events occur. This keeps your code clean and responsive.

When to Use This

Reference this when:
  • Building custom recording UI
  • Handling recording lifecycle events
  • Responding to user interactions
  • Processing audio data
  • Tracking save progress

Delegate Types

LCK uses two kinds of delegates:
TypeUsageBinding Method
Dynamic MulticastBlueprint-compatible, multiple bindingsAddDynamic()
Raw/LambdaC++ only, single or multiple bindingsBindLambda() or AddLambda()
Audio delegates are SINGLE delegates (not multicast). Use BindLambda(), not AddLambda(). Each bind replaces the previous one.

Recording Lifecycle Delegates

FOnRecordingSaveFinished

What it’s for: Know when recording save completes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRecordingSaveFinished, bool, Success);
Parameters:
  • Success (bool) — True if save succeeded, false on error
Usage:
// Subscribe
Service->OnRecordingSaveFinished.AddDynamic(this, &AMyActor::HandleSaveFinished);

// Handler
UFUNCTION()
void AMyActor::HandleSaveFinished(bool bSuccess)
{
    if (bSuccess)
    {
        ShowNotification(TEXT("Recording saved!"));
        PlaySuccessSound();
    }
    else
    {
        ShowError(TEXT("Failed to save recording"));
    }
}

FOnRecordingSaveProgress

What it’s for: Update progress bar during save
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRecordingSaveProgress, float, Progress);
Parameters:
  • Progress (float) — Value from 0.0 (start) to 1.0 (complete)
Usage:
Service->OnRecordingSaveProgress.AddDynamic(this, &AMyUI::UpdateProgressBar);

UFUNCTION()
void AMyUI::UpdateProgressBar(float Progress)
{
    ProgressBar->SetPercent(Progress);
    ProgressText->SetText(FText::AsPercent(Progress));
}

FOnRecordingError

What it’s for: Handle recording errors with context
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
    FOnRecordingError,
    FString, ErrorMessage,
    int32, ErrorCode
);
Parameters:
  • ErrorMessage (FString) — Human-readable error description
  • ErrorCode (int32) — Numeric error code (see ELCKError)
Usage:
Service->OnRecordingError.AddDynamic(this, &AMyActor::HandleRecordingError);

UFUNCTION()
void AMyActor::HandleRecordingError(FString ErrorMessage, int32 ErrorCode)
{
    UE_LOG(LogLCK, Error, TEXT("Recording error %d: %s"), ErrorCode, *ErrorMessage);
    
    // Show user-friendly message
    ShowErrorDialog(ErrorMessage);
    
    // Log to analytics
    Analytics->TrackError(TEXT("Recording"), ErrorCode);
}

Camera & Settings Delegates

FOnCameraModeChanged

What it’s for: React to camera mode switches
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCameraModeChanged, UClass*, ModeClass);
Parameters:
  • ModeClass (UClass*) — The new camera mode class
Usage:
DataModel->OnCameraModeChanged.AddDynamic(this, &AMyUI::HandleCameraMode);

UFUNCTION()
void AMyUI::HandleCameraMode(UClass* ModeClass)
{
    if (ModeClass->IsChildOf(USelfieMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("Selfie"));
    }
    else if (ModeClass->IsChildOf(UFirstPersonMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("First Person"));
    }
    else if (ModeClass->IsChildOf(UThirdPersonMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("Third Person"));
    }
}

FOnMicStateChanged

What it’s for: Update UI when mic state changes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMicStateChanged, ELCKMicState, NewState);
Parameters:
  • NewState (ELCKMicState) — New microphone state (On, Off, No_Access)
Usage:
DataModel->OnMicStateChanged.AddDynamic(this, &AMicButton::UpdateIcon);

UFUNCTION()
void AMicButton::UpdateIcon(ELCKMicState NewState)
{
    switch (NewState)
    {
        case ELCKMicState::On:
            Icon->SetBrush(MicOnTexture);
            Icon->SetColorAndOpacity(FLinearColor::White);
            break;
        case ELCKMicState::Off:
            Icon->SetBrush(MicOffTexture);
            Icon->SetColorAndOpacity(FLinearColor::Gray);
            break;
        case ELCKMicState::No_Access:
            Icon->SetBrush(MicBlockedTexture);
            ShowPermissionPrompt();
            break;
    }
}

FOnQualityChanged

What it’s for: React to quality profile changes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnQualityChanged, ELCKVideoQuality, NewQuality);

UI Interaction Delegates

FOnTapStarted

What it’s for: Handle button presses
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTapStarted);
Usage:
RecordButton->OnTapStarted.AddDynamic(this, &AMyActor::HandleRecordButton);

UFUNCTION()
void AMyActor::HandleRecordButton()
{
    if (Service->IsRecording())
    {
        Service->StopRecording();
    }
    else
    {
        Service->StartRecording();
    }
}
LCK buttons include automatic 0.25s cooldown. Don’t add your own debouncing.

FOnStepperValueChanged

What it’s for: Handle stepper control changes (increment/decrement buttons)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStepperValueChanged, int8, NewValue);
Parameters:
  • NewValue (int8) — Direction: -1 (decrease) or +1 (increase)
Usage:
FOVStepper->OnStepperValueChanged.AddDynamic(this, &ACamera::AdjustFOV);

UFUNCTION()
void ACamera::AdjustFOV(int8 Direction)
{
    CurrentFOV += Direction * 5.0f; // Adjust by 5 degrees
    CurrentFOV = FMath::Clamp(CurrentFOV, 30.0f, 120.0f);
    UpdateCamera();
}

FOnPad2DChanged

What it’s for: Handle 2D directional pad input
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPad2DChanged, FIntPoint, NewValue);
Parameters:
  • NewValue (FIntPoint) — Direction as (X, Y) where values are -1, 0, or +1
Usage:
Pad2D->OnPad2DChanged.AddDynamic(this, &ANavigator::HandleDirection);

UFUNCTION()
void ANavigator::HandleDirection(FIntPoint Direction)
{
    // Direction.X: -1 (left), 0 (none), +1 (right)
    // Direction.Y: -1 (down), 0 (none), +1 (up)
    
    if (Direction.X != 0)
    {
        // Handle horizontal
        ScrollHorizontal(Direction.X);
    }
    
    if (Direction.Y != 0)
    {
        // Handle vertical
        ScrollVertical(Direction.Y);
    }
}

Audio Delegates

FOnRenderAudioDelegate

What it’s for: Process raw audio data from audio sources
DECLARE_MULTICAST_DELEGATE_ThreeParams(
    FDelegateRenderAudio,
    TArrayView<const float>, // PCM samples
    int32,                    // Channels
    ELCKAudioChannel          // Source type
);

typedef FDelegateRenderAudio::FDelegate FOnRenderAudioDelegate;
Parameters:
  • PCM (TArrayView<const float>) — Interleaved audio samples (32-bit float)
  • Channels (int32) — Number of channels (typically 2 for stereo)
  • Source (ELCKAudioChannel) — Audio source type (Game, Microphone, VoiceChat)
This is a SINGLE delegate (not multicast). Use BindLambda(), not AddLambda(). Each bind replaces the previous one.Audio callbacks may come from different threads. Use AsyncTask(ENamedThreads::GameThread, ...) if accessing game thread objects.
Usage:
// Bind audio callback (replaces any existing binding)
AudioSource->OnAudioDataDelegate.BindLambda([this](
    TArrayView<const float> PCM,
    int32 Channels,
    ELCKAudioChannel SourceChannel)
{
    // PCM format: interleaved 32-bit float samples
    // Example: [L, R, L, R, L, R, ...] for stereo
    
    // Calculate RMS volume
    float Sum = 0.0f;
    for (float Sample : PCM)
    {
        Sum += Sample * Sample;
    }
    float RMS = FMath::Sqrt(Sum / PCM.Num());
    
    // Update UI on game thread
    AsyncTask(ENamedThreads::GameThread, [this, RMS]()
    {
        UpdateVolumeIndicator(RMS);
    });
});
Fire the delegate (from audio source implementation):
// Inside ILCKAudioSource::Render() or similar
TArray<float> PCMData = GetAudioBuffer();
OnAudioDataDelegate.ExecuteIfBound(PCMData, 2, ELCKAudioChannel::Game);

Async Operation Delegates

FOnLCKRecorderBoolResult

What it’s for: Handle async operation results
DECLARE_DELEGATE_OneParam(FOnLCKRecorderBoolResult, bool /*bSuccess*/);
Usage:
Recorder->StartRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
    {
        if (bSuccess)
        {
            ShowRecordingIndicator();
            UE_LOG(LogLCK, Log, TEXT("Recording started"));
        }
        else
        {
            ShowError(TEXT("Failed to start recording"));
        }
    })
);

FOnLCKRecorderProgress

What it’s for: Track save/encoding progress
DECLARE_DELEGATE_OneParam(FOnLCKRecorderProgress, float /*Progress*/);
Usage:
Recorder->StopRecordingAsync(
    FOnLCKRecorderBoolResult::CreateLambda([this](bool bSuccess)
    {
        if (bSuccess)
        {
            ShowSuccess(TEXT("Recording saved"));
        }
    }),
    FOnLCKRecorderProgress::CreateLambda([this](float Progress)
    {
        ProgressBar->SetPercent(Progress);
        StatusText->SetText(FText::Format(
            LOCTEXT("SavingProgress", "Saving... {0}%"),
            FText::AsPercent(Progress)
        ));
    })
);

Binding Patterns

Dynamic Delegates (Blueprint-Compatible)

// Header
UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()
    
    UFUNCTION()
    void HandleButtonTap();
};

// Implementation
void AMyActor::BeginPlay()
{
    Super::BeginPlay();
    Button->OnTapStarted.AddDynamic(this, &AMyActor::HandleButtonTap);
}

void AMyActor::HandleButtonTap()
{
    // Handle tap
}

Lambda Binding (C++ Only)

// Multicast delegate (UI events)
Button->OnTapStarted.AddLambda([this]()
{
    ToggleRecording();
});

// Single delegate (audio)
AudioSource->OnAudioDataDelegate.BindLambda([this](auto PCM, auto Channels, auto Source)
{
    ProcessAudio(PCM);
});

Removing Bindings

// Dynamic delegate
Button->OnTapStarted.RemoveDynamic(this, &AMyActor::HandleButtonTap);

// Lambda with handle
FDelegateHandle Handle = Delegate.AddLambda([](){ /* ... */ });
Delegate.Remove(Handle);

// Clear all
Delegate.Clear();

Complete Example: Recording UI

UCLASS()
class ARecordingUI : public AActor
{
    GENERATED_BODY()
    
protected:
    virtual void BeginPlay() override
    {
        Super::BeginPlay();
        
        ULCKService* Service = GetLCKService();
        ALCKTablet* Tablet = FindTablet();
        
        if (!Service || !Tablet) return;
        
        ULCKTabletDataModel* DataModel = Tablet->GetDataModel();
        
        // Recording lifecycle
        Service->OnRecordingSaveFinished.AddDynamic(this, &ARecordingUI::OnSaveFinished);
        Service->OnRecordingSaveProgress.AddDynamic(this, &ARecordingUI::OnSaveProgress);
        Service->OnRecordingError.AddDynamic(this, &ARecordingUI::OnError);
        
        // State changes
        DataModel->OnRecordingStateChanged.AddDynamic(this, &ARecordingUI::OnStateChanged);
        DataModel->OnMicStateChanged.AddDynamic(this, &ARecordingUI::OnMicChanged);
        
        // UI interactions
        RecordButton->OnTapStarted.AddDynamic(this, &ARecordingUI::OnRecordButton);
        MicButton->OnTapStarted.AddDynamic(this, &ARecordingUI::OnMicButton);
    }
    
    UFUNCTION()
    void OnRecordButton()
    {
        if (Service->IsRecording())
            Service->StopRecording();
        else
            Service->StartRecording();
    }
    
    UFUNCTION()
    void OnMicButton()
    {
        bool bCurrentState = Service->IsMicrophoneEnabled();
        Service->SetMicrophoneEnabled(!bCurrentState);
    }
    
    UFUNCTION()
    void OnStateChanged(ELCKRecordingState NewState)
    {
        switch (NewState)
        {
            case ELCKRecordingState::Recording:
                RecordButton->SetText(FText::FromString("Stop"));
                RecordingIndicator->SetVisibility(ESlateVisibility::Visible);
                break;
            case ELCKRecordingState::Idle:
                RecordButton->SetText(FText::FromString("Record"));
                RecordingIndicator->SetVisibility(ESlateVisibility::Hidden);
                break;
            case ELCKRecordingState::Saving:
                ProgressPanel->SetVisibility(ESlateVisibility::Visible);
                break;
        }
    }
    
    UFUNCTION()
    void OnSaveProgress(float Progress)
    {
        ProgressBar->SetPercent(Progress);
    }
    
    UFUNCTION()
    void OnSaveFinished(bool bSuccess)
    {
        ProgressPanel->SetVisibility(ESlateVisibility::Hidden);
        
        if (bSuccess)
            ShowNotification(TEXT("Recording saved!"));
    }
    
    UFUNCTION()
    void OnError(FString ErrorMessage, int32 ErrorCode)
    {
        ShowErrorDialog(ErrorMessage);
    }
    
    UFUNCTION()
    void OnMicChanged(ELCKMicState NewState)
    {
        switch (NewState)
        {
            case ELCKMicState::On:
                MicIcon->SetBrush(MicOnTexture);
                break;
            case ELCKMicState::Off:
                MicIcon->SetBrush(MicOffTexture);
                break;
            case ELCKMicState::No_Access:
                ShowPermissionDialog();
                break;
        }
    }
};

Key Takeaways

Use delegates for events — Don’t poll state every frame
Audio delegates are SINGLE — Use BindLambda, not AddLambda
Thread safety matters — Audio callbacks may come from any thread
Remove bindings on cleanup — Prevent dangling pointers
Dynamic delegates for Blueprint — Use UFUNCTION handlers