#include "CAVEOverlay/CAVEOverlayController.h" #include "CoreMinimal.h" #include "EnhancedInputComponent.h" #include "IDisplayCluster.h" #include "MotionControllerComponent.h" #include "CAVEOverlay/DoorOverlayData.h" #include "Camera/CameraComponent.h" #include "Cluster/DisplayClusterClusterEvent.h" #include "Cluster/IDisplayClusterClusterManager.h" #include "Components/StaticMeshComponent.h" #include "Engine/CollisionProfile.h" #include "Logging/StructuredLog.h" #include "Materials/MaterialInstanceDynamic.h" #include "Utility/RWTHVRClusterUtilities.h" DEFINE_LOG_CATEGORY(LogCAVEOverlay); // Helper function to check if a string is contained within an array of strings, ignoring case. bool ContainsFString(const TArray<FString>& Array, const FString& Entry) { for (FString Current_Entry : Array) { if (Current_Entry.Equals(Entry, ESearchCase::IgnoreCase)) return true; } return false; } UStaticMeshComponent* ACAVEOverlayController::CreateMeshComponent(const FName& Name, USceneComponent* Parent) { UStaticMeshComponent* Result = CreateDefaultSubobject<UStaticMeshComponent>(Name); Result->SetupAttachment(Parent); Result->SetVisibility(false); Result->SetCollisionEnabled(ECollisionEnabled::NoCollision); return Result; } // Sets default values ACAVEOverlayController::ACAVEOverlayController() { // Set this actor to call Tick() every frame. PrimaryActorTick.bCanEverTick = true; bAllowTickBeforeBeginPlay = false; // Creation of sub-components Root = CreateDefaultSubobject<USceneComponent>("DefaultSceneRoot"); SetRootComponent(Root); Tape = CreateMeshComponent("Tape", Root); SignsStaticMeshComponents.Reserve(2); MotionControllers.Reserve(2); } void ACAVEOverlayController::CycleDoorType() { DoorCurrentMode = static_cast<EDoorMode>((DoorCurrentMode + 1) % DOOR_NUM_MODES); // Send out a cluster event to the whole cluster that the door mode has been changed if (auto* const Manager = IDisplayCluster::Get().GetClusterMgr()) { FDisplayClusterClusterEventJson cluster_event; cluster_event.Name = "CAVEOverlay Change Door to " + DoorModeNames[DoorCurrentMode]; cluster_event.Type = "DoorChange"; cluster_event.Category = "CAVEOverlay"; cluster_event.Parameters.Add("NewDoorState", FString::FromInt(DoorCurrentMode)); Manager->EmitClusterEventJson(cluster_event, true); } } void ACAVEOverlayController::HandleClusterEvent(const FDisplayClusterClusterEventJson& Event) { if (Event.Category.Equals("CAVEOverlay") && Event.Type.Equals("DoorChange") && Event.Parameters.Contains("NewDoorState")) { SetDoorMode(static_cast<EDoorMode>(FCString::Atoi(*Event.Parameters["NewDoorState"]))); } } void ACAVEOverlayController::SetDoorMode(const EDoorMode NewMode) { DoorCurrentMode = NewMode; switch (DoorCurrentMode) { case EDoorMode::DOOR_DEBUG: case EDoorMode::DOOR_PARTIALLY_OPEN: DoorCurrentOpeningWidthAbsolute = DoorOpeningWidthAbsolute; if (ScreenType == SCREEN_DOOR) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); if (ScreenType == SCREEN_DOOR_PARTIAL) Overlay->BlackBox->SetRenderScale(FVector2D(DoorOpeningWidthRelative, 1)); if (ScreenType == SCREEN_PRIMARY) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); Overlay->BlackBox->SetVisibility(ESlateVisibility::Visible); break; case EDoorMode::DOOR_OPEN: DoorCurrentOpeningWidthAbsolute = WallDistance * 2; if (ScreenType == SCREEN_DOOR) Overlay->BlackBox->SetRenderScale(FVector2D(1, 1)); if (ScreenType == SCREEN_DOOR_PARTIAL) Overlay->BlackBox->SetRenderScale(FVector2D(1, 1)); if (ScreenType == SCREEN_PRIMARY) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); Overlay->BlackBox->SetVisibility(ESlateVisibility::Visible); break; case EDoorMode::DOOR_CLOSED: DoorCurrentOpeningWidthAbsolute = 0; if (ScreenType == SCREEN_DOOR) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); if (ScreenType == SCREEN_DOOR_PARTIAL) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); if (ScreenType == SCREEN_PRIMARY) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); Overlay->BlackBox->SetVisibility(ESlateVisibility::Hidden); break; default:; } // On the secondary nodes that are not the door, hide the overlay completely // It might make more sense to just not add it there... if (ScreenType == SCREEN_NORMAL) Overlay->BlackBox->SetRenderScale(FVector2D(0, 1)); UE_LOGFMT(LogCAVEOverlay, Log, "Switched door state to {State}. New opening width is {Width}.", *DoorModeNames[DoorCurrentMode], DoorCurrentOpeningWidthAbsolute); // On the primary node, show which door mode is currently active. if (ScreenType == SCREEN_PRIMARY) { Overlay->CornerText->SetText(FText::FromString(DoorModeNames[DoorCurrentMode])); } } // Called when the game starts or when spawned void ACAVEOverlayController::BeginPlay() { Super::BeginPlay(); // Don't do anything if we're a dedicated server. We shouldn't even exist there. if (GetNetMode() == NM_DedicatedServer) return; // Currently, there is no support for multi-user systems in general, as we only depend on the local pawn. // In a MU setting, the relevant pawn isn't our local one, but the primary node's pawn. if (GetNetMode() != NM_Standalone) return; // If we're not in room-mounted mode, return as well if (!URWTHVRClusterUtilities::IsRoomMountedMode()) return; // This should return the respective client's local playercontroller or, if we're a listen server, our own PC. auto* PC = GetWorld() ? GetWorld()->GetFirstPlayerController() : nullptr; // it can happen that the PC is valid, but we have no player attached to it yet. // Check for this - however, we should work around it by somehow getting notified when that happens. // Not sure which place would be best... const bool bValidPC = PC && PC->GetLocalPlayer(); if (!bValidPC) return; // Input config if (URWTHVRClusterUtilities::IsPrimaryNode()) { if (CycleDoorTypeInputAction == nullptr) { UE_LOGFMT(LogCAVEOverlay, Error, "Input action and mapping not set in CaveOverlayController!"); return; } UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PC->InputComponent); Input->BindAction(CycleDoorTypeInputAction, ETriggerEvent::Triggered, this, &ACAVEOverlayController::CycleDoorType); } // Bind the cluster events that manage the door state. IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr(); if (ClusterManager && !ClusterEventListenerDelegate.IsBound()) { ClusterEventListenerDelegate = FOnClusterEventJsonListener::CreateUObject(this, &ACAVEOverlayController::HandleClusterEvent); ClusterManager->AddClusterEventJsonListener(ClusterEventListenerDelegate); } // Determine the screen-type for later usage if (IDisplayCluster::Get().GetClusterMgr()->GetNodeId().Equals(ScreenMain, ESearchCase::IgnoreCase)) { ScreenType = SCREEN_PRIMARY; } else if (ContainsFString(ScreensDoor, IDisplayCluster::Get().GetClusterMgr()->GetNodeId())) { ScreenType = SCREEN_DOOR; } else if (ContainsFString(ScreensDoorPartial, IDisplayCluster::Get().GetClusterMgr()->GetNodeId())) { ScreenType = SCREEN_DOOR_PARTIAL; } else { ScreenType = SCREEN_NORMAL; } // Create and add widget to local playercontroller. if (!OverlayClass) { UE_LOGFMT(LogCAVEOverlay, Error, "OverlayClass not set in CaveOverlayController!"); return; } Overlay = CreateWidget<UDoorOverlayData>(PC, OverlayClass); Overlay->AddToViewport(0); // Set the default door mode (partially open) SetDoorMode(DoorCurrentMode); // Set Text to "" until someone presses the key for the first time Overlay->CornerText->SetText(FText::FromString("")); if (!SignStaticMesh) { UE_LOGFMT(LogCAVEOverlay, Error, "SignStaticMesh not set in CaveOverlayController!"); return; } PC->OnPossessedPawnChanged.AddUniqueDynamic(this, &ACAVEOverlayController::UpdatePossessedPawn); // I think this breaks in multiplayer mode InitFromPawn(PC->GetPawn()); // Create dynamic material for tape TapeMaterialDynamic = Tape->CreateDynamicMaterialInstance(0); UE_LOGFMT(LogCAVEOverlay, Display, "CaveOverlay Initialization was successfull."); } void ACAVEOverlayController::UpdatePossessedPawn(APawn* OldPawn, APawn* NewPawn) { InitFromPawn(NewPawn); } void ACAVEOverlayController::InitFromPawn(const APawn* CurrentPawn) { // Clear previous properties. We could reuse the SMCs and MIDs, clearing them might be dangerous. // Too much in a hurry here to make this better. MotionControllers.Empty(); SignsStaticMeshComponents.Empty(); SignsMIDs.Empty(); // Get the pawn so we can have access to head and hand positions if (CurrentPawn) { PawnCamera = CurrentPawn->GetComponentByClass<UCameraComponent>(); auto FoundMotionControllers = CurrentPawn->K2_GetComponentsByClass(UMotionControllerComponent::StaticClass()); for (const auto FoundMotionController : FoundMotionControllers) { if (auto* MC = Cast<UMotionControllerComponent>(FoundMotionController); MC && MC->MotionSource != EName::None) { // Create new static mesh for them auto* SignStaticMeshComp = NewObject<UStaticMeshComponent>(this); SignStaticMeshComp->SetStaticMesh(SignStaticMesh); SignStaticMeshComp->SetupAttachment(RootComponent); SignStaticMeshComp->RegisterComponent(); AddInstanceComponent(SignStaticMeshComp); SignStaticMeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision); MotionControllers.Add(MC); SignsStaticMeshComponents.Add(SignStaticMeshComp); SignsMIDs.Add(SignStaticMeshComp->CreateAndSetMaterialInstanceDynamic(0)); } } if (MotionControllers.Num() != 2) { UE_LOGFMT(LogCAVEOverlay, Display, "Found unexpected number of MotionControllers on Pawn. Expected 2, found {Number}. This might " "lead to weird results", MotionControllers.Num()); } // we're good to go! bInitialized = true; } else { bInitialized = false; UE_LOGFMT(LogCAVEOverlay, Error, "No VirtualRealityPawn found which we could attach to!"); } } void ACAVEOverlayController::EndPlay(const EEndPlayReason::Type EndPlayReason) { IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr(); if (ClusterManager && ClusterEventListenerDelegate.IsBound()) { ClusterManager->RemoveClusterEventJsonListener(ClusterEventListenerDelegate); } Super::EndPlay(EndPlayReason); } double ACAVEOverlayController::CalculateOpacityFromPosition(const FVector& Position) const { // Calculate opacity value depending on how far we are from the walls. Further away == lower opacity, // fully opaque when WallFadeDistance away from the wall. Could just use a lerp here.. return FMath::Max( FMath::Clamp((FMath::Abs(Position.X) - (WallDistance - WallCloseDistance)) / WallFadeDistance, 0.0, 1.0), FMath::Clamp((FMath::Abs(Position.Y) - (WallDistance - WallCloseDistance)) / WallFadeDistance, 0.0, 1.0)); } bool ACAVEOverlayController::PositionInDoorOpening(const FVector& Position) const { // The position of the corner with 10cm of buffer. In negative direction because the door is in negative direction // of the cave const float CornerValue = -(WallDistance + 10); // Check whether our X position is within the door zone. This zone starts 10cm further away from the wall // than the WallCloseDistance, and ends 10cm outside of the wall (door). As the door is in negative X direction, // the signs need to be negated. const bool bXWithinDoor = FMath::IsWithinInclusive(Position.X, CornerValue, -(WallDistance - WallCloseDistance - 10)); // Checks whether our Y position is between the lower corner with some overlap and // the door corner (CornerValue + DoorCurrentOpeningWidthAbsolute) const bool bYWithinDoor = FMath::IsWithinInclusive(Position.Y, CornerValue, CornerValue + DoorCurrentOpeningWidthAbsolute); return bXWithinDoor && bYWithinDoor; } void ACAVEOverlayController::SetSignsForHand(UStaticMeshComponent* Sign, const FVector& HandPosition, UMaterialInstanceDynamic* HandMaterial) const { const bool bHandIsCloseToWall = FMath::IsWithinInclusive(HandPosition.GetAbsMax(), WallDistance - WallCloseDistance, WallDistance); if (bHandIsCloseToWall && !PositionInDoorOpening(HandPosition)) { Sign->SetVisibility(true); HandMaterial->SetScalarParameterValue("SignOpacity", CalculateOpacityFromPosition(HandPosition)); // Which wall are we closest to? This is the wall we project the sign onto const bool bXWallCloser = FMath::Abs(HandPosition.X) > FMath::Abs(HandPosition.Y); // Set the position towards the closest wall to the wall itself, keep the other positions const double X = bXWallCloser ? FMath::Sign(HandPosition.X) * WallDistance : HandPosition.X; const double Y = bXWallCloser ? HandPosition.Y : FMath::Sign(HandPosition.Y) * WallDistance; const double Z = HandPosition.Z; // Rotate the sign by 90° if we're on a side wall const auto Rot = bXWallCloser ? FRotator(0, 0, 0) : FRotator(0, 90, 0); const auto Pos = FVector(X, Y, Z); Sign->SetRelativeLocationAndRotation(Pos, Rot); } else { Sign->SetVisibility(false); } } void ACAVEOverlayController::Tick(float DeltaTime) { Super::Tick(DeltaTime); // If we're not yet initialized, do nothing. This shouldn't really happen as we only spawn on the cave anyway if (!bInitialized) { return; } // Head/Tape Logic if (PawnCamera) { const FVector HeadPosition = PawnCamera->GetRelativeTransform().GetLocation(); const bool bHeadIsCloseToWall = FMath::IsWithinInclusive(HeadPosition.GetAbsMax(), WallDistance - WallCloseDistance, WallDistance); // Only show the tape when close to a wall and not within the door opening if (bHeadIsCloseToWall && !PositionInDoorOpening(HeadPosition)) { Tape->SetVisibility(true); // Offset the tape in z direction to always be at head height Tape->SetRelativeLocation(HeadPosition * FVector(0, 0, 1)); TapeMaterialDynamic->SetScalarParameterValue("BarrierOpacity", CalculateOpacityFromPosition(HeadPosition)); if (FMath::IsWithin(FVector2D(HeadPosition).GetAbsMax(), WallDistance - WallWarningDistance, WallDistance)) { // in warning distance == red tape TapeMaterialDynamic->SetVectorParameterValue("StripeColor", FVector(1, 0, 0)); } else { // otherwise we're just yellow TapeMaterialDynamic->SetVectorParameterValue("StripeColor", FVector(1, 1, 0)); } } else { Tape->SetVisibility(false); } } // Hand Logic for (int i = 0; i < MotionControllers.Num(); i++) { if (MotionControllers[i] == nullptr) { UE_LOGFMT(LogCAVEOverlay, Error, "Motion Controller was nullptr, disabling overlay!"); bInitialized = false; return; } const FVector HandPosition = MotionControllers[i]->GetRelativeLocation(); // Set the position rotation, opacity, visibility of the hand warning signs. SetSignsForHand(SignsStaticMeshComponents[i], HandPosition, SignsMIDs[i]); } }