Compare commits

...

3 Commits

30 changed files with 783 additions and 122 deletions

View File

@@ -19,7 +19,7 @@ public static final XAPKFile[] xAPKS = {
new XAPKFile(
true, // true signifies a main file
"1", // the version of the APK that the file was uploaded against
97538444L // the length of the file in bytes
98985607L // the length of the file in bytes
)
};
};

File diff suppressed because one or more lines are too long

View File

@@ -85,7 +85,7 @@ bAllowClientSideNavigation=True
+ActiveGameNameRedirects=(OldGameName="/Script/TP_VirtualRealityBP",NewGameName="/Script/MosisUnreal")
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
bEnablePlugin=True
bEnablePlugin=False
bAllowNetworkConnection=True
SecurityToken=40AD4D57409C43F8A8ADC6BE2AFF4735
bIncludeInShipping=False

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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"));
@@ -50,6 +56,11 @@ void AMosisPhoneActor::BeginPlay()
{
// Default plane is 100x100 units, so bounds are -50 to 50 in X and Y
ScreenBoundsLocal = FVector4(-50.0f, -50.0f, 50.0f, 50.0f);
// Flip the mesh on Y axis to correct texture orientation
// (OpenGL renders with origin at bottom-left, Vulkan/UE5 expects top-left)
FVector CurrentScale = PhoneMesh->GetRelativeScale3D();
PhoneMesh->SetRelativeScale3D(FVector(CurrentScale.X, -CurrentScale.Y, CurrentScale.Z));
}
// Create dynamic material for the screen
@@ -137,5 +148,8 @@ bool AMosisPhoneActor::WorldToPhoneUV(FVector WorldLocation, FVector2D& OutUV) c
OutUV.X = (LocalLocation.X - MinX) / (MaxX - MinX);
OutUV.Y = (LocalLocation.Y - MinY) / (MaxY - MinY);
// Flip Y to match the flipped mesh scale (corrects for OpenGL vs Vulkan coordinate systems)
OutUV.Y = 1.0f - OutUV.Y;
return true;
}

View File

@@ -171,8 +171,8 @@ void UMosisPhoneComponent::OnBufferAvailable()
UE_LOG(LogMosisPhone, Log, TEXT("OnBufferAvailable: Created phone texture"));
}
// Import the hardware buffer
PhoneTexture->UpdateFromHardwareBuffer(Buffer);
// Import the hardware buffer for GPU-accelerated updates
PhoneTexture->ImportHardwareBuffer(Buffer);
// Update material if set
if (PhoneMaterial && PhoneTexture)
@@ -185,20 +185,19 @@ void UMosisPhoneComponent::OnBufferAvailable()
void UMosisPhoneComponent::OnFrameAvailable()
{
#if PLATFORM_ANDROID
if (!PhoneTexture)
if (!PhoneTexture || !PhoneTexture->HasImportedBuffer())
{
return;
}
// Notify texture that a new frame is available
// This schedules the render thread copy
PhoneTexture->NotifyFrameAvailable();
// Perform GPU-to-GPU copy from imported hardware buffer
PhoneTexture->CopyFromImportedBuffer();
// Log occasionally to avoid spam
static int32 FrameCount = 0;
if (++FrameCount % 60 == 0)
{
UE_LOG(LogMosisPhone, Log, TEXT("OnFrameAvailable: Frame %d"), FrameCount);
UE_LOG(LogMosisPhone, Log, TEXT("OnFrameAvailable: Frame %d (GPU copy)"), FrameCount);
}
#endif
}

View File

@@ -2,21 +2,40 @@
#include "MosisPhoneTexture.h"
#include "RenderingThread.h"
#include "RHICommandList.h"
#if PLATFORM_ANDROID
#include "IVulkanDynamicRHI.h"
#endif
DEFINE_LOG_CATEGORY_STATIC(LogMosisPhoneTexture, Log, All);
UMosisPhoneTexture::UMosisPhoneTexture()
{
// UTexture2DDynamic defaults are fine
SRGB = true;
Filter = TF_Bilinear;
}
UMosisPhoneTexture::~UMosisPhoneTexture()
{
#if PLATFORM_ANDROID
// Release imported texture RHI resource
if (ImportedTextureRHI.IsValid())
{
ImportedTextureRHI.SafeRelease();
}
CurrentBuffer = nullptr;
#endif
}
void UMosisPhoneTexture::Initialize(uint32 InWidth, uint32 InHeight)
{
UE_LOG(LogMosisPhoneTexture, Log, TEXT("Initialize: %dx%d"), InWidth, InHeight);
// Use UTexture2DDynamic's Init method
TextureWidth = InWidth;
TextureHeight = InHeight;
// Initialize the UTexture2DDynamic base - this creates our destination texture
Init(InWidth, InHeight, PF_R8G8B8A8, false);
bIsReady = true;
@@ -24,127 +43,118 @@ void UMosisPhoneTexture::Initialize(uint32 InWidth, uint32 InHeight)
#if PLATFORM_ANDROID
void UMosisPhoneTexture::UpdateFromHardwareBuffer(AHardwareBuffer* Buffer)
void UMosisPhoneTexture::ImportHardwareBuffer(AHardwareBuffer* Buffer)
{
if (!Buffer)
{
UE_LOG(LogMosisPhoneTexture, Warning, TEXT("UpdateFromHardwareBuffer: null buffer"));
UE_LOG(LogMosisPhoneTexture, Warning, TEXT("ImportHardwareBuffer: null buffer"));
return;
}
CurrentBuffer = Buffer;
// Get buffer dimensions
AHardwareBuffer_Desc Desc{};
AHardwareBuffer_describe(Buffer, &Desc);
UE_LOG(LogMosisPhoneTexture, Log, TEXT("UpdateFromHardwareBuffer: %dx%d"), Desc.width, Desc.height);
UE_LOG(LogMosisPhoneTexture, Log, TEXT("ImportHardwareBuffer: %dx%d, format=%d, usage=0x%x"),
Desc.width, Desc.height, Desc.format, Desc.usage);
// Lock buffer and read data
void* LockedData = nullptr;
int32 Result = AHardwareBuffer_lock(Buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &LockedData);
if (Result != 0 || !LockedData)
// Verify buffer has GPU sampled image usage (required for Vulkan import)
if ((Desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) == 0)
{
UE_LOG(LogMosisPhoneTexture, Error, TEXT("UpdateFromHardwareBuffer: Failed to lock buffer, result=%d"), Result);
UE_LOG(LogMosisPhoneTexture, Error,
TEXT("ImportHardwareBuffer: Buffer missing AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE flag"));
return;
}
uint8* PixelData = static_cast<uint8*>(LockedData);
// Copy data to a buffer for update (accounting for stride)
uint32 StridePixels = Desc.stride > 0 ? Desc.stride : Desc.width;
TArray<uint8> TextureData;
TextureData.SetNumUninitialized(Desc.width * Desc.height * 4);
for (uint32 y = 0; y < Desc.height; y++)
// Check if RHI is Vulkan
if (GDynamicRHI->GetInterfaceType() != ERHIInterfaceType::Vulkan)
{
FMemory::Memcpy(
TextureData.GetData() + y * Desc.width * 4,
PixelData + y * StridePixels * 4,
Desc.width * 4
);
UE_LOG(LogMosisPhoneTexture, Error,
TEXT("ImportHardwareBuffer: Vulkan RHI required for hardware buffer import"));
return;
}
AHardwareBuffer_unlock(Buffer, nullptr);
// Update the texture using UTexture2DDynamic's update regions
FTexture2DDynamicResource* TextureResource = static_cast<FTexture2DDynamicResource*>(GetResource());
if (TextureResource)
// Release old imported texture if any
if (ImportedTextureRHI.IsValid())
{
ENQUEUE_RENDER_COMMAND(UpdateMosisTexture)(
[TextureResource, Data = MoveTemp(TextureData), Width = Desc.width, Height = Desc.height](FRHICommandListImmediate& RHICmdList)
{
FUpdateTextureRegion2D Region(0, 0, 0, 0, Width, Height);
RHIUpdateTexture2D(
TextureResource->GetTexture2DRHI(),
0,
Region,
Width * 4,
Data.GetData()
);
}
);
ImportedTextureRHI.SafeRelease();
}
CurrentBuffer = Buffer;
// Import the hardware buffer using UE5's Vulkan RHI API
// This creates a VkImage backed by the AHardwareBuffer's shared memory (zero-copy)
IVulkanDynamicRHI* VulkanRHI = GetIVulkanDynamicRHI();
ImportedTextureRHI = VulkanRHI->RHICreateTexture2DFromAndroidHardwareBuffer(Buffer);
if (ImportedTextureRHI.IsValid())
{
UE_LOG(LogMosisPhoneTexture, Log,
TEXT("ImportHardwareBuffer: Successfully imported as Vulkan texture"));
// Initialize our destination texture if not done yet
if (!bIsReady)
{
Initialize(Desc.width, Desc.height);
}
}
else
{
UE_LOG(LogMosisPhoneTexture, Error,
TEXT("ImportHardwareBuffer: Failed to create Vulkan texture from hardware buffer"));
}
}
void UMosisPhoneTexture::NotifyFrameAvailable()
void UMosisPhoneTexture::CopyFromImportedBuffer()
{
if (!CurrentBuffer || !bIsReady)
if (!ImportedTextureRHI.IsValid() || !bIsReady)
{
return;
}
// Get buffer dimensions
AHardwareBuffer_Desc Desc{};
AHardwareBuffer_describe(CurrentBuffer, &Desc);
// Lock buffer and read data
void* LockedData = nullptr;
int32 Result = AHardwareBuffer_lock(CurrentBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, -1, nullptr, &LockedData);
if (Result != 0 || !LockedData)
{
return;
}
uint8* PixelData = static_cast<uint8*>(LockedData);
// Copy data to a buffer for update (accounting for stride)
uint32 StridePixels = Desc.stride > 0 ? Desc.stride : Desc.width;
TArray<uint8> TextureData;
TextureData.SetNumUninitialized(Desc.width * Desc.height * 4);
for (uint32 y = 0; y < Desc.height; y++)
{
FMemory::Memcpy(
TextureData.GetData() + y * Desc.width * 4,
PixelData + y * StridePixels * 4,
Desc.width * 4
);
}
AHardwareBuffer_unlock(CurrentBuffer, nullptr);
// Update the texture
// Get our destination texture resource
FTexture2DDynamicResource* TextureResource = static_cast<FTexture2DDynamicResource*>(GetResource());
if (TextureResource)
if (!TextureResource)
{
ENQUEUE_RENDER_COMMAND(UpdateMosisTextureFrame)(
[TextureResource, Data = MoveTemp(TextureData), Width = Desc.width, Height = Desc.height](FRHICommandListImmediate& RHICmdList)
return;
}
FTextureRHIRef DestTextureRHI = TextureResource->GetTexture2DRHI();
if (!DestTextureRHI.IsValid())
{
FUpdateTextureRegion2D Region(0, 0, 0, 0, Width, Height);
RHIUpdateTexture2D(
TextureResource->GetTexture2DRHI(),
0,
Region,
Width * 4,
Data.GetData()
);
return;
}
// Capture references for the render thread lambda
FTextureRHIRef SrcTexture = ImportedTextureRHI;
FTextureRHIRef DstTexture = DestTextureRHI;
uint32 Width = TextureWidth;
uint32 Height = TextureHeight;
// Enqueue GPU-to-GPU copy on render thread
ENQUEUE_RENDER_COMMAND(CopyMosisTexture)(
[SrcTexture, DstTexture, Width, Height](FRHICommandListImmediate& RHICmdList)
{
// Transition source to copy source state
RHICmdList.Transition(FRHITransitionInfo(SrcTexture, ERHIAccess::Unknown, ERHIAccess::CopySrc));
// Transition destination to copy dest state
RHICmdList.Transition(FRHITransitionInfo(DstTexture, ERHIAccess::Unknown, ERHIAccess::CopyDest));
// Perform GPU copy
FRHICopyTextureInfo CopyInfo;
CopyInfo.Size.X = Width;
CopyInfo.Size.Y = Height;
CopyInfo.Size.Z = 1;
CopyInfo.NumMips = 1;
CopyInfo.NumSlices = 1;
RHICmdList.CopyTexture(SrcTexture, DstTexture, CopyInfo);
// Transition destination back to shader resource for rendering
RHICmdList.Transition(FRHITransitionInfo(DstTexture, ERHIAccess::CopyDest, ERHIAccess::SRVMask));
}
);
}
}
#endif // PLATFORM_ANDROID

View File

@@ -14,6 +14,7 @@
* UMosisPhoneTexture - Dynamic texture that displays the Mosis phone screen.
*
* This texture is updated from an AHardwareBuffer received from MosisService.
* Uses GPU-to-GPU copy via Vulkan external memory import for optimal performance.
* Inherits from UTexture2DDynamic for proper material integration.
*/
UCLASS()
@@ -23,26 +24,27 @@ class UMosisPhoneTexture : public UTexture2DDynamic
public:
UMosisPhoneTexture();
virtual ~UMosisPhoneTexture();
/** Initialize the texture with dimensions */
void Initialize(uint32 InWidth, uint32 InHeight);
#if PLATFORM_ANDROID
/**
* Update the texture from a hardware buffer.
* Import a hardware buffer for GPU-accelerated texture updates.
* Creates a Vulkan image from the AHardwareBuffer for zero-copy import.
* @param Buffer The AHardwareBuffer from MosisService
*/
void UpdateFromHardwareBuffer(AHardwareBuffer* Buffer);
void ImportHardwareBuffer(AHardwareBuffer* Buffer);
/**
* Notify that a new frame is available.
* Copies buffer data to the texture.
* Perform GPU-to-GPU copy from imported buffer to this texture.
* Called each frame when new content is available.
*/
void NotifyFrameAvailable();
void CopyFromImportedBuffer();
/** Store the current hardware buffer reference */
void SetHardwareBuffer(AHardwareBuffer* Buffer) { CurrentBuffer = Buffer; }
AHardwareBuffer* GetHardwareBuffer() const { return CurrentBuffer; }
/** Check if the imported buffer is valid */
bool HasImportedBuffer() const { return ImportedTextureRHI.IsValid(); }
#endif
/** Check if the texture is ready for rendering */
@@ -50,10 +52,17 @@ public:
private:
#if PLATFORM_ANDROID
/** Current hardware buffer */
/** RHI texture imported from AHardwareBuffer (zero-copy Vulkan import) */
FTextureRHIRef ImportedTextureRHI;
/** Current hardware buffer reference */
AHardwareBuffer* CurrentBuffer = nullptr;
#endif
/** True when texture has been initialized */
bool bIsReady = false;
/** Texture dimensions */
uint32 TextureWidth = 0;
uint32 TextureHeight = 0;
};

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;
};

207
build-unreal.bat Normal file
View File

@@ -0,0 +1,207 @@
@echo off
setlocal enabledelayedexpansion
:: MosisUnreal Build and Deploy Script
:: Usage: build-unreal.bat [build|install|deploy|launch|clean]
:: build - Build Android APK only
:: install - Install APK to device only
:: deploy - Build and install (default)
:: launch - Launch app (starts MosisService first)
:: clean - Clean build artifacts
set ENGINE_PATH=D:\Epic\UE_5.5
set PROJECT_PATH=%~dp0MosisUnreal.uproject
set UAT_PATH=%ENGINE_PATH%\Engine\Build\BatchFiles\RunUAT.bat
set CONFIG=Development
:: Parse command line argument
set ACTION=%1
if "%ACTION%"=="" set ACTION=deploy
:: Validate engine path
if not exist "%UAT_PATH%" (
echo ERROR: Unreal Engine not found at %ENGINE_PATH%
echo Please update ENGINE_PATH in this script.
exit /b 1
)
:: Execute action
if "%ACTION%"=="build" goto :build
if "%ACTION%"=="install" goto :install
if "%ACTION%"=="deploy" goto :deploy
if "%ACTION%"=="launch" goto :launch
if "%ACTION%"=="clean" goto :clean
echo Unknown action: %ACTION%
echo Usage: build-unreal.bat [build^|install^|deploy^|launch^|clean]
exit /b 1
:build
echo.
echo ============================================
echo Building MosisUnreal for Android...
echo ============================================
echo.
call "%UAT_PATH%" BuildCookRun ^
-project="%PROJECT_PATH%" ^
-platform=Android ^
-clientconfig=%CONFIG% ^
-build -cook -stage -pak -package ^
-noP4 ^
-utf8output
if errorlevel 1 (
echo.
echo ERROR: Build failed!
exit /b 1
)
echo.
echo Build completed successfully!
echo APK: %~dp0Binaries\Android\MosisUnreal-arm64.apk
goto :eof
:install
echo.
echo ============================================
echo Installing MosisUnreal to device...
echo ============================================
echo.
:: Check for connected device
adb devices | findstr /r /c:"device$" >nul
if errorlevel 1 (
echo ERROR: No Android device connected!
echo Connect a device and enable USB debugging.
exit /b 1
)
:: Get device ID
for /f "tokens=1" %%d in ('adb devices ^| findstr /r /c:"device$"') do (
set DEVICE=%%d
goto :found_device
)
:found_device
echo Device: %DEVICE%
set APK_PATH=%~dp0Binaries\Android\MosisUnreal-arm64.apk
set OBB_PATH=%~dp0Binaries\Android\main.1.com.omixlab.MosisUnreal.obb
:: Check APK exists
if not exist "%APK_PATH%" (
echo ERROR: APK not found at %APK_PATH%
echo Run 'build-unreal.bat build' first.
exit /b 1
)
:: Install APK
echo Installing APK...
adb -s %DEVICE% install -r "%APK_PATH%"
if errorlevel 1 (
echo ERROR: APK installation failed!
exit /b 1
)
:: Install OBB if exists
if exist "%OBB_PATH%" (
echo.
echo Installing OBB...
:: Create temp directory
adb -s %DEVICE% shell "mkdir -p /data/local/tmp/obb/com.omixlab.MosisUnreal"
:: Push OBB (use forward slashes for adb)
set OBB_UNIX=%OBB_PATH:\=/%
adb -s %DEVICE% push "!OBB_UNIX!" /data/local/tmp/obb/com.omixlab.MosisUnreal/
:: Move to final location
adb -s %DEVICE% shell "rm -rf /sdcard/Android/obb/com.omixlab.MosisUnreal"
adb -s %DEVICE% shell "mv /data/local/tmp/obb/com.omixlab.MosisUnreal /sdcard/Android/obb/"
echo OBB installed.
) else (
echo No OBB file found, skipping.
)
echo.
echo Installation completed!
echo.
echo To launch: adb -s %DEVICE% shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity
goto :eof
:deploy
call :build
if errorlevel 1 exit /b 1
call :install
if errorlevel 1 exit /b 1
call :launch
goto :eof
:launch
echo.
echo ============================================
echo Launching MosisUnreal...
echo ============================================
echo.
:: Check for connected device
adb devices | findstr /r /c:"device$" >nul
if errorlevel 1 (
echo ERROR: No Android device connected!
exit /b 1
)
:: Get device ID
for /f "tokens=1" %%d in ('adb devices ^| findstr /r /c:"device$"') do (
set DEVICE=%%d
goto :found_device_launch
)
:found_device_launch
echo Device: %DEVICE%
:: Stop any running instances
echo Stopping existing instances...
adb -s %DEVICE% shell am force-stop com.omixlab.MosisUnreal >nul 2>&1
adb -s %DEVICE% shell am force-stop com.omixlab.mosis >nul 2>&1
timeout /t 1 /nobreak >nul
:: Start MosisService first
echo Starting MosisService...
adb -s %DEVICE% shell am start -n com.omixlab.mosis/.MainActivity
timeout /t 2 /nobreak >nul
:: Start MosisUnreal
echo Starting MosisUnreal...
adb -s %DEVICE% shell am start -n com.omixlab.MosisUnreal/com.epicgames.unreal.GameActivity
echo.
echo Launched! Monitoring logs (Ctrl+C to stop)...
echo.
adb -s %DEVICE% logcat -s MosisSDK MosisOS MosisTest UE
goto :eof
:clean
echo.
echo ============================================
echo Cleaning build artifacts...
echo ============================================
echo.
if exist "%~dp0Binaries" (
echo Removing Binaries...
rmdir /s /q "%~dp0Binaries"
)
if exist "%~dp0Intermediate\Build" (
echo Removing Intermediate\Build...
rmdir /s /q "%~dp0Intermediate\Build"
)
if exist "%~dp0Saved\StagedBuilds" (
echo Removing Saved\StagedBuilds...
rmdir /s /q "%~dp0Saved\StagedBuilds"
)
echo Clean completed!
goto :eof