add MosisPointerComponent for VR ray interaction

This commit is contained in:
2026-01-17 20:03:41 +01:00
parent dc1bd14ff0
commit 9cf3ffdbaf
5 changed files with 385 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "MosisSDK",
"Description": "",
"Description": "Mosis Virtual Phone SDK for VR",
"Category": "Other",
"CreatedBy": "OmixLab LTD",
"CreatedByURL": "",
@@ -20,5 +20,11 @@
"Type": "Runtime",
"LoadingPhase": "Default"
}
],
"Plugins": [
{
"Name": "EnhancedInput",
"Enabled": true
}
]
}

View File

@@ -26,7 +26,9 @@ public class MosisSDK : ModuleRules
"Slate",
"SlateCore",
"RenderCore",
"RHI"
"RHI",
"EnhancedInput",
"InputCore"
}
);

View File

@@ -17,6 +17,12 @@ AMosisPhoneActor::AMosisPhoneActor()
PhoneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PhoneMesh"));
RootComponent = PhoneMesh;
// Configure collision for raycast detection
PhoneMesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
PhoneMesh->SetCollisionResponseToAllChannels(ECR_Ignore);
PhoneMesh->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block);
PhoneMesh->SetGenerateOverlapEvents(false);
// Load the default plane mesh
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneMeshFinder(
TEXT("/Engine/BasicShapes/Plane.Plane"));

View File

@@ -0,0 +1,236 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "MosisPointerComponent.h"
#include "MosisPhoneActor.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "InputAction.h"
#include "GameFramework/PlayerController.h"
#include "DrawDebugHelpers.h"
DEFINE_LOG_CATEGORY_STATIC(LogMosisPointer, Log, All);
UMosisPointerComponent::UMosisPointerComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
}
void UMosisPointerComponent::BeginPlay()
{
Super::BeginPlay();
// Try to setup input bindings after a short delay to ensure player controller is ready
if (TriggerAction)
{
SetupInputBindings();
}
UE_LOG(LogMosisPointer, Log, TEXT("MosisPointerComponent initialized. RayLength: %.1f"), RayLength);
}
void UMosisPointerComponent::SetupInputBindings()
{
if (bInputBound)
{
return;
}
AActor* Owner = GetOwner();
if (!Owner)
{
return;
}
// Find the owning pawn and its controller
APawn* OwningPawn = Cast<APawn>(Owner);
if (!OwningPawn)
{
OwningPawn = Owner->GetInstigator<APawn>();
}
if (!OwningPawn)
{
UE_LOG(LogMosisPointer, Warning, TEXT("Could not find owning pawn for input binding"));
return;
}
APlayerController* PC = Cast<APlayerController>(OwningPawn->GetController());
if (!PC)
{
UE_LOG(LogMosisPointer, Warning, TEXT("Could not find player controller for input binding"));
return;
}
// Get the Enhanced Input component from the pawn
UEnhancedInputComponent* EnhancedInput = Cast<UEnhancedInputComponent>(OwningPawn->InputComponent);
if (!EnhancedInput)
{
UE_LOG(LogMosisPointer, Warning, TEXT("Could not find Enhanced Input component on pawn"));
return;
}
// Bind the trigger action
EnhancedInput->BindAction(TriggerAction, ETriggerEvent::Triggered, this, &UMosisPointerComponent::OnTriggerAction);
EnhancedInput->BindAction(TriggerAction, ETriggerEvent::Completed, this, &UMosisPointerComponent::OnTriggerAction);
bInputBound = true;
UE_LOG(LogMosisPointer, Log, TEXT("Successfully bound trigger input action"));
}
void UMosisPointerComponent::OnTriggerAction(const FInputActionInstance& Instance)
{
// Triggered = pressed, Completed = released
bIsTriggerPressed = (Instance.GetTriggerEvent() == ETriggerEvent::Triggered);
}
void UMosisPointerComponent::SetTriggerPressed(bool bPressed)
{
bIsTriggerPressed = bPressed;
}
void UMosisPointerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Try to bind input if not yet bound and we have an action
if (TriggerAction && !bInputBound)
{
SetupInputBindings();
}
// Perform raycast to find phone
PerformRaycast();
// Update touch state and send events
UpdateTouchState();
// Draw debug visualization
if (bShowDebugRay)
{
DrawDebugRay();
}
}
void UMosisPointerComponent::PerformRaycast()
{
FVector Start = GetComponentLocation();
FVector Direction = GetForwardVector();
FVector End = Start + Direction * RayLength;
FHitResult Hit;
FCollisionQueryParams Params;
Params.AddIgnoredActor(GetOwner());
// Also ignore any parent actors
AActor* Parent = GetOwner() ? GetOwner()->GetAttachParentActor() : nullptr;
while (Parent)
{
Params.AddIgnoredActor(Parent);
Parent = Parent->GetAttachParentActor();
}
bool bHit = GetWorld()->LineTraceSingleByChannel(
Hit, Start, End, TraceChannel, Params);
if (bHit)
{
AMosisPhoneActor* HitPhone = Cast<AMosisPhoneActor>(Hit.GetActor());
if (HitPhone)
{
CurrentPhone = HitPhone;
CurrentHitLocation = Hit.ImpactPoint;
bIsOverPhone = true;
}
else
{
CurrentPhone = nullptr;
bIsOverPhone = false;
}
}
else
{
CurrentPhone = nullptr;
bIsOverPhone = false;
}
}
void UMosisPointerComponent::UpdateTouchState()
{
// Detect state transitions
bool bJustPressed = bIsTriggerPressed && !bWasTriggerPressed;
bool bJustReleased = !bIsTriggerPressed && bWasTriggerPressed;
if (bIsOverPhone && CurrentPhone.IsValid())
{
if (bJustPressed)
{
// Touch down
CurrentPhone->SendTouchAtWorldLocation(CurrentHitLocation, EMosisTouchType::Down);
LastTouchedPhone = CurrentPhone;
LastHitLocation = CurrentHitLocation;
UE_LOG(LogMosisPointer, Verbose, TEXT("Touch Down at (%.1f, %.1f, %.1f)"),
CurrentHitLocation.X, CurrentHitLocation.Y, CurrentHitLocation.Z);
}
else if (bIsTriggerPressed)
{
// Touch move (only if position changed significantly)
if (FVector::DistSquared(CurrentHitLocation, LastHitLocation) > 0.01f)
{
CurrentPhone->SendTouchAtWorldLocation(CurrentHitLocation, EMosisTouchType::Move);
LastHitLocation = CurrentHitLocation;
}
}
else if (bJustReleased)
{
// Touch up
CurrentPhone->SendTouchAtWorldLocation(CurrentHitLocation, EMosisTouchType::Up);
LastTouchedPhone = nullptr;
UE_LOG(LogMosisPointer, Verbose, TEXT("Touch Up at (%.1f, %.1f, %.1f)"),
CurrentHitLocation.X, CurrentHitLocation.Y, CurrentHitLocation.Z);
}
}
else if (bJustReleased && LastTouchedPhone.IsValid())
{
// Ray moved off phone while pressed, but trigger was just released
// Send up event to the last touched phone
LastTouchedPhone->SendTouchAtWorldLocation(LastHitLocation, EMosisTouchType::Up);
LastTouchedPhone = nullptr;
UE_LOG(LogMosisPointer, Verbose, TEXT("Touch Up (off-phone) at (%.1f, %.1f, %.1f)"),
LastHitLocation.X, LastHitLocation.Y, LastHitLocation.Z);
}
bWasTriggerPressed = bIsTriggerPressed;
}
void UMosisPointerComponent::DrawDebugRay() const
{
FVector Start = GetComponentLocation();
FVector Direction = GetForwardVector();
FVector End = bIsOverPhone ? CurrentHitLocation : (Start + Direction * RayLength);
FColor Color = bIsOverPhone ? DebugRayHitColor.ToFColor(true) : DebugRayColor.ToFColor(true);
DrawDebugLine(GetWorld(), Start, End, Color, false, -1.0f, 0, 1.0f);
if (bIsOverPhone)
{
// Draw hit point
DrawDebugSphere(GetWorld(), CurrentHitLocation, 2.0f, 8, Color, false, -1.0f, 0, 0.5f);
}
}
AMosisPhoneActor* UMosisPointerComponent::GetTargetPhone() const
{
return CurrentPhone.Get();
}
FVector UMosisPointerComponent::GetRayOrigin() const
{
return GetComponentLocation();
}
FVector UMosisPointerComponent::GetRayDirection() const
{
return GetForwardVector();
}

View File

@@ -0,0 +1,133 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/SceneComponent.h"
#include "MosisPhoneComponent.h"
#include "MosisPointerComponent.generated.h"
class AMosisPhoneActor;
class UInputAction;
struct FInputActionInstance;
/**
* UMosisPointerComponent - VR ray interaction component for Mosis phone.
*
* Attach this component as a child of a motion controller to enable
* VR pointer-based touch interaction with MosisPhoneActor.
*
* Usage:
* 1. Add as child of MotionControllerComponent
* 2. Set TriggerAction to your trigger input action (optional)
* 3. Touch events are sent automatically when pointing at a phone and triggering
*
* Manual control (without Enhanced Input):
* - Call SetTriggerPressed(true/false) from your input handling code
*/
UCLASS(ClassGroup=(Mosis), meta=(BlueprintSpawnableComponent))
class MOSISSDK_API UMosisPointerComponent : public USceneComponent
{
GENERATED_BODY()
public:
UMosisPointerComponent();
// Configuration
/** Maximum ray length for detecting phone actors (in cm) */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mosis|Pointer")
float RayLength = 500.0f;
/** Draw debug ray visualization */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mosis|Pointer")
bool bShowDebugRay = false;
/** Color of the debug ray when not hitting anything */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mosis|Pointer", meta=(EditCondition="bShowDebugRay"))
FLinearColor DebugRayColor = FLinearColor::Red;
/** Color of the debug ray when hitting a phone */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mosis|Pointer", meta=(EditCondition="bShowDebugRay"))
FLinearColor DebugRayHitColor = FLinearColor::Green;
/** Collision channel used for raycast */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mosis|Pointer")
TEnumAsByte<ECollisionChannel> TraceChannel = ECC_Visibility;
// Input
/**
* Enhanced Input Action for trigger press.
* If set, the component will automatically bind to this action.
* Leave null to use manual SetTriggerPressed() calls.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mosis|Input")
TObjectPtr<UInputAction> TriggerAction;
// State queries
/** Check if the pointer is currently pointing at a phone */
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
bool IsPointingAtPhone() const { return bIsOverPhone; }
/** Get the phone actor currently being pointed at (nullptr if none) */
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
AMosisPhoneActor* GetTargetPhone() const;
/** Get the world location where the ray hits the phone */
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
FVector GetHitLocation() const { return CurrentHitLocation; }
/** Get the current ray origin in world space */
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
FVector GetRayOrigin() const;
/** Get the current ray direction in world space */
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
FVector GetRayDirection() const;
/** Check if the trigger is currently pressed */
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
bool IsTriggerPressed() const { return bIsTriggerPressed; }
// Manual control
/**
* Manually set the trigger pressed state.
* Use this when not using Enhanced Input, or for custom input handling.
*/
UFUNCTION(BlueprintCallable, Category = "Mosis|Pointer")
void SetTriggerPressed(bool bPressed);
protected:
// UActorComponent interface
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
/** Perform raycast and update CurrentPhone/CurrentHitLocation */
void PerformRaycast();
/** Update touch state machine and send touch events */
void UpdateTouchState();
/** Draw debug visualization */
void DrawDebugRay() const;
/** Callback for Enhanced Input trigger action */
void OnTriggerAction(const FInputActionInstance& Instance);
/** Setup Enhanced Input bindings */
void SetupInputBindings();
// State
TWeakObjectPtr<AMosisPhoneActor> CurrentPhone;
TWeakObjectPtr<AMosisPhoneActor> LastTouchedPhone;
FVector CurrentHitLocation = FVector::ZeroVector;
FVector LastHitLocation = FVector::ZeroVector;
bool bIsTriggerPressed = false;
bool bWasTriggerPressed = false;
bool bIsOverPhone = false;
bool bInputBound = false;
};