// Copyright Epic Games, Inc. All Rights Reserved. #include "BuoyancyComponent.h" #include "BuoyancyComponentSimulation.h" #include "BuoyancyManager.h" #include "Engine/World.h" #include "Physics/Experimental/PhysScene_Chaos.h" #include "WaterSplineComponent.h" #include "WaterBodyComponent.h" #include "WaterVersion.h" #include "Physics/SimpleSuspension.h" #include "PBDRigidsSolver.h" #include "WaterSubsystem.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(BuoyancyComponent) TAutoConsoleVariable CVarWaterDebugBuoyancy( TEXT("r.Water.DebugBuoyancy"), 0, TEXT("Enable debug drawing for water interactions."), ECVF_Default); TAutoConsoleVariable CVarWaterBuoyancyDebugPoints( TEXT("r.Water.BuoyancyDebugPoints"), 10, TEXT("Number of points in one dimension for buoyancy debug."), ECVF_Default); TAutoConsoleVariable CVarWaterBuoyancyDebugSize( TEXT("r.Water.BuoyancyDebugSize"), 1000, TEXT("Side length of square for buoyancy debug."), ECVF_Default); TAutoConsoleVariable CVarWaterUseSplineKeyOptimization( TEXT("r.Water.UseSplineKeyOptimization"), 1, TEXT("Whether to cache spline input key for water bodies."), ECVF_Default); TAutoConsoleVariable CVarWaterBuoyancyUseAsyncPath( TEXT("r.Water.UseBuoyancyAsyncPath"), 1, TEXT("Whether to use async physics callback for buoyancy."), ECVF_Default); UBuoyancyComponent::UBuoyancyComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , SimulatingComponent(nullptr) , PontoonConfiguration(0) , VelocityPontoonIndex(0) , bIsOverlappingWaterBody(false) , bCanBeActive(true) , bIsInWaterBody(false) , bUseAsyncPath(true) { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bStartWithTickEnabled = false; PrimaryComponentTick.TickGroup = TG_PrePhysics; CurAsyncInput = nullptr; CurAsyncOutput = nullptr; } void UBuoyancyComponent::BeginPlay() { Super::BeginPlay(); if (AActor* Owner = GetOwner()) { SimulatingComponent = Cast(Owner->GetRootComponent()); } if(SimulatingComponent) { for (FSphericalPontoon& Pontoon : BuoyancyData.Pontoons) { if (Pontoon.CenterSocket != NAME_None && SimulatingComponent->DoesSocketExist(Pontoon.CenterSocket)) { Pontoon.bUseCenterSocket = true; Pontoon.SocketTransform = SimulatingComponent->GetSocketTransform(Pontoon.CenterSocket, RTS_Actor); } } SetupWaterBodyOverlaps(); } // Call this before registering with manager FinalizeAuxData(); if (UWorld* World = GetWorld()) { if (UWaterSubsystem* WaterSubsystem = UWaterSubsystem::GetWaterSubsystem(World)) { if (ABuoyancyManager* Manager = WaterSubsystem->GetBuoyancyManager()) { Manager->Register(this); } } } } void UBuoyancyComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (UWorld* World = GetWorld()) { if (UWaterSubsystem* WaterSubsystem = UWaterSubsystem::GetWaterSubsystem(World)) { if (ABuoyancyManager* Manager = WaterSubsystem->GetBuoyancyManager()) { Manager->Unregister(this); } } } Super::EndPlay(EndPlayReason); } void UBuoyancyComponent::PostLoad() { Super::PostLoad(); if (GetLinkerCustomVersion(FWaterCustomVersion::GUID) < FWaterCustomVersion::UpdateBuoyancyComponentPontoonsData) { if (Pontoons_DEPRECATED.Num()) { BuoyancyData.Pontoons = Pontoons_DEPRECATED; } Pontoons_DEPRECATED.Empty(); } } void UBuoyancyComponent::Serialize(FArchive& Ar) { Super::Serialize(Ar); Ar.UsingCustomVersion(FWaterCustomVersion::GUID); } const float ToKmh(float SpeedCms) { return SpeedCms * 0.036f; //cm/s to km/h } void UBuoyancyComponent::Update(float DeltaTime) { if (!SimulatingComponent) { return; } UpdatePontoonCoefficients(); if (const FBuoyancyComponentBaseAsyncOutput* Output = static_cast(CurAsyncOutput)) { if (Output->bValid) { const FBuoyancySimOutput& SimOut = Output->SimOutput; bIsInWaterBody = SimOut.bIsInWaterBody; // We may have deleted/added a pontoon on the game thread int32 PontoonsNum = FMath::Min(BuoyancyData.Pontoons.Num(), Output->AuxData.Pontoons.Num()); for (int32 i = 0; i < PontoonsNum; ++i) { BuoyancyData.Pontoons[i].CopyDataFromPT(Output->AuxData.Pontoons[i]); } } } if (CurAsyncInput) { if (const FBodyInstance* BodyInstance = SimulatingComponent->GetBodyInstance()) { if (auto Handle = BodyInstance->GetPhysicsActor()) { CurAsyncInput->Proxy = Handle; } } FBuoyancyComponentBaseAsyncInput* BuoyancyInputState = static_cast(CurAsyncInput); BuoyancyInputState->WaterBodyComponents = GetCurrentWaterBodyComponents(); BuoyancyInputState->Pontoons = BuoyancyData.Pontoons; bool bSetSmoothedTime = false; for (UWaterBodyComponent* WaterBody : GetCurrentWaterBodyComponents()) { if (WaterBody && WaterBody->HasWaves()) { BuoyancyInputState->SmoothedWorldTimeSeconds = WaterBody->GetWaveReferenceTime(); bSetSmoothedTime = true; break; } } if(!bSetSmoothedTime) { BuoyancyInputState->SmoothedWorldTimeSeconds = GetWorld()->GetTimeSeconds(); } } if (!IsUsingAsyncPath()) { const FVector PhysicsVelocity = SimulatingComponent->GetComponentVelocity(); const FVector ForwardDir = SimulatingComponent->GetForwardVector(); const FVector RightDir = SimulatingComponent->GetRightVector(); const float ForwardSpeed = FVector::DotProduct(ForwardDir, PhysicsVelocity); const float ForwardSpeedKmh = ToKmh(ForwardSpeed); const float RightSpeed = FVector::DotProduct(RightDir, PhysicsVelocity); const float RightSpeedKmh = ToKmh(RightSpeed); ApplyForces(DeltaTime, PhysicsVelocity, ForwardSpeed, ForwardSpeedKmh, SimulatingComponent); } } void UBuoyancyComponent::ApplyForces(float DeltaTime, FVector LinearVelocity, float ForwardSpeed, float ForwardSpeedKmh, UPrimitiveComponent* PrimitiveComponent) { if (IsUsingAsyncPath()) { return; } const int32 NumPontoonsInWater = UpdatePontoons(DeltaTime, ForwardSpeed, ForwardSpeedKmh, SimulatingComponent); bIsInWaterBody = NumPontoonsInWater > 0; if (SimulatingComponent->IsSimulatingPhysics()) { const ECollisionEnabled::Type Collision = SimulatingComponent->GetCollisionEnabled(); if (Collision == ECollisionEnabled::QueryAndPhysics || Collision == ECollisionEnabled::PhysicsOnly) { ApplyBuoyancy(SimulatingComponent); FVector TotalForce = FVector::ZeroVector; FVector TotalTorque = FVector::ZeroVector; TotalForce += ComputeWaterForce(DeltaTime, LinearVelocity); if (BuoyancyData.bApplyDragForcesInWater) { TotalForce += ComputeLinearDragForce(LinearVelocity); TotalTorque += ComputeAngularDragTorque(SimulatingComponent->GetPhysicsAngularVelocityInDegrees()); } SimulatingComponent->AddForce(TotalForce, NAME_None, /*bAccelChange=*/true); SimulatingComponent->AddTorqueInDegrees(TotalTorque, NAME_None, /*bAccelChange=*/true); } } } void UBuoyancyComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Update(DeltaTime); } void UBuoyancyComponent::GetResourceSizeEx(FResourceSizeEx& CumulativeResourceSize) { Super::GetResourceSizeEx(CumulativeResourceSize); // Account for all non-editor data properties : CumulativeResourceSize.AddDedicatedSystemMemoryBytes(sizeof(CurAsyncInput) + sizeof(CurAsyncOutput) + sizeof(NextAsyncOutput) + sizeof(CurAsyncType) + sizeof(OutputInterpAlpha) + OutputsWaitingOn.GetAllocatedSize() + sizeof(PontoonConfiguration) + ConfiguredPontoonCoefficients.GetAllocatedSize() + sizeof(VelocityPontoonIndex)); } void UBuoyancyComponent::SetupWaterBodyOverlaps() { //SimulatingComponent->SetCollisionObjectType(ECollisionChannel::ECC_PhysicsBody); if (SimulatingComponent->GetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic) == ECollisionResponse::ECR_Ignore) { SimulatingComponent->SetCollisionResponseToChannel(ECollisionChannel::ECC_WorldStatic, ECollisionResponse::ECR_Overlap); } SimulatingComponent->SetGenerateOverlapEvents(true); } void UBuoyancyComponent::AddCustomPontoon(float Radius, FName CenterSocketName) { FSphericalPontoon Pontoon; Pontoon.Radius = Radius; Pontoon.CenterSocket = CenterSocketName; BuoyancyData.Pontoons.Add(Pontoon); } void UBuoyancyComponent::AddCustomPontoon(float Radius, const FVector& RelativeLocation) { FSphericalPontoon Pontoon; Pontoon.Radius = Radius; Pontoon.RelativeLocation = RelativeLocation; BuoyancyData.Pontoons.Add(Pontoon); } void UBuoyancyComponent::EnteredWaterBody(UWaterBodyComponent* WaterBodyComponent) { bool bIsFirstBody = !CurrentWaterBodyComponents.Num() && WaterBodyComponent; CurrentWaterBodyComponents.AddUnique(WaterBodyComponent); for (FSphericalPontoon& Pontoon : BuoyancyData.Pontoons) { Pontoon.SplineSegments.FindOrAdd(WaterBodyComponent, -1); } if (bIsFirstBody) { bIsOverlappingWaterBody = -1; } } void UBuoyancyComponent::ExitedWaterBody(UWaterBodyComponent* WaterBodyComponent) { GetCurrentWaterBodyComponents().Remove(WaterBodyComponent); for (FSphericalPontoon& Pontoon : BuoyancyData.Pontoons) { Pontoon.SplineSegments.Remove(WaterBodyComponent); } if (!CurrentWaterBodyComponents.Num()) { bIsOverlappingWaterBody = false; bIsInWaterBody = false; } } void UBuoyancyComponent::ApplyBuoyancy(UPrimitiveComponent* PrimitiveComponent) { check(GetOwner()); if (PrimitiveComponent && bIsOverlappingWaterBody) { int PontoonIndex = 0; for (const FSphericalPontoon& Pontoon : BuoyancyData.Pontoons) { if (PontoonConfiguration & (1 << PontoonIndex)) { PrimitiveComponent->AddForceAtLocation(Pontoon.LocalForce, Pontoon.CenterLocation); } PontoonIndex++; } } } void UBuoyancyComponent::ComputeBuoyancy(FSphericalPontoon& Pontoon, float ForwardSpeedKmh) { AActor* Owner = GetOwner(); check(Owner); auto ComputeBuoyantForce = [&](FVector CenterLocation, float Radius, float InBuoyancyCoefficient, float CurrentWaterLevel) -> float { const float Bottom = CenterLocation.Z - Radius; const float SubDiff = FMath::Clamp(CurrentWaterLevel - Bottom, 0.f, 2.f * Radius); // The following was obtained by integrating the volume of a sphere // over a linear section of SubmersionDiff length. static const float Pi = (float)PI; const float SubDiffSq = SubDiff * SubDiff; const float SubVolume = (Pi / 3.f) * SubDiffSq * ((3.f * Radius) - SubDiff); #if ENABLE_DRAW_DEBUG if (CVarWaterDebugBuoyancy.GetValueOnAnyThread()) { const FVector WaterPoint = FVector(CenterLocation.X, CenterLocation.Y, CurrentWaterLevel); DrawDebugLine(GetWorld(), WaterPoint - 50.f * FVector::ForwardVector, WaterPoint + 50.f * FVector::ForwardVector, FColor::Blue, false, -1.f, 0, 3.f); DrawDebugLine(GetWorld(), WaterPoint - 50.f * FVector::RightVector, WaterPoint + 50.f * FVector::RightVector, FColor::Blue, false, -1.f, 0, 3.f); } #endif check(SimulatingComponent && SimulatingComponent->GetBodyInstance()); const float VelocityZ = SimulatingComponent->GetBodyInstance()->GetUnrealWorldVelocity().Z; const float FirstOrderDrag = BuoyancyData.BuoyancyDamp * VelocityZ; const float SecondOrderDrag = FMath::Sign(VelocityZ) * BuoyancyData.BuoyancyDamp2 * VelocityZ * VelocityZ; const float DampingFactor = -FMath::Max(FirstOrderDrag + SecondOrderDrag, 0.f); // The buoyant force scales with submersed volume return SubVolume * (InBuoyancyCoefficient) + DampingFactor; }; const float MinVelocity = BuoyancyData.BuoyancyRampMinVelocity; const float MaxVelocity = BuoyancyData.BuoyancyRampMaxVelocity; const float RampFactor = FMath::Clamp((ForwardSpeedKmh - MinVelocity) / (MaxVelocity - MinVelocity), 0.f, 1.f); const float BuoyancyRamp = RampFactor * (BuoyancyData.BuoyancyRampMax - 1); float BuoyancyCoefficientWithRamp = BuoyancyData.BuoyancyCoefficient * (1 + BuoyancyRamp); const float BuoyantForce = FMath::Clamp(ComputeBuoyantForce(Pontoon.CenterLocation, Pontoon.Radius, BuoyancyCoefficientWithRamp, Pontoon.WaterHeight), 0.f, BuoyancyData.MaxBuoyantForce); Pontoon.LocalForce = FVector::UpVector * BuoyantForce * Pontoon.PontoonCoefficient; } void UBuoyancyComponent::ComputePontoonCoefficients() { TArray& PontoonCoefficients = ConfiguredPontoonCoefficients.FindOrAdd(PontoonConfiguration); if (PontoonCoefficients.Num() == 0) { if (FBodyInstance* BodyInstance = SimulatingComponent->GetBodyInstance()) { TArray LocalPontoonLocations; const FVector& LocalCOM = BodyInstance->GetMassSpaceLocal().GetLocation(); if (!SimulatingComponent) { return; } for (int32 PontoonIndex = 0; PontoonIndex < BuoyancyData.Pontoons.Num(); ++PontoonIndex) { const FSphericalPontoon& Pontoon = BuoyancyData.Pontoons[PontoonIndex]; if (PontoonConfiguration & (1 << PontoonIndex)) { if (Pontoon.bUseCenterSocket) { const FVector LocalPosition = SimulatingComponent->GetSocketTransform(Pontoon.CenterSocket, ERelativeTransformSpace::RTS_ParentBoneSpace).GetLocation(); LocalPontoonLocations.Add(LocalPosition); } else { // If using the relative location for the pontoon and the buoyancydata indicates // that we should center on COM, then shift the relative location to be centered. const FVector PontoonRelativeLocation = BuoyancyData.bCenterPontoonsOnCOM ? Pontoon.RelativeLocation - LocalCOM : Pontoon.RelativeLocation; LocalPontoonLocations.Add(PontoonRelativeLocation); } } } PontoonCoefficients.AddZeroed(LocalPontoonLocations.Num()); //Distribute a mass of 1 to each pontoon so that we get a scaling factor based on position relative to CoM FString ErrMsg; bool ComputeSuccess = FSimpleSuspensionHelpers::ComputeSprungMasses(LocalPontoonLocations, LocalCOM, 1.f, PontoonCoefficients, &ErrMsg); ensureMsgf(ComputeSuccess, TEXT("Failed to compute %d sprung masses for: %s\nErrMsg: \"%s\""), LocalPontoonLocations.Num(), *GetOwner()->GetActorNameOrLabel(), *ErrMsg); } } // Apply the coefficients for (int32 PontoonIndex = 0, CoefficientIdx = 0; PontoonIndex < BuoyancyData.Pontoons.Num(); ++PontoonIndex) { if (PontoonConfiguration & (1 << PontoonIndex)) { BuoyancyData.Pontoons[PontoonIndex].PontoonCoefficient = PontoonCoefficients[CoefficientIdx++]; } } } int32 UBuoyancyComponent::UpdatePontoons(float DeltaTime, float ForwardSpeed, float ForwardSpeedKmh, UPrimitiveComponent* PrimitiveComponent) { AActor* Owner = GetOwner(); check(Owner); int32 NumPontoonsInWater = 0; if (bIsOverlappingWaterBody) { int PontoonIndex = 0; for (FSphericalPontoon& Pontoon : BuoyancyData.Pontoons) { if (PontoonConfiguration & (1 << PontoonIndex)) { if (Pontoon.bUseCenterSocket) { const FTransform& SimulatingComponentTransform = PrimitiveComponent->GetSocketTransform(Pontoon.CenterSocket); Pontoon.CenterLocation = SimulatingComponentTransform.GetLocation() + Pontoon.Offset; Pontoon.SocketRotation = SimulatingComponentTransform.GetRotation(); } else { Pontoon.CenterLocation = PrimitiveComponent->GetComponentTransform().TransformPosition(Pontoon.RelativeLocation); } GetWaterSplineKey(Pontoon.CenterLocation, Pontoon.SplineInputKeys, Pontoon.SplineSegments); const FVector PontoonBottom = Pontoon.CenterLocation - FVector(0, 0, Pontoon.Radius); UWaterBodyComponent* TempWaterBodyComponent = Pontoon.CurrentWaterBodyComponent; /*Pass in large negative default value so we don't accidentally assume we're in water when we're not.*/ Pontoon.WaterHeight = GetWaterHeight(PontoonBottom - FVector::UpVector * 100.f, Pontoon.SplineInputKeys, -100000.f, TempWaterBodyComponent, Pontoon.WaterDepth, Pontoon.WaterPlaneLocation, Pontoon.WaterPlaneNormal, Pontoon.WaterSurfacePosition, Pontoon.WaterVelocity, Pontoon.WaterBodyIndex); Pontoon.CurrentWaterBodyComponent = TempWaterBodyComponent; const bool bPrevIsInWater = Pontoon.bIsInWater; const float ImmersionDepth = Pontoon.WaterHeight - PontoonBottom.Z; /*check if the pontoon is currently in water*/ if (ImmersionDepth >= 0.f) { Pontoon.bIsInWater = true; Pontoon.ImmersionDepth = ImmersionDepth; NumPontoonsInWater++; } else { Pontoon.bIsInWater = false; Pontoon.ImmersionDepth = 0.f; } #if ENABLE_DRAW_DEBUG if (CVarWaterDebugBuoyancy.GetValueOnAnyThread()) { DrawDebugSphere(GetWorld(), Pontoon.CenterLocation, Pontoon.Radius, 16, FColor::Red, false, -1.f, 0, 1.f); } #endif ComputeBuoyancy(Pontoon, ForwardSpeedKmh); if (Pontoon.bIsInWater && !bPrevIsInWater) { Pontoon.SplineSegments.Reset(); // BlueprintImplementables don't really work on the actor component level unfortunately, so call back in to the function defined on the actor itself. OnPontoonEnteredWater(Pontoon); } if (!Pontoon.bIsInWater && bPrevIsInWater) { Pontoon.SplineSegments.Reset(); OnPontoonExitedWater(Pontoon); } } PontoonIndex++; } #if ENABLE_DRAW_DEBUG if (CVarWaterDebugBuoyancy.GetValueOnAnyThread()) { const float NumPoints = CVarWaterBuoyancyDebugPoints.GetValueOnAnyThread(); const float Size = CVarWaterBuoyancyDebugSize.GetValueOnAnyThread(); const float StartOffset = NumPoints * 0.5f; const float Scale = Size / NumPoints; TMap DebugSplineKeyMap; TMap DebugSplineSegmentsMap; for (int i = 0; i < NumPoints; ++i) { for (int j = 0; j < NumPoints; ++j) { FVector Location = PrimitiveComponent->GetComponentLocation() + (FVector::RightVector * (i - StartOffset) * Scale) + (FVector::ForwardVector * (j - StartOffset) * Scale); GetWaterSplineKey(Location, DebugSplineKeyMap, DebugSplineSegmentsMap); FVector Point(Location.X, Location.Y, GetWaterHeight(Location - FVector::UpVector * 200.f, DebugSplineKeyMap, GetOwner()->GetActorLocation().Z)); DrawDebugPoint(GetWorld(), Point, 5.f, IsOverlappingWaterBody() ? FColor::Green : FColor::Red, false, -1.f, 0); } } } #endif } return NumPontoonsInWater; } float GetWaterSplineKeyFast(FVector Location, const UWaterBodyComponent* WaterBodyComponent, TMap& OutSegmentMap)/*const*/ { if (!OutSegmentMap.Contains(WaterBodyComponent)) { OutSegmentMap.Add(WaterBodyComponent, -1); } const UWaterSplineComponent* WaterSpline = WaterBodyComponent->GetWaterSpline(); const FVector LocalLocation = WaterBodyComponent->GetComponentTransform().InverseTransformPosition(Location); const FInterpCurveVector& InterpCurve = WaterSpline->GetSplinePointsPosition(); float& Segment = OutSegmentMap[WaterBodyComponent]; if (Segment == -1) { float DummyDistance; return InterpCurve.FindNearest(LocalLocation, DummyDistance, Segment); } //We have the cached segment, so search for the best point as in FInterpCurve::FindNearest //but only in the current segment and the two immediate neighbors //River splines aren't looped, so we don't have to handle that case const int32 NumPoints = InterpCurve.Points.Num(); const int32 LastSegmentIdx = FMath::Max(0, NumPoints - 2); if (NumPoints > 1) { float BestDistanceSq = BIG_NUMBER; float BestResult = BIG_NUMBER; float BestSegment = Segment; for (int32 i = Segment - 1; i <= Segment + 1; ++i) { const int32 SegmentIdx = FMath::Clamp(i, 0, LastSegmentIdx); float LocalDistanceSq; float LocalResult = InterpCurve.FindNearestOnSegment(LocalLocation, SegmentIdx, LocalDistanceSq); if (LocalDistanceSq < BestDistanceSq) { BestDistanceSq = LocalDistanceSq; BestResult = LocalResult; BestSegment = SegmentIdx; } } if (FMath::IsNearlyEqual(BestResult, Segment - 1) || FMath::IsNearlyEqual(BestResult, Segment + 1)) { //We're at either end of the search - it's possible we skipped a segment so just do a full lookup in this case float DummyDistance; return InterpCurve.FindNearest(LocalLocation, DummyDistance, Segment); } Segment = BestSegment; return BestResult; } if (NumPoints == 1) { Segment = 0; return InterpCurve.Points[0].InVal; } return 0.0f; } void UBuoyancyComponent::GetWaterSplineKey(FVector Location, TMap& OutMap, TMap& OutSegmentMap) const { OutMap.Reset(); for (const UWaterBodyComponent* WaterBodyComponent : CurrentWaterBodyComponents) { if (WaterBodyComponent && WaterBodyComponent->GetWaterBodyType() == EWaterBodyType::River) { float SplineInputKey; if (CVarWaterUseSplineKeyOptimization.GetValueOnAnyThread()) { SplineInputKey = GetWaterSplineKeyFast(Location, WaterBodyComponent, OutSegmentMap); } else { SplineInputKey = WaterBodyComponent->FindInputKeyClosestToWorldLocation(Location); } OutMap.Add(WaterBodyComponent, SplineInputKey); } } } float UBuoyancyComponent::GetWaterHeight(FVector Position, const TMap& SplineKeyMap, float DefaultHeight, UWaterBodyComponent*& OutWaterBodyComponent, float& OutWaterDepth, FVector& OutWaterPlaneLocation, FVector& OutWaterPlaneNormal, FVector& OutWaterSurfacePosition, FVector& OutWaterVelocity, int32& OutWaterBodyIdx, bool bShouldIncludeWaves) { float WaterHeight = DefaultHeight; OutWaterBodyComponent = nullptr; OutWaterDepth = 0.f; OutWaterPlaneLocation = FVector::ZeroVector; OutWaterPlaneNormal = FVector::UpVector; float MaxImmersionDepth = -1.f; for (UWaterBodyComponent* CurrentWaterBodyComponent : CurrentWaterBodyComponents) { if (CurrentWaterBodyComponent) { const float SplineInputKey = SplineKeyMap.FindRef(CurrentWaterBodyComponent); EWaterBodyQueryFlags QueryFlags = EWaterBodyQueryFlags::ComputeLocation | EWaterBodyQueryFlags::ComputeNormal | EWaterBodyQueryFlags::ComputeImmersionDepth | EWaterBodyQueryFlags::ComputeVelocity; if (bShouldIncludeWaves) { QueryFlags |= EWaterBodyQueryFlags::IncludeWaves; } FWaterBodyQueryResult QueryResult = CurrentWaterBodyComponent->QueryWaterInfoClosestToWorldLocation(Position, QueryFlags, SplineInputKey); if (QueryResult.IsInWater() && QueryResult.GetImmersionDepth() > MaxImmersionDepth) { check(!QueryResult.IsInExclusionVolume()); WaterHeight = Position.Z + QueryResult.GetImmersionDepth(); OutWaterBodyComponent = CurrentWaterBodyComponent; if (EnumHasAnyFlags(QueryResult.GetQueryFlags(), EWaterBodyQueryFlags::ComputeDepth)) { OutWaterDepth = QueryResult.GetWaterSurfaceDepth(); } OutWaterPlaneLocation = QueryResult.GetWaterPlaneLocation(); OutWaterPlaneNormal = QueryResult.GetWaterPlaneNormal(); OutWaterSurfacePosition = QueryResult.GetWaterSurfaceLocation(); OutWaterVelocity = QueryResult.GetVelocity(); OutWaterBodyIdx = CurrentWaterBodyComponent ? CurrentWaterBodyComponent->GetWaterBodyIndex() : 0; MaxImmersionDepth = QueryResult.GetImmersionDepth(); } } } return WaterHeight; } float UBuoyancyComponent::GetWaterHeight(FVector Position, const TMap& SplineKeyMap, float DefaultHeight, bool bShouldIncludeWaves /*= true*/) { UWaterBodyComponent* DummyComponent; float DummyDepth; FVector DummyWaterPlaneLocation; FVector DummyWaterPlaneNormal; FVector DummyWaterSurfacePosition; FVector DummyWaterVelocity; int32 DummyWaterBodyIndex; return GetWaterHeight(Position, SplineKeyMap, DefaultHeight, DummyComponent, DummyDepth, DummyWaterPlaneLocation, DummyWaterPlaneNormal, DummyWaterSurfacePosition, DummyWaterVelocity, DummyWaterBodyIndex, bShouldIncludeWaves); } void UBuoyancyComponent::OnPontoonEnteredWater(const FSphericalPontoon& Pontoon) { OnEnteredWaterDelegate.Broadcast(Pontoon); } void UBuoyancyComponent::OnPontoonExitedWater(const FSphericalPontoon& Pontoon) { OnExitedWaterDelegate.Broadcast(Pontoon); } void UBuoyancyComponent::GetLastWaterSurfaceInfo(FVector& OutWaterPlaneLocation, FVector& OutWaterPlaneNormal, FVector& OutWaterSurfacePosition, float& OutWaterDepth, int32& OutWaterBodyIdx, FVector& OutWaterVelocity) { for (int i = 0; i < BuoyancyData.Pontoons.Num(); ++i) { if (BuoyancyData.Pontoons[i].ImmersionDepth > 0.0f) { OutWaterPlaneLocation = BuoyancyData.Pontoons[i].WaterPlaneLocation; OutWaterPlaneNormal = BuoyancyData.Pontoons[i].WaterPlaneNormal; OutWaterSurfacePosition = BuoyancyData.Pontoons[i].WaterSurfacePosition; OutWaterDepth = BuoyancyData.Pontoons[i].WaterDepth; OutWaterBodyIdx = BuoyancyData.Pontoons[i].WaterBodyIndex; OutWaterVelocity = BuoyancyData.Pontoons[i].WaterVelocity; return; } } } void UBuoyancyComponent::UpdatePontoonCoefficients() { // Get current configuration mask uint32 NewPontoonConfiguration = 0; for (int32 PontoonIndex = 0; PontoonIndex < BuoyancyData.Pontoons.Num(); ++PontoonIndex) { if (BuoyancyData.Pontoons[PontoonIndex].bEnabled) { NewPontoonConfiguration |= 1 << PontoonIndex; } } // Store the new configuration, and return true if its value has changed. const bool bConfigurationChanged = PontoonConfiguration != NewPontoonConfiguration; PontoonConfiguration = NewPontoonConfiguration; // If the configuration changed, update coefficients if (bConfigurationChanged) { // Apply new configuration, recomputing coefficients if necessary ComputePontoonCoefficients(); } } FVector UBuoyancyComponent::ComputeWaterForce(const float DeltaTime, const FVector LinearVelocity) const { AActor* Owner = GetOwner(); check(Owner); if (BuoyancyData.Pontoons.Num()) { const FSphericalPontoon& Pontoon = BuoyancyData.Pontoons[VelocityPontoonIndex]; const UWaterBodyComponent* WaterBodyComponent = Pontoon.CurrentWaterBodyComponent;; if (WaterBodyComponent && WaterBodyComponent->GetWaterBodyType() == EWaterBodyType::River) { float InputKey = Pontoon.SplineInputKeys[WaterBodyComponent]; const float WaterSpeed = WaterBodyComponent->GetWaterVelocityAtSplineInputKey(InputKey); const FVector SplinePointLocation = WaterBodyComponent->GetWaterSpline()->GetLocationAtSplineInputKey(InputKey, ESplineCoordinateSpace::World); // Move away from spline const FVector ShoreDirection = (Pontoon.CenterLocation - SplinePointLocation).GetSafeNormal2D(); const float WaterShorePushFactor = BuoyancyData.WaterShorePushFactor; const FVector WaterDirection = WaterBodyComponent->GetWaterSpline()->GetDirectionAtSplineInputKey(InputKey, ESplineCoordinateSpace::World) * (1 - WaterShorePushFactor) + ShoreDirection * (WaterShorePushFactor); const FVector WaterVelocity = WaterDirection * WaterSpeed; check(SimulatingComponent && SimulatingComponent->GetBodyInstance()); const FVector ActorVelocity = SimulatingComponent->GetBodyInstance()->GetUnrealWorldVelocity(); const float ActorSpeedInWaterDir = FMath::Abs(FVector::DotProduct(ActorVelocity, WaterDirection)); if (ActorSpeedInWaterDir < WaterSpeed) { const FVector Acceleration = (WaterVelocity / DeltaTime) * BuoyancyData.WaterVelocityStrength; const float MaxWaterAcceleration = BuoyancyData.MaxWaterForce; return Acceleration.GetClampedToSize(-MaxWaterAcceleration, MaxWaterAcceleration); } } } return FVector::ZeroVector; } FVector UBuoyancyComponent::ComputeLinearDragForce(const FVector& PhyiscsVelocity) const { FVector DragForce = FVector::ZeroVector; if (BuoyancyData.bApplyDragForcesInWater && IsInWaterBody() && SimulatingComponent) { FVector PlaneVelocity = PhyiscsVelocity; PlaneVelocity.Z = 0; const FVector VelocityDir = PlaneVelocity.GetSafeNormal(); const float SpeedKmh = ToKmh(PlaneVelocity.Size()); const float ClampedSpeed = FMath::Clamp(SpeedKmh, -BuoyancyData.MaxDragSpeed, BuoyancyData.MaxDragSpeed); const float Resistance = ClampedSpeed * BuoyancyData.DragCoefficient; DragForce += -Resistance * VelocityDir; const float Resistance2 = ClampedSpeed * ClampedSpeed * BuoyancyData.DragCoefficient2; DragForce += -Resistance2 * VelocityDir * FMath::Sign(SpeedKmh); } return DragForce; } FVector UBuoyancyComponent::ComputeAngularDragTorque(const FVector& AngularVelocity) const { FVector DragTorque = FVector::ZeroVector; if (BuoyancyData.bApplyDragForcesInWater && IsInWaterBody()) { DragTorque = -AngularVelocity * BuoyancyData.AngularDragCoefficient; } return DragTorque; } TUniquePtr UBuoyancyComponent::SetCurrentAsyncInputOutput(int32 InputIdx, FBuoyancyManagerAsyncOutput* CurOutput, FBuoyancyManagerAsyncOutput* NextOutput, float Alpha, int32 BuoyancyManagerTimestamp) { if (IsUsingAsyncPath()) { TUniquePtr CurInput = MakeUnique(); SetCurrentAsyncInputOutputInternal(CurInput.Get(), InputIdx, CurOutput, NextOutput, Alpha, BuoyancyManagerTimestamp); return CurInput; } return nullptr; } void UBuoyancyComponent::SetCurrentAsyncInputOutputInternal(FBuoyancyComponentAsyncInput* CurInput, int32 InputIdx, FBuoyancyManagerAsyncOutput* CurOutput, FBuoyancyManagerAsyncOutput* NextOutput, float Alpha, int32 BuoyancyManagerTimestamp) { ensure(CurAsyncInput == nullptr); //should be reset after it was filled ensure(CurAsyncOutput == nullptr); //should get reset after update is done CurAsyncInput = CurInput; CurAsyncInput->BuoyancyComponent = this; CurAsyncType = CurInput->Type; NextAsyncOutput = nullptr; OutputInterpAlpha = 0.f; // We need to find our component in the output given if (CurOutput) { for (int32 PendingOutputIdx = 0; PendingOutputIdx < OutputsWaitingOn.Num(); ++PendingOutputIdx) { // Found the correct pending output, use index to get the component. if (OutputsWaitingOn[PendingOutputIdx].Timestamp == CurOutput->Timestamp) { const int32 ComponentIdx = OutputsWaitingOn[PendingOutputIdx].Idx; FBuoyancyComponentAsyncOutput* ComponentOutput = CurOutput->Outputs[ComponentIdx].Get(); if (ComponentOutput && ComponentOutput->bValid && ComponentOutput->Type == CurAsyncType) { CurAsyncOutput = ComponentOutput; if (NextOutput && NextOutput->Timestamp == CurOutput->Timestamp) { // This can occur when substepping - in this case, VehicleOutputs will be in the same order in NextOutput and CurOutput. FBuoyancyComponentAsyncOutput* ComponentNextOutput = NextOutput->Outputs[ComponentIdx].Get(); if (ComponentNextOutput && ComponentNextOutput->bValid && ComponentNextOutput->Type == CurAsyncType) { NextAsyncOutput = ComponentNextOutput; OutputInterpAlpha = Alpha; } } } // these are sorted by timestamp, we are using latest, so remove entries that came before it. TArray NewOutputsWaitingOn; for (int32 CopyIndex = PendingOutputIdx; CopyIndex < OutputsWaitingOn.Num(); ++CopyIndex) { NewOutputsWaitingOn.Add(OutputsWaitingOn[CopyIndex]); } OutputsWaitingOn = MoveTemp(NewOutputsWaitingOn); break; } } } if (NextOutput && CurOutput) { if (NextOutput->Timestamp != CurOutput->Timestamp) { // NextOutput and CurOutput occurred in different steps, so we need to search for our specific component. for (int32 PendingOutputIdx = 0; PendingOutputIdx < OutputsWaitingOn.Num(); ++PendingOutputIdx) { // Found the correct pending output, use index to get the vehicle. if (OutputsWaitingOn[PendingOutputIdx].Timestamp == NextOutput->Timestamp) { FBuoyancyComponentAsyncOutput* ComponentOutput = NextOutput->Outputs[OutputsWaitingOn[PendingOutputIdx].Idx].Get(); if (ComponentOutput && ComponentOutput->bValid && ComponentOutput->Type == CurAsyncType) { NextAsyncOutput = ComponentOutput; OutputInterpAlpha = Alpha; } break; } } } } FAsyncOutputWrapper& NewOutput = OutputsWaitingOn.AddDefaulted_GetRef(); NewOutput.Timestamp = BuoyancyManagerTimestamp; NewOutput.Idx = InputIdx; } void UBuoyancyComponent::FinalizeSimCallbackData(FBuoyancyManagerAsyncInput& Input) { for (UWaterBodyComponent* WaterBodyComponent : GetCurrentWaterBodyComponents()) { if (WaterBodyComponent && !Input.WaterBodyComponentToSolverData.Contains(WaterBodyComponent)) { TUniquePtr WaterBodyData = MakeUnique(WaterBodyComponent); Input.WaterBodyComponentToSolverData.Add(WaterBodyComponent, MoveTemp(WaterBodyData)); } } CurAsyncInput = nullptr; CurAsyncOutput = nullptr; } void UBuoyancyComponent::GameThread_ProcessIntermediateAsyncOutput(const FBuoyancyComponentAsyncOutput& Output) { if (Output.Type != EAsyncBuoyancyComponentDataType::AsyncBuoyancyInvalid) { const FBuoyancyComponentBaseAsyncOutput& BaseOutput = static_cast(Output); for (const TPair& Event : BaseOutput.SimOutput.Events) { if (Event.Value == EBuoyancyEvent::EnteredWaterBody) { OnPontoonEnteredWater(Event.Key); } else if (Event.Value == EBuoyancyEvent::ExitedWaterBody) { OnPontoonExitedWater(Event.Key); } } } } void UBuoyancyComponent::GameThread_ProcessIntermediateAsyncOutput(const FBuoyancyManagerAsyncOutput& AsyncOutput) { if (!IsUsingAsyncPath()) { return; } for (int32 PendingOutputIdx = 0; PendingOutputIdx < OutputsWaitingOn.Num(); ++PendingOutputIdx) { // Found the correct pending output, use index to get the vehicle. if (OutputsWaitingOn[PendingOutputIdx].Timestamp == AsyncOutput.Timestamp) { const FBuoyancyComponentAsyncOutput* Output = AsyncOutput.Outputs[OutputsWaitingOn[PendingOutputIdx].Idx].Get(); if (Output && Output->bValid) { GameThread_ProcessIntermediateAsyncOutput(*Output); } } } } bool UBuoyancyComponent::IsUsingAsyncPath() const { bool bAsyncSolver = false; if (UWorld* World = GetWorld()) { if (const FPhysScene* PhysScene = World->GetPhysicsScene()) { if (const Chaos::FPBDRigidsSolver* Solver = PhysScene->GetSolver()) { bAsyncSolver = Solver->IsUsingAsyncResults(); } } } return bAsyncSolver && bUseAsyncPath && (CVarWaterBuoyancyUseAsyncPath.GetValueOnAnyThread() > 0); } TUniquePtr UBuoyancyComponent::CreateAsyncAux() const { TUniquePtr Aux = MakeUnique(); Aux->BuoyancyData = BuoyancyData; return Aux; }