diff --git a/Plugins/MosisSDK/MosisSDK.uplugin b/Plugins/MosisSDK/MosisSDK.uplugin index fc7a6d1..651d296 100644 --- a/Plugins/MosisSDK/MosisSDK.uplugin +++ b/Plugins/MosisSDK/MosisSDK.uplugin @@ -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 + } ] } \ No newline at end of file diff --git a/Plugins/MosisSDK/Source/MosisSDK/MosisSDK.Build.cs b/Plugins/MosisSDK/Source/MosisSDK/MosisSDK.Build.cs index 5b4b763..7984725 100644 --- a/Plugins/MosisSDK/Source/MosisSDK/MosisSDK.Build.cs +++ b/Plugins/MosisSDK/Source/MosisSDK/MosisSDK.Build.cs @@ -26,7 +26,9 @@ public class MosisSDK : ModuleRules "Slate", "SlateCore", "RenderCore", - "RHI" + "RHI", + "EnhancedInput", + "InputCore" } ); diff --git a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp index 815c983..d13c339 100644 --- a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp +++ b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPhoneActor.cpp @@ -17,6 +17,12 @@ AMosisPhoneActor::AMosisPhoneActor() PhoneMesh = CreateDefaultSubobject(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 PlaneMeshFinder( TEXT("/Engine/BasicShapes/Plane.Plane")); diff --git a/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPointerComponent.cpp b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPointerComponent.cpp new file mode 100644 index 0000000..6961d1d --- /dev/null +++ b/Plugins/MosisSDK/Source/MosisSDK/Private/MosisPointerComponent.cpp @@ -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(Owner); + if (!OwningPawn) + { + OwningPawn = Owner->GetInstigator(); + } + + if (!OwningPawn) + { + UE_LOG(LogMosisPointer, Warning, TEXT("Could not find owning pawn for input binding")); + return; + } + + APlayerController* PC = Cast(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(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(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(); +} diff --git a/Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h b/Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h new file mode 100644 index 0000000..4fff24c --- /dev/null +++ b/Plugins/MosisSDK/Source/MosisSDK/Public/MosisPointerComponent.h @@ -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 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 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 CurrentPhone; + TWeakObjectPtr LastTouchedPhone; + FVector CurrentHitLocation = FVector::ZeroVector; + FVector LastHitLocation = FVector::ZeroVector; + bool bIsTriggerPressed = false; + bool bWasTriggerPressed = false; + bool bIsOverPhone = false; + bool bInputBound = false; +};