diff --git a/Content/Components/Grabbing/BP_GrabbableTestObject.uasset b/Content/Components/Grabbing/BP_GrabbableTestObject.uasset deleted file mode 100644 index 9ee3c8f375ef05e90f3c3f8300b6a13f94def5c6..0000000000000000000000000000000000000000 Binary files a/Content/Components/Grabbing/BP_GrabbableTestObject.uasset and /dev/null differ diff --git a/Content/Input/Default_IMC/IMC_General.uasset b/Content/Input/Default_IMC/IMC_General.uasset index eda268398333a091619597aacd0c1dcf9c968807..dabec22388c3ef3047d45c15e89d474731f1174e 100644 Binary files a/Content/Input/Default_IMC/IMC_General.uasset and b/Content/Input/Default_IMC/IMC_General.uasset differ diff --git a/Content/Input/Default_IMC/IMC_MovementRightHand.uasset b/Content/Input/Default_IMC/IMC_MovementRightHand.uasset index 44f4a88ed6719fb9bce0d3df925eb10fb58257d0..5058e44e0052ca7da16ffc411113ac08683698e9 100644 Binary files a/Content/Input/Default_IMC/IMC_MovementRightHand.uasset and b/Content/Input/Default_IMC/IMC_MovementRightHand.uasset differ diff --git a/Content/Pawn/BP_RWTHVRPawn_Default.uasset b/Content/Pawn/BP_RWTHVRPawn_Default.uasset index 7bcca686b189d765bd7476bf9ffb1ee4bd0ade71..9531919e30f6d97207deeb2364f67332592dafce 100644 Binary files a/Content/Pawn/BP_RWTHVRPawn_Default.uasset and b/Content/Pawn/BP_RWTHVRPawn_Default.uasset differ diff --git a/Content/TestContent/BP_GrabbableTestObject.uasset b/Content/TestContent/BP_GrabbableTestObject.uasset new file mode 100644 index 0000000000000000000000000000000000000000..380295d3caa15f50cc4cdc7928bc7a57bbffd9c3 Binary files /dev/null and b/Content/TestContent/BP_GrabbableTestObject.uasset differ diff --git a/Content/TestContent/TestMap.umap b/Content/TestContent/TestMap.umap new file mode 100644 index 0000000000000000000000000000000000000000..4beb781c7d3920ab556c0e0cd61231a3dfc2e383 Binary files /dev/null and b/Content/TestContent/TestMap.umap differ diff --git a/Content/TestMap.umap b/Content/TestMap.umap deleted file mode 100644 index d06f89d892794ba53dbaca472d6fb990297b0dae..0000000000000000000000000000000000000000 Binary files a/Content/TestMap.umap and /dev/null differ diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp index 0ff95d0c6fc823faef1016badc7fb8330beb9679..1537243da0b6c92a901decadce793bc8bd819bbb 100644 --- a/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp +++ b/Source/RWTHVRToolkit/Private/Interaction/Interactables/GrabBehavior.cpp @@ -3,8 +3,9 @@ #include "Interaction/Interactables/GrabBehavior.h" #include "Interaction/Interactables/InteractableComponent.h" -#include "Kismet/GameplayStatics.h" +#include "Logging/StructuredLog.h" #include "Serialization/JsonTypes.h" +#include "Utility/RWTHVRUtilities.h" UPrimitiveComponent* UGrabBehavior::GetFirstComponentSimulatingPhysics(const AActor* TargetActor) { @@ -29,34 +30,42 @@ UPrimitiveComponent* UGrabBehavior::GetHighestParentSimulatingPhysics(UPrimitive { return GetHighestParentSimulatingPhysics(Cast<UPrimitiveComponent>(Comp->GetAttachParent())); } - else - { - return Comp; - } + + return Comp; } void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction, const FInputActionValue& Value) { - const APawn* Player = UGameplayStatics::GetPlayerPawn(GetWorld(), 0); - - USceneComponent* Hand = Cast<USceneComponent>(TriggeredComponent->GetAttachParent()); + if (bObjectGrabbed) + { + return; + } + USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggeredComponent->GetAttachParent()); const FAttachmentTransformRules Rules = FAttachmentTransformRules(EAttachmentRule::KeepWorld, false); - MyPhysicsComponent = GetFirstComponentSimulatingPhysics(GetOwner()); - - if (MyPhysicsComponent) + if (MyPhysicsComponent = GetFirstComponentSimulatingPhysics(GetOwner()); MyPhysicsComponent != nullptr) { + bWasSimulatingPhysics = MyPhysicsComponent->IsSimulatingPhysics(); MyPhysicsComponent->SetSimulatePhysics(false); - MyPhysicsComponent->AttachToComponent(Hand, Rules); + bObjectGrabbed = MyPhysicsComponent->AttachToComponent(CurrentAttachParent, Rules); } else { - GetOwner()->GetRootComponent()->AttachToComponent(Hand, Rules); + bObjectGrabbed = GetOwner()->GetRootComponent()->AttachToComponent(CurrentAttachParent, Rules); } + if (!bObjectGrabbed) + { + UE_LOGFMT(Toolkit, Warning, "Grab failed! Cannot attach grabbed component to attach parent ({Parent})", + CurrentAttachParent->GetName()); + return; + } + // If we want to restrict other interactions while this component is grabbed we add the component + // that triggered the interaction to the whitelist of all interactables that are attached to the + // affected actor if (bBlockOtherInteractionsWhileGrabbed) { TArray<UInteractableComponent*> Interactables; @@ -66,21 +75,28 @@ void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UIn Interactable->RestrictInteractionToComponent(TriggeredComponent); } } + + OnGrabStartEvent.Broadcast(CurrentAttachParent, MyPhysicsComponent); } void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction, const FInputActionValue& Value) { - if (MyPhysicsComponent) - { - MyPhysicsComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); - MyPhysicsComponent->SetSimulatePhysics(true); - } - else + + USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggeredComponent->GetAttachParent()); + + // We try to release the attached component. If it is not succesful we log and return. Otherwise, we continue. + if (!TryRelease()) { - GetOwner()->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + UE_LOGFMT(Toolkit, Display, + "UGrabBehavior::OnActionEnd: TryRelease failed to release with AttachParent {Parent}", + CurrentAttachParent->GetName()); + return; } + OnGrabEndEvent.Broadcast(CurrentAttachParent, MyPhysicsComponent); + + // Release the interation restriction on all component if (bBlockOtherInteractionsWhileGrabbed) { TArray<UInteractableComponent*> Interactables; @@ -91,3 +107,23 @@ void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInpu } } } + +bool UGrabBehavior::TryRelease() +{ + if (!bObjectGrabbed) + { + return false; + } + + if (MyPhysicsComponent) + { + MyPhysicsComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + MyPhysicsComponent->SetSimulatePhysics(bWasSimulatingPhysics); + } + else + { + GetOwner()->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform); + } + bObjectGrabbed = false; + return true; +} diff --git a/Source/RWTHVRToolkit/Private/Interaction/Interactors/GrabComponent.cpp b/Source/RWTHVRToolkit/Private/Interaction/Interactors/GrabComponent.cpp index 5dcc020c60082f9055c810c05ad74f968bf78dbb..7429a615013b4fa35b77171cb82e58ff9e738075 100644 --- a/Source/RWTHVRToolkit/Private/Interaction/Interactors/GrabComponent.cpp +++ b/Source/RWTHVRToolkit/Private/Interaction/Interactors/GrabComponent.cpp @@ -8,6 +8,8 @@ #include "Interaction/Interactables/InteractionBitSet.h" #include "Kismet/GameplayStatics.h" +#include "Logging/StructuredLog.h" +#include "Utility/RWTHVRUtilities.h" // Sets default values for this component's properties UGrabComponent::UGrabComponent() @@ -15,7 +17,6 @@ UGrabComponent::UGrabComponent() // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these // features off to improve performance if you don't need them. PrimaryComponentTick.bCanEverTick = true; - // ... } @@ -23,10 +24,8 @@ void UGrabComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorC { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); - TArray<UInteractableComponent*> CurrentGrabCompsInRange; - TArray<AActor*> ActorsToIgnore; TArray<FHitResult> OutHits; const ETraceTypeQuery TraceType = UEngineTypes::ConvertToTraceType(ECollisionChannel::ECC_PhysicsBody); @@ -41,42 +40,42 @@ void UGrabComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorC AActor* HitActor = Hit.GetActor(); if (HitActor) { - UInteractableComponent* Grabbable = HitActor->FindComponentByClass<UInteractableComponent>(); + UInteractableComponent* Grabbable = SearchForInteractable(HitActor); if (Grabbable && Grabbable->HasInteractionTypeFlag(EInteractorType::Grab) && Grabbable->IsInteractable) { Grabbable->HitResult = Hit; - CurrentGrabCompsInRange.Add(Grabbable); + CurrentGrabCompsInRange.AddUnique(Grabbable); } } } - CurrentGrabbableInRange = CurrentGrabCompsInRange; + CurrentGrabBehavioursInRange = CurrentGrabCompsInRange; // Call hover start events on all components that were not in range before - for (UInteractableComponent* CurrentGrabbale : CurrentGrabCompsInRange) + for (UInteractableComponent* CurrentGrabbable : CurrentGrabCompsInRange) { - if (!PreviousGrabbablesInRange.Contains(CurrentGrabbale)) + if (!PreviousGrabBehavioursInRange.Contains(CurrentGrabbable)) { - PreviousGrabbablesInRange.Add(CurrentGrabbale); - CurrentGrabbale->HandleOnHoverStartEvents(this, EInteractorType::Grab); + PreviousGrabBehavioursInRange.AddUnique(CurrentGrabbable); + CurrentGrabbable->HandleOnHoverStartEvents(this, EInteractorType::Grab); } } TArray<UInteractableComponent*> ComponentsToRemove; // Call hover end events on all components that were previously in range, but not anymore - for (UInteractableComponent* PrevGrabbale : PreviousGrabbablesInRange) + for (UInteractableComponent* PrevGrabbale : PreviousGrabBehavioursInRange) { if (!CurrentGrabCompsInRange.Contains(PrevGrabbale)) { - ComponentsToRemove.Add(PrevGrabbale); + ComponentsToRemove.AddUnique(PrevGrabbale); PrevGrabbale->HandleOnHoverEndEvents(this, EInteractorType::Grab); } } for (UInteractableComponent* CompToRemove : ComponentsToRemove) { - PreviousGrabbablesInRange.Remove(CompToRemove); + PreviousGrabBehavioursInRange.Remove(CompToRemove); } } @@ -88,6 +87,9 @@ void UGrabComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent) if (!Pawn) return; + // Probably not the best place to add this. + ActorsToIgnore.AddUnique(GetOwner()); + UEnhancedInputComponent* EI = Cast<UEnhancedInputComponent>(Pawn->InputComponent); if (EI == nullptr) return; @@ -98,16 +100,79 @@ void UGrabComponent::SetupPlayerInput(UInputComponent* PlayerInputComponent) void UGrabComponent::OnBeginGrab(const FInputActionValue& Value) { - for (UInteractableComponent* Grabbale : CurrentGrabbableInRange) + const FVector GrabLocation = GetAttachParent()->GetComponentLocation(); + + if (CurrentGrabBehavioursInRange.IsEmpty()) + return; + + if (bOnlyGrabClosestActor) + { + auto MinElement = *Algo::MinElementBy( + CurrentGrabBehavioursInRange, + [&](auto Element) { return FVector(Element->GetOwner()->GetActorLocation() - GrabLocation).Size(); }); + MinElement->HandleOnActionStartEvents(this, GrabInputAction, Value, EInteractorType::Grab); + CurrentlyGrabbedComponents = {MinElement}; + } + else { - Grabbale->HandleOnActionStartEvents(this, GrabInputAction, Value, EInteractorType::Grab); + CurrentlyGrabbedComponents.Reserve(CurrentlyGrabbedComponents.Num() + CurrentGrabBehavioursInRange.Num()); + for (UInteractableComponent* Grabbable : CurrentGrabBehavioursInRange) + { + Grabbable->HandleOnActionStartEvents(this, GrabInputAction, Value, EInteractorType::Grab); + CurrentlyGrabbedComponents.Add(Grabbable); + } } } void UGrabComponent::OnEndGrab(const FInputActionValue& Value) { - for (UInteractableComponent* Grabbale : CurrentGrabbableInRange) + for (auto& Component : CurrentlyGrabbedComponents) { - Grabbale->HandleOnActionEndEvents(this, GrabInputAction, Value, EInteractorType::Grab); + if (Component.IsValid()) + { + Component->HandleOnActionEndEvents(this, GrabInputAction, Value, EInteractorType::Grab); + } + } +} + +UInteractableComponent* UGrabComponent::SearchForInteractable(AActor* HitActor) +{ + UInteractableComponent* Grabbable = nullptr; + if (!HitActor) + { + UE_LOGFMT(Toolkit, Warning, "UGrabComponent::SearchForInteractable: HitActor was nullptr, returning nullptr"); + return nullptr; + } + + if (HitActor->IsChildActor()) + { + // search for UInteractable upwards from hit geometry and return first one found + Grabbable = HitActor->FindComponentByClass<UInteractableComponent>(); + // if Grabbable is not valid search at parent + if (!Grabbable) + { + HitActor = HitActor->GetParentActor(); + if (HitActor) + { + bSearchAtParent = true; + return SearchForInteractable(HitActor); + } + } + } + else if (!HitActor->IsChildActor()) + { + Grabbable = HitActor->FindComponentByClass<UInteractableComponent>(); + } + + if (Grabbable) + { + // in the case, were we had to iterate up the hierarchy, check if we are allowed + // to grab the parent via child geometry + if (bSearchAtParent && !Grabbable->bAllowInteractionFromChildGeometry) + { + Grabbable = nullptr; + } } + bSearchAtParent = false; + return Grabbable; } diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h index 9978823ad96b612d920c6135886d333717a90d7a..d10affdff4aa25e2e4c22818219375580f342c8f 100644 --- a/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h +++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/GrabBehavior.h @@ -8,6 +8,12 @@ #include "GrabBehavior.generated.h" +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnGrabStart, USceneComponent*, NewAttachParent, UPrimitiveComponent*, + HeldComponent); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnGrabEnd, USceneComponent*, PreviousAttachParent, UPrimitiveComponent*, + HeldComponent); + UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) class RWTHVRTOOLKIT_API UGrabBehavior : public UActionBehaviour { @@ -22,6 +28,19 @@ public: virtual void OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction, const FInputActionValue& Value) override; + + /** + * Called after the object was successfully attached to the hand + */ + UPROPERTY(BlueprintAssignable) + FOnGrabStart OnGrabStartEvent; + + /** + * Called after the object was successfully detached from the hand + */ + UPROPERTY(BlueprintAssignable) + FOnGrabEnd OnGrabEndEvent; + UPrimitiveComponent* GetFirstComponentSimulatingPhysics(const AActor* TargetActor); // recursively goes up the hierarchy and returns the highest parent simulating physics @@ -29,4 +48,20 @@ public: UPROPERTY() UPrimitiveComponent* MyPhysicsComponent; + + UFUNCTION(BlueprintPure) + bool IsObjectGrabbed() const { return bObjectGrabbed; } + +private: + /** + * Try to detach the object from the hand. Keep this private for now as this does not broadcast the GrabEnd Event + * correctly. + * @return true if object was successfully detached. If detachment failed or if object was not grabbed before, + * return false. + */ + bool TryRelease(); + + bool bObjectGrabbed = false; + + bool bWasSimulatingPhysics; }; diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h index 8332a897126c46ccb62418b04da0808b6a1bd2b0..da85ad8b003814098fc21e112de423a097a80535 100644 --- a/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h +++ b/Source/RWTHVRToolkit/Public/Interaction/Interactables/InteractableComponent.h @@ -46,6 +46,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite) TArray<UActionBehaviour*> OnActionBehaviours; + /** + * If true, allow a grab to be triggered by the geometry of a child actor. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + bool bAllowInteractionFromChildGeometry = true; + UFUNCTION(BlueprintCallable) FORCEINLINE bool HasInteractionTypeFlag(EInteractorType type) { return type & InteractorFilter; } @@ -76,7 +82,7 @@ public: const FInputActionValue& Value, const EInteractorType Interactor); /** - * @brief If click and grab behaviors are not explicitly specified, load all existing ones + * @brief If hover and action behaviors are not explicitly specified, load all existing ones */ void InitDefaultBehaviourReferences(); @@ -91,7 +97,4 @@ public: TArray<USceneComponent*> AllowedComponents; bool IsComponentAllowed(USceneComponent* Component) const; - -private: - bool bInitOnce = true; }; diff --git a/Source/RWTHVRToolkit/Public/Interaction/Interactors/GrabComponent.h b/Source/RWTHVRToolkit/Public/Interaction/Interactors/GrabComponent.h index f2ef437a3c2ec1b11809c61b8716075ac8f673ea..4e18d911a41ad79fbcb4cf4d6d1e914d11f74b5f 100644 --- a/Source/RWTHVRToolkit/Public/Interaction/Interactors/GrabComponent.h +++ b/Source/RWTHVRToolkit/Public/Interaction/Interactors/GrabComponent.h @@ -31,6 +31,12 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Grabbing") bool bShowDebugTrace = false; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Grabbing") + bool bOnlyGrabClosestActor = false; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Grabbing") + TArray<AActor*> ActorsToIgnore; + virtual void SetupPlayerInput(UInputComponent* PlayerInputComponent) override; private: @@ -41,8 +47,14 @@ private: void OnEndGrab(const FInputActionValue& Value); UPROPERTY() - TArray<UInteractableComponent*> PreviousGrabbablesInRange; + TArray<UInteractableComponent*> PreviousGrabBehavioursInRange; UPROPERTY() - TArray<UInteractableComponent*> CurrentGrabbableInRange; + TArray<UInteractableComponent*> CurrentGrabBehavioursInRange; + + TArray<TWeakObjectPtr<UInteractableComponent>> CurrentlyGrabbedComponents; + + UInteractableComponent* SearchForInteractable(AActor* HitActor); + + bool bSearchAtParent = false; };