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()
Non-Dynamic MulticastC++ only, multiple bindingsAddUObject() or AddLambda()
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.
DataModel delegates are non-dynamic multicast. Most delegates on ULCKTabletDataModel (camera mode, mic state, video quality, recording state) use DECLARE_MULTICAST_DELEGATE, not DECLARE_DYNAMIC_MULTICAST_DELEGATE. Bind with AddUObject() or AddLambda(), not AddDynamic().

Recording Lifecycle Delegates

FOnRecordingSaveFinished

What it’s for: Know when recording save completes
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRecordingSaveFinished, bool, Success);
Declared on: ULCKService Parameters:
  • Success (bool) — True if save succeeded, false on error
Usage:
// Subscribe (dynamic multicast -- use AddDynamic)
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);
Declared on: ULCKService 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
);
Declared on: ULCKService 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

FOnTabletCameraModeChanged

What it’s for: React to camera mode switches
DECLARE_MULTICAST_DELEGATE_OneParam(FOnTabletCameraModeChanged, UClass*);
Declared on: ULCKTabletDataModel Parameters:
  • UClass* — The new camera mode class (e.g., ULCKSelfieCameraMode, ULCKFirstPersonCameraMode, ULCKThirdPersonCameraMode)
This is a non-dynamic multicast delegate. Use AddUObject() or AddLambda(), not AddDynamic().
Usage:
// Subscribe (non-dynamic -- use AddUObject, not AddDynamic)
DataModel->OnTabletCameraModeChanged.AddUObject(this, &AMyUI::HandleCameraMode);

// Handler (no UFUNCTION() needed for non-dynamic delegates)
void AMyUI::HandleCameraMode(UClass* ModeClass)
{
    if (ModeClass->IsChildOf(ULCKSelfieCameraMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("Selfie"));
    }
    else if (ModeClass->IsChildOf(ULCKFirstPersonCameraMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("First Person"));
    }
    else if (ModeClass->IsChildOf(ULCKThirdPersonCameraMode::StaticClass()))
    {
        CameraModeText->SetText(FText::FromString("Third Person"));
    }
}

FOnMicStateChanged

What it’s for: Update UI when mic state changes
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMicStateChanged, ELCKMicState);
Declared on: ULCKTabletDataModel Parameters:
  • ELCKMicState — New microphone state (On, Off, No_Access)
This is a non-dynamic multicast delegate. Use AddUObject() or AddLambda(), not AddDynamic().
Usage:
// Subscribe (non-dynamic -- use AddUObject, not AddDynamic)
DataModel->OnMicStateChanged.AddUObject(this, &AMicButton::UpdateIcon);

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;
    }
}

FOnVideoQualityChanged

What it’s for: React to quality profile changes
DECLARE_MULTICAST_DELEGATE_OneParam(FOnVideoQualityChanged, ELCKVideoQuality);
Declared on: ULCKTabletDataModel
This is a non-dynamic multicast delegate. Use AddUObject() or AddLambda(), not AddDynamic().
Usage:
DataModel->OnVideoQualityChanged.AddUObject(this, &AMyUI::HandleQualityChanged);

void AMyUI::HandleQualityChanged(ELCKVideoQuality NewQuality)
{
    // Update quality display
}

FOnRecordStateChange

What it’s for: React to recording state changes on the DataModel
DECLARE_MULTICAST_DELEGATE_OneParam(FOnRecordStateChange, ELCKRecordingState);
Declared on: ULCKTabletDataModel
This is a non-dynamic multicast delegate. Use AddUObject() or AddLambda(), not AddDynamic().
Usage:
DataModel->OnRecordStateChanged.AddUObject(this, &AMyUI::HandleRecordStateChanged);

void AMyUI::HandleRecordStateChanged(ELCKRecordingState NewState)
{
    switch (NewState)
    {
        case ELCKRecordingState::Idle:
            // Ready to record
            break;
        case ELCKRecordingState::Recording:
            // Recording in progress
            break;
        case ELCKRecordingState::Saving:
            // Saving to disk
            break;
    }
}

FOnScreenOrientationChanged

What it’s for: React to screen orientation changes
DECLARE_MULTICAST_DELEGATE_OneParam(FOnScreenOrientationChanged, ELCKScreenOrientation);
Declared on: ULCKTabletDataModel
This is a non-dynamic multicast delegate. Use AddUObject() or AddLambda(), not AddDynamic().
Usage:
DataModel->OnScreenOrientationChanged.AddUObject(this, &AMyUI::HandleOrientationChanged);

void AMyUI::HandleOrientationChanged(ELCKScreenOrientation NewOrientation)
{
    // Update UI layout for new orientation
}

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_FourParams(
    FDelegateRenderAudio,
    TArrayView<const float>, // PCM samples (interleaved)
    int32,                    // Channels
    int32,                    // Sample rate (Hz)
    ELCKAudioChannel          // Source channel 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)
  • SampleRate (int32) — Audio sample rate in Hz (typically 48000)
  • 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,
    int32 SampleRate,
    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, 48000, 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)

Used for FOnRecordingSaveFinished, FOnRecordingSaveProgress, FOnRecordingError, and UI delegates like FOnTapStarted.
// 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
}

Non-Dynamic Multicast (C++ Only)

Used for DataModel delegates like FOnTabletCameraModeChanged, FOnMicStateChanged, FOnVideoQualityChanged, FOnRecordStateChange.
// Header -- no UFUNCTION() needed
UCLASS()
class AMyActor : public AActor
{
    GENERATED_BODY()

    void HandleCameraMode(UClass* ModeClass);
    void HandleMicState(ELCKMicState NewState);
};

// Implementation
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    ULCKTabletDataModel* DataModel = Tablet->GetDataModel();

    // Use AddUObject for member functions
    DataModel->OnTabletCameraModeChanged.AddUObject(this, &AMyActor::HandleCameraMode);
    DataModel->OnMicStateChanged.AddUObject(this, &AMyActor::HandleMicState);

    // Or use AddLambda for inline handlers
    DataModel->OnVideoQualityChanged.AddLambda([this](ELCKVideoQuality NewQuality)
    {
        UpdateQualityDisplay(NewQuality);
    });
}

Lambda Binding (C++ Only)

// Non-dynamic multicast delegate (DataModel events)
DataModel->OnTabletCameraModeChanged.AddLambda([this](UClass* ModeClass)
{
    UpdateCameraModeUI(ModeClass);
});

// Dynamic multicast delegate (UI events -- use AddDynamic, not AddLambda)
Button->OnTapStarted.AddDynamic(this, &AMyActor::OnRecordButtonTapped);

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

Removing Bindings

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

// Non-dynamic delegate with handle
FDelegateHandle Handle = DataModel->OnMicStateChanged.AddUObject(this, &AMyActor::HandleMicState);
DataModel->OnMicStateChanged.Remove(Handle);

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

// 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 (dynamic multicast -- use AddDynamic)
        Service->OnRecordingSaveFinished.AddDynamic(this, &ARecordingUI::OnSaveFinished);
        Service->OnRecordingSaveProgress.AddDynamic(this, &ARecordingUI::OnSaveProgress);
        Service->OnRecordingError.AddDynamic(this, &ARecordingUI::OnError);

        // State changes (non-dynamic multicast -- use AddUObject)
        DataModel->OnRecordStateChanged.AddUObject(this, &ARecordingUI::OnStateChanged);
        DataModel->OnMicStateChanged.AddUObject(this, &ARecordingUI::OnMicChanged);

        // UI interactions (dynamic multicast -- use AddDynamic)
        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);
    }

    // Non-dynamic delegate handler (no UFUNCTION needed)
    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);
    }

    // Non-dynamic delegate handler (no UFUNCTION needed)
    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
Dynamic vs non-dynamic matters — DataModel delegates use AddUObject/AddLambda, Service delegates use AddDynamic
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 with AddDynamic