Overview
LCK UI components automatically reflect the current recording state. This page covers recording state enumeration, visual feedback per state, state transitions, and synchronizing custom UI with LCK state.
ELCKRecordingState
The recording system has six states:
| State | Description | UI Behavior |
|---|
Idle | Ready to record | Default colors, all buttons enabled |
Recording | Actively capturing | Red record indicator, timer visible |
Saving | Writing file to disk | Progress indicator visible |
Processing | Finalizing video | Progress indicator, buttons disabled |
Error | Something went wrong | Error indicator, retry available |
Paused | Recording paused | Pause indicator, resume available |
UENUM(BlueprintType)
enum class ELCKRecordingState : uint8
{
Idle UMETA(DisplayName = "Idle"),
Recording UMETA(DisplayName = "Recording"),
Saving UMETA(DisplayName = "Saving"),
Processing UMETA(DisplayName = "Processing"),
Error UMETA(DisplayName = "Error"),
Paused UMETA(DisplayName = "Paused")
};
State Machine
StartRecording()
┌────────────────────────────┐
│ │
v │
┌─────────┐ ┌─────────┐
│ Idle │ │Recording│◄──────┐
└─────────┘ └─────────┘ │
^ │ │ │
│ Pause()│ │Resume()│
│ v │ │
│ ┌──────────┐│ │
│ │ Paused │┘ │
│ └──────────┘ │
│ │ │
│ StopRecording() │ │
│ v │
┌─────────┐ ┌──────────┐ │
│ Error │<─────────────────│ Saving │ │
└─────────┘ Save Failed └──────────┘ │
│ │ │
│ v │
│ ┌──────────┐ │
│ │Processing│ │
│ └──────────┘ │
│ │ │
└────────────────────────────┴──────────┘
Complete
Visual Feedback
Default State Colors
| State | Primary Color | Recommended Hex |
|---|
| Idle | Blue/Gray | #5E45FF / #808080 |
| Recording | Red | #FF4444 |
| Saving | Yellow/Orange | #FFAA00 |
| Processing | Yellow/Orange | #FFAA00 |
| Error | Red/Dark | #CC0000 |
| Paused | Gray/Blue | #666699 |
Recording Indicator
During recording, the UI shows a pulsing red record dot, recording duration timer, and microphone level meter.
void ULCKRecordingIndicator::UpdateRecordingState(ELCKRecordingState State)
{
switch (State)
{
case ELCKRecordingState::Idle:
RecordDot->SetVisibility(ESlateVisibility::Hidden);
TimerText->SetVisibility(ESlateVisibility::Hidden);
ProgressBar->SetVisibility(ESlateVisibility::Hidden);
break;
case ELCKRecordingState::Recording:
RecordDot->SetVisibility(ESlateVisibility::Visible);
TimerText->SetVisibility(ESlateVisibility::Visible);
StartPulseAnimation();
break;
case ELCKRecordingState::Paused:
RecordDot->SetVisibility(ESlateVisibility::Visible);
TimerText->SetVisibility(ESlateVisibility::Visible);
StopPulseAnimation(); // Static indicator when paused
break;
case ELCKRecordingState::Saving:
case ELCKRecordingState::Processing:
RecordDot->SetVisibility(ESlateVisibility::Hidden);
ProgressBar->SetVisibility(ESlateVisibility::Visible);
break;
case ELCKRecordingState::Error:
ErrorIcon->SetVisibility(ESlateVisibility::Visible);
break;
}
}
Cooldown Timer
Buttons have a cooldown to prevent accidental double-presses:
// Default cooldown: 0.25 seconds
static constexpr float ButtonCooldownTime = 0.25f;
void ULCKButton::OnPressed()
{
if (bIsOnCooldown)
{
return; // Ignore press during cooldown
}
// Execute button action
OnButtonPressed.Broadcast();
// Start cooldown
bIsOnCooldown = true;
GetWorld()->GetTimerManager().SetTimer(
CooldownHandle,
this,
&ULCKButton::EndCooldown,
ButtonCooldownTime
);
}
Two-Phase Interaction
LCK buttons use a two-phase interaction model: Overlap Begin (hand enters button collision) and Overlap End (hand exits button collision).
void ULCKButton::OnOverlapBegin(AActor* OtherActor)
{
// Validate it's a hand
if (!OtherActor->ActorHasTag(TEXT("Hand")))
{
return;
}
// Check touch direction (dot product validation)
FVector TouchDirection = GetTouchDirection(OtherActor);
if (FVector::DotProduct(TouchDirection, GetForwardVector()) > 0.5f)
{
// Valid front-facing touch
SetState(ELCKButtonState::Pressed);
OnPressed();
}
}
Subscribing to State Changes
ULCKTabletDataModel
The tablet data model provides state change delegates:
// Get data model
ULCKTabletDataModel* DataModel = Tablet->GetDataModel();
// Subscribe to state changes (non-dynamic delegate — use AddLambda or AddUObject)
DataModel->OnRecordStateChanged.AddLambda([this](ELCKRecordingState NewState)
{
switch (NewState)
{
case ELCKRecordingState::Recording:
// Show recording UI
break;
case ELCKRecordingState::Saving:
case ELCKRecordingState::Processing:
// Show progress bar
break;
case ELCKRecordingState::Paused:
// Show pause indicator
break;
case ELCKRecordingState::Error:
// Show error message
break;
case ELCKRecordingState::Idle:
// Reset to default state
break;
}
});
// Or bind to a UObject member function
DataModel->OnRecordStateChanged.AddUObject(this, &UMyComponent::HandleStateChange);
Reactive Properties
The data model uses non-dynamic multicast delegates that broadcast on change:
DECLARE_MULTICAST_DELEGATE_OneParam(FOnRecordStateChage, ELCKRecordingState);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnMicStateChanged, ELCKMicState);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnScreenOrientationChanged, ELCKScreenOrientation);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnVideoQualityChanged, ELCKVideoQuality);
These are non-dynamic delegates. Use AddLambda() or AddUObject() to subscribe — NOT AddDynamic().
Synchronizing Custom UI
Example: Custom Recording Indicator
UCLASS()
class UCustomRecordingUI : public UUserWidget
{
GENERATED_BODY()
protected:
virtual void NativeConstruct() override
{
Super::NativeConstruct();
// Find tablet and subscribe
ALCKTablet* Tablet = FindLCKTablet();
if (Tablet)
{
ULCKTabletDataModel* DataModel = Tablet->GetDataModel();
DataModel->OnRecordStateChanged.AddUObject(
this, &UCustomRecordingUI::OnStateChanged
);
}
}
void OnStateChanged(ELCKRecordingState NewState)
{
// Update custom UI
RecordingIcon->SetBrushFromTexture(
GetIconForState(NewState)
);
StatusText->SetText(
GetTextForState(NewState)
);
}
private:
UPROPERTY(meta = (BindWidget))
UImage* RecordingIcon;
UPROPERTY(meta = (BindWidget))
UTextBlock* StatusText;
};
Example: Duration Timer
void URecordingTimer::Tick(float DeltaTime)
{
if (ULCKService* Service = GetLCKService())
{
if (Service->IsRecording())
{
float Duration = Service->GetRecordingDuration();
// Format as MM:SS
int32 Minutes = FMath::FloorToInt(Duration / 60.0f);
int32 Seconds = FMath::FloorToInt(FMath::Fmod(Duration, 60.0f));
TimerText->SetText(FText::FromString(
FString::Printf(TEXT("%02d:%02d"), Minutes, Seconds)
));
}
}
}
State Persistence
Recording state does NOT persist across sessions. On game start, state is always Idle.
Settings that DO persist: last used camera mode, last used quality profile, and audio configuration.
// Load saved settings on startup
void ALCKTablet::BeginPlay()
{
Super::BeginPlay();
// Recording state always starts Idle
DataModel->SetRecordingState(ELCKRecordingState::Idle);
// But camera mode is loaded from saved settings
DataModel->SetCameraMode(LoadSavedCameraMode());
}
Error Handling
Error State Recovery
void ULCKTabletDataModel::HandleRecordingError(ELCKError Error)
{
// Transition to error state
SetRecordingState(ELCKRecordingState::Error);
// Broadcast error for UI to display
OnRecordingError.Broadcast(Error);
// After timeout, return to Idle
FTimerHandle Handle;
GetWorld()->GetTimerManager().SetTimer(
Handle,
[this]() { SetRecordingState(ELCKRecordingState::Idle); },
3.0f, // 3 second error display
false
);
}
See Also