Skip to main content

Overview

This guide covers:
  • Accessing the LCK subsystem and service
  • Tablet actor lifecycle
  • Widget component setup
  • Async callback patterns

Accessing LCK Service

Via World Subsystem

The recommended way to access LCK functionality:
ULCKSubsystem* GetLCKSubsystem(UWorld* World)
{
    if (World)
    {
        return World->GetSubsystem<ULCKSubsystem>();
    }
    return nullptr;
}

ULCKService* GetLCKService(UWorld* World)
{
    if (ULCKSubsystem* Subsystem = GetLCKSubsystem(World))
    {
        return Subsystem->GetService();
    }
    return nullptr;
}

From Any Actor

void AMyActor::UseRecording()
{
    ULCKSubsystem* Subsystem = GetWorld()->GetSubsystem<ULCKSubsystem>();
    if (!Subsystem)
    {
        UE_LOG(LogTemp, Error, TEXT("LCK Subsystem not available"));
        return;
    }

    ULCKService* Service = Subsystem->GetService();
    if (Service)
    {
        Service->StartRecording();
    }
}

Blueprint Access

UFUNCTION(BlueprintCallable, Category = "LCK", meta = (WorldContext = "WorldContextObject"))
static ULCKService* GetLCKService(UObject* WorldContextObject)
{
    if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))
    {
        if (ULCKSubsystem* Subsystem = World->GetSubsystem<ULCKSubsystem>())
        {
            return Subsystem->GetService();
        }
    }
    return nullptr;
}

Tablet Lifecycle

Spawning

void AMyGameMode::SpawnLCKTablet()
{
    if (!TabletClass)
    {
        UE_LOG(LogTemp, Error, TEXT("TabletClass not set"));
        return;
    }

    FVector SpawnLocation = GetTabletSpawnLocation();
    FRotator SpawnRotation = FRotator::ZeroRotator;

    FActorSpawnParameters SpawnParams;
    SpawnParams.SpawnCollisionHandlingOverride =
        ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;

    SpawnedTablet = GetWorld()->SpawnActor<ALCKTablet>(
        TabletClass,
        SpawnLocation,
        SpawnRotation,
        SpawnParams
    );

    if (SpawnedTablet)
    {
        OnTabletSpawned(SpawnedTablet);
    }
}

Destruction

void AMyGameMode::DestroyLCKTablet()
{
    if (SpawnedTablet)
    {
        // Stop any active recording first
        if (ULCKService* Service = GetLCKService(GetWorld()))
        {
            if (Service->IsRecording())
            {
                Service->StopRecording();
            }
        }

        SpawnedTablet->Destroy();
        SpawnedTablet = nullptr;
    }
}

Lifecycle Events

void ALCKTablet::BeginPlay()
{
    Super::BeginPlay();

    // Load saved settings
    LoadSettings();

    // Initialize camera
    InitializeCamera();

    // Register with telemetry
    if (ULCKTelemetrySubsystem* Telemetry = GetGameInstance()->GetSubsystem<ULCKTelemetrySubsystem>())
    {
        Telemetry->SendEvent(ELCKTelemetryEventType::TabletSpawned);
    }
}

void ALCKTablet::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    // Save current settings
    SaveSettings();

    // Stop any active recording
    if (ULCKService* Service = GetLCKService(GetWorld()))
    {
        if (Service->IsRecording())
        {
            Service->StopRecording();
        }
    }

    // Telemetry
    if (ULCKTelemetrySubsystem* Telemetry = GetGameInstance()->GetSubsystem<ULCKTelemetrySubsystem>())
    {
        Telemetry->SendEvent(ELCKTelemetryEventType::TabletDestroyed);
    }

    Super::EndPlay(EndPlayReason);
}

Enabling Grabbing

The UI doesn’t handle grabbing mechanics. Assume that it’s game-specific and it’s up to developers to integrate this. Here’s an example of how to add the GrabComponent from Unreal’s VR Template. Make sure that you call GrabStarted and GrabEnded methods on LCKTablet. You can set them up in C++ or Blueprint. GrabComponent setup

Widget Component Setup

ULCKWidgetComponent

The tablet uses a widget component for its 3D UI:
UCLASS()
class ALCKTablet : public AActor
{
    GENERATED_BODY()

protected:
    UPROPERTY(VisibleAnywhere, Category = "Components")
    UWidgetComponent* TabletWidget;

    UPROPERTY(VisibleAnywhere, Category = "Components")
    USceneCaptureComponent2D* CameraCapture;

    UPROPERTY(VisibleAnywhere, Category = "Components")
    UStaticMeshComponent* TabletMesh;

public:
    ALCKTablet()
    {
        // Root component
        TabletMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TabletMesh"));
        RootComponent = TabletMesh;

        // Widget for UI
        TabletWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("TabletWidget"));
        TabletWidget->SetupAttachment(TabletMesh);
        TabletWidget->SetWidgetSpace(EWidgetSpace::World);
        TabletWidget->SetDrawSize(FVector2D(800, 600));

        // Camera capture
        CameraCapture = CreateDefaultSubobject<USceneCaptureComponent2D>(TEXT("CameraCapture"));
        CameraCapture->SetupAttachment(TabletMesh);
    }
};

Collision Setup

void ALCKTablet::SetupCollision()
{
    // Configure collision for VR hand interaction
    TabletMesh->SetCollisionProfileName(TEXT("OverlapOnlyPawn"));
    TabletMesh->SetGenerateOverlapEvents(true);

    // Set custom collision responses
    TabletMesh->SetCollisionResponseToChannel(
        ECC_Pawn,
        ECR_Overlap
    );
}

Weak Pointer Patterns

For safe async callbacks, always use weak pointers:

TWeakObjectPtr Pattern

void UMyComponent::StartRecordingWithCallback()
{
    ULCKService* Service = GetLCKService(GetWorld());
    if (!Service)
    {
        return;
    }

    // Capture weak pointer to this
    TWeakObjectPtr<UMyComponent> WeakThis(this);

    Service->StartRecordingAsync(
        FOnLCKRecorderBoolResult::CreateLambda([WeakThis](bool bSuccess)
        {
            // Check if component still exists
            if (UMyComponent* This = WeakThis.Get())
            {
                This->HandleRecordingStarted(bSuccess);
            }
        })
    );
}

void UMyComponent::HandleRecordingStarted(bool bSuccess)
{
    if (bSuccess)
    {
        UpdateUIForRecording();
    }
    else
    {
        ShowRecordingError();
    }
}

TWeakPtr for Shared Pointers

void FMyAudioHandler::BindToSource(TSharedPtr<ILCKAudioSource> Source)
{
    TWeakPtr<FMyAudioHandler> WeakThis = AsShared();

    Source->OnAudioDataDelegate.BindLambda(
        [WeakThis](TArrayView<const float> PCM, int32 Channels, ELCKAudioChannel Channel)
        {
            if (TSharedPtr<FMyAudioHandler> This = WeakThis.Pin())
            {
                This->ProcessAudio(PCM, Channels, Channel);
            }
        }
    );
}

Event Subscription

Safe Delegate Binding

void UMyWidget::NativeConstruct()
{
    Super::NativeConstruct();

    // Find tablet and bind to events
    if (ALCKTablet* Tablet = FindLCKTablet())
    {
        ULCKTabletDataModel* DataModel = Tablet->GetDataModel();

        // Store binding handle for cleanup
        RecordingStateHandle = DataModel->OnRecordingStateChanged.AddUObject(
            this, &UMyWidget::OnRecordingStateChanged
        );

        CameraModeHandle = DataModel->OnCameraModeChanged.AddUObject(
            this, &UMyWidget::OnCameraModeChanged
        );
    }
}

void UMyWidget::NativeDestruct()
{
    // Unbind all delegates
    if (ALCKTablet* Tablet = FindLCKTablet())
    {
        ULCKTabletDataModel* DataModel = Tablet->GetDataModel();
        DataModel->OnRecordingStateChanged.Remove(RecordingStateHandle);
        DataModel->OnCameraModeChanged.Remove(CameraModeHandle);
    }

    Super::NativeDestruct();
}

Initialization Order

void ALCKTablet::BeginPlay()
{
    Super::BeginPlay();

    // 1. Create and initialize data model
    DataModel = NewObject<ULCKTabletDataModel>(this);
    DataModel->Initialize();

    // 2. Load saved settings
    LoadSettings();

    // 3. Setup widget
    if (TabletWidget)
    {
        TabletWidget->SetWidget(CreateWidget<ULCKTabletUI>(GetWorld()));
    }

    // 4. Initialize camera system
    InitializeCameraCapture();

    // 5. Bind to service events
    BindToServiceEvents();

    // 6. Send spawn telemetry
    SendSpawnTelemetry();
}

Troubleshooting Integration

Cause: World not valid or subsystem not registered.Solution:
// Ensure world is valid
if (!GetWorld())
{
    UE_LOG(LogLCK, Error, TEXT("No world available"));
    return;
}

// Check if game is still running
if (GetWorld()->bIsTearingDown)
{
    return;
}
Cause: Object destroyed before callback or delegate unbound.Solution: Use weak pointers and verify binding:
// Verify delegate is bound
if (Service->OnRecordingStarted.IsBound())
{
    UE_LOG(LogLCK, Log, TEXT("Delegate properly bound"));
}
Cause: Widget component not properly configured.Solution:
TabletWidget->SetWidgetSpace(EWidgetSpace::World);
TabletWidget->SetDrawSize(FVector2D(800, 600));
TabletWidget->SetTwoSided(true);  // Visible from both sides
TabletWidget->RequestRedraw();

See Also