Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 4.21
  • 4.22
  • 4.26
  • 4.27
  • 5.0
  • 5.3
  • 5.4
  • 5.5
  • IntenSelect5.3
  • _IntenSelect5.3
  • dev/5.3_downgrade
  • dev/5.4
  • dev/5.5
  • dev/5.6
  • feature/experimenttime_hack
  • UE5.3-2023.1-rc
  • UE5.3-2023.1-rc2
  • UE5.3-2023.1-rc3
  • UE5.4-2024.1
  • UE5.4-2024.1-rc1
20 results

Target

Select target project
  • vr-vis/VR-Group/unreal-development/plugins/rwth-vr-toolkit
  • daniel.rupp/rwth-vr-toolkit
2 results
Select Git revision
  • 4.21
  • 4.22
  • 4.26
  • 4.27
  • 5.0
  • 5.3
  • ReworkedToolkit
  • TempNav
  • dev/5.1
  • dev/5.2
  • dev/5.3
  • feature/make_interaction_ray_accesible
  • feature/scaleAndHeightTeleport
  • feature/scaleTeleport
  • fix/DisplayClusterTemplateCode
  • fix_5.3/DisplayClusterTemplateFix
16 results
Show changes
Showing
with 201 additions and 1075 deletions
...@@ -15,15 +15,6 @@ ...@@ -15,15 +15,6 @@
"Installed": false, "Installed": false,
"EnabledByDefault": true, "EnabledByDefault": true,
"Modules": [ "Modules": [
{
"Name": "RWTHVRCluster",
"Type": "Runtime",
"LoadingPhase": "Default",
"WhitelistPlatforms": [
"Win64",
"Linux"
]
},
{ {
"Name": "RWTHVRToolkit", "Name": "RWTHVRToolkit",
"Type": "Runtime", "Type": "Runtime",
...@@ -36,23 +27,10 @@ ...@@ -36,23 +27,10 @@
} }
], ],
"Plugins": [ "Plugins": [
{
"Name": "nDisplay",
"Enabled": true
},
{ {
"Name": "LiveLink", "Name": "LiveLink",
"Enabled": true "Enabled": true
}, },
{
"Name": "LiveLinkOvernDisplay",
"Enabled": true
},
{
"Name": "DTrackPlugin",
"Enabled": true,
"Optional": true
},
{ {
"Name": "EnhancedInput", "Name": "EnhancedInput",
"Enabled": true "Enabled": true
...@@ -62,9 +40,8 @@ ...@@ -62,9 +40,8 @@
"Enabled": true "Enabled": true
}, },
{ {
"Name": "Switchboard", "Name": "XRBase",
"Enabled": true, "Enabled": true
"Optional": true
} }
] ]
} }
#include "CAVEOverlay/CAVEOverlayController.h"
#include "CoreMinimal.h"
#include "EnhancedInputComponent.h"
#include "IDisplayCluster.h"
#include "MotionControllerComponent.h"
#include "Camera/CameraComponent.h"
#include "CAVEOverlay/DoorOverlayData.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 "Pawn/RWTHVRPawn.h"
#include "Utility/RWTHVRUtilities.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->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
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);
SignRightHand = CreateMeshComponent("SignRightHand", Root);
SignLeftHand = CreateMeshComponent("SignLeftHand", Root);
}
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;
// 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 || !URWTHVRUtilities::IsRoomMountedMode())
return;
// Input config
if (URWTHVRUtilities::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(""));
// Get the pawn so we can have access to head and hand positions
VRPawn = Cast<ARWTHVRPawn>(PC->GetPawnOrSpectator());
if (VRPawn)
{
// we're good to go!
bInitialized = true;
}
else
{
UE_LOGFMT(LogCAVEOverlay, Error, "No VirtualRealityPawn found which we could attach to!");
}
// Create dynamic materials at runtime
TapeMaterialDynamic = Tape->CreateDynamicMaterialInstance(0);
RightSignMaterialDynamic = SignRightHand->CreateDynamicMaterialInstance(0);
LeftSignMaterialDynamic = SignLeftHand->CreateDynamicMaterialInstance(0);
UE_LOGFMT(LogCAVEOverlay, Display, "CaveOverlay Initialization was successfull.");
}
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
const FVector HeadPosition = VRPawn->HeadCameraComponent->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
const FVector RightPosition = VRPawn->RightHand->GetRelativeTransform().GetLocation();
const FVector LeftPosition = VRPawn->LeftHand->GetRelativeTransform().GetLocation();
// Set the position rotation, opacity, visibility of the hand warning signs.
SetSignsForHand(SignRightHand, RightPosition, RightSignMaterialDynamic);
SetSignsForHand(SignLeftHand, LeftPosition, LeftSignMaterialDynamic);
}
// Fill out your copyright notice in the Description page of Project Settings.
#include "CaveSetup.h"
#include "Logging/StructuredLog.h"
#include "Utility/RWTHVRUtilities.h"
// Sets default values
ACaveSetup::ACaveSetup()
{
PrimaryActorTick.bCanEverTick = false;
SetActorEnableCollision(false);
// Actor needs to replicate, as it is attached to the pawn on the server.
bReplicates = true;
}
// Called when the game starts or when spawned
void ACaveSetup::BeginPlay()
{
Super::BeginPlay();
if (!URWTHVRUtilities::IsRoomMountedMode())
{
return;
}
// Spawn all actors that are set in the blueprint asset.
for (const auto ActorClass : ActorsToSpawnOnCave)
{
if (const auto World = GetWorld())
{
const auto Actor = World->SpawnActor(ActorClass);
Actor->AttachToActor(this, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
UE_LOGFMT(LogTemp, Display, "CaveSetup: Spawned Actor {Actor} on the Cave and attached it.",
Actor->GetName());
}
}
// Apply the DTrack LiveLink Preset. Only do this if we are the primaryNode
if (URWTHVRUtilities::IsPrimaryNode())
{
if (LiveLinkPresetToApplyOnCave && LiveLinkPresetToApplyOnCave->IsValidLowLevelFast())
{
LiveLinkPresetToApplyOnCave->ApplyToClientLatent();
UE_LOGFMT(LogTemp, Display, "CaveSetup: Applied LiveLinkPreset {Preset} to Client.",
LiveLinkPresetToApplyOnCave->GetName());
}
}
}
#include "ClusterConsole/ClusterConsole.h"
#include "IDisplayCluster.h"
#include "Cluster/DisplayClusterClusterEvent.h"
void FClusterConsole::Register()
{
/* Registering console command */
ClusterConsoleCommand = IConsoleManager::Get().RegisterConsoleCommand(
TEXT("ClusterExecute"),
TEXT("<Your Command> - Execute commands on every node of the nDisplay cluster by prepending ClusterExecute"),
FConsoleCommandWithArgsDelegate::CreateLambda(
[](const TArray<FString>& Args)
{
if (IDisplayCluster::Get().GetClusterMgr() == nullptr || Args.Num() == 0)
return;
/* Emitting cluster event */
FDisplayClusterClusterEventJson ClusterEvent;
ClusterEvent.Name = "ClusterExecute " + Args[0];
ClusterEvent.Type = Args[0];
ClusterEvent.Category = "NDisplayClusterExecute";
ClusterEvent.Parameters.Add("Command", FString::Join(Args, TEXT(" ")));
IDisplayCluster::Get().GetClusterMgr()->EmitClusterEventJson(ClusterEvent, false);
}));
/* Register cluster event handling */
const IDisplayCluster* DisplayCluster = FModuleManager::LoadModulePtr<IDisplayCluster>(IDisplayCluster::ModuleName);
if (DisplayCluster && !ClusterEventListenerDelegate.IsBound())
{
ClusterEventListenerDelegate = FOnClusterEventJsonListener::CreateLambda(
[](const FDisplayClusterClusterEventJson& Event)
{
/* Actual handling */
if (Event.Category.Equals("NDisplayClusterExecute") && Event.Parameters.Contains("Command") && GEngine)
{
GEngine->Exec(GEngine->GetCurrentPlayWorld(), *Event.Parameters["Command"]);
}
});
DisplayCluster->GetClusterMgr()->AddClusterEventJsonListener(ClusterEventListenerDelegate);
}
}
void FClusterConsole::Unregister() const
{
IConsoleManager::Get().UnregisterConsoleObject(ClusterConsoleCommand);
IDisplayCluster::Get().GetClusterMgr()->RemoveClusterEventJsonListener(ClusterEventListenerDelegate);
}
#include "RWTHVRCluster.h"
#define LOCTEXT_NAMESPACE "FRWTHVRClusterModule"
void FRWTHVRClusterModule::StartupModule() { ClusterConsole.Register(); }
void FRWTHVRClusterModule::ShutdownModule() { ClusterConsole.Unregister(); }
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FRWTHVRClusterModule, RWTHVRCluster)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CAVEOverlay/DoorOverlayData.h"
#include "Cluster/IDisplayClusterClusterManager.h"
#include "Pawn/RWTHVRPawn.h"
#include "CAVEOverlayController.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogCAVEOverlay, Log, All);
/**
* Actor which controls the cave overlay. The overlay displays a warning tape around the cave
* when the user moves their head too close to the wall, and a warning sign when the hands are
* too close.
*/
UCLASS()
class RWTHVRCLUSTER_API ACAVEOverlayController : public AActor
{
GENERATED_BODY()
public:
ACAVEOverlayController();
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
// Types of cave screens defined in the cluster config.
enum EScreen_Type
{
// the primary node screen
SCREEN_PRIMARY,
// any secondary node screen
SCREEN_NORMAL,
// the screens that cover the partially opened door
SCREEN_DOOR_PARTIAL,
// additional screens that cover the door
SCREEN_DOOR
};
// which screen type this node is running on
EScreen_Type ScreenType = SCREEN_NORMAL;
// which additional node names define the screens that cover the door
const TArray<FString> ScreensDoor = {"node_bul_left_eye", "node_bul_right_eye", "node_bll_left_eye",
"node_bll_right_eye"};
// which node names define the screens that cover the partial door
const TArray<FString> ScreensDoorPartial = {"node_bur_left_eye", "node_bur_right_eye", "node_blr_left_eye",
"node_blr_right_eye"};
const FString ScreenMain = "node_main";
// Door Mode
enum EDoorMode
{
DOOR_PARTIALLY_OPEN = 0,
DOOR_OPEN = 1,
DOOR_CLOSED = 2,
DOOR_DEBUG = 3,
DOOR_NUM_MODES = 4
};
const FString DoorModeNames[DOOR_NUM_MODES] = {"Partially Open", "Open", "Closed", "Debug"};
EDoorMode DoorCurrentMode = DOOR_PARTIALLY_OPEN;
const float DoorOpeningWidthRelative = 0.522; //%, used for the overlay width on the screen
const float DoorOpeningWidthAbsolute = 165; // cm, used for the non tape part at the door
const float WallDistance = 262.5; // cm, distance from center to a wall, *2 = wall width
const float WallCloseDistance = 75; // cm, the distance considered to be too close to the walls
const float WallFadeDistance = 35; // cm, the distance over which the tape is faded
const float WallWarningDistance = 40; // cm, distance on which the tape turns red, measured from wall
float DoorCurrentOpeningWidthAbsolute = 0;
// Helper function to create a mesh component in the constructor
UStaticMeshComponent* CreateMeshComponent(const FName& Name, USceneComponent* Parent);
// Calculates opacity value used for the dynamic materials of the tape and sign. The closer the more opaque.
double CalculateOpacityFromPosition(const FVector& Position) const;
// Check whether the given position is within the door area of the (partially) open door.
bool PositionInDoorOpening(const FVector& Position) const;
// Sets the position, orientation and opacity/visibility of the Sign according to the HandPosition.
void SetSignsForHand(UStaticMeshComponent* Sign, const FVector& HandPosition,
UMaterialInstanceDynamic* HandMaterial) const;
// Only calculate positions and material values when we're fully initialized.
bool bInitialized = false;
// Reference to the currently active pawn that we're tracking positions of.
UPROPERTY()
ARWTHVRPawn* VRPawn;
// Cluster Events
FOnClusterEventJsonListener ClusterEventListenerDelegate;
void HandleClusterEvent(const FDisplayClusterClusterEventJson& Event);
public:
virtual void Tick(float DeltaTime) override;
// Change door mode manually between open, partially open and closed.
void CycleDoorType();
void SetDoorMode(EDoorMode M);
// Root component
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "CAVEOverlay", meta = (AllowPrivateAccess = "true"))
USceneComponent* Root;
// Tape Static Mesh component. Reference to static mesh needs to be set in the corresponding BP.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "CAVEOverlay", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* Tape;
// Right Hand Sign Static Mesh component. Reference to static mesh needs to be set in the corresponding BP.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "CAVEOverlay", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* SignRightHand;
// Left Hand Sign Static Mesh component. Reference to static mesh needs to be set in the corresponding BP.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "CAVEOverlay", meta = (AllowPrivateAccess = "true"))
UStaticMeshComponent* SignLeftHand;
// UI Overlay
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "CAVEOverlay")
TSubclassOf<UDoorOverlayData> OverlayClass;
// UI Overlay
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "CAVEOverlay")
UInputAction* CycleDoorTypeInputAction;
UPROPERTY()
UDoorOverlayData* Overlay;
// Dynamic Materials to control opacity
UPROPERTY()
UMaterialInstanceDynamic* TapeMaterialDynamic;
UPROPERTY()
UMaterialInstanceDynamic* RightSignMaterialDynamic;
UPROPERTY()
UMaterialInstanceDynamic* LeftSignMaterialDynamic;
};
#pragma once
#include "CoreMinimal.h"
#include "Engine/DeveloperSettings.h"
#include "CAVEOverlaySettings.generated.h"
UENUM(BlueprintType)
enum DefaultActivationType
{
DefaultActivationType_OFF UMETA(DisplayName = "Off by default"),
DefaultActivationType_ON UMETA(DisplayName = "On by default")
};
UCLASS(config = Game, defaultconfig, meta = (DisplayName = "CAVE Overlay"))
class RWTHVRCLUSTER_API UCAVEOverlaySettings : public UDeveloperSettings
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, config, Category = "General", meta = (DisplayName = "Default Activation Type"))
TEnumAsByte<DefaultActivationType> DefaultActivationType = DefaultActivationType_ON;
UPROPERTY(EditAnywhere, config, Category = Maps, meta = (AllowedClasses = "/Script/Engine.World"))
TArray<FSoftObjectPath> ExcludedMaps;
};
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/TextBlock.h"
#include "Components/Border.h"
#include "DoorOverlayData.generated.h"
/**
* Used as a parent-class in the overlay widget. Like this we can access the UMG properties in C++
*/
UCLASS()
class RWTHVRCLUSTER_API UDoorOverlayData : public UUserWidget
{
GENERATED_BODY()
public:
// These declarations are magically bound to the UMG blueprints elements,
// if they are named the same
UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
UTextBlock* CornerText;
UPROPERTY(BlueprintReadWrite, meta = (BindWidget))
UBorder* BlackBox;
};
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "LiveLinkPreset.h"
#include "GameFramework/Actor.h"
#include "CaveSetup.generated.h"
/**
* Simple Actor that needs to be added to the level which spawns Cave-related actors
* such as the CaveOverlay.
* It attaches itself to the Primary Node's Pawn and then replicates on the server.
*/
UCLASS(hideCategories = (Rendering, Input, Actor, Base, Collision, Shape, Physics, HLOD))
class RWTHVRCLUSTER_API ACaveSetup : public AActor
{
GENERATED_BODY()
public:
ACaveSetup();
UPROPERTY(EditAnywhere)
TArray<UClass*> ActorsToSpawnOnCave;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
ULiveLinkPreset* LiveLinkPresetToApplyOnCave;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
};
#pragma once
#include "CoreMinimal.h"
#include "HAL/IConsoleManager.h"
#include "Cluster/IDisplayClusterClusterManager.h"
#include "ClusterConsole.generated.h"
/**
* The ClusterConsole provides the console command "ClusterExecute"
* The code catches your command, broadcasts it to every nDisplay node and executes it everywhere
*
* This class has to be registered and unregistered. This can easily be done in every StartupModule/ShutdownModule
* functions.
*/
USTRUCT()
struct RWTHVRCLUSTER_API FClusterConsole
{
GENERATED_BODY()
private:
/* Used for ClusterExecute console command */
IConsoleCommand* ClusterConsoleCommand = nullptr;
FOnClusterEventJsonListener ClusterEventListenerDelegate;
public:
void Register();
void Unregister() const;
};
#pragma once
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "type_traits"
template <typename ParameterType, typename... RemainingParameterTypes>
inline void SerializeParameters(FMemoryWriter* MemoryWriter, ParameterType&& Parameter,
RemainingParameterTypes&&... RemainingParameters)
{
using NonConstType = typename std::remove_cv_t<typename TRemoveReference<ParameterType>::Type>;
// const_cast because the same operator (<<) is used for reading and writing
(*MemoryWriter) << const_cast<NonConstType&>(Parameter);
SerializeParameters(MemoryWriter, Forward<RemainingParameterTypes>(RemainingParameters)...);
}
inline void SerializeParameters(FMemoryWriter* MemoryWriter) {}
// This is a wrapper function to recursively fill the argument tuple. This overload is only used if the index indicating
// the currently handled attribute is less than the number of total attributes. I.e., if the attribute index is valid.
template <int CurrentIndex, typename... ArgTypes>
inline typename TEnableIf<(CurrentIndex < sizeof...(ArgTypes))>::Type
FillArgumentTuple(FMemoryReader* MemoryReader, TTuple<ArgTypes...>* ArgumentTuple)
{
// Read the "<<" as ">>" operator here. FArchive uses the same for both and decides based on an internal type on
// what to do. So this statement parses the bytes that were passed into reader and puts the parsed object into the
// tuple at index CurrentIndex.
(*MemoryReader) << ArgumentTuple->template Get<CurrentIndex>();
// Recursive call for the remaining attributes.
FillArgumentTuple<CurrentIndex + 1>(MemoryReader, Forward<TTuple<ArgTypes...>*>(ArgumentTuple));
}
// The overload that is called if we are "passed the end" of attributes.
template <int CurrentIndex, typename... ArgTypes>
inline typename TEnableIf<(CurrentIndex >= sizeof...(ArgTypes))>::Type
FillArgumentTuple(FMemoryReader* MemoryReader, TTuple<ArgTypes...>* ArgumentTuple)
{
}
template <typename RetType, typename... ArgTypes>
inline RetType CallDelegateWithParameterMap(const TDelegate<RetType, ArgTypes...>& Delegate,
const TMap<FString, FString>& Parameters)
{
// Create a tuple that holds all arguments. This assumes that all argument types are default constructible. However,
// all types that overload the FArchive "<<" operator probably are.
TTuple<typename std::remove_cv_t<typename TRemoveReference<ArgTypes>::Type>...> ArgumentTuple;
// This call will parse the string map and fill all values in the tuple appropriately.
FillArgumentTuple<0>(&ArgumentTuple, Parameters);
// The lambda function is only necessary because delegates do not overload the ()-operator but use the Execute()
// method instead. So, the lambda acts as a wrapper.
return ArgumentTuple.ApplyBefore([Delegate](ArgTypes&&... Arguments)
{ Delegate.Execute(Forward<ArgTypes>(Arguments)...); });
}
#pragma once
#include "IDisplayCluster.h"
#include "Cluster/IDisplayClusterClusterManager.h"
#include "Cluster/DisplayClusterClusterEvent.h"
#include "DisplayClusterEventParameterHelper.h"
#include "Templates/IsInvocable.h"
static constexpr int32 CLUSTER_EVENT_WRAPPER_EVENT_ID = 1337420;
template <typename MemberFunctionType, MemberFunctionType MemberFunction>
class ClusterEventWrapperEvent;
template <typename ObjectType, typename ReturnType, typename... ArgTypes,
ReturnType (ObjectType::*MemberFunction)(ArgTypes...)>
class ClusterEventWrapperEvent<ReturnType (ObjectType::*)(ArgTypes...), MemberFunction>
{
static_assert(TIsDerivedFrom<ObjectType, UObject>::IsDerived, "Object needs to derive from UObject");
public:
using MemberFunctionType = decltype(MemberFunction);
ClusterEventWrapperEvent(const TCHAR* MethodName) : MethodName{MethodName} {}
void Attach(ObjectType* NewObject)
{
checkf(Object == nullptr, TEXT("The event is already attached."));
Object = NewObject;
ObjectId = Object->GetUniqueID();
EDisplayClusterOperationMode OperationMode = IDisplayCluster::Get().GetOperationMode();
if (OperationMode == EDisplayClusterOperationMode::Cluster)
{
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
check(ClusterManager != nullptr);
check(!ClusterEventListenerDelegate.IsBound());
ClusterEventListenerDelegate = FOnClusterEventBinaryListener::CreateLambda(
[this](const FDisplayClusterClusterEventBinary& Event)
{
if (Event.EventId != CLUSTER_EVENT_WRAPPER_EVENT_ID)
{
return;
}
FMemoryReader MemoryReader(Event.EventData);
uint32 EventObjectId;
// This reads the value!
MemoryReader << EventObjectId;
if (EventObjectId != ObjectId)
{
// Event does not belong to this object.
return;
}
FString EventMethodName;
// This reads the value!
MemoryReader << EventMethodName;
if (EventMethodName != MethodName)
{
// This event does not belong to this method.
return;
}
// Create a tuple that holds all arguments. This assumes that all
// argument types are default constructible. However, all
// types that overload the FArchive "<<" operator probably are.
TTuple<typename std::remove_cv_t<typename TRemoveReference<ArgTypes>::Type>...> ArgumentTuple;
// This call will deserialze the values and fill all values in the tuple appropriately.
FillArgumentTuple<0>(&MemoryReader, &ArgumentTuple);
ArgumentTuple.ApplyBefore([this](const ArgTypes&... Arguments)
{ (Object->*MemberFunction)(Forward<const ArgTypes&>(Arguments)...); });
});
ClusterManager->AddClusterEventBinaryListener(ClusterEventListenerDelegate);
}
}
void Detach()
{
checkf(Object != nullptr, TEXT("The event was never attached."));
EDisplayClusterOperationMode OperationMode = IDisplayCluster::Get().GetOperationMode();
if (OperationMode == EDisplayClusterOperationMode::Cluster)
{
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
check(ClusterManager != nullptr);
// check(ClusterEventListenerDelegate.IsBound());
ClusterManager->RemoveClusterEventBinaryListener(ClusterEventListenerDelegate);
}
}
void Send(ArgTypes... Arguments)
{
checkf(Object != nullptr, TEXT("The event was not attached."));
EDisplayClusterOperationMode OperationMode = IDisplayCluster::Get().GetOperationMode();
if (OperationMode != EDisplayClusterOperationMode::Cluster)
{
(Object->*MemberFunction)(Forward<ArgTypes>(Arguments)...);
}
else
{
IDisplayClusterClusterManager* ClusterManager = IDisplayCluster::Get().GetClusterMgr();
check(ClusterManager != nullptr);
FDisplayClusterClusterEventBinary ClusterEvent;
ClusterEvent.EventId = CLUSTER_EVENT_WRAPPER_EVENT_ID;
ClusterEvent.bShouldDiscardOnRepeat = false;
FMemoryWriter MemoryWriter(ClusterEvent.EventData);
MemoryWriter << ObjectId;
MemoryWriter << const_cast<FString&>(MethodName);
SerializeParameters(&MemoryWriter, Forward<ArgTypes>(Arguments)...);
ClusterManager->EmitClusterEventBinary(ClusterEvent, true);
}
}
private:
const FString MethodName;
uint32 ObjectId;
ObjectType* Object = nullptr;
FOnClusterEventBinaryListener ClusterEventListenerDelegate;
};
#define DCEW_STRINGIFY(x) #x
#define DCEW_TOSTRING(x) DCEW_STRINGIFY(x)
#define DECLARE_DISPLAY_CLUSTER_EVENT(OwningType, MethodIdentifier) \
ClusterEventWrapperEvent<decltype(&OwningType::MethodIdentifier), &OwningType::MethodIdentifier> \
MethodIdentifier##Event \
{ \
TEXT(DCEW_TOSTRING(OwningType) DCEW_TOSTRING(MethodIdentifier)) \
}
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
#include "ClusterConsole/ClusterConsole.h"
class FRWTHVRClusterModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
FClusterConsole ClusterConsole;
};
using UnrealBuildTool;
public class RWTHVRCluster : ModuleRules
{
public RWTHVRCluster(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] { }
);
PrivateIncludePaths.AddRange(
new string[] { }
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"DeveloperSettings",
"EnhancedInput",
"UMG",
"Slate",
"SlateCore",
"RWTHVRToolkit",
"LiveLink"
}
);
if (IsPluginEnabledForTarget("nDisplay", base.Target))
{
PublicDependencyModuleNames.AddRange(
new string[]
{
"DisplayCluster"
}
);
}
if (IsPluginEnabledForTarget("DTrackPlugin", base.Target))
{
PublicDependencyModuleNames.AddRange(
new string[]
{
"DTrackPlugin",
"DTrackInput"
}
);
}
PrivateDependencyModuleNames.AddRange(
new string[] { }
);
DynamicallyLoadedModuleNames.AddRange(
new string[] { }
);
}
private static bool IsPluginEnabledForTarget(string PluginName, ReadOnlyTargetRules Target)
{
var PL = Plugins.GetPlugin(PluginName);
return PL != null && Target.ProjectFile != null && Plugins.IsPluginEnabledForTarget(PL,
ProjectDescriptor.FromFile(Target.ProjectFile), Target.Platform, Target.Configuration, Target.Type);
}
}
\ No newline at end of file
...@@ -26,8 +26,8 @@ void UClientTransformReplication::UpdateState(float DeltaTime) ...@@ -26,8 +26,8 @@ void UClientTransformReplication::UpdateState(float DeltaTime)
// Only do this if we actually replicate the actor // Only do this if we actually replicate the actor
if (GetIsReplicated()) if (GetIsReplicated())
{ {
const FVector Loc = OwningActor->GetActorLocation(); const FVector Loc = OwningActor->GetRootComponent()->GetRelativeLocation();
const FRotator Rot = OwningActor->GetActorRotation(); const FRotator Rot = OwningActor->GetRootComponent()->GetRelativeRotation();
// Only update state if the local state changed // Only update state if the local state changed
if (!Loc.Equals(ReplicatedTransform.Position) || !Rot.Equals(ReplicatedTransform.Rotation)) if (!Loc.Equals(ReplicatedTransform.Position) || !Rot.Equals(ReplicatedTransform.Rotation))
......
...@@ -7,9 +7,15 @@ ...@@ -7,9 +7,15 @@
#include "GameFramework/SpectatorPawn.h" #include "GameFramework/SpectatorPawn.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "Logging/StructuredLog.h" #include "Logging/StructuredLog.h"
#include "Pawn/ClusterRepresentationActor.h"
#include "Utility/RWTHVRUtilities.h" #include "Utility/RWTHVRUtilities.h"
ARWTHVRGameModeBase::ARWTHVRGameModeBase(const FObjectInitializer& ObjectInitializer)
{
PlayerStateClass = ARWTHVRPlayerState::StaticClass();
}
FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId, FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerController, const FUniqueNetIdRepl& UniqueId,
const FString& Options, const FString& Portal) const FString& Options, const FString& Portal)
{ {
...@@ -17,6 +23,7 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle ...@@ -17,6 +23,7 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
// but I don't really want to introduce a hard dependency here. // but I don't really want to introduce a hard dependency here.
const FString NodeNameKey = "node"; const FString NodeNameKey = "node";
const FString PrimaryNodeIdKey = "PrimaryNodeId"; const FString PrimaryNodeIdKey = "PrimaryNodeId";
const FString ClusterIdKey = "ClusterId";
// Check if we're using our custom PlayerState so that we can save the player type there. // Check if we're using our custom PlayerState so that we can save the player type there.
// If not, just ingore all related args. // If not, just ingore all related args.
...@@ -24,6 +31,7 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle ...@@ -24,6 +31,7 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
if (State != nullptr) if (State != nullptr)
{ {
int32 ClusterId = -1;
if (UGameplayStatics::HasOption(Options, PrimaryNodeIdKey)) if (UGameplayStatics::HasOption(Options, PrimaryNodeIdKey))
{ {
const FString PrimaryNodeId = UGameplayStatics::ParseOption(Options, PrimaryNodeIdKey); const FString PrimaryNodeId = UGameplayStatics::ParseOption(Options, PrimaryNodeIdKey);
...@@ -34,10 +42,22 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle ...@@ -34,10 +42,22 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
? UGameplayStatics::ParseOption(Options, NodeNameKey) ? UGameplayStatics::ParseOption(Options, NodeNameKey)
: PrimaryNodeId; : PrimaryNodeId;
ClusterId = UGameplayStatics::HasOption(Options, ClusterIdKey)
? FCString::Atoi(*UGameplayStatics::ParseOption(Options, ClusterIdKey))
: -1;
const EPlayerType Type = const EPlayerType Type =
NodeName == PrimaryNodeId ? EPlayerType::nDisplayPrimary : EPlayerType::nDisplaySecondary; NodeName == PrimaryNodeId ? EPlayerType::nDisplayPrimary : EPlayerType::nDisplaySecondary;
State->RequestSetPlayerType(Type); State->RequestSetPlayerType(Type);
} }
else if (GetNetMode() == NM_Standalone && URWTHVRUtilities::IsRoomMountedMode())
{
ClusterId = 0;
const EPlayerType Type =
URWTHVRUtilities::IsPrimaryNode() ? EPlayerType::nDisplayPrimary : EPlayerType::nDisplaySecondary;
State->RequestSetPlayerType(Type);
}
State->SetCorrespondingClusterId(ClusterId);
} }
return Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal); return Super::InitNewPlayer(NewPlayerController, UniqueId, Options, Portal);
...@@ -45,8 +65,47 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle ...@@ -45,8 +65,47 @@ FString ARWTHVRGameModeBase::InitNewPlayer(APlayerController* NewPlayerControlle
void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer) void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer)
{ {
if (const ARWTHVRPlayerState* State = Cast<ARWTHVRPlayerState>(NewPlayer->PlayerState); State != nullptr) if (ARWTHVRPlayerState* State = Cast<ARWTHVRPlayerState>(NewPlayer->PlayerState); State != nullptr)
{
// If we're in none-standalone netmode, this is only executed on the server, as the GM only exists there.
// On standalone, this is executed on every node.
int32 ClusterId = State->GetCorrespondingClusterId();
if (ClusterId >= 0) // we're either standalone (0) or in an acutal cluster
{
AClusterRepresentationActor** ClusterRepresentationPtr = ConnectedClusters.Find(ClusterId);
AClusterRepresentationActor* ClusterRepresentation;
if (!ClusterRepresentationPtr)
{
// No actor there yet, spawn it
FActorSpawnParameters SpawnParameters;
SpawnParameters.Name = FName(*FString::Printf(TEXT("ClusterRepresentation_%d"), ClusterId));
SpawnParameters.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
ClusterRepresentation = GetWorld()->SpawnActor<AClusterRepresentationActor>(SpawnParameters);
UE_LOGFMT(Toolkit, Display,
"ARWTHVRGameModeBase: Spawned ClusterRepresentationActor {Name} for Cluster {Id}",
ClusterRepresentation->GetName(), ClusterId);
ConnectedClusters.Add(ClusterId, ClusterRepresentation);
}
else
{
ClusterRepresentation = *ClusterRepresentationPtr;
}
UE_LOGFMT(Toolkit, Display, "ARWTHVRGameModeBase: Using ClusterRepresentationActor {Name} for Cluster {Id}",
*ClusterRepresentation->GetName(), ClusterId);
// Double check for sanity
check(ClusterRepresentation != nullptr);
State->SetCorrespondingClusterActor(ClusterRepresentation);
if (State->GetPlayerType() == EPlayerType::nDisplayPrimary)
{ {
// We're the owner of the actor!
ClusterRepresentation->SetOwner(NewPlayer);
}
}
// Do we already have an auto-possessing pawn possessed? // Do we already have an auto-possessing pawn possessed?
if (NewPlayer->GetPawn() && NewPlayer->GetPawn()->IsValidLowLevelFast()) if (NewPlayer->GetPawn() && NewPlayer->GetPawn()->IsValidLowLevelFast())
{ {
...@@ -56,10 +115,11 @@ void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer) ...@@ -56,10 +115,11 @@ void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer)
return; return;
} }
// When we're not in standalone:
// If the new player is a secondary nDisplay node, spawn it only as a Spectator // If the new player is a secondary nDisplay node, spawn it only as a Spectator
// Potentially we can use MustSpectate instead. // Potentially we can use MustSpectate instead.
UClass* PawnClass; UClass* PawnClass;
if (State->GetPlayerType() == EPlayerType::nDisplaySecondary) if (GetNetMode() != NM_Standalone && State->GetPlayerType() == EPlayerType::nDisplaySecondary)
{ {
// For now, simply use the BP approach of spawning the pawn here. Can do this in a better way potentially. // For now, simply use the BP approach of spawning the pawn here. Can do this in a better way potentially.
PawnClass = SpectatorClass; PawnClass = SpectatorClass;
...@@ -83,11 +143,20 @@ void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer) ...@@ -83,11 +143,20 @@ void ARWTHVRGameModeBase::PostLogin(APlayerController* NewPlayer)
} }
} }
if (GetNetMode() == NM_Standalone)
{
const FName BaseName = PawnClass->HasAnyFlags(RF_ClassDefaultObject)
? PawnClass->GetFName()
: *PawnClass->GetFName().GetPlainNameString();
SpawnInfo.Name = BaseName;
SpawnInfo.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
}
// Spawn and possess the pawn // Spawn and possess the pawn
APawn* ResultPawn = GetWorld()->SpawnActor<APawn>(PawnClass, StartSpot->GetTransform(), SpawnInfo); APawn* ResultPawn = GetWorld()->SpawnActor<APawn>(PawnClass, StartSpot->GetTransform(), SpawnInfo);
NewPlayer->Possess(ResultPawn); NewPlayer->Possess(ResultPawn);
} }
Super::PostLogin(NewPlayer); Super::PostLogin(NewPlayer);
} }
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
#include "Core/RWTHVRPlayerState.h" #include "Core/RWTHVRPlayerState.h"
#include "Logging/StructuredLog.h"
#include "Net/UnrealNetwork.h" #include "Net/UnrealNetwork.h"
#include "Net/Core/PushModel/PushModel.h" #include "Net/Core/PushModel/PushModel.h"
#include "Utility/RWTHVRUtilities.h"
// Boilerplate, copies properties to new state // Boilerplate, copies properties to new state
void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState) void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState)
...@@ -17,6 +19,8 @@ void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState) ...@@ -17,6 +19,8 @@ void ARWTHVRPlayerState::CopyProperties(class APlayerState* PlayerState)
if (IsValid(RWTHVRPlayerState)) if (IsValid(RWTHVRPlayerState))
{ {
RWTHVRPlayerState->SetPlayerType(GetPlayerType()); RWTHVRPlayerState->SetPlayerType(GetPlayerType());
RWTHVRPlayerState->SetCorrespondingClusterId(CorrespondingClusterId);
RWTHVRPlayerState->SetCorrespondingClusterActor(CorrespondingClusterActor);
} }
} }
} }
...@@ -32,6 +36,8 @@ void ARWTHVRPlayerState::OverrideWith(class APlayerState* PlayerState) ...@@ -32,6 +36,8 @@ void ARWTHVRPlayerState::OverrideWith(class APlayerState* PlayerState)
if (IsValid(RWTHVRPlayerState)) if (IsValid(RWTHVRPlayerState))
{ {
SetPlayerType(RWTHVRPlayerState->GetPlayerType()); SetPlayerType(RWTHVRPlayerState->GetPlayerType());
SetCorrespondingClusterId(RWTHVRPlayerState->CorrespondingClusterId);
SetCorrespondingClusterActor(RWTHVRPlayerState->CorrespondingClusterActor);
} }
} }
} }
...@@ -45,6 +51,8 @@ void ARWTHVRPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& O ...@@ -45,6 +51,8 @@ void ARWTHVRPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& O
SharedParams.bIsPushBased = true; SharedParams.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, PlayerType, SharedParams); DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, PlayerType, SharedParams);
DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, CorrespondingClusterId, SharedParams);
DOREPLIFETIME_WITH_PARAMS_FAST(ARWTHVRPlayerState, CorrespondingClusterActor, SharedParams);
} }
void ARWTHVRPlayerState::ServerSetPlayerTypeRpc_Implementation(const EPlayerType NewPlayerType) void ARWTHVRPlayerState::ServerSetPlayerTypeRpc_Implementation(const EPlayerType NewPlayerType)
...@@ -57,6 +65,28 @@ void ARWTHVRPlayerState::SetPlayerType(const EPlayerType NewPlayerType) ...@@ -57,6 +65,28 @@ void ARWTHVRPlayerState::SetPlayerType(const EPlayerType NewPlayerType)
MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, PlayerType, this); MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, PlayerType, this);
PlayerType = NewPlayerType; PlayerType = NewPlayerType;
} }
void ARWTHVRPlayerState::SetCorrespondingClusterId(int32 NewCorrespondingClusterId)
{
if (!HasAuthority())
{
UE_LOGFMT(Toolkit, Warning, "ARWTHVRPlayerState: Cannot set cluster Id on non-authority!");
return;
}
MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, CorrespondingClusterId, this);
CorrespondingClusterId = NewCorrespondingClusterId;
}
void ARWTHVRPlayerState::SetCorrespondingClusterActor(AClusterRepresentationActor* NewCorrespondingClusterActor)
{
if (!HasAuthority())
{
UE_LOGFMT(Toolkit, Warning, "ARWTHVRPlayerState: Cannot set cluster actor ref on non-authority!");
return;
}
MARK_PROPERTY_DIRTY_FROM_NAME(ARWTHVRPlayerState, CorrespondingClusterActor, this);
CorrespondingClusterActor = NewCorrespondingClusterActor;
}
void ARWTHVRPlayerState::RequestSetPlayerType(const EPlayerType NewPlayerType) void ARWTHVRPlayerState::RequestSetPlayerType(const EPlayerType NewPlayerType)
{ {
......
...@@ -6,12 +6,7 @@ ...@@ -6,12 +6,7 @@
// We disable ticking here, as we are mainly interested in the events // We disable ticking here, as we are mainly interested in the events
UActionBehaviour::UActionBehaviour() { PrimaryComponentTick.bCanEverTick = false; } UActionBehaviour::UActionBehaviour() { PrimaryComponentTick.bCanEverTick = false; }
void UActionBehaviour::OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction, void UActionBehaviour::OnActionEvent(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
const FInputActionValue& Value)
{
}
void UActionBehaviour::OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction,
const FInputActionValue& Value) const FInputActionValue& Value)
{ {
} }
...@@ -20,13 +15,5 @@ void UActionBehaviour::BeginPlay() ...@@ -20,13 +15,5 @@ void UActionBehaviour::BeginPlay()
{ {
Super::BeginPlay(); Super::BeginPlay();
OnActionBeginEvent.AddDynamic(this, &UActionBehaviour::OnActionStart); OnActionEventEvent.AddDynamic(this, &UActionBehaviour::OnActionEvent);
OnActionEndEvent.AddDynamic(this, &UActionBehaviour::OnActionEnd);
}
// Called every frame
void UActionBehaviour::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
} }
// Fill out your copyright notice in the Description page of Project Settings.
#include "Interaction/Interactables/BaseBehaviour.h"
...@@ -3,10 +3,26 @@ ...@@ -3,10 +3,26 @@
#include "Interaction/Interactables/GrabBehavior.h" #include "Interaction/Interactables/GrabBehavior.h"
#include "Interaction/Interactables/InteractableComponent.h" #include "Interaction/Interactables/InteractableComponent.h"
#include "Interaction/Interactables/InteractionEventType.h"
#include "Logging/StructuredLog.h" #include "Logging/StructuredLog.h"
#include "Pawn/Navigation/CollisionHandlingMovement.h"
#include "Serialization/JsonTypes.h" #include "Serialization/JsonTypes.h"
#include "Utility/RWTHVRUtilities.h" #include "Utility/RWTHVRUtilities.h"
UGrabBehavior::UGrabBehavior()
{
SetIsReplicatedByDefault(true);
bExecuteOnServer = true;
bExecuteOnAllClients = false;
}
void UGrabBehavior::BeginPlay()
{
Super::BeginPlay();
OnActionReplicationStartedOriginatorEvent.AddDynamic(this, &UGrabBehavior::ReplicationOriginaterClientCallback);
}
UPrimitiveComponent* UGrabBehavior::GetFirstComponentSimulatingPhysics(const AActor* TargetActor) UPrimitiveComponent* UGrabBehavior::GetFirstComponentSimulatingPhysics(const AActor* TargetActor)
{ {
TArray<UPrimitiveComponent*> PrimitiveComponents; TArray<UPrimitiveComponent*> PrimitiveComponents;
...@@ -33,16 +49,79 @@ UPrimitiveComponent* UGrabBehavior::GetHighestParentSimulatingPhysics(UPrimitive ...@@ -33,16 +49,79 @@ UPrimitiveComponent* UGrabBehavior::GetHighestParentSimulatingPhysics(UPrimitive
return Comp; return Comp;
} }
void UGrabBehavior::ReplicationOriginaterClientCallback(USceneComponent* TriggerComponent,
const EInteractionEventType EventType,
const FInputActionValue& Value)
{
const USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggerComponent->GetAttachParent());
HandleCollisionHandlingMovement(CurrentAttachParent, EventType);
}
void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UInputAction* InputAction, void UGrabBehavior::HandleCollisionHandlingMovement(const USceneComponent* CurrentAttachParent,
const EInteractionEventType EventType)
{
auto CHM = CurrentAttachParent->GetOwner()->GetComponentByClass<UCollisionHandlingMovement>();
if (!CHM)
return;
if (EventType == EInteractionEventType::InteractionStart)
{
// Add to ignore list for collision handling movement, if it exists
if (bIgnoreGrabbedActorInCollisionMovement)
{
bWasAddedToIgnore = CHM->AddActorToIgnore(GetOwner());
}
}
else
{
// If our attach parent has a collision handling component, remove
if (bWasAddedToIgnore)
{
CHM->RemoveActorFromIgnore(GetOwner());
}
}
}
void UGrabBehavior::OnActionEvent(USceneComponent* TriggerComponent, const EInteractionEventType EventType,
const FInputActionValue& Value) const FInputActionValue& Value)
{
if (EventType == EInteractionEventType::InteractionStart)
{
StartGrab(TriggerComponent);
}
else
{
EndGrab(TriggerComponent);
}
}
bool UGrabBehavior::TryRelease()
{
if (!bObjectGrabbed)
{
UE_LOGFMT(Toolkit, Display, "UGrabBehavior::TryRelease: bObjectGrabbed was false!");
return false;
}
if (MyPhysicsComponent)
{
MyPhysicsComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
MyPhysicsComponent->SetSimulatePhysics(bWasSimulatingPhysics);
}
else
{
GetOwner()->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
}
bObjectGrabbed = false;
return true;
}
void UGrabBehavior::StartGrab(USceneComponent* TriggerComponent)
{ {
if (bObjectGrabbed) if (bObjectGrabbed)
{ {
return; return;
} }
USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggeredComponent->GetAttachParent()); USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggerComponent->GetAttachParent());
const FAttachmentTransformRules Rules = FAttachmentTransformRules(EAttachmentRule::KeepWorld, false); const FAttachmentTransformRules Rules = FAttachmentTransformRules(EAttachmentRule::KeepWorld, false);
if (MyPhysicsComponent = GetFirstComponentSimulatingPhysics(GetOwner()); MyPhysicsComponent != nullptr) if (MyPhysicsComponent = GetFirstComponentSimulatingPhysics(GetOwner()); MyPhysicsComponent != nullptr)
...@@ -72,18 +151,19 @@ void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UIn ...@@ -72,18 +151,19 @@ void UGrabBehavior::OnActionStart(USceneComponent* TriggeredComponent, const UIn
GetOwner()->GetComponents<UInteractableComponent>(Interactables, false); GetOwner()->GetComponents<UInteractableComponent>(Interactables, false);
for (UInteractableComponent* Interactable : Interactables) for (UInteractableComponent* Interactable : Interactables)
{ {
Interactable->RestrictInteractionToComponent(TriggeredComponent); Interactable->RestrictInteractionToComponent(TriggerComponent);
} }
} }
OnGrabStartEvent.Broadcast(CurrentAttachParent, MyPhysicsComponent); OnGrabStartEvent.Broadcast(CurrentAttachParent, MyPhysicsComponent);
// Add to ignore list for collision handling movement, if it exists
HandleCollisionHandlingMovement(CurrentAttachParent, InteractionStart);
} }
void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInputAction* InputAction, void UGrabBehavior::EndGrab(USceneComponent* TriggerComponent)
const FInputActionValue& Value)
{ {
USceneComponent* CurrentAttachParent = Cast<USceneComponent>(TriggerComponent->GetAttachParent());
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. // We try to release the attached component. If it is not succesful we log and return. Otherwise, we continue.
if (!TryRelease()) if (!TryRelease())
...@@ -106,24 +186,7 @@ void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInpu ...@@ -106,24 +186,7 @@ void UGrabBehavior::OnActionEnd(USceneComponent* TriggeredComponent, const UInpu
Interactable->ResetRestrictInteraction(); Interactable->ResetRestrictInteraction();
} }
} }
}
bool UGrabBehavior::TryRelease() // If our attach parent has a collision handling component, remove
{ HandleCollisionHandlingMovement(CurrentAttachParent, InteractionEnd);
if (!bObjectGrabbed)
{
return false;
}
if (MyPhysicsComponent)
{
MyPhysicsComponent->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
MyPhysicsComponent->SetSimulatePhysics(bWasSimulatingPhysics);
}
else
{
GetOwner()->GetRootComponent()->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
}
bObjectGrabbed = false;
return true;
} }