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:
| Type | Usage | Binding Method |
|---|
| Dynamic Multicast | Blueprint-compatible, multiple bindings | AddDynamic() |
| Raw/Lambda | C++ only, single or multiple bindings | BindLambda() 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