add MosisPointerComponent for VR ray interaction
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"Version": 1,
|
"Version": 1,
|
||||||
"VersionName": "1.0",
|
"VersionName": "1.0",
|
||||||
"FriendlyName": "MosisSDK",
|
"FriendlyName": "MosisSDK",
|
||||||
"Description": "",
|
"Description": "Mosis Virtual Phone SDK for VR",
|
||||||
"Category": "Other",
|
"Category": "Other",
|
||||||
"CreatedBy": "OmixLab LTD",
|
"CreatedBy": "OmixLab LTD",
|
||||||
"CreatedByURL": "",
|
"CreatedByURL": "",
|
||||||
@@ -20,5 +20,11 @@
|
|||||||
"Type": "Runtime",
|
"Type": "Runtime",
|
||||||
"LoadingPhase": "Default"
|
"LoadingPhase": "Default"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"Plugins": [
|
||||||
|
{
|
||||||
|
"Name": "EnhancedInput",
|
||||||
|
"Enabled": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,9 @@ public class MosisSDK : ModuleRules
|
|||||||
"Slate",
|
"Slate",
|
||||||
"SlateCore",
|
"SlateCore",
|
||||||
"RenderCore",
|
"RenderCore",
|
||||||
"RHI"
|
"RHI",
|
||||||
|
"EnhancedInput",
|
||||||
|
"InputCore"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ AMosisPhoneActor::AMosisPhoneActor()
|
|||||||
PhoneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PhoneMesh"));
|
PhoneMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("PhoneMesh"));
|
||||||
RootComponent = 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
|
// Load the default plane mesh
|
||||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneMeshFinder(
|
static ConstructorHelpers::FObjectFinder<UStaticMesh> PlaneMeshFinder(
|
||||||
TEXT("/Engine/BasicShapes/Plane.Plane"));
|
TEXT("/Engine/BasicShapes/Plane.Plane"));
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
133
Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h
Normal file
133
Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user