#include "Life/DescentFauna.h" #include "Core/DescentMath.h" #include "Core/DescentSimSubsystem.h" #include "Debug/DescentStateRegistry.h" #include "Player/DescentCharacter.h" #include "Player/DescentPlayerController.h" #include "Vehicles/CarrierHullShape.h" #include "Vehicles/DescentCarrier.h" #include "Vehicles/KazahTender.h" #include "Vehicles/TenderFlight.h" #include "World/DescentTerrainSubsystem.h" #include "DescentUnits.h" #include "TheDescent.h" #include "Components/InstancedStaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "Engine/World.h" #include "Materials/MaterialInstanceDynamic.h" namespace DF = Descent::Fauna; namespace Descent::Fauna { const TArray& Homes() { // silverfin above Z -400, lanternfish below (section 6.9). The Seam Gallery home is left out: the world definition has no such place yet. static const TArray Table = { { FVector(150, 20, -90), 0 }, { FVector(450, 10, -150), 0 }, { FVector(700, -30, -200), 0 }, { FVector(520, -470, -215), 0 }, { FVector(355, 262, -226), 0 }, { FVector(710, 205, -272), 1 }, { FVector(692, 212, -266), 1 }, { FVector(500, -200, -680), 1 }, { FVector(760, -300, -720), 1 }, { FVector(640, -420, -640), 1 }, { FVector(480, -700, -900), 1 }, { FVector(850, -400, -1010), 1 }, { FVector(1000, -480, -1040), 1 }, }; return Table; } FName SpeciesName(uint8 Species) { return Species == 1 ? FName(TEXT("lanternfish")) : FName(TEXT("silverfin")); } FVector HullExclusion(FVector& L, bool* bOutWasInside) { double D = Descent::Carrier::HullDistance(L, false); if (bOutWasInside) { *bOutWasInside = D < 0.0; } if (D >= HullSteerMarginM) { return FVector::ZeroVector; } const double H = 0.3; FVector N(Descent::Carrier::HullDistance(L + FVector(H, 0, 0), false) - Descent::Carrier::HullDistance(L - FVector(H, 0, 0), false), Descent::Carrier::HullDistance(L + FVector(0, H, 0), false) - Descent::Carrier::HullDistance(L - FVector(0, H, 0), false), Descent::Carrier::HullDistance(L + FVector(0, 0, H), false) - Descent::Carrier::HullDistance(L - FVector(0, 0, H), false)); if (!N.Normalize()) { N = FVector(0.0, L.Y, L.Z).GetSafeNormal(); if (N.IsNearlyZero()) { N = FVector::UpVector; } } // found inside the hard margin: projected out in the same step (repeated, because the distance field is not exact deep inside) for (int32 I = 0; I < 6 && D < HullHardMarginM; ++I) { L += N * (HullHardMarginM - D + 0.05); D = Descent::Carrier::HullDistance(L, false); } return N * (30.0 * FMath::Max(0.0, HullSteerMarginM - D)); } } namespace { UMaterialInstanceDynamic* FaunaSurface(UObject* Outer, const FLinearColor& Base, const FLinearColor& Emissive, float Strength) { UMaterialInterface* Parent = LoadObject(nullptr, TEXT("/Game/Materials/M_DescentSurface.M_DescentSurface"), nullptr, LOAD_NoWarn); if (!Parent) { return nullptr; } UMaterialInstanceDynamic* MID = UMaterialInstanceDynamic::Create(Parent, Outer); MID->SetVectorParameterValue(TEXT("BaseColor"), Base); MID->SetVectorParameterValue(TEXT("EmissiveColor"), Emissive); MID->SetScalarParameterValue(TEXT("EmissiveStrength"), Strength); MID->SetScalarParameterValue(TEXT("Roughness"), 0.35f); return MID; } } UDescentFaunaSubsystem* UDescentFaunaSubsystem::Get(const UWorld* World) { return World ? World->GetSubsystem() : nullptr; } void UDescentFaunaSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); for (const DF::FHomeDef& D : DF::Homes()) { FHome H; H.Def = D; H.Target = D.PosM; HomeStates.Add(H); } if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(&InWorld)) { FDescentSimRegisterOptions Opt; Opt.Owner = TEXT("Fauna"); Opt.RateHz = 20.0; StepHandle = Sim->RegisterStep(EDescentSimStep::Fauna, [this](const FDescentSimFrame& F) { Step(F.Dt); }, Opt); } } void UDescentFaunaSubsystem::Deinitialize() { if (UDescentSimSubsystem* Sim = UDescentSimSubsystem::Get(GetWorld())) { Sim->Unregister(StepHandle); } Super::Deinitialize(); } int32 UDescentFaunaSubsystem::ActiveHomes() const { int32 N = 0; for (const FHome& H : HomeStates) { N += H.bActive ? 1 : 0; } return N; } int32 UDescentFaunaSubsystem::FishAlive() const { int32 N = 0; for (const FHome& H : HomeStates) { N += H.Fish.Num(); } return N; } int32 UDescentFaunaSubsystem::NumPolyps() const { int32 N = 0; for (const FPolyp& P : Polyps) { N += P.bAlive ? 1 : 0; } return N; } bool UDescentFaunaSubsystem::NearestFish(const FVector& PosM, FVector& OutM) const { double Best = 1e18; for (const FHome& H : HomeStates) { for (const FFish& F : H.Fish) { const double D = FVector::DistSquared(F.P, PosM); if (D < Best) { Best = D; OutM = F.P; } } } return Best < 1e17; } bool UDescentFaunaSubsystem::NearestPolyp(const FVector& PosM, double MaxM, FVector& OutM) const { double Best = MaxM * MaxM; bool bFound = false; for (const FPolyp& P : Polyps) { const double D = FVector::DistSquared(P.P, PosM); if (P.bAlive && D <= Best) { Best = D; OutM = P.P; bFound = true; } } return bFound; } FName UDescentFaunaSubsystem::TakeByHand(const FVector& PosM) { FPolyp* Best = nullptr; double BestD = DF::HandReachM * DF::HandReachM; for (FPolyp& P : Polyps) { const double D = FVector::DistSquared(P.P, PosM); if (P.bAlive && D <= BestD) { BestD = D; Best = &P; } } if (!Best) { return NAME_None; } Best->bAlive = false; Best->Regrow = 600.0; ++TakenByHand; return TEXT("polyp"); } void UDescentFaunaSubsystem::AddPolypCluster(const FVector& PosM, const FVector& Normal) { // three to six polyps on the wall around the point FVector T1, T2; Normal.GetSafeNormal().FindBestAxisVectors(T1, T2); const int32 N = Rng.RandRange(3, 6); for (int32 I = 0; I < N; ++I) { FPolyp P; P.P = PosM + T1 * Rng.FRandRange(-0.5, 0.5) + T2 * Rng.FRandRange(-0.5, 0.5) + Normal.GetSafeNormal() * 0.15; Polyps.Add(P); } } bool UDescentFaunaSubsystem::FreeWater(const FVector& PosM, double HullMargin) const { UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(GetWorld()); const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(GetWorld()); if (Terrain && Terrain->Density(PosM) > -1.0) { return false; } if (C && Descent::Carrier::HullDistance(C->ToLocalM(PosM * 100.0), false) < HullMargin) { return false; } return true; } void UDescentFaunaSubsystem::Activate(FHome& H) { H.bActive = true; H.Fish.Reset(); // the school starts in free water near its home, never within 6 m of the hull (U6) FVector Centre = H.Def.PosM; for (int32 Try = 0; Try < 40 && !FreeWater(Centre, DF::HullSpawnMarginM + 2.0); ++Try) { Centre = H.Def.PosM + Rng.VRand() * Rng.FRandRange(4.0, 30.0); } for (int32 I = 0; I < H.Alive; ++I) { FFish F; F.P = Centre; for (int32 Try = 0; Try < 10; ++Try) { const FVector Q = Centre + Rng.VRand() * Rng.FRandRange(0.5, 6.0); if (FreeWater(Q, DF::HullSpawnMarginM)) { F.P = Q; break; } } F.V = Rng.VRand() * 1.5; if (FreeWater(F.P, DF::HullSpawnMarginM)) { H.Fish.Add(F); } } H.Target = Centre; H.TargetTimer = 0.0; // lantern polyps on the walls around the home, made once UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(GetWorld()); if (!H.bPolypsMade && Terrain) { H.bPolypsMade = true; for (int32 I = 0; I < 8; ++I) { const FVector Dir = Rng.VRand(); const double D = Terrain->Raycast(Centre, Dir, 70.0); if (D > 1.0 && D < 69.0) { const FVector Hit = Centre + Dir * D; AddPolypCluster(Hit, -Terrain->Gradient(Hit)); } } } } double UDescentFaunaSubsystem::Suck(const FVector& NozzleM, const FVector& Dir, double RangeM, double Pull, double Dt, double RoomFU, bool& bOutTankFull) { double Got = 0.0; for (FHome& H : HomeStates) { const double Value = H.Def.Species == 1 ? 0.15 : 0.10; for (int32 I = H.Fish.Num() - 1; I >= 0; --I) { FFish& F = H.Fish[I]; const FVector To = NozzleM - F.P; const double D = To.Size(); if (D > RangeM || FVector::DotProduct(-To.GetSafeNormal(), Dir) < 0.7) { continue; } F.V += To.GetSafeNormal() * (Pull * 2.5 * Dt); F.Sucked = 0.5; if (D <= DF::CaptureM) { if (Got + Value > RoomFU + 1e-9) { bOutTankFull = true; continue; } Got += Value; ++Captured; --H.Alive; H.Fish.RemoveAtSwap(I); } } } // polyps within 6 m of the nozzle come off the wall too for (FPolyp& P : Polyps) { if (P.bAlive && FVector::DistSquared(P.P, NozzleM) < 36.0 && FVector::DotProduct((P.P - NozzleM).GetSafeNormal(), Dir) > 0.7) { if (Got + 0.4 > RoomFU + 1e-9) { bOutTankFull = true; continue; } P.bAlive = false; P.Regrow = 600.0; Got += 0.4; ++Captured; } } return Got; } void UDescentFaunaSubsystem::Step(double Dt) { UWorld* World = GetWorld(); const ADescentPlayerController* PC = World ? Cast(World->GetFirstPlayerController()) : nullptr; UDescentTerrainSubsystem* Terrain = UDescentTerrainSubsystem::Get(World); const ADescentCarrier* C = UDescentCarrierSubsystem::GetCarrier(World); if (!PC || !Terrain || Dt <= 0.0) { return; } Dt = FMath::Min(Dt, 0.1); const FVector Ref = PC->RefPointM(); // which homes live: the six nearest within 220 m TArray Order; for (int32 I = 0; I < HomeStates.Num(); ++I) { if (FVector::Dist(HomeStates[I].Def.PosM, Ref) <= DF::ActiveRangeM) { Order.Add(I); } } Order.Sort([this, &Ref](int32 A, int32 B) { return FVector::DistSquared(HomeStates[A].Def.PosM, Ref) < FVector::DistSquared(HomeStates[B].Def.PosM, Ref); }); Order.SetNum(FMath::Min(Order.Num(), DF::MaxActiveHomes)); for (int32 I = 0; I < HomeStates.Num(); ++I) { FHome& H = HomeStates[I]; const bool bWant = Order.Contains(I); if (bWant && !H.bActive) { Activate(H); } if (!bWant && H.bActive) { H.bActive = false; H.Fish.Reset(); } // a school regrows by one every 30 s while the player is more than 100 m away if (H.Alive < DF::SchoolSize && FVector::Dist(H.Def.PosM, Ref) > 100.0) { H.Regrow += Dt; if (H.Regrow >= 30.0) { H.Regrow = 0.0; ++H.Alive; } } } for (FPolyp& P : Polyps) { if (!P.bAlive) { P.Regrow -= Dt; if (P.Regrow <= 0.0) { P.bAlive = true; } } } // what the fish flee: Tenders that are out, and a swimmer TArray Threats; if (const UDescentTenderFleet* Fleet = UDescentTenderFleet::Get(World)) { for (const TWeakObjectPtr& W : Fleet->All()) { const AKazahTender* T = W.Get(); if (T && T->Flight->IsOut()) { Threats.Add(T->GetActorLocation() / 100.0); } } } if (PC->GetMode() == EDescentMode::Swim) { Threats.Add(Ref); } InsideHull = 0; MinHullDistM = 1e9; NearestFishM = 1e9; for (FHome& H : HomeStates) { if (!H.bActive || H.Fish.Num() == 0) { continue; } // a new target in free water near the home, never within 6 m of the hull H.TargetTimer -= Dt; if (H.TargetTimer <= 0.0 || !FreeWater(H.Target, DF::HullSpawnMarginM)) { H.TargetTimer = Rng.FRandRange(5.0, 9.0); for (int32 Try = 0; Try < 12; ++Try) { const FVector Q = H.Def.PosM + Rng.VRand() * Rng.FRandRange(4.0, 28.0); if (FreeWater(Q, DF::HullSpawnMarginM)) { H.Target = Q; break; } } } FVector Centre = FVector::ZeroVector, AvgV = FVector::ZeroVector; for (const FFish& F : H.Fish) { Centre += F.P; AvgV += F.V; } Centre /= H.Fish.Num(); AvgV /= H.Fish.Num(); const double Scale = H.Def.Species == 1 ? 0.8 : 1.0; const double VMin = 1.0 * (0.8 + 0.3 * Scale), VMax = 3.6 * (0.8 + 0.3 * Scale); for (int32 I = 0; I < H.Fish.Num(); ++I) { FFish& F = H.Fish[I]; FVector A = (Centre - F.P) * 0.35 + (H.Target - F.P).GetClampedToMaxSize(8.0) * 0.25 + (AvgV - F.V) * 0.6; for (int32 J = 0; J < H.Fish.Num(); ++J) { if (J == I) { continue; } const FVector Away = F.P - H.Fish[J].P; const double D2 = Away.SizeSquared(); if (D2 < 1.0 && D2 > 1e-6) { A += Away / D2 * 0.8; } } F.Sucked = FMath::Max(0.0, F.Sucked - Dt); if (F.Sucked <= 0.0) { for (const FVector& T : Threats) { const FVector Away = F.P - T; const double D = Away.Size(); if (D < 10.0 && D > 1e-3) { A += Away / D * ((10.0 - D) * 2.5); } } } // walls: look ahead along the swim direction const FVector Ahead = F.P + F.V.GetSafeNormal() * 3.0; if (Terrain->Density(Ahead) > -0.6) { A -= Terrain->Gradient(Ahead) * 10.0; } const FVector Home = H.Def.PosM - F.P; if (Home.SizeSquared() > DF::LeashM * DF::LeashM) { A += Home.GetSafeNormal() * 4.0; } // U6: never inside the hull if (C && F.Sucked <= 0.0) { FVector L = C->ToLocalM(F.P * 100.0); const FVector Before = L; const FVector Push = DF::HullExclusion(L); if (!Push.IsZero()) { A += C->GetActorTransform().TransformVectorNoScale(Push); if (!L.Equals(Before, 1e-6)) { F.P = C->ToWorldUU(L) / 100.0; ++ProjectedOut; const FVector Nw = C->GetActorTransform().TransformVectorNoScale(Push.GetSafeNormal()); F.V -= Nw * FMath::Min(0.0, FVector::DotProduct(F.V, Nw)); } } } F.V += A * Dt; const double Speed = F.V.Size(); if (F.Sucked <= 0.0) { F.V = F.V.GetSafeNormal() * FMath::Clamp(Speed, VMin, VMax); } F.P += F.V * Dt; if (Terrain->Density(F.P) > 0.0) { Terrain->PushOutSphere(F.P, 0.3); } if (C) { const double D = Descent::Carrier::HullDistance(C->ToLocalM(F.P * 100.0), false); MinHullDistM = FMath::Min(MinHullDistM, D); if (D < 0.0 && F.Sucked <= 0.0) { ++InsideHull; } } NearestFishM = FMath::Min(NearestFishM, FVector::Dist(F.P, Ref)); } } InsideHullEver += InsideHull; UpdateVisuals(); } void UDescentFaunaSubsystem::UpdateVisuals() { UWorld* World = GetWorld(); if (!Visual && World) { Visual = World->SpawnActor(); USceneComponent* Root = NewObject(Visual, TEXT("Root")); Visual->SetRootComponent(Root); Root->RegisterComponent(); UStaticMesh* Cube = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube")); UStaticMesh* Sphere = LoadObject(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere")); const struct { const TCHAR* Name; UStaticMesh* Mesh; FLinearColor Base, Emissive; float Strength; } Defs[3] = { { TEXT("Silverfin"), Cube, FLinearColor(0.55f, 0.6f, 0.65f), FLinearColor(0.5f, 0.6f, 0.7f), 0.06f }, { TEXT("Lanternfish"), Cube, FLinearColor(0.05f, 0.1f, 0.12f), FLinearColor(0.2f, 1.0f, 0.8f), 2.2f }, { TEXT("Polyps"), Sphere, FLinearColor(0.1f, 0.05f, 0.02f), FLinearColor(1.0f, 0.55f, 0.15f), 2.5f } }; for (const auto& D : Defs) { UInstancedStaticMeshComponent* M = NewObject(Visual, D.Name); M->SetMobility(EComponentMobility::Movable); M->SetupAttachment(Root); M->SetStaticMesh(D.Mesh); M->SetCollisionEnabled(ECollisionEnabled::NoCollision); M->SetCastShadow(false); if (UMaterialInstanceDynamic* MID = FaunaSurface(Visual, D.Base, D.Emissive, D.Strength)) { M->SetMaterial(0, MID); } M->RegisterComponent(); Meshes.Add(M); } } if (Meshes.Num() < 3) { return; } for (UInstancedStaticMeshComponent* M : Meshes) { M->ClearInstances(); } for (const FHome& H : HomeStates) { const double S = H.Def.Species == 1 ? 0.8 : 1.0; for (const FFish& F : H.Fish) { Meshes[H.Def.Species]->AddInstance(FTransform(F.V.Rotation(), F.P * 100.0, FVector(0.45, 0.08, 0.18) * S), true); } } for (const FPolyp& P : Polyps) { if (P.bAlive) { Meshes[2]->AddInstance(FTransform(FQuat::Identity, P.P * 100.0, FVector(0.22)), true); } } } // ---- state section `fauna` (section 9.4) ----------------------------------------------------------------------------- static FDescentStateProviderRegistrar GFaunaStateSection(TEXT("fauna"), [](UWorld* World, FJsonObject& Out) { const UDescentFaunaSubsystem* S = UDescentFaunaSubsystem::Get(World); Out.SetBoolField(TEXT("available"), S != nullptr); if (!S) { return; } Out.SetNumberField(TEXT("homes"), Descent::Fauna::Homes().Num()); Out.SetNumberField(TEXT("homes_active"), S->ActiveHomes()); Out.SetNumberField(TEXT("fish_alive"), S->FishAlive()); Out.SetNumberField(TEXT("inside_hull"), S->InsideHull); Out.SetNumberField(TEXT("inside_hull_ever"), S->InsideHullEver); Out.SetNumberField(TEXT("projected_out"), S->ProjectedOut); Out.SetNumberField(TEXT("min_hull_dist_m"), Descent::Json::Round(FMath::Min(S->MinHullDistM, 9999.0))); Out.SetNumberField(TEXT("captured"), S->Captured); Out.SetNumberField(TEXT("taken_by_hand"), S->TakenByHand); Out.SetNumberField(TEXT("polyps"), S->NumPolyps()); });