#include "Player/Arm/DescentArmDebugRig.h" #include "Player/Arm/DescentArmViewmodelComponent.h" #include "Core/Tuning/DescentTune_Arm.h" #include "Camera/CameraComponent.h" #include "Components/PointLightComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "EngineUtils.h" #include "GameFramework/PlayerController.h" #include "Materials/MaterialInstanceDynamic.h" #include "UObject/ConstructorHelpers.h" namespace { namespace ArmRigTune = Descent::Tune::Arm; // Unitless light intensities (the game's convention), chosen against the environment's fixed exposure on the // arm_showcase / arm_jack pictures. constexpr float ArmRigKey = 5.0f; constexpr float ArmRigFill = 3.5f; constexpr float ArmRigRim = 4.0f; constexpr float ArmRigRoom = 3.0f; constexpr float ArmRigPortGlow = 1.5f; // The blockout port, centimetres. The collar stands out of the wall; the socket is the plate at its bottom. constexpr double ArmRigCollarDepthCm = 3.0; constexpr double ArmRigCollarBarCm = 1.5; constexpr double ArmRigOpeningWidthCm = 12.0; constexpr double ArmRigOpeningHeightCm = 7.5; constexpr double ArmRigSocketPlateCm = 0.4; struct FArmRigBox { const TCHAR* Name; FVector CentreCm; FVector SizeCm; FLinearColor Color; }; } ADescentArmDebugRig::ADescentArmDebugRig() { PrimaryActorTick.bCanEverTick = false; USceneComponent* Root = CreateDefaultSubobject(TEXT("Root")); SetRootComponent(Root); Stand = CreateDefaultSubobject(TEXT("Stand")); Stand->SetupAttachment(Root); Stand->SetRelativeLocation(FVector(0.0, 0.0, ArmRigTune::RigShowcaseShoulderHeightCm)); // The eye: at the character's eye height, with the game's field of view, looking at the port as a player about to use it would. const double PortDropCm = ArmRigTune::RigEyeHeightCm - ArmRigTune::RigPortHeightCm; Camera = CreateDefaultSubobject(TEXT("Eye")); Camera->SetupAttachment(Root); Camera->SetRelativeLocation(FVector(0.0, 0.0, ArmRigTune::RigEyeHeightCm)); Camera->SetRelativeRotation(FRotator(-FMath::RadiansToDegrees(FMath::Atan2(PortDropCm, ArmRigTune::RigWallDistanceCm)), 0.0, 0.0)); Camera->SetFieldOfView(static_cast(ArmRigTune::GameFovDeg)); Arm = CreateDefaultSubobject(TEXT("Arm")); Arm->SetupAttachment(Stand); auto MakeLight = [this, Root](const TCHAR* Name, const FVector& LocationCm, const FLinearColor& Color, float Intensity, float RadiusCm, bool bShadows) { UPointLightComponent* L = CreateDefaultSubobject(Name); L->SetupAttachment(Root); L->SetMobility(EComponentMobility::Movable); // The game's light convention (DESIGN_UE.md 7.5): no inverse-square falloff, exponent 3, unitless intensity. L->bUseInverseSquaredFalloff = false; L->SetLightFalloffExponent(3.f); L->SetIntensity(Intensity); L->SetLightColor(Color); L->SetAttenuationRadius(RadiusCm); L->SetSourceRadius(6.f); // a soft highlight on the armour instead of a pin-prick L->SetCastShadows(bShadows); L->SetVolumetricScatteringIntensity(0.f); // the rig may stand in water: no glowing haze around its lamps L->SetLightingChannels(true, true, false); // the arm is on channels 0 and 1 L->SetRelativeLocation(LocationCm); return L; }; // Showcase: the shoulder is at (0, 0, 150), the arm lies along +X for about 80 cm. // A short reach (2.8 m): the lamps light the arm and nothing of the sea floor that may be a few metres away. Key = MakeLight(TEXT("Key"), FVector(70.0, 95.0, 245.0), FLinearColor(1.0f, 0.96f, 0.9f), ArmRigKey, 280.f, true); Fill = MakeLight(TEXT("Fill"), FVector(55.0, -120.0, 120.0), FLinearColor(0.55f, 0.75f, 1.0f), ArmRigFill, 280.f, false); Rim = MakeLight(TEXT("Rim"), FVector(-70.0, -30.0, 215.0), FLinearColor(0.2f, 0.85f, 0.8f), ArmRigRim, 280.f, false); // First person: a lamp above and behind the viewer, and the faint glow of the port itself. Room = MakeLight(TEXT("Room"), FVector(-45.0, 35.0, 225.0), FLinearColor(1.0f, 0.93f, 0.82f), ArmRigRoom, 800.f, true); PortGlow = MakeLight(TEXT("PortGlow"), FVector(ArmRigTune::RigWallDistanceCm - 9.0, 0.0, ArmRigTune::RigPortHeightCm + 9.0), FLinearColor(0.1f, 1.0f, 0.75f), ArmRigPortGlow, 60.f, false); // The wall and the port. The engine cube is 100 cm on a side, centred. static ConstructorHelpers::FObjectFinder CubeFinder(TEXT("/Engine/BasicShapes/Cube.Cube")); const double WallX = ArmRigTune::RigWallDistanceCm; const double PortZ = ArmRigTune::RigPortHeightCm; const double BarX = WallX - 0.5 * ArmRigCollarDepthCm; const double HalfW = 0.5 * ArmRigOpeningWidthCm, HalfH = 0.5 * ArmRigOpeningHeightCm, Bar = ArmRigCollarBarCm; const FLinearColor WallColor(0.045f, 0.055f, 0.06f), CollarColor(0.22f, 0.22f, 0.24f), SocketColor(0.01f, 0.07f, 0.06f); const FArmRigBox Boxes[] = { { TEXT("Wall"), FVector(WallX + 5.0, 0.0, 130.0), FVector(10.0, 320.0, 260.0), WallColor }, { TEXT("Floor"), FVector(-40.0, 0.0, -5.0), FVector(200.0, 320.0, 10.0), WallColor }, { TEXT("PortSocket"), FVector(WallX - 0.5 * ArmRigSocketPlateCm, 0.0, PortZ), FVector(ArmRigSocketPlateCm, ArmRigOpeningWidthCm, ArmRigOpeningHeightCm), SocketColor }, { TEXT("CollarTop"), FVector(BarX, 0.0, PortZ + HalfH + 0.5 * Bar), FVector(ArmRigCollarDepthCm, ArmRigOpeningWidthCm + 2.0 * Bar, Bar), CollarColor }, { TEXT("CollarBottom"), FVector(BarX, 0.0, PortZ - HalfH - 0.5 * Bar), FVector(ArmRigCollarDepthCm, ArmRigOpeningWidthCm + 2.0 * Bar, Bar), CollarColor }, { TEXT("CollarLeft"), FVector(BarX, -HalfW - 0.5 * Bar, PortZ), FVector(ArmRigCollarDepthCm, Bar, ArmRigOpeningHeightCm), CollarColor }, { TEXT("CollarRight"), FVector(BarX, HalfW + 0.5 * Bar, PortZ), FVector(ArmRigCollarDepthCm, Bar, ArmRigOpeningHeightCm), CollarColor }, }; for (const FArmRigBox& B : Boxes) { UStaticMeshComponent* M = CreateDefaultSubobject(B.Name); M->SetupAttachment(Root); M->SetMobility(EComponentMobility::Movable); M->SetCollisionEnabled(ECollisionEnabled::NoCollision); if (CubeFinder.Succeeded()) { M->SetStaticMesh(CubeFinder.Object); } M->SetRelativeLocation(B.CentreCm); M->SetRelativeScale3D(B.SizeCm / 100.0); WallParts.Add(M); WallColors.Add(B.Color); // becomes a material instance in OnConstruction } // No fog, exposure or post-process here: ADescentEnvironment is the only authority for those (Docs/STATUS.md section 8). } void ADescentArmDebugRig::OnConstruction(const FTransform& Transform) { Super::OnConstruction(Transform); // Tint each box with a dynamic instance of the engine's plain shape material. for (int32 I = 0; I < WallParts.Num() && I < WallColors.Num(); ++I) { UStaticMeshComponent* M = WallParts[I].Get(); if (!M || M->GetNumMaterials() == 0 || Cast(M->GetMaterial(0))) { continue; } if (UMaterialInstanceDynamic* MID = M->CreateAndSetMaterialInstanceDynamic(0)) { MID->SetVectorParameterValue(TEXT("Color"), WallColors[I]); } } } ADescentArmDebugRig* ADescentArmDebugRig::Find(UWorld* World) { if (World) { for (TActorIterator It(World); It; ++It) { return *It; } } return nullptr; } ADescentArmDebugRig* ADescentArmDebugRig::FindOrSpawn(UWorld* World, EDescentArmRigMode InMode, const FVector& LocationCm) { if (!World) { return nullptr; } ADescentArmDebugRig* Rig = Find(World); if (!Rig) { FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Rig = World->SpawnActor(LocationCm, FRotator::ZeroRotator, Params); } if (Rig) { Rig->SetMode(InMode); } return Rig; } void ADescentArmDebugRig::SetMode(EDescentArmRigMode InMode) { Mode = InMode; const bool bFirst = Mode == EDescentArmRigMode::FirstPerson; if (Arm) { if (bFirst) { Arm->MountOnCamera(Camera, EDescentArmSide::Right); } else { Arm->Unmount(Stand); } // On the stand the Hidden pose is something to look at; on the camera it is really hidden. Arm->bHideWhenHidden = bFirst; Arm->Advance(0.0); } for (UPointLightComponent* L : { Key.Get(), Fill.Get(), Rim.Get() }) { if (L) { L->SetVisibility(!bFirst); } } for (UPointLightComponent* L : { Room.Get(), PortGlow.Get() }) { if (L) { L->SetVisibility(bFirst); } } for (const TObjectPtr& Part : WallParts) { if (Part) { Part->SetVisibility(bFirst); } } } FVector ADescentArmDebugRig::GetPortWorldCm() const { // On the face of the socket plate, in the middle of the collar's opening. return GetActorTransform().TransformPosition(FVector(ArmRigTune::RigWallDistanceCm - ArmRigSocketPlateCm, 0.0, ArmRigTune::RigPortHeightCm)); } FVector ADescentArmDebugRig::GetPlugDirectionWorld() const { return GetActorTransform().TransformVectorNoScale(FVector::ForwardVector); } FVector ADescentArmDebugRig::GetShowcaseShoulderWorldCm() const { return Stand ? Stand->GetComponentLocation() : GetActorLocation(); } void ADescentArmDebugRig::TakeView() { UWorld* World = GetWorld(); if (APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr) { PC->SetViewTarget(this); } } bool ADescentArmDebugRig::StartJack() { return Arm && Arm->StartJackIn(GetPortWorldCm(), GetPlugDirectionWorld()); }