#include "Player/Arm/DescentArmViewmodelComponent.h" #include "Core/DescentEnvQuery.h" #include "Core/Tuning/DescentTune_Arm.h" #include "Debug/DescentStateRegistry.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Camera/CameraComponent.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" namespace { namespace ArmTune = Descent::Tune::Arm; /** Every live arm component, for the commands and the state dump. */ TArray>& ArmLiveList() { static TArray> List; return List; } } UDescentArmViewmodelComponent::UDescentArmViewmodelComponent() { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bStartWithTickEnabled = true; PrimaryComponentTick.TickGroup = TG_PrePhysics; bWantsInitializeComponent = false; SetMobility(EComponentMobility::Movable); Blender.Reset(Descent::Arm::NamedPose(EDescentArmPose::Hidden)); } // --------------------------------------------------------------------------------------------------------- // Registry and life cycle // --------------------------------------------------------------------------------------------------------- void UDescentArmViewmodelComponent::OnRegister() { Super::OnRegister(); ArmLiveList().AddUnique(this); } void UDescentArmViewmodelComponent::OnUnregister() { ArmLiveList().Remove(this); Super::OnUnregister(); } void UDescentArmViewmodelComponent::OnComponentDestroyed(bool bDestroyingHierarchy) { // The segments are components of the owner made at run time: they must not outlive the arm that drives them. for (int32 I = Parts.Num() - 1; I >= 0; --I) { if (USceneComponent* C = Parts[I].Get()) { C->DestroyComponent(); } } Parts.Reset(); bBuilt = false; Super::OnComponentDestroyed(bDestroyingHierarchy); } void UDescentArmViewmodelComponent::GetAllInWorld(UWorld* World, TArray& Out) { Out.Reset(); for (const TWeakObjectPtr& W : ArmLiveList()) { UDescentArmViewmodelComponent* C = W.Get(); if (C && IsValid(C) && C->GetWorld() == World) { Out.Add(C); } } } UDescentArmViewmodelComponent* UDescentArmViewmodelComponent::FindFirst(UWorld* World) { TArray All; GetAllInWorld(World, All); return All.Num() > 0 ? All[0] : nullptr; } void UDescentArmViewmodelComponent::BeginPlay() { Super::BeginPlay(); if (bAutoBuild) { BuildParts(); } } void UDescentArmViewmodelComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (bTickSelf) { Advance(DeltaTime); } // DESIGN_UE.md 7.4: the sea's light (lighting channel 0: the sun, the hull's floodlights) never falls on anything inside // the hull. The arm is on channels 0 and 1 like the body (5.1), so while its camera is in dry air it drops channel 0. // (This follows the design; it made no visible difference in the shots taken inside the carrier.) const UCameraComponent* Camera = MountedCamera.Get(); const bool bDry = Camera && Descent::IsDry(GetWorld(), Camera->GetComponentLocation()); if (bDry != bInDryAir) { bInDryAir = bDry; ApplyRenderSettings(); } } // --------------------------------------------------------------------------------------------------------- // Parts // --------------------------------------------------------------------------------------------------------- void UDescentArmViewmodelComponent::BuildParts() { AActor* Owner = GetOwner(); if (bBuilt || !Owner) { return; } bBuilt = true; Parts.Reset(); Parts.SetNum(Descent::Arm::PartCount); for (int32 I = 0; I < Descent::Arm::PartCount; ++I) { const FDescentArmPartDef& D = Descent::Arm::Def(static_cast(I)); USceneComponent* Parent = D.ParentIndex < 0 ? static_cast(this) : Parts[D.ParentIndex].Get(); USceneComponent* C = nullptr; if (FCString::Strlen(D.MeshAsset) > 0) { UStaticMeshComponent* M = NewObject(Owner, MakeUniqueObjectName(Owner, UStaticMeshComponent::StaticClass(), D.Name)); const FString Path = FString(Descent::Arm::MeshFolder()) + D.MeshAsset + TEXT(".") + D.MeshAsset; if (UStaticMesh* Mesh = LoadObject(nullptr, *Path, nullptr, LOAD_NoWarn | LOAD_Quiet)) { M->SetStaticMesh(Mesh); } else if (!bLoggedMissingMesh) { bLoggedMissingMesh = true; UE_LOG(LogDescent, Error, TEXT("Arm: mesh %s is missing. Run Tools\\export_arm.ps1 then Tools\\import_assets.ps1."), *Path); } C = M; } else { C = NewObject(Owner, MakeUniqueObjectName(Owner, USceneComponent::StaticClass(), D.Name)); } C->SetMobility(EComponentMobility::Movable); C->SetupAttachment(Parent); if (GetWorld()) { C->RegisterComponent(); } Parts[I] = C; } ApplyRenderSettings(); ApplyCurrentPose(); UpdateVisibility(); } int32 UDescentArmViewmodelComponent::CountPartsWithMesh() const { int32 N = 0; for (const TObjectPtr& C : Parts) { const UStaticMeshComponent* M = Cast(C.Get()); N += (M && M->GetStaticMesh()) ? 1 : 0; } return N; } USceneComponent* UDescentArmViewmodelComponent::GetPartComponent(EDescentArmPart Part) const { const int32 I = static_cast(Part); return Parts.IsValidIndex(I) ? Parts[I].Get() : nullptr; } UStaticMeshComponent* UDescentArmViewmodelComponent::GetPartMesh(EDescentArmPart Part) const { return Cast(GetPartComponent(Part)); } void UDescentArmViewmodelComponent::ApplyRenderSettings() { for (const TObjectPtr& C : Parts) { UStaticMeshComponent* M = Cast(C.Get()); if (!M) { continue; } M->SetCollisionEnabled(ECollisionEnabled::NoCollision); M->SetGenerateOverlapEvents(false); M->SetReceivesDecals(false); M->SetCanEverAffectNavigation(false); // First person: drawn with the camera's first-person FOV and scale (cannot clip into a wall), and it must not // cast a shadow onto the world the player is looking at. A third-person showcase wants the shadows back. M->SetFirstPersonPrimitiveType(bFirstPersonRendering ? EFirstPersonPrimitiveType::FirstPerson : EFirstPersonPrimitiveType::None); M->SetCastShadow(!bFirstPersonRendering); // It is the viewer's own arm: any other camera (a Tender's, a scenario's, the map's) must not see an arm floating in the room. M->SetOnlyOwnerSee(bFirstPersonRendering && bOnlyOwnerSeeWhenMounted); // DESIGN_UE.md 7.4: the first-person arm renders custom depth with stencil 1 ("dry"), so the water post-process leaves it alone. M->SetRenderCustomDepth(true); M->SetCustomDepthStencilValue(ArmTune::DryStencilValue); // In the sea: channels 0 and 1 like the body. In dry air: channel 2 only, which carries the lamps of the room the player stands // in (UCarrierBlockoutComponent::SetViewerRoom) and his own flashlight, so the hand takes the room's light and nothing through a wall. M->SetLightingChannels(ArmTune::LightChannel0 && !bInDryAir, ArmTune::LightChannel1 && !bInDryAir, bInDryAir); } } void UDescentArmViewmodelComponent::SetFirstPersonRendering(bool bOn) { bFirstPersonRendering = bOn; ApplyRenderSettings(); } void UDescentArmViewmodelComponent::UpdateVisibility() { // A hidden, settled arm is not drawn at all. const bool bHide = bHideWhenHidden && NamedPose == EDescentArmPose::Hidden && !bCustomPose && !Blender.IsBlending() && !bJackActive; const bool bShow = !bHide; if (bShow != bMeshesVisible) { bMeshesVisible = bShow; if (USceneComponent* Root = GetPartComponent(EDescentArmPart::Root)) { Root->SetVisibility(bShow, /*bPropagateToChildren*/ true); } } } void UDescentArmViewmodelComponent::ApplyCurrentPose() { if (!bBuilt) { return; } FDescentArmTransforms Local; Descent::Arm::ComputeLocal(Blender.Current(), Local); for (int32 I = 0; I < Descent::Arm::PartCount && I < Parts.Num(); ++I) { if (USceneComponent* C = Parts[I].Get()) { C->SetRelativeTransform(Local[I]); } } } // --------------------------------------------------------------------------------------------------------- // Poses // --------------------------------------------------------------------------------------------------------- void UDescentArmViewmodelComponent::SetPose(EDescentArmPose Pose, double BlendSeconds) { if (bJackActive) { return; // the jack-in owns the arm until it ends or is aborted } if (BlendSeconds < 0.0) { BlendSeconds = (Pose == EDescentArmPose::Hidden || NamedPose == EDescentArmPose::Hidden) ? ArmTune::HideBlendSeconds : ArmTune::PoseBlendSeconds; } NamedPose = Pose; bCustomPose = false; LastReachSource = EDescentArmReachSource::None; Blender.BlendTo(Descent::Arm::NamedPose(Pose), BlendSeconds); ApplyCurrentPose(); UpdateVisibility(); } void UDescentArmViewmodelComponent::SnapToPose(EDescentArmPose Pose) { SetPose(Pose, 0.0); } void UDescentArmViewmodelComponent::BlendToPose(const FDescentArmPose& Pose, double BlendSeconds) { if (bJackActive) { return; } bCustomPose = true; // a custom pose is always drawn (UpdateVisibility) Blender.BlendTo(Pose, BlendSeconds); ApplyCurrentPose(); UpdateVisibility(); } void UDescentArmViewmodelComponent::SetFingerCurl(int32 Finger, double Curl, double BlendSeconds) { if (Finger < 0 || Finger >= 5) { return; } FDescentArmPose T = Blender.TargetPose(); T.FingerCurl[Finger] = FMath::Clamp(Curl, 0.0, 1.0); BlendToPose(T, BlendSeconds); } void UDescentArmViewmodelComponent::Advance(double Dt) { if (bJackActive) { StepJack(Dt); } else { Blender.Advance(Dt); } ApplyCurrentPose(); UpdateVisibility(); } // --------------------------------------------------------------------------------------------------------- // Reach // --------------------------------------------------------------------------------------------------------- FDescentArmReachResult UDescentArmViewmodelComponent::ReachTo(const FDescentArmReachRequest& Request, double BlendSeconds) { const FDescentArmReachResult R = Descent::Arm::SolveReach(Request, Blender.TargetPose()); LastReach = R; LastReachTargetArm = Request.TargetCm; LastReachSource = EDescentArmReachSource::Manual; BlendToPose(R.Pose, BlendSeconds); return R; } FDescentArmReachResult UDescentArmViewmodelComponent::ReachToWorld(const FVector& TargetWorldCm, const FVector& HandDirectionWorld, EDescentArmEffector Effector, double BlendSeconds) { FDescentArmReachRequest Req; Req.TargetCm = WorldToArm(TargetWorldCm); Req.Effector = Effector; const FVector DirArm = GetComponentTransform().InverseTransformVector(HandDirectionWorld).GetSafeNormal(); if (DirArm.IsNearlyZero()) { Req.bAimAtTarget = true; } else { Req.HandDirection = DirArm; } return ReachTo(Req, BlendSeconds); } // --------------------------------------------------------------------------------------------------------- // The jack-in // --------------------------------------------------------------------------------------------------------- bool UDescentArmViewmodelComponent::StartJackIn(const FVector& PortWorldCm, const FVector& PlugDirectionWorld) { if (bJackActive) { return false; } BuildParts(); // Back to where it was: Hidden normally; Idle if the character keeps the arm up on foot. A custom pose has no name to return to. JackResumePose = bCustomPose ? EDescentArmPose::Hidden : NamedPose; JackInput.Start = Blender.Current(); JackInput.End = Descent::Arm::NamedPose(JackResumePose); JackPortWorld = PortWorldCm; JackDirWorld = PlugDirectionWorld.GetSafeNormal(); JackTime = 0.0; JackPhase = EDescentArmJackPhase::Raise; LastJackFrame = FDescentArmJackFrame(); bJackActive = true; bCustomPose = false; LastReachSource = EDescentArmReachSource::Jack; ApplyCameraScale(); StepJack(0.0); // the port in arm space and the first frame's pose, so the state is right before the next tick ApplyCurrentPose(); UpdateVisibility(); // drawn from the first frame return true; } void UDescentArmViewmodelComponent::AbortJackIn(EDescentArmPose ResumePose) { if (!bJackActive) { return; } bJackActive = false; JackPhase = EDescentArmJackPhase::Inactive; ApplyCameraScale(); Blender.Reset(LastJackFrame.Pose); SetPose(ResumePose); } void UDescentArmViewmodelComponent::StepJack(double Dt) { JackTime += FMath::Max(Dt, 0.0); // The socket belongs to the world; the character is walking up to it and turning, so convert it every frame. JackInput.PortCm = WorldToArm(JackPortWorld); const FVector DirArm = GetComponentTransform().InverseTransformVector(JackDirWorld).GetSafeNormal(); JackInput.PlugDirection = DirArm.IsNearlyZero() ? FVector::ForwardVector : DirArm; const FDescentArmJackFrame Frame = Descent::Arm::EvaluateJack(JackTime, JackInput); const bool bSeated = Frame.Phase == EDescentArmJackPhase::Feed && JackPhase != EDescentArmJackPhase::Feed; JackPhase = Frame.Phase; LastJackFrame = Frame; Blender.Reset(Frame.Pose); LastReach = FDescentArmReachResult(); LastReach.Pose = Frame.Pose; LastReach.ErrorCm = Frame.ReachErrorCm; LastReach.bReachable = Frame.bReachable; LastReach.HandDirErrorDeg = Frame.HandDirErrorDeg; LastReach.DistanceCm = JackInput.PortCm.Size(); LastReachTargetArm = JackInput.PortCm; if (bSeated) { OnJackPlugged.Broadcast(); } if (Frame.Phase == EDescentArmJackPhase::Done) { FinishJack(); } } void UDescentArmViewmodelComponent::FinishJack() { bJackActive = false; ApplyCameraScale(); NamedPose = JackResumePose; Blender.Reset(JackInput.End); OnJackFinished.Broadcast(); } // --------------------------------------------------------------------------------------------------------- // Mounting // --------------------------------------------------------------------------------------------------------- void UDescentArmViewmodelComponent::SetSide(EDescentArmSide InSide) { Side = InSide; const bool bLeft = Side == EDescentArmSide::Left; SetRelativeScale3D(FVector(1.0, bLeft ? -1.0 : 1.0, 1.0)); // the left arm is the model mirrored if (MountedCamera.IsValid()) { const FVector& O = ArmTune::ShoulderOffsetCm; SetRelativeLocation(FVector(O.X, bLeft ? -O.Y : O.Y, O.Z)); } } void UDescentArmViewmodelComponent::MountOnCamera(UCameraComponent* Camera, EDescentArmSide InSide, bool bConfigureCamera) { if (!Camera) { return; } AttachToComponent(Camera, FAttachmentTransformRules::KeepRelativeTransform); MountedCamera = Camera; SetRelativeRotation(FRotator::ZeroRotator); SetSide(InSide); if (bConfigureCamera) { // The arm is drawn with the camera's own field of view, whatever it is at the moment (no separate first-person FOV: // the arm reaches into the world, so it must share the world's perspective), pulled toward the eye in depth by the // first-person scale of the tuning header. Camera->SetEnableFirstPersonFieldOfView(false); } bCameraConfigured = bConfigureCamera; ApplyCameraScale(); BuildParts(); SetFirstPersonRendering(true); } void UDescentArmViewmodelComponent::ApplyCameraScale() { UCameraComponent* Camera = MountedCamera.Get(); if (!Camera || !bCameraConfigured) { return; } // True depth while the hand is at the port, the pulled-in depth otherwise (see the tuning header). const double Scale = bJackActive ? ArmTune::JackFirstPersonScale : ArmTune::FirstPersonScale; Camera->SetEnableFirstPersonScale(Scale < 1.0); Camera->SetFirstPersonScale(static_cast(FMath::Clamp(Scale, 0.001, 1.0))); } void UDescentArmViewmodelComponent::Unmount(USceneComponent* NewParent) { if (UCameraComponent* Camera = MountedCamera.Get()) { if (bCameraConfigured) { Camera->SetEnableFirstPersonScale(false); } } MountedCamera.Reset(); bCameraConfigured = false; if (NewParent) { AttachToComponent(NewParent, FAttachmentTransformRules::KeepRelativeTransform); } SetRelativeLocationAndRotation(FVector::ZeroVector, FRotator::ZeroRotator); BuildParts(); SetFirstPersonRendering(false); } // --------------------------------------------------------------------------------------------------------- // State dump: section "arm" // --------------------------------------------------------------------------------------------------------- static FDescentStateProviderRegistrar GArmStateSection(TEXT("arm"), [](UWorld* World, FJsonObject& Out) { TArray All; if (World) { UDescentArmViewmodelComponent::GetAllInWorld(World, All); } TArray> Items; for (UDescentArmViewmodelComponent* A : All) { using namespace Descent::Json; const FDescentArmPose& P = A->GetCurrentPose(); const FDescentArmPose& T = A->GetTargetPose(); TSharedRef E = MakeShared(); E->SetStringField(TEXT("owner"), A->GetOwner() ? A->GetOwner()->GetName() : TEXT("")); E->SetStringField(TEXT("side"), A->GetSide() == EDescentArmSide::Right ? TEXT("right") : TEXT("left")); E->SetBoolField(TEXT("mounted_on_camera"), A->IsMountedOnCamera()); E->SetBoolField(TEXT("first_person_rendering"), A->UsesFirstPersonRendering()); E->SetBoolField(TEXT("in_dry_air"), A->IsInDryAir()); E->SetStringField(TEXT("pose"), A->IsCustomPose() ? TEXT("custom") : Descent::Arm::PoseName(A->GetNamedPose())); E->SetStringField(TEXT("named_pose_before_custom"), Descent::Arm::PoseName(A->GetNamedPose())); E->SetNumberField(TEXT("parts_built"), A->ArePartsBuilt() ? Descent::Arm::PartCount : 0); E->SetNumberField(TEXT("parts_with_mesh"), A->CountPartsWithMesh()); TSharedRef Blend = MakeShared(); Blend->SetBoolField(TEXT("blending"), A->GetBlender().IsBlending()); Blend->SetNumberField(TEXT("progress"), Round(A->GetBlender().Progress())); Blend->SetNumberField(TEXT("weight"), Round(A->GetBlender().Weight())); E->SetObjectField(TEXT("blend"), Blend); TSharedRef J = MakeShared(); J->SetNumberField(TEXT("shoulder_yaw_deg"), Round(P.ShoulderYawDeg)); J->SetNumberField(TEXT("shoulder_pitch_deg"), Round(P.ShoulderPitchDeg)); J->SetNumberField(TEXT("shoulder_roll_deg"), Round(P.ShoulderRollDeg)); J->SetNumberField(TEXT("elbow_flex_deg"), Round(P.ElbowFlexDeg)); J->SetNumberField(TEXT("wrist_roll_deg"), Round(P.WristRollDeg)); J->SetNumberField(TEXT("wrist_flex_deg"), Round(P.WristFlexDeg)); J->SetNumberField(TEXT("rod_slide_cm"), Round(Descent::Arm::RodSlideCm(P.ElbowFlexDeg))); TArray> Curl; for (int32 F = 0; F < 5; ++F) { Curl.Add(MakeShared(Round(P.FingerCurl[F]))); } J->SetArrayField(TEXT("finger_curl"), Curl); E->SetObjectField(TEXT("joints"), J); E->SetNumberField(TEXT("target_pose_distance"), Round(P.MaxDifference(T))); const FDescentArmReachResult& R = A->GetLastReach(); TSharedRef Reach = MakeShared(); Reach->SetStringField(TEXT("source"), A->GetLastReachSource() == EDescentArmReachSource::Jack ? TEXT("jack") : A->GetLastReachSource() == EDescentArmReachSource::Manual ? TEXT("manual") : TEXT("none")); Reach->SetField(TEXT("target_arm_m"), Vec3(Descent::UUToMetres(A->GetLastReachTargetArm()))); Reach->SetNumberField(TEXT("error_cm"), Round(R.ErrorCm)); Reach->SetBoolField(TEXT("reachable"), R.bReachable); Reach->SetBoolField(TEXT("limited"), R.bLimited); Reach->SetNumberField(TEXT("distance_cm"), Round(R.DistanceCm)); Reach->SetNumberField(TEXT("max_reach_cm"), Round(R.MaxReachCm)); Reach->SetNumberField(TEXT("hand_dir_error_deg"), Round(R.HandDirErrorDeg)); Reach->SetNumberField(TEXT("twist_error_deg"), Round(R.TwistErrorDeg)); Reach->SetNumberField(TEXT("tolerance_cm"), ArmTune::ReachToleranceCm); E->SetObjectField(TEXT("reach"), Reach); TSharedRef Jack = MakeShared(); Jack->SetBoolField(TEXT("active"), A->IsJackActive()); Jack->SetStringField(TEXT("phase"), Descent::Arm::JackPhaseName(A->GetJackPhase())); Jack->SetNumberField(TEXT("time_s"), Round(A->GetJackTime())); Jack->SetBoolField(TEXT("feeding"), A->IsJackFeeding()); Jack->SetNumberField(TEXT("reach_error_cm"), Round(A->GetLastJackFrame().ReachErrorCm)); E->SetObjectField(TEXT("jack"), Jack); E->SetField(TEXT("fingertip_world_m"), Vec3(Descent::UUToMetres(A->EffectorWorld(EDescentArmEffector::Fingertip)))); E->SetField(TEXT("shoulder_world_m"), Vec3(Descent::UUToMetres(A->GetComponentLocation()))); Items.Add(MakeShared(E)); } Out.SetNumberField(TEXT("count"), Items.Num()); Out.SetArrayField(TEXT("items"), Items); });