#pragma once #include "CoreMinimal.h" #include "Components/SceneComponent.h" #include "Player/Arm/DescentArmJack.h" #include "Player/Arm/DescentArmPose.h" #include "Player/Arm/DescentArmReach.h" #include "DescentArmViewmodelComponent.generated.h" class UCameraComponent; class UStaticMeshComponent; UENUM() enum class EDescentArmSide : uint8 { Right, // the model as built Left, // the model mirrored in Y (negative scale on this component) }; /** How the last reach was asked for. */ enum class EDescentArmReachSource : uint8 { None, Manual, Jack }; /** * The player's cybernetic arm as a first-person viewmodel. It builds the 20 segments of the exported model as static mesh * components attached in the model's own chain (UpperArm > Forearm > Wrist > Hand > 15 phalanges, PistonRod on the Forearm), places * them from the generated table (Player/Arm/ArmLayout.inl) and moves them ONLY by rotating parts about their joints (plus the * rod's slide). The origin of this component is the SHOULDER; +X is along the arm at rest. * * What it does: named poses with smooth blending (SetPose), a curl per finger (SetFingerCurl), a reach that puts a point of the hand * on a target in space (ReachTo*), the whole wall-plug sequence (StartJackIn), and the mounting on a camera (MountOnCamera). * All movement is computed by the pure functions in DescentArmPose / DescentArmReach / DescentArmJack; this class only owns time, * the components and the rendering settings. * * HOW TO MOUNT IT ON THE CHARACTER (the carrier track's ADescentCharacter; this track must not edit it): * in the character's constructor: Arm = CreateDefaultSubobject(TEXT("Arm")); * Arm->SetupAttachment(Camera); * in BeginPlay (after the camera exists): Arm->MountOnCamera(Camera, EDescentArmSide::Right); * the arm starts in the Hidden pose and is not drawn there. Call Arm->SetPose(EDescentArmPose::Grip) while a box is carried, * HoldTool with a hotbar tool out, Hidden otherwise, and Arm->StartJackIn(PortSocketWorldCm, -PortOutwardNormal) at the * Foot -> Jack transition (see StartJackIn). The component ticks itself (bTickSelf), no other wiring is needed. * The full note is in Docs/status/arm.md. */ UCLASS(ClassGroup = (Descent), meta = (BlueprintSpawnableComponent)) class THEDESCENT_API UDescentArmViewmodelComponent : public USceneComponent { GENERATED_BODY() public: UDescentArmViewmodelComponent(); // ---- construction ---- /** Creates and attaches the segment components (idempotent). Called by BeginPlay when bAutoBuild. */ void BuildParts(); bool ArePartsBuilt() const { return bBuilt; } /** How many segments got their mesh (20 when the assets were imported). */ int32 CountPartsWithMesh() const; UStaticMeshComponent* GetPartMesh(EDescentArmPart Part) const; USceneComponent* GetPartComponent(EDescentArmPart Part) const; // ---- poses ---- /** Blends to a named pose. BlendSeconds < 0 uses the tuning default (longer for Hidden). Ignored while a jack-in runs. */ void SetPose(EDescentArmPose Pose, double BlendSeconds = -1.0); /** Blends to any pose value (for the reach and for tests). */ void BlendToPose(const FDescentArmPose& Pose, double BlendSeconds); /** Sets the pose at once, no blend. */ void SnapToPose(EDescentArmPose Pose); /** Curl of one finger (0 Thumb .. 4 Pinky), 0 straight .. 1 fist, blended on top of the current target. */ void SetFingerCurl(int32 Finger, double Curl, double BlendSeconds = 0.15); EDescentArmPose GetNamedPose() const { return NamedPose; } const FDescentArmPose& GetCurrentPose() const { return Blender.Current(); } const FDescentArmPose& GetTargetPose() const { return Blender.TargetPose(); } const FDescentArmBlender& GetBlender() const { return Blender; } /** Advances time by hand (the component calls this from its tick when bTickSelf). Deterministic. */ void Advance(double DeltaSeconds); // ---- reaching ---- /** Puts the effector on a target given in ARM SPACE (this component's local space, cm). Blends to the solution. */ FDescentArmReachResult ReachTo(const FDescentArmReachRequest& Request, double BlendSeconds = 0.3); /** Same, target and direction given in the world (cm). */ FDescentArmReachResult ReachToWorld(const FVector& TargetWorldCm, const FVector& HandDirectionWorld, EDescentArmEffector Effector = EDescentArmEffector::Fingertip, double BlendSeconds = 0.3); const FDescentArmReachResult& GetLastReach() const { return LastReach; } EDescentArmReachSource GetLastReachSource() const { return LastReachSource; } /** The target of the last reach (or the port during a jack-in), in arm space, cm. */ const FVector& GetLastReachTargetArm() const { return LastReachTargetArm; } /** True when the pose is not one of the named ones (a reach result or a hand-set finger curl). */ bool IsCustomPose() const { return bCustomPose; } // ---- the wall-plug sequence (DESIGN_UE.md 6.9) ---- /** * Starts raise > reach > plug > hold while feeding > withdraw > lower, 4.8 s in all, against a socket in the WORLD (cm) and the direction * the hand moves into it (the socket's outward normal reversed). The socket is converted to arm space every frame, so the hand keeps * tracking it while the character walks up and turns. Ends in the named pose the arm had when it started (Hidden when it was in * a custom pose). False if a sequence is already running. Look at GetLastJackFrame().bReachable to know whether the fingertips * really are on the socket: a port too far away makes the arm stretch toward it and say so. */ bool StartJackIn(const FVector& PortWorldCm, const FVector& PlugDirectionWorld); /** Stops the sequence and blends to a pose. */ void AbortJackIn(EDescentArmPose ResumePose = EDescentArmPose::Hidden); bool IsJackActive() const { return bJackActive; } double GetJackTime() const { return JackTime; } EDescentArmJackPhase GetJackPhase() const { return JackPhase; } bool IsJackFeeding() const { return bJackActive && JackPhase == EDescentArmJackPhase::Feed; } const FDescentArmJackFrame& GetLastJackFrame() const { return LastJackFrame; } /** Fired once when the plug seats (the feed begins: flash, sound, port ring) and once when the sequence ends. */ FSimpleMulticastDelegate OnJackPlugged; FSimpleMulticastDelegate OnJackFinished; // ---- mounting and rendering ---- /** * Attaches this component to the camera at the shoulder offset of a right or left arm and applies the first-person settings: * parts are tagged as first-person primitives, cast no shadow, carry custom-depth stencil 1 (the "dry" mask, DESIGN 7.4) and * lighting channels 0 and 1 (DESIGN 5.1). They are drawn with the camera's own field of view (the game's 72 degrees) at true depth. * bConfigureCamera turns the camera's separate first-person FOV off and sets its first-person scale from the tuning header. */ void MountOnCamera(UCameraComponent* Camera, EDescentArmSide InSide, bool bConfigureCamera = true); /** The opposite: attaches the shoulder to NewParent's origin as an ordinary world object (first-person settings off, shadows on). */ void Unmount(USceneComponent* NewParent); /** Right or left arm: the left is the same model mirrored. */ void SetSide(EDescentArmSide InSide); EDescentArmSide GetSide() const { return Side; } bool IsMountedOnCamera() const { return MountedCamera.IsValid(); } /** Applies (or removes) the first-person rendering settings to all parts. Third-person showcases turn them off. */ void SetFirstPersonRendering(bool bOn); bool UsesFirstPersonRendering() const { return bFirstPersonRendering; } /** True while the camera the arm is mounted on is in dry air (inside the hull): the arm then takes no light from channel 0, the sea's. */ bool IsInDryAir() const { return bInDryAir; } // ---- space conversion and queries ---- FVector ArmToWorld(const FVector& ArmSpaceCm) const { return GetComponentTransform().TransformPosition(ArmSpaceCm); } FVector WorldToArm(const FVector& WorldCm) const { return GetComponentTransform().InverseTransformPosition(WorldCm); } FVector EffectorWorld(EDescentArmEffector Effector) const { return ArmToWorld(Descent::Arm::EffectorPosition(GetCurrentPose(), Effector)); } /** Drive Advance() from TickComponent (default). Tests and the sim can turn it off and call Advance themselves. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm") bool bTickSelf = true; /** Build the parts in BeginPlay. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm") bool bAutoBuild = true; /** Stop drawing the arm once it has settled in the Hidden pose (default). The showcase turns it off to photograph that pose. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm") bool bHideWhenHidden = true; /** Mounted on a camera, only the owner's own view draws the arm (default). Call SetFirstPersonRendering again after changing it. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Arm") bool bOnlyOwnerSeeWhenMounted = true; /** All live arm components of a world (for the commands and the state dump). */ static void GetAllInWorld(UWorld* World, TArray& Out); static UDescentArmViewmodelComponent* FindFirst(UWorld* World); protected: virtual void BeginPlay() override; virtual void OnRegister() override; virtual void OnUnregister() override; virtual void OnComponentDestroyed(bool bDestroyingHierarchy) override; virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; private: void ApplyCurrentPose(); void ApplyRenderSettings(); void ApplyCameraScale(); void UpdateVisibility(); void StepJack(double Dt); void FinishJack(); UPROPERTY(Transient) TArray> Parts; UPROPERTY(Transient) TWeakObjectPtr MountedCamera; FDescentArmBlender Blender; EDescentArmPose NamedPose = EDescentArmPose::Hidden; EDescentArmSide Side = EDescentArmSide::Right; FDescentArmReachResult LastReach; EDescentArmReachSource LastReachSource = EDescentArmReachSource::None; FVector LastReachTargetArm = FVector::ZeroVector; bool bCustomPose = false; FDescentArmJackInput JackInput; FVector JackPortWorld = FVector::ZeroVector; FVector JackDirWorld = FVector::ForwardVector; FDescentArmJackFrame LastJackFrame; EDescentArmJackPhase JackPhase = EDescentArmJackPhase::Inactive; EDescentArmPose JackResumePose = EDescentArmPose::Hidden; double JackTime = 0.0; bool bJackActive = false; bool bBuilt = false; bool bCameraConfigured = false; bool bInDryAir = false; // the camera it is mounted on is inside a dry volume: lighting channel 0 off bool bFirstPersonRendering = false; bool bMeshesVisible = true; bool bLoggedMissingMesh = false; };