#include "Player/Body/DescentPlayerBodyComponent.h" #include "Player/Body/DescentBodyAnim.h" #include "Player/Body/DescentBodyProfile.h" #include "Player/Arm/DescentArmViewmodelComponent.h" #include "Player/DescentCharacter.h" #include "Player/DescentCharacterMovement.h" #include "Core/DescentEnvQuery.h" #include "Core/Tuning/DescentTune_Arm.h" #include "Core/Tuning/DescentTune_Body.h" #include "Core/Tuning/DescentTune_Player.h" #include "Debug/DescentStateRegistry.h" #include "TheDescent.h" #include "Camera/CameraComponent.h" #include "Components/CapsuleComponent.h" #include "Components/SkeletalMeshComponent.h" #include "Engine/SkeletalMesh.h" #include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "HAL/IConsoleManager.h" #include "Materials/MaterialInterface.h" namespace { namespace BodyTune = Descent::Tune::Body; TAutoConsoleVariable CVarBodyOldArm(TEXT("descent.Body.OldArm"), BodyTune::OldArmViewDefault, TEXT("0 = the Twinblast body is the player's first-person view (default). 1 = the old cybernetic arm viewmodel, no body.")); TAutoConsoleVariable CVarBodyGuns(TEXT("descent.Body.Guns"), BodyTune::GunsShownDefault, TEXT("0 = the body's two pistols are not drawn (default). 1 = drawn.")); // The view numbers as console variables, so they can be tried in a running game; the defaults are the tuning header's. TAutoConsoleVariable CVarBodyViewBack(TEXT("descent.Body.ViewBackCm"), static_cast(BodyTune::ViewBackCm), TEXT("Own view: the mesh stands this far behind the capsule axis.")); TAutoConsoleVariable CVarBodyViewBackDown(TEXT("descent.Body.ViewBackDownCm"), static_cast(BodyTune::ViewBackLookDownCm), TEXT("Own view: added to ViewBackCm in proportion to the down pitch.")); TAutoConsoleVariable CVarBodyFpScale(TEXT("descent.Body.FpScale"), static_cast(BodyTune::FirstPersonScale), TEXT("Own view: Unreal's first-person scale for the body (1 = true depth).")); TAutoConsoleVariable CVarBodyArmRaiseL(TEXT("descent.Body.ArmRaiseL"), static_cast(BodyTune::ArmRaiseLeftDeg), TEXT("Own view: left upper arm turned up, degrees.")); TAutoConsoleVariable CVarBodyArmInL(TEXT("descent.Body.ArmInL"), static_cast(BodyTune::ArmInLeftDeg), TEXT("Own view: left upper arm turned toward the middle, degrees.")); TAutoConsoleVariable CVarBodyArmRaiseR(TEXT("descent.Body.ArmRaiseR"), static_cast(BodyTune::ArmRaiseRightDeg), TEXT("Own view: right upper arm turned up, degrees.")); TAutoConsoleVariable CVarBodyArmInR(TEXT("descent.Body.ArmInR"), static_cast(BodyTune::ArmInRightDeg), TEXT("Own view: right upper arm turned toward the middle, degrees.")); TAutoConsoleVariable CVarBodyArmsUp(TEXT("descent.Body.ArmsUp"), static_cast(BodyTune::ArmsUpDefault), TEXT("Own view: 0 = the arms hang and swing as the animation has them (default). 1 = the upper arms turned up and inward so the hands hover in view (the look of 2026-10-11).")); } namespace { TAutoConsoleVariable CVarBodyAnchor(TEXT("descent.Body.Anchor"), BodyTune::AnchorDefault, TEXT("Own view: 1 = the body is placed under the camera every frame (neck behind the eye, leaning back when looking down; default). 0 = the fixed offsets of before.")); TAutoConsoleVariable CVarBodyNeckBehind(TEXT("descent.Body.NeckBehindCm"), static_cast(BodyTune::NeckBehindCm), TEXT("Own view: the neck bone this far behind the eye.")); TAutoConsoleVariable CVarBodyLean(TEXT("descent.Body.LookDownLeanDeg"), static_cast(BodyTune::LookDownLeanDeg), TEXT("Own view: how far the body leans back (legs forward into the picture) at 60 degrees of down pitch.")); } double UDescentPlayerBodyComponent::ArmsUp() { return FMath::Clamp(static_cast(CVarBodyArmsUp.GetValueOnGameThread()), 0.0, 1.0); } namespace { TAutoConsoleVariable CVarBodySwimArms(TEXT("descent.Body.SwimArms"), BodyTune::SwimArmsDefault, TEXT("Swimming: 1 = the arm stroke is made in code, hands placed in view (breaststroke forward, sculling while treading; default). 0 = the swim clips' own arms.")); } double UDescentPlayerBodyComponent::SwimArms() { return CVarBodySwimArms.GetValueOnGameThread() != 0 ? 1.0 : 0.0; } double UDescentPlayerBodyComponent::LookDownLeanDeg(double DownDeg, double LeanMaxDeg) { // none looking ahead, all of it at LeanFullDownDeg, then less again toward the pitch limit (straight down the feet are under him anyway) const double Up = FMath::SmoothStep(BodyTune::LeanFromDownDeg, BodyTune::LeanFullDownDeg, DownDeg); const double Past = FMath::Clamp((DownDeg - BodyTune::LeanFullDownDeg) / (Descent::Tune::Player::PitchLimitDeg - BodyTune::LeanFullDownDeg), 0.0, 1.0); return LeanMaxDeg * Up * FMath::Lerp(1.0, BodyTune::LeanAtLimitShare, Past); } void UDescentPlayerBodyComponent::StartJack(const FVector* PortWorldUU) { JackSec = 0.0; const AActor* Owner = GetOwner(); bJackPort = PortWorldUU != nullptr && Owner != nullptr; if (bJackPort) { JackPortLocal = Owner->GetActorTransform().InverseTransformPosition(*PortWorldUU); } } void UDescentPlayerBodyComponent::NotifyUse() { UseSec = 0.0; } bool UDescentPlayerBodyComponent::GetViewBobSignal(double AheadSec, double& OutSignal, double* OutFootfallHz) const { OutSignal = 0.0; if (OutFootfallHz) { *OutFootfallHz = 0.0; } const UDescentBodyAnimInstance* Anim = bShown ? GetAnim() : nullptr; return Anim && Anim->GetState().ViewBob(Anim->GetLengths(), AheadSec, OutSignal, OutFootfallHz); } UDescentPlayerBodyComponent::UDescentPlayerBodyComponent() { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bStartWithTickEnabled = true; PrimaryComponentTick.TickGroup = TG_PrePhysics; } bool UDescentPlayerBodyComponent::IsOldArmView() { return CVarBodyOldArm.GetValueOnGameThread() != 0; } void UDescentPlayerBodyComponent::SetOldArmView(bool bOldArm) { CVarBodyOldArm->Set(bOldArm ? 1 : 0, ECVF_SetByConsole); } bool UDescentPlayerBodyComponent::AreGunsWanted() { return CVarBodyGuns.GetValueOnGameThread() != 0; } void UDescentPlayerBodyComponent::SetGunsWanted(bool bShow) { CVarBodyGuns->Set(bShow ? 1 : 0, ECVF_SetByConsole); } void UDescentPlayerBodyComponent::ConfigureMeshDefaults(USkeletalMeshComponent* Mesh, double CapsuleHalfHeightCm) { if (!Mesh) { return; } Mesh->SetRelativeLocation(FVector(0.0, 0.0, -CapsuleHalfHeightCm)); // yaw and scale are the model's: Setup takes them from its profile Mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); // the capsule is the collision Mesh->SetGenerateOverlapEvents(false); Mesh->SetCanEverAffectNavigation(false); Mesh->SetReceivesDecals(false); Mesh->SetVisibility(false); // nothing to draw until Setup gave it the mesh } USkeletalMeshComponent* UDescentPlayerBodyComponent::GetBodyMesh() const { const ACharacter* Ch = Cast(GetOwner()); return Ch ? Ch->GetMesh() : nullptr; } UDescentBodyAnimInstance* UDescentPlayerBodyComponent::GetAnim() const { const USkeletalMeshComponent* Mesh = GetBodyMesh(); return Mesh ? Cast(Mesh->GetAnimInstance()) : nullptr; } void UDescentPlayerBodyComponent::BeginPlay() { Super::BeginPlay(); Setup(); // After the movement (the velocity of this frame), before the mesh (which evaluates the pose from what is set here). if (ACharacter* Ch = Cast(GetOwner())) { AddTickPrerequisiteActor(Ch); if (Ch->GetCharacterMovement()) { AddTickPrerequisiteComponent(Ch->GetCharacterMovement()); } if (Ch->GetMesh()) { Ch->GetMesh()->AddTickPrerequisiteComponent(this); } } } bool UDescentPlayerBodyComponent::Setup() { if (bSetUp) { return true; } USkeletalMeshComponent* Mesh = GetBodyMesh(); if (!Mesh) { return false; } // Everything of the model comes from its profile (Config/DescentBody.json): nothing below names a model. const FDescentBodyProfile& Profile = FDescentBodyProfile::Get(); USkeletalMesh* Asset = Profile.bValid ? LoadObject(nullptr, *Profile.MeshPath, nullptr, LOAD_NoWarn | LOAD_Quiet) : nullptr; if (!Asset) { if (!bLoggedMissing) { bLoggedMissing = true; UE_LOG(LogDescent, Error, TEXT("Body: model '%s': %s. The player has no body."), *Profile.Name, Profile.bValid ? *FString::Printf(TEXT("mesh %s is missing"), *Profile.MeshPath) : *Profile.Error); } return false; } Mesh->SetSkeletalMeshAsset(Asset); Mesh->SetRelativeRotation(FRotator(0.0, Profile.YawDeg, 0.0)); Mesh->SetAnimInstanceClass(UDescentBodyAnimInstance::StaticClass()); UDescentBodyAnimInstance* NewAnim = GetAnim(); if (NewAnim) { NewAnim->LoadSequences(); } Mesh->SetRelativeScale3D(FVector((NewAnim && NewAnim->ActiveSet() == 0) ? Profile.LegacyScale : Profile.Scale)); // the guns: material slots not drawn (descent.Body.Guns draws them) GunMaterialIndices.Reset(); for (const FName& Slot : Profile.GunSlots) { const int32 Index = Mesh->GetMaterialIndex(Slot); if (Index != INDEX_NONE) { GunMaterialIndices.Add(Index); } else { UE_LOG(LogDescent, Error, TEXT("Body: model '%s' has no material slot %s (gun_slots)."), *Profile.Name, *Slot.ToString()); } } // our own materials on slots of THIS component (Twinblast: the machine legs). The model's material assets are untouched. OverrideIndices.Reset(); OverrideMaterials.Reset(); for (const TPair& Override : Profile.MaterialOverrides) { const int32 Index = Mesh->GetMaterialIndex(Override.Key); UMaterialInterface* Material = LoadObject(nullptr, *Override.Value, nullptr, LOAD_NoWarn | LOAD_Quiet); if (Index == INDEX_NONE || !Material) { UE_LOG(LogDescent, Error, TEXT("Body: model '%s': material override %s -> %s cannot be applied (slot %d, material %s). Run Tools\\import_assets.ps1."), *Profile.Name, *Override.Key.ToString(), *Override.Value, Index, Material ? TEXT("found") : TEXT("missing")); continue; } Mesh->SetMaterial(Index, Material); OverrideIndices.Add(Index); OverrideMaterials.Add(Material); } bOverridesComplete = OverrideIndices.Num() == Profile.MaterialOverrides.Num(); bSetUp = true; bGunsShown = true; ApplyGuns(AreGunsWanted()); bViewApplied = false; bLightApplied = false; Refresh(0.0); return true; } void UDescentPlayerBodyComponent::ApplyGuns(bool bShow) { USkeletalMeshComponent* Mesh = GetBodyMesh(); if (!Mesh) { return; } // Guns are material sections of the body mesh (Twinblast: both pistols are one, in every LOD). A hidden material is not drawn in // any pose, including the reference pose where the pistol bones lie at the feet. (An invalid section index: the material index is used as it is.) for (const int32 Index : GunMaterialIndices) { for (int32 Lod = 0; Lod < Mesh->GetNumLODs(); ++Lod) { Mesh->ShowMaterialSection(Index, INDEX_NONE, bShow, Lod); } } bGunsShown = bShow; } bool UDescentPlayerBodyComponent::IsPistolSectionShown() const { USkeletalMeshComponent* Mesh = GetBodyMesh(); if (!Mesh) { return false; } for (const int32 Index : GunMaterialIndices) { for (int32 Lod = 0; Lod < Mesh->GetNumLODs(); ++Lod) { if (Mesh->IsMaterialSectionShown(Index, Lod)) { return true; } } } return false; } bool UDescentPlayerBodyComponent::IsHeadHidden() const { USkeletalMeshComponent* Mesh = GetBodyMesh(); return Mesh && Mesh->GetSkeletalMeshAsset() && Mesh->IsBoneHiddenByName(FDescentBodyProfile::Get().Bones.Head); } bool UDescentPlayerBodyComponent::IsLegMaterialApplied() const { // (the name is from when Twinblast's legs were the only override: every material override of the profile is on its slot) const USkeletalMeshComponent* Mesh = GetBodyMesh(); if (!Mesh || !bOverridesComplete) { return false; } for (int32 I = 0; I < OverrideIndices.Num(); ++I) { if (Mesh->GetMaterial(OverrideIndices[I]) != OverrideMaterials[I]) { return false; } } return true; } double UDescentPlayerBodyComponent::MeasureEyeHeightCm() const { const ADescentCharacter* Ch = Cast(GetOwner()); const USkeletalMeshComponent* Mesh = GetBodyMesh(); const FName Socket = FDescentBodyProfile::Get().EyeSocket; if (!Ch || !Mesh || !Mesh->DoesSocketExist(Socket)) { return -1.0; } return Mesh->GetSocketLocation(Socket).Z - Ch->GetFeetWorldUU().Z; } void UDescentPlayerBodyComponent::ApplyView(USkeletalMeshComponent* Mesh, bool bOwn) { bOwnView = bOwn; bViewApplied = true; // The player's own view: a first-person primitive (drawn at the camera's first-person scale, see Refresh), no head in front of // the eye. Any other camera sees the whole man where he stands. Mesh->SetFirstPersonPrimitiveType(bOwn ? EFirstPersonPrimitiveType::FirstPerson : EFirstPersonPrimitiveType::None); Mesh->SetCastShadow(bOwn ? BodyTune::CastShadowInOwnView : BodyTune::CastShadowOtherViews); for (const FName& Bone : FDescentBodyProfile::Get().OwnViewHiddenBones) { if (bOwn) { Mesh->HideBoneByName(Bone, PBO_None); } else { Mesh->UnHideBoneByName(Bone); } } // DESIGN 7.4: like the old arm, the body carries the "dry" stencil, so the water post-process leaves it alone. Mesh->SetRenderCustomDepth(true); Mesh->SetCustomDepthStencilValue(BodyTune::DryStencilValue); } void UDescentPlayerBodyComponent::Refresh(double DeltaSeconds) { ADescentCharacter* Ch = Cast(GetOwner()); USkeletalMeshComponent* Mesh = Ch ? Ch->GetMesh() : nullptr; if (!Ch || !Mesh || !bSetUp) { return; } const bool bOldArm = IsOldArmView(); const APlayerController* PC = Cast(Ch->GetController()); const bool bOwn = !bForceThirdPerson && PC && PC->GetViewTarget() == Ch; // Not drawn in a Tender (the cockpit has its own view and the seat is not modelled for him) and not with the old arm. const bool bShow = !bOldArm && !Ch->IsInTender(); if (bShow != bShown || !bViewApplied) { bShown = bShow; Mesh->SetVisibility(bShow, false); } if (!bViewApplied || bOwn != bOwnView) { ApplyView(Mesh, bOwn); } // The camera's first-person scale belongs to whichever of the two is the view: the body's here, the arm's own otherwise. if (UCameraComponent* Camera = Ch->Camera) { if (!bOldArm) { const double Scale = FMath::Clamp(static_cast(CVarBodyFpScale.GetValueOnGameThread()), 0.05, 1.0); Camera->SetEnableFirstPersonFieldOfView(false); // the body stands in the world: it shares the world's perspective Camera->SetEnableFirstPersonScale(Scale < 1.0); Camera->SetFirstPersonScale(static_cast(Scale)); } else if (!bWasOldArm && !(Ch->Arm && Ch->Arm->IsJackActive())) { Camera->SetEnableFirstPersonScale(Descent::Tune::Arm::FirstPersonScale < 1.0); Camera->SetFirstPersonScale(static_cast(Descent::Tune::Arm::FirstPersonScale)); } } bWasOldArm = bOldArm; // Where the mesh stands under the camera (see ViewBackCm). const double HalfHeight = Ch->GetCapsuleComponent()->GetUnscaledCapsuleHalfHeight(); const double Pitch = FRotator::NormalizeAxis(Ch->GetControlRotation().Pitch); const double Down = FMath::Clamp(-Pitch / Descent::Tune::Player::PitchLimitDeg, 0.0, 1.0); UDescentBodyAnimInstance* AnimNow = GetAnim(); const int32 Set = AnimNow ? AnimNow->ActiveSet() : 0; // The retargeted set stands upright where the pack's idle crouches: its own scale keeps the eyes at the camera. const FDescentBodyProfile& Profile = FDescentBodyProfile::Get(); const double Scale = Set == 1 ? Profile.Scale : Profile.LegacyScale; if (!FMath::IsNearlyEqual(Mesh->GetRelativeScale3D().Z, Scale, 1e-4)) { Mesh->SetRelativeScale3D(FVector(Scale)); } const double Back = bOwn ? CVarBodyViewBack.GetValueOnGameThread() + (Set == 1 ? BodyTune::ViewBackRetargetedCm : 0.0) + CVarBodyViewBackDown.GetValueOnGameThread() * Down : 0.0; // Swimming with the retargeted clips the body leans forward: it is moved so the head stays where the camera is. FVector Shift = FVector::ZeroVector; if (AnimNow && AnimNow->GetState().SwimAlpha > 1e-3) { const FDescentBodyAnimState& St = AnimNow->GetState(); Shift = St.SwimAlpha * FMath::Lerp(BodyTune::SwimShiftIdleCm(), BodyTune::SwimShiftMoveCm(), St.SwimMove); } // Since the second task of 2026-10-12 the own view places the body inside the pose instead (the neck under the camera, see the // animation input below): the component then stands on the capsule axis. descent.Body.Anchor 0 gives the fixed offsets back. const bool bAnchorOn = CVarBodyAnchor.GetValueOnGameThread() != 0; Mesh->SetRelativeLocation(bAnchorOn ? FVector(0.0, 0.0, -HalfHeight) : FVector(-Back + Shift.X, Shift.Y, -HalfHeight + Shift.Z)); // the clocks of the jack-in (it ends with the freeze that holds him at the port) and of the last "use" if (JackSec >= 0.0) { JackSec += DeltaSeconds; if (JackSec >= BodyTune::JackEndSec || (JackSec > 0.2 && !Ch->IsFrozen())) { JackSec = -1.0; } } if (UseSec >= 0.0) { UseSec += DeltaSeconds; if (UseSec > 5.0) { UseSec = -1.0; } } if (AreGunsWanted() != bGunsShown) { ApplyGuns(AreGunsWanted()); } // Light: as the old arm took it. In dry air only channel 2 (the lamps of the room the player stands in, his flashlight and the // room fill beside the eye), in the water channels 0 and 1 (the sea's light and the hull's floodlights). const bool bDry = Descent::IsDry(GetWorld(), Ch->GetActorLocation()); if (!bLightApplied || bDry != bInDryAir) { bLightApplied = true; bInDryAir = bDry; Mesh->SetLightingChannels(!bDry, !bDry, bDry); } // The animation follows the real movement. if (UDescentBodyAnimInstance* Anim = GetAnim()) { const UCharacterMovementComponent* Move = Ch->GetCharacterMovement(); const FVector Local = Move ? Ch->GetActorRotation().UnrotateVector(Move->Velocity) : FVector::ZeroVector; FDescentBodyAnimInput& In = Anim->Input; In.VelLocalCmS = FVector2D(Local.X, Local.Y); In.VerticalCmS = Local.Z; In.bInAir = Move && Move->IsFalling(); In.bSwimming = Ch->IsSwimming(); In.bHeld = Ch->IsSeated() || Ch->IsFrozen(); In.bRelaxed = Ch->IsSeated() && !Ch->IsInTender(); // at the helm: arms down, not aimed at the console // The arm turn of the own view: full looking ahead, a little more looking up, gone looking down (see the tuning header). const double Follow = Pitch > 0.0 ? BodyTune::ArmFollowLookUp * Pitch : 0.0; // descent.Body.ArmsUp scales the whole turn; 0 (default) leaves the arms where the animation has them. const double Arms = bOwn ? ArmsUp() * (1.0 - FMath::SmoothStep(BodyTune::ArmFadeFromDownDeg, BodyTune::ArmFadeToDownDeg, FMath::Max(0.0, -Pitch))) : 0.0; In.ArmRaiseLeftDeg = Arms * (CVarBodyArmRaiseL.GetValueOnGameThread() + Follow); In.ArmInLeftDeg = Arms * CVarBodyArmInL.GetValueOnGameThread(); In.ArmRaiseRightDeg = Arms * (CVarBodyArmRaiseR.GetValueOnGameThread() + Follow); In.ArmInRightDeg = Arms * CVarBodyArmInR.GetValueOnGameThread(); In.bOwnView = bOwn; In.JackSec = JackSec; In.UseSec = UseSec; // The body under the camera (own view). Everything is in the capsule's frame (x ahead, z up, cm) and handed over in the mesh // component's space; the camera's own place (with its bob) is only read. const FDescentBodyAnimState& St = Anim->GetState(); const double DownDeg = FMath::Max(0.0, -Pitch); const double Swim = FMath::Clamp(St.SwimAlpha, 0.0, 1.0); // On the ground only the own view anchors (and leans when he looks down). Swimming every view does: the body hangs on the // eye and turns with the view's pitch as one piece, so it is always behind and under the picture and the code-made arms, // whose hands are placed in the eye's frame, meet their shoulders. In.AnchorWeight = bAnchorOn ? (bOwn ? 1.0 : Swim) : 0.0; const double GroundLean = (bOwn && bAnchorOn) ? LookDownLeanDeg(DownDeg, CVarBodyLean.GetValueOnGameThread()) : 0.0; In.LeanDeg = bAnchorOn ? FMath::Lerp(GroundLean, Pitch, Swim) : 0.0; In.bFlatFeet = Swim < 0.5; In.AnchorVertical = Swim; In.SwimArms = bAnchorOn ? SwimArms() : 0.0; if (Ch->Camera) { const FVector Eye = Ch->Camera->GetRelativeLocation(); const FTransform EyeFrame(FRotator(Pitch, 0.0, 0.0), Eye); // in the capsule's frame: the view's pitch at the camera's place const FVector Ground = Eye + FVector(-(CVarBodyNeckBehind.GetValueOnGameThread() + BodyTune::NeckBehindPerLeanDeg * GroundLean), 0.0, 0.0); const FVector Water = EyeFrame.TransformPosition(FVector(BodyTune::SwimNeckAheadCm, 0.0, -BodyTune::SwimNeckBelowCm)); const FTransform CapsuleToComponent = Mesh->GetRelativeTransform().Inverse(); In.AnchorTarget = CapsuleToComponent.TransformPosition(FMath::Lerp(Ground, Water, Swim)); In.EyeToComponent = EyeFrame * CapsuleToComponent; } // The jack-in: the right hand goes to the port (two-bone IK in the pose). In the own view the port is usually further than // the arm is long: the hand then goes to the point on the line from the eye to the port that the arm reaches, so it is seen // ON the port (a first-person body is drawn nearer than it stands anyway). In.bJackTarget = JackSec >= 0.0 && bJackPort; if (In.bJackTarget) { const FVector Port = Ch->GetActorTransform().TransformPosition(JackPortLocal); FVector Hand = Port; if (bOwn) { const FVector EyeW = Ch->EyeWorldUU(), Shoulder = Mesh->GetBoneLocation(Profile.Bones.UpperArmR); const FVector Dir = (Port - EyeW).GetSafeNormal(), W = EyeW - Shoulder; const double B = FVector::DotProduct(W, Dir), Disc = B * B - (W.SizeSquared() - FMath::Square(BodyTune::JackArmReachCm)); if (Disc >= 0.0) { Hand = EyeW + Dir * FMath::Min(-B + FMath::Sqrt(Disc), FVector::Dist(Port, EyeW)); } } In.JackTarget = Mesh->GetComponentTransform().InverseTransformPosition(Hand); } } } void UDescentPlayerBodyComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Refresh(DeltaTime); } // ---- state section "body" ------------------------------------------------------------------------------------------------ static FDescentStateProviderRegistrar GBodyStateSection(TEXT("body"), [](UWorld* World, FJsonObject& Out) { using namespace Descent::Json; const APlayerController* PC = World ? World->GetFirstPlayerController() : nullptr; const ADescentCharacter* Ch = PC ? Cast(PC->GetPawn()) : nullptr; const UDescentPlayerBodyComponent* Body = Ch ? Ch->Body.Get() : nullptr; Out.SetBoolField(TEXT("available"), Body != nullptr && Body->IsSetUp()); Out.SetBoolField(TEXT("old_arm_view"), UDescentPlayerBodyComponent::IsOldArmView()); Out.SetBoolField(TEXT("guns_wanted"), UDescentPlayerBodyComponent::AreGunsWanted()); if (!Body || !Body->IsSetUp()) { return; } const USkeletalMeshComponent* Mesh = Body->GetBodyMesh(); Out.SetStringField(TEXT("mesh"), (Mesh && Mesh->GetSkeletalMeshAsset()) ? Mesh->GetSkeletalMeshAsset()->GetName() : FString()); Out.SetBoolField(TEXT("shown"), Body->IsBodyShown()); Out.SetBoolField(TEXT("own_view"), Body->IsOwnView()); Out.SetBoolField(TEXT("head_hidden"), Body->IsHeadHidden()); Out.SetBoolField(TEXT("guns_shown"), Body->AreGunsShown()); Out.SetBoolField(TEXT("pistol_section_shown"), Body->IsPistolSectionShown()); Out.SetBoolField(TEXT("leg_material"), Body->IsLegMaterialApplied()); Out.SetBoolField(TEXT("in_dry_air"), Body->IsInDryAir()); Out.SetNumberField(TEXT("scale"), Mesh ? Round(Mesh->GetRelativeScale3D().Z) : 0.0); Out.SetNumberField(TEXT("mesh_back_cm"), Mesh ? Round(-Mesh->GetRelativeLocation().X) : 0.0); Out.SetNumberField(TEXT("eye_height_cm"), Round(Body->MeasureEyeHeightCm())); if (const UDescentBodyAnimInstance* Anim = Body->GetAnim()) { const FDescentBodyAnimState& S = Anim->GetState(); Out.SetNumberField(TEXT("sequences_loaded"), Anim->CountLoadedSequences()); Out.SetStringField(TEXT("motion"), Descent::Body::MotionName(S.Motion())); Out.SetNumberField(TEXT("speed_cm_s"), Round(S.SpeedCmS)); Out.SetNumberField(TEXT("move_alpha"), Round(S.MoveAlpha)); Out.SetNumberField(TEXT("action_alpha"), Round(S.ActionAlpha)); Out.SetStringField(TEXT("loco_clip"), Descent::Body::SeqAssetName(S.Loco.Cur)); Out.SetNumberField(TEXT("loco_rate"), Round(S.LocoRate)); Out.SetStringField(TEXT("action_clip"), Descent::Body::SeqAssetName(S.Act.Cur)); } });