#include "Vehicles/DescentCarrier.h" #include "Vehicles/CarrierBlockout.h" #include "Vehicles/CarrierCarousel.h" #include "Vehicles/CarrierLayout.h" #include "Core/DescentBoot.h" #include "Core/DescentEnvQuery.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Core/DescentStreamFocus.h" #include "Core/Tuning/DescentTune_Carrier.h" #include "Debug/DescentStateRegistry.h" #include "DescentUnits.h" #include "Systems/DescentFixture.h" #include "TheDescent.h" #include "Components/SceneComponent.h" #include "Engine/World.h" #include "Camera/CameraComponent.h" #include "Framework/Application/SlateApplication.h" #include "Systems/DescentCargo.h" #include "UI/DescentDash.h" #include "World/DescentTerrainSubsystem.h" #include "GameFramework/Pawn.h" #include "GameFramework/PlayerController.h" #include "EngineUtils.h" ADescentCarrier* UDescentCarrierSubsystem::GetCarrier(const UWorld* World) { const UDescentCarrierSubsystem* Sub = World ? World->GetSubsystem() : nullptr; return Sub ? Sub->Carrier.Get() : nullptr; } ADescentCarrier::ADescentCarrier() { PrimaryActorTick.bCanEverTick = true; // cosmetics only (lamp pulse, mast flash); state changes in the sim step USceneComponent* Root = CreateDefaultSubobject(TEXT("Root")); Root->SetMobility(EComponentMobility::Movable); SetRootComponent(Root); Blockout = CreateDefaultSubobject(TEXT("Blockout")); Blockout->SetupAttachment(Root); Carousel = CreateDefaultSubobject(TEXT("Carousel")); Carousel->SetupAttachment(Root); // one camera, moved between the five helm views (section 3.6) HelmCamera = CreateDefaultSubobject(TEXT("HelmCamera")); HelmCamera->SetupAttachment(Root); HelmCamera->bUsePawnControlRotation = false; HelmCamera->bConstrainAspectRatio = false; } bool ADescentCarrier::IsDry(const FVector& LocalM) const { return Descent::Carrier::IsDryLocal(LocalM); } void ADescentCarrier::BeginPlay() { Super::BeginPlay(); UWorld* World = GetWorld(); if (UDescentCarrierSubsystem* Sub = World->GetSubsystem()) { Sub->Carrier = this; } BaseYawDeg = GetActorRotation().Yaw; Blockout->Build(); Carousel->Build(); Helm.PosM = Descent::UUToMetres(GetActorLocation()); Helm.YawDeg = Helm.AimYawDeg = GetActorRotation().Yaw; SetHelmView(0); // the helm dash over the console (V6), fed in the Dashboards late step if (FSlateApplication::IsInitialized()) { HelmDash = NewObject(this, TEXT("HelmDash")); HelmDash->SetupAttachment(GetRootComponent()); HelmDash->RegisterComponent(); HelmDash->Setup(EDescentDashKind::Main); HelmDash->SetWorldWidthM(1.9f); HelmDash->SetRelativeLocationAndRotation(Descent::MetresToUU(FVector(45.0, 0.0, 2.95)), FRotator(8.0, 180.0, 0.0)); HelmDash->SetCollisionEnabled(ECollisionEnabled::NoCollision); HelmDash->SetCastShadow(false); UCarrierBlockoutComponent::MakeInterior(HelmDash); if (UDescentSimSubsystem* DashSim = UDescentSimSubsystem::Get(World)) { FDescentSimRegisterOptions DashOpt; DashOpt.Owner = TEXT("CarrierDash"); DashOpt.RateHz = 8.0; DashHandle = DashSim->RegisterLateStep(EDescentLateStep::Dashboards, [this](const FDescentSimFrame&) { if (HelmDash) { HelmDash->SetDashModel(MakeDashModel()); } }, DashOpt); } } // U5, U6: the carrier owns the dry space and its 26 degrees. if (UDescentEnvQuerySubsystem* Env = UDescentEnvQuerySubsystem::Get(World)) { TWeakObjectPtr Weak(this); DryHandle = Env->RegisterDryVolume(TEXT("Carrier"), [Weak](const FVector& WorldUU) { const ADescentCarrier* C = Weak.Get(); return C && C->IsDry(C->ToLocalM(WorldUU)); }); } // the terrain keeps the rock around the carrier loaded wherever the player is (section 1.5 step 4) if (UDescentStreamFocusSubsystem* Focus = UDescentStreamFocusSubsystem::Get(World)) { Focus->RegisterActorFocus(TEXT("Carrier"), this, Descent::Tune::Carrier::StreamFocusRadiusM); } if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(World)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Carrier"); StepHandle = Sim->RegisterStep(EDescentSimStep::Carrier, [this](const FDescentSimFrame& F) { SimStep(F.Dt); }, Opt); } // fixed stations (section 3.4) for (const Descent::Carrier::FAnchorDef& A : Descent::Carrier::FLayout::Get().Anchors) { ADescentFixture::SpawnFor(this, A); } } void ADescentCarrier::EndPlay(const EEndPlayReason::Type Reason) { UWorld* World = GetWorld(); if (Carousel) { Carousel->Shutdown(); } if (World) { if (UDescentEnvQuerySubsystem* Env = UDescentEnvQuerySubsystem::Get(World)) { Env->UnregisterDryVolume(DryHandle); } if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(World)) { Sim->Unregister(StepHandle); Sim->Unregister(DashHandle); } if (UDescentCarrierSubsystem* Sub = World->GetSubsystem()) { if (Sub->Carrier.Get() == this) { Sub->Carrier.Reset(); } } } Super::EndPlay(Reason); } void ADescentCarrier::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); if (Blockout && Blockout->IsBuilt()) { Blockout->UpdateCosmetics(GetWorld()->GetTimeSeconds()); // which room's lamps light the first-person arm (see UCarrierBlockoutComponent::SetViewerRoom) const APlayerController* PC = GetWorld()->GetFirstPlayerController(); if (const APawn* P = PC ? PC->GetPawn() : nullptr) { const Descent::Carrier::FRoomDef* Room = Descent::Carrier::FLayout::Get().RoomAt(ToLocalM(P->GetActorLocation())); Blockout->SetViewerRoom(Room ? Room->Id : NAME_None); } } } void ADescentCarrier::SetDebugMotion(const FCarrierDebugMotion& Motion) { DebugMotion = Motion; DebugMotion.bActive = true; DebugMotion.ElapsedSec = 0.0; BaseYawDeg = GetActorRotation().Yaw; } void ADescentCarrier::ClearDebugMotion() { DebugMotion = FCarrierDebugMotion(); VelMps = FVector::ZeroVector; YawRateDpsMeasured = 0.0; } void ADescentCarrier::SimStep(double Dt) { namespace T = Descent::Tune::Carrier; if (Dt <= 0.0) { return; } if (!DebugMotion.bActive) { // The helm (section 3.6). Nobody at it: all stop, and once the hull is at rest nothing is computed. if (!bHelmHeld && !Autopilot.bOn) { Helm.AllStop(); } Autopilot.Step(Helm, Dt); if (!bHelmHeld && !Autopilot.bOn && Helm.IsAtRest()) { VelMps = FVector::ZeroVector; YawRateDpsMeasured = 0.0; Helm.Contacts = 0; return; } Helm.Step(Dt, UDescentTerrainSubsystem::Get(GetWorld())); const double Roll = FMath::RadiansToDegrees(Descent::Tune::Helm::RollPerYawRate * Helm.YawRate); SetActorLocationAndRotation(Descent::MetresToUU(Helm.PosM), FRotator(Helm.PitchDeg, Helm.YawDeg, Roll), /*bSweep*/ false, nullptr, ETeleportType::None); VelMps = Helm.VelocityMps(); YawRateDpsMeasured = FMath::RadiansToDegrees(Helm.YawRate); ProximityTimer -= Dt; if (ProximityTimer <= 0.0) { ProximityTimer = 1.0 / Descent::Tune::Helm::ProximityHz; UpdateProximity(); } return; } DebugMotion.ElapsedSec += Dt; const FRotator OldRot = GetActorRotation(); const FVector OldPos = GetActorLocation(); const double Yaw = OldRot.Yaw + DebugMotion.YawRateDps * Dt; const double YawRateRad = FMath::DegreesToRadians(DebugMotion.YawRateDps); const double Pitch = DebugMotion.PitchAmplitudeDeg * FMath::Sin(2.0 * UE_DOUBLE_PI * DebugMotion.ElapsedSec / FMath::Max(0.1, DebugMotion.PitchPeriodSec)); const double Roll = FMath::RadiansToDegrees(T::RollPerYawRateRad * YawRateRad); const FRotator NewRot(Pitch, Yaw, Roll); const FVector WorldVel = NewRot.RotateVector(DebugMotion.VelocityLocalMps); // metres per second const FVector NewPos = OldPos + Descent::MetresToUU(WorldVel * Dt); SetActorLocationAndRotation(NewPos, NewRot, /*bSweep*/ false, nullptr, ETeleportType::None); VelMps = WorldVel; YawRateDpsMeasured = DebugMotion.YawRateDps; // the helm takes over from where the debug motion leaves the hull Helm.PosM = Descent::UUToMetres(NewPos); Helm.YawDeg = Helm.AimYawDeg = NewRot.Yaw; Helm.PitchDeg = 0.0; if (DebugMotion.RemainingSec > 0.0) { DebugMotion.RemainingSec -= Dt; if (DebugMotion.RemainingSec <= 0.0) { ClearDebugMotion(); SetActorRotation(FRotator(0.0, GetActorRotation().Yaw, 0.0), ETeleportType::None); // level again } } } // ---- boot: the carrier at the start pose (section 1.5 step 2) ----------------------------------------------------- DESCENT_BOOT_STEP(SpawnCarrier, Descent::Boot::Order::SpawnCarrier) { FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; Params.Name = TEXT("CANTICLE"); ADescentCarrier* C = Ctx.World->SpawnActor(Descent::MetresToUU(FVector(0.0, 0.0, Descent::Tune::Carrier::StartZM)), FRotator::ZeroRotator, Params); if (!C) { Ctx.Fail(TEXT("could not spawn the carrier")); } } // ---- state section `carrier` (section 9.4, the fields that exist at M2) ------------------------------------------- static FDescentStateProviderRegistrar GCarrierStateSection(TEXT("carrier"), [](UWorld* World, FJsonObject& Out) { using namespace Descent::Json; const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(World); Out.SetBoolField(TEXT("available"), C != nullptr); if (!C) { return; } const FRotator R = C->GetActorRotation(); const FVector P = Descent::UUToMetres(C->GetActorLocation()); Out.SetField(TEXT("pos_m"), Vec3(P)); Out.SetNumberField(TEXT("yaw_deg"), Round(R.Yaw)); Out.SetNumberField(TEXT("pitch_deg"), Round(R.Pitch)); Out.SetNumberField(TEXT("roll_deg"), Round(R.Roll)); Out.SetField(TEXT("vel_mps"), Vec3(C->VelocityMps())); Out.SetNumberField(TEXT("speed_mps"), Round(C->VelocityMps().Size())); Out.SetNumberField(TEXT("yaw_rate_dps"), Round(C->YawRateDps())); Out.SetNumberField(TEXT("depth_m"), Round(Descent::DepthOfZ(P.Z))); const FCarrierBlockoutStats S = C->Blockout->GatherStats(); Out.SetNumberField(TEXT("exterior_lights"), S.ExteriorLights); TSharedRef B = MakeShared(); B->SetNumberField(TEXT("rooms"), S.Rooms); B->SetNumberField(TEXT("mesh_components"), S.MeshComponents); B->SetNumberField(TEXT("sections"), S.Sections); B->SetNumberField(TEXT("triangles"), S.Triangles); B->SetNumberField(TEXT("convex_hulls"), S.ConvexHulls); B->SetNumberField(TEXT("interior_primitives"), S.InteriorPrimitives); B->SetNumberField(TEXT("interior_with_stencil_1"), S.InteriorWithStencil); B->SetNumberField(TEXT("interior_on_channel_1_only"), S.InteriorOnChannel1Only); B->SetNumberField(TEXT("room_lamps"), S.RoomLamps); B->SetNumberField(TEXT("signs"), S.Signs); Out.SetObjectField(TEXT("blockout"), B); if (const UCarrierCarouselComponent* K = C->Carousel) { static const TCHAR* const PhaseNames[] = { TEXT("Idle"), TEXT("Raising"), TEXT("Rotating"), TEXT("Lowering") }; TSharedRef Car = MakeShared(); Car->SetNumberField(TEXT("angle_deg"), Round(K->GetAngleDeg())); Car->SetBoolField(TEXT("busy"), K->IsBusy()); Car->SetStringField(TEXT("phase"), PhaseNames[static_cast(K->GetPhase())]); Car->SetNumberField(TEXT("launch_slot"), K->GetLaunchSlot()); Car->SetNumberField(TEXT("hoist_z"), Round(K->GetHoistZ())); Car->SetNumberField(TEXT("min_clearance_m"), Round(K->GetMinClearanceM())); Car->SetNumberField(TEXT("min_clearance_seen_m"), Round(K->GetMinClearanceSeenM())); Car->SetNumberField(TEXT("rotations"), K->GetRotationsDone()); Car->SetBoolField(TEXT("tender_out"), K->AnyTenderOut()); Out.SetObjectField(TEXT("carousel"), Car); } // the helm (section 9.4) Out.SetBoolField(TEXT("helm_held"), C->IsHelmHeld()); Out.SetNumberField(TEXT("throttle"), Round(C->Helm.Throttle)); Out.SetNumberField(TEXT("aim_yaw_deg"), Round(C->Helm.AimYawDeg)); Out.SetNumberField(TEXT("aim_pitch_deg"), Round(C->Helm.AimPitchDeg)); Out.SetNumberField(TEXT("ballast"), Round(C->Helm.Ballast)); Out.SetNumberField(TEXT("hull"), Round(C->Helm.Hull)); Out.SetNumberField(TEXT("power"), Round(C->Helm.Power)); Out.SetNumberField(TEXT("contacts"), C->Helm.Contacts); Out.SetNumberField(TEXT("scrape"), Round(C->Helm.Scrape)); Out.SetBoolField(TEXT("crippled"), C->Helm.IsCrippled()); Out.SetNumberField(TEXT("view"), C->GetHelmView()); { TSharedRef A = MakeShared(); A->SetBoolField(TEXT("on"), C->Autopilot.bOn); A->SetNumberField(TEXT("route"), C->Autopilot.Route + 1); A->SetNumberField(TEXT("index"), C->Autopilot.Index); A->SetBoolField(TEXT("stalled"), C->Autopilot.bStalled); A->SetBoolField(TEXT("done"), C->Autopilot.bDone); Out.SetObjectField(TEXT("autopilot"), A); } const FCarrierDebugMotion& M = C->GetDebugMotion(); TSharedRef D = MakeShared(); D->SetBoolField(TEXT("active"), M.bActive); D->SetNumberField(TEXT("elapsed_s"), Round(M.ElapsedSec)); Out.SetObjectField(TEXT("debug_motion"), D); }); // ---- the helm: views, dash, proximity (section 3.6) ---------------------------------------------------------------------- void ADescentCarrier::SetHelmHeld(bool bHeld) { bHelmHeld = bHeld; if (!bHeld) { Helm.AllStop(); SetHelmView(0); } } void ADescentCarrier::SetHelmView(int32 View) { HelmView = ((View % Descent::Tune::Helm::NumViews) + Descent::Tune::Helm::NumViews) % Descent::Tune::Helm::NumViews; struct FView { FVector PosM; FRotator Rot; float Fov; }; const FVector ChaseAt(-85.0, 0.0, 28.0); const FView Views[5] = { { FVector(44.2, 0.0, 3.15), FRotator::ZeroRotator, 72.f }, // BRIDGE: first person at the console { ChaseAt, (FVector(30.0, 0.0, 0.0) - ChaseAt).Rotation(), 60.f }, // CHASE { FVector(0.0, 0.0, 130.0), FRotator(-90.0, 0.0, 0.0), 60.f }, // TOP-DOWN, heading up { FVector(60.0, 0.0, -10.5), FRotator(-8.6, 0.0, 0.0), 85.f }, // BOW CAM { FVector(-5.0, 0.0, -14.5), FRotator(-69.0, 0.0, 0.0), 95.f } }; // KEEL CAM HelmCamera->SetRelativeLocationAndRotation(Descent::MetresToUU(Views[HelmView].PosM), Views[HelmView].Rot); HelmCamera->SetFieldOfView(Views[HelmView].Fov); } void ADescentCarrier::TeleportCarrier(const FVector& PosM, double YawDeg) { Helm.PosM = PosM; Helm.YawDeg = Helm.AimYawDeg = YawDeg; Helm.PitchDeg = Helm.AimPitchDeg = 0.0; Helm.VFwd = Helm.VLat = Helm.VBal = Helm.YawRate = 0.0; SetActorLocationAndRotation(Descent::MetresToUU(PosM), FRotator(0.0, YawDeg, 0.0), false, nullptr, ETeleportType::TeleportPhysics); if (UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(GetWorld())) { Terrain->RequestReplan(); } } FTenderDashModel ADescentCarrier::MakeDashModel() const { FTenderDashModel M; M.Title = TEXT("CANTICLE"); M.ToolText = FString::Printf(TEXT("HELM \u00B7 THROTTLE %d%% \u00B7 HDG %03d\u00B0 \u00B7 CONTACTS %d"), FMath::RoundToInt32(Helm.Throttle * 100.0), FMath::RoundToInt32(FMath::Fmod(Helm.YawDeg + 360.0, 360.0)), Helm.Contacts); M.DepthM = Descent::DepthOfZ(Descent::UUToMetres(GetActorLocation()).Z); // the carrier origin (section 5.9) M.RatingM = Descent::Tune::Helm::RatingM; M.HullPct = static_cast(Helm.Hull); M.PowerPct = static_cast(Helm.Power); M.TempC = Descent::Tune::Env::DryCabinTemperatureC; M.SpeedMps = static_cast(VelMps.Size()); if (const UDescentCargoSubsystem* Cargo = UDescentCargoSubsystem::Get(GetWorld())) { M.HoldUsed = static_cast(Cargo->StorageUsed()); // STORAGE u/c on the helm dash M.HoldCap = static_cast(Cargo->StorageCap); } return M; } void ADescentCarrier::UpdateProximity() { // A reduced proximity panel: six rays from the hull (the design has 22), 30 m range, refreshed at 4 Hz. UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(GetWorld()); Proximity.Reset(); if (!Terrain) { return; } const struct { const TCHAR* Label; FVector From, Dir; } Rays[6] = { { TEXT("BOW"), FVector(60, 0, 0), FVector(1, 0, 0) }, { TEXT("STERN"), FVector(-60, 0, 0), FVector(-1, 0, 0) }, { TEXT("PORT"), FVector(0, -12, 0), FVector(0, -1, 0) }, { TEXT("STBD"), FVector(0, 12, 0), FVector(0, 1, 0) }, { TEXT("KEEL"), FVector(0, 0, -12.9), FVector(0, 0, -1) }, { TEXT("SAIL"), FVector(8, 0, 18.2), FVector(0, 0, 1) } }; const FTransform Xf = GetActorTransform(); for (const auto& R : Rays) { const double D = Terrain->Raycast(Descent::UUToMetres(Xf.TransformPosition(Descent::MetresToUU(R.From))), Xf.TransformVectorNoScale(R.Dir), Descent::Tune::Helm::ProximityRangeM); FProximityRow Row; Row.Label = R.Label; Row.DistanceM = D < 0.0 ? Descent::Tune::Helm::ProximityRangeM : D; Proximity.Add(Row); } }