// Copyright Epic Games, Inc. All Rights Reserved. #include "WaterBodyOceanComponent.h" #include "DynamicMesh/InfoTypes.h" #include "WaterModule.h" #include "WaterSplineComponent.h" #include "PhysicsEngine/ConvexElem.h" #include "OceanCollisionComponent.h" #include "WaterBooleanUtils.h" #include "WaterSubsystem.h" #include "ConstrainedDelaunay2.h" #include "Operations/InsetMeshRegion.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "Curve/PolygonOffsetUtils.h" #include "Engine/World.h" #if WITH_EDITOR #include "Misc/UObjectToken.h" #include "WaterRuntimeSettings.h" #endif // WITH_EDITOR #include UE_INLINE_GENERATED_CPP_BY_NAME(WaterBodyOceanComponent) #define LOCTEXT_NAMESPACE "Water" // ---------------------------------------------------------------------------------- UWaterBodyOceanComponent::UWaterBodyOceanComponent(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { CollisionExtents = FVector(50000.f, 50000.f, 10000.f); OceanExtents = FVector2D(51200., 51200.); // @todo_water : Remove these checks (Once AWaterBody is no more Blueprintable, these methods should become PURE_VIRTUAL and this class should overload them) check(IsFlatSurface()); check(IsWaterSplineClosedLoop()); check(IsHeightOffsetSupported()); } TArray UWaterBodyOceanComponent::GetCollisionComponents(bool bInOnlyEnabledComponents) const { TArray Result; Result.Reserve(CollisionBoxes.Num() + CollisionHullSets.Num()); Algo::TransformIf(CollisionBoxes, Result, [bInOnlyEnabledComponents](UOceanBoxCollisionComponent* Comp) { return ((Comp != nullptr) && (!bInOnlyEnabledComponents || (Comp->GetCollisionEnabled() != ECollisionEnabled::NoCollision))); }, [](UOceanBoxCollisionComponent* Comp) { return Comp; }); Algo::TransformIf(CollisionHullSets, Result, [bInOnlyEnabledComponents](UOceanCollisionComponent* Comp) { return ((Comp != nullptr) && (!bInOnlyEnabledComponents || (Comp->GetCollisionEnabled() != ECollisionEnabled::NoCollision))); }, [](UOceanCollisionComponent* Comp) { return Comp; }); return Result; } void UWaterBodyOceanComponent::SetHeightOffset(float InHeightOffset) { const float ClampedHeightOffset = FMath::Max(0.0f, InHeightOffset); if (HeightOffset != ClampedHeightOffset) { HeightOffset = ClampedHeightOffset; // the physics volume needs to be adjusted : FOnWaterBodyChangedParams Params; Params.bShapeOrPositionChanged = true; OnWaterBodyChanged(Params); } } void UWaterBodyOceanComponent::BeginUpdateWaterBody() { Super::BeginUpdateWaterBody(); // Update WaterSubsystem's OceanActor if (UWaterSubsystem* WaterSubsystem = UWaterSubsystem::GetWaterSubsystem(GetWorld())) { WaterSubsystem->SetOceanBodyComponent(this); } } #if WITH_EDITOR void UWaterBodyOceanComponent::SetCollisionExtents(const FVector& NewExtents) { CollisionExtents = NewExtents; FOnWaterBodyChangedParams Params; Params.bShapeOrPositionChanged = true; Params.bWeightmapSettingsChanged = false; UpdateAll(Params); } void UWaterBodyOceanComponent::SetOceanExtent(const FVector2D& NewExtents) { OceanExtents = NewExtents; UpdateWaterBodyRenderData(); } void UWaterBodyOceanComponent::FillWaterZoneWithOcean() { if (const AWaterZone* WaterZone = GetWaterZone()) { OceanExtents = WaterZone->GetZoneExtent(); UpdateWaterBodyRenderData(); } } void UWaterBodyOceanComponent::OnPostEditChangeProperty(FOnWaterBodyChangedParams& InOutOnWaterBodyChangedParams) { Super::OnPostEditChangeProperty(InOutOnWaterBodyChangedParams); const FPropertyChangedEvent& PropertyChangedEvent = InOutOnWaterBodyChangedParams.PropertyChangedEvent; if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UWaterBodyOceanComponent, CollisionExtents)) { // Affects the physics shape InOutOnWaterBodyChangedParams.bShapeOrPositionChanged = true; } else if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UWaterBodyOceanComponent, OceanExtents) || PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UWaterBodyOceanComponent, SavedZoneLocation)) { InOutOnWaterBodyChangedParams.bShapeOrPositionChanged = true; } } const TCHAR* UWaterBodyOceanComponent::GetWaterSpriteTextureName() const { return TEXT("/Water/Icons/WaterBodyOceanSprite"); } #endif // Adds a quad to a dynamic mesh reusing passed vertex indices if they are not INDEX_NONE static void AddAABBQuadToDynamicMesh(UE::Geometry::FDynamicMesh3& EditMesh, FVector3d Min, FVector3d Max, TArray>& InOutVertices) { using namespace UE::Geometry; int32 VertexAIndex = InOutVertices[0]; int32 VertexBIndex = InOutVertices[1]; int32 VertexCIndex = InOutVertices[2]; int32 VertexDIndex = InOutVertices[3]; FDynamicMeshColorOverlay* ColorOverlay = EditMesh.Attributes()->PrimaryColors(); FDynamicMeshNormalOverlay* NormalOverlay = EditMesh.Attributes()->PrimaryNormals(); if (VertexAIndex == INDEX_NONE) { FVertexInfo VertexA; VertexA.Position = Min; VertexAIndex = EditMesh.AppendVertex(VertexA); ColorOverlay->AppendElement(FVector4f(0.0)); NormalOverlay->AppendElement(FVector3f(0., 0., 1.)); } if (VertexBIndex == INDEX_NONE) { FVertexInfo VertexB; VertexB.Position = FVector3d(Max.X, Min.Y, 0); VertexBIndex = EditMesh.AppendVertex(VertexB); ColorOverlay->AppendElement(FVector4f(0.0)); NormalOverlay->AppendElement(FVector3f(0., 0., 1.)); } if (VertexCIndex == INDEX_NONE) { FVertexInfo VertexC; VertexC.Position = Max; VertexCIndex = EditMesh.AppendVertex(VertexC); ColorOverlay->AppendElement(FVector4f(0.0)); NormalOverlay->AppendElement(FVector3f(0., 0., 1.)); } if (VertexDIndex == INDEX_NONE) { FVertexInfo VertexD; VertexD.Position = FVector3d(Min.X, Max.Y, 0); VertexDIndex = EditMesh.AppendVertex(VertexD); ColorOverlay->AppendElement(FVector4f(0.0)); NormalOverlay->AppendElement(FVector3f(0., 0., 1.)); } // Only add triangles for this quad if the min/max are actually the min/max. This inversion could happen if // the island spline extends outside the ocean extent thereby flipping the corner positions around. if (Min.X < Max.X && Min.Y < Max.Y) { const int TriOne = EditMesh.AppendTriangle(VertexAIndex, VertexDIndex, VertexBIndex); ColorOverlay->SetTriangle(TriOne, FIndex3i(VertexAIndex, VertexDIndex, VertexBIndex)); const int TriTwo = EditMesh.AppendTriangle(VertexBIndex, VertexDIndex, VertexCIndex); ColorOverlay->SetTriangle(TriTwo, FIndex3i(VertexBIndex, VertexDIndex, VertexCIndex)); } InOutVertices = {VertexAIndex, VertexBIndex, VertexCIndex, VertexDIndex}; } bool UWaterBodyOceanComponent::GenerateWaterBodyMesh(UE::Geometry::FDynamicMesh3& OutMesh, UE::Geometry::FDynamicMesh3* OutDilatedMesh) const { TRACE_CPUPROFILER_EVENT_SCOPE(GenerateOceanMesh); using namespace UE::Geometry; const UWaterSplineComponent* SplineComp = GetWaterSpline(); const FVector OceanLocation = GetComponentLocation(); if (SplineComp->GetNumberOfSplineSegments() < 3) { return false; } FPolygon2d Island; TArray PolyLineVertices; SplineComp->ConvertSplineToPolyLine(ESplineCoordinateSpace::Local, FMath::Square(10.f), PolyLineVertices); FAxisAlignedBox2d IslandBounds = FAxisAlignedBox2d(FVector2d(0.f), FVector2d(0.f)); // Construct a 2D polygon describing the central island for (int32 i = PolyLineVertices.Num() - 2; i >= 0; --i) // skip the last vertex since it's the same as the first vertex { Island.AppendVertex(FVector2D(PolyLineVertices[i])); IslandBounds.Contain(FVector2d(PolyLineVertices[i])); } // Expand the island slightly so we aren't intersecting with the spline IslandBounds.Expand(1); const FVector2D OceanExtentScaled = OceanExtents / FVector2D(GetComponentScale()); const FBox OceanBounds3d = CalcBounds(FTransform::Identity).GetBox(); const FAxisAlignedBox2d OceanBounds = FAxisAlignedBox2d(FVector2d(OceanBounds3d.Min), FVector2d(OceanBounds3d.Max)); FPolygon2d IslandBoundingPolygon = FPolygon2d::MakeRectangle(IslandBounds.Center(), IslandBounds.Extents().X * 2., IslandBounds.Extents().Y * 2.); FConstrainedDelaunay2d Triangulation; Triangulation.FillRule = FConstrainedDelaunay2d::EFillRule::Positive; Triangulation.Add(IslandBoundingPolygon); if (!Island.IsClockwise()) { Island.Reverse(); } Triangulation.Add(Island); if (!Triangulation.Triangulate()) { UE_LOG(LogWater, Warning, TEXT("Failed to triangulate Ocean mesh for %s. Ensure that the Ocean's spline does not form any loops."), *GetOwner()->GetActorNameOrLabel()); } if (Triangulation.Triangles.Num() == 0) { return false; } int32 IslandBottomLeft = INDEX_NONE; FVector3d IslandBottomLeftVertex = FVector3d::ZeroVector; int32 IslandBottomRight = INDEX_NONE; FVector3d IslandBottomRightVertex = FVector3d::ZeroVector; int32 IslandTopRight = INDEX_NONE; FVector3d IslandTopRightVertex = FVector3d::ZeroVector; int32 IslandTopLeft = INDEX_NONE; FVector3d IslandTopLeftVertex = FVector3d::ZeroVector; check(OutMesh.Attributes()); FDynamicMeshColorOverlay* ColorOverlay = OutMesh.Attributes()->PrimaryColors(); FDynamicMeshNormalOverlay* NormalOverlay = OutMesh.Attributes()->PrimaryNormals(); for (const FVector2d& Vertex : Triangulation.Vertices) { const FVertexInfo VertexInfo(FVector3d(Vertex, 0.f)); const int32 Index = OutMesh.AppendVertex(VertexInfo); ColorOverlay->AppendElement(FVector4f(0.0)); NormalOverlay->AppendElement(FVector3f(0., 0., 1.)); // Collect the corner vertices of the island bounding box so we can stitch the outer quads to them. if ((IslandBottomLeft == INDEX_NONE) || (VertexInfo.Position.X < IslandBottomLeftVertex.X || VertexInfo.Position.Y < IslandBottomLeftVertex.Y)) { IslandBottomLeft = Index; IslandBottomLeftVertex = VertexInfo.Position; } if ((IslandBottomRight == INDEX_NONE) || (VertexInfo.Position.X > IslandBottomRightVertex.X || VertexInfo.Position.Y < IslandBottomRightVertex.Y)) { IslandBottomRight = Index; IslandBottomRightVertex = VertexInfo.Position; } if ((IslandTopRight == INDEX_NONE) || (VertexInfo.Position.X > IslandTopRightVertex.X || VertexInfo.Position.Y > IslandTopRightVertex.Y)) { IslandTopRight = Index; IslandTopRightVertex = VertexInfo.Position; } if ((IslandTopLeft == INDEX_NONE) || (VertexInfo.Position.X < IslandTopLeftVertex.X || VertexInfo.Position.Y > IslandTopLeftVertex.Y)) { IslandTopLeft = Index; IslandTopLeftVertex = VertexInfo.Position; } } check(IslandBottomLeft != INDEX_NONE && IslandBottomRight != INDEX_NONE && IslandTopRight != INDEX_NONE && IslandTopLeft != INDEX_NONE); for (const FIndex3i& Triangle : Triangulation.Triangles) { const int TriangleID = OutMesh.AppendTriangle(Triangle); ColorOverlay->SetTriangle(TriangleID, Triangle); NormalOverlay->SetTriangle(TriangleID, Triangle); } // Add the bounding quads: // Each quad has four vertices laid out like so // D --- C // | \ | // A --- B { TArray> Vertices({ INDEX_NONE, INDEX_NONE, INDEX_NONE, INDEX_NONE }); // Bottom left quad Vertices[2] = IslandBottomLeft; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(OceanBounds.Min, 0), FVector3d(IslandBounds.Min, 0), Vertices); // We need the B and C vertices of the first quad to attach to the last quad int32 FirstQuadB = Vertices[1]; int32 FirstQuadC = Vertices[2]; // Left middle quad Vertices[0] = Vertices[3]; Vertices[1] = Vertices[2]; Vertices[2] = IslandTopLeft; Vertices[3] = INDEX_NONE; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(OceanBounds.Min.X, IslandBounds.Min.Y, 0), FVector3d(IslandBounds.Min.X, IslandBounds.Max.Y, 0), Vertices); // Top left quad Vertices[0] = Vertices[3]; Vertices[1] = Vertices[2]; Vertices[2] = INDEX_NONE; Vertices[3] = INDEX_NONE; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(OceanBounds.Min.X, IslandBounds.Max.Y, 0), FVector3d(IslandBounds.Min.X, OceanBounds.Max.Y, 0), Vertices); // Top middle quad Vertices[0] = Vertices[1]; Vertices[3] = Vertices[2]; Vertices[1] = IslandTopRight; Vertices[2] = INDEX_NONE; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(IslandBounds.Min.X, IslandBounds.Max.Y, 0), FVector3d(IslandBounds.Max.X, OceanBounds.Max.Y, 0), Vertices); // Top right quad Vertices[0] = Vertices[1]; Vertices[3] = Vertices[2]; Vertices[1] = INDEX_NONE; Vertices[2] = INDEX_NONE; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(IslandBounds.Max, 0), FVector3d(OceanBounds.Max, 0), Vertices); // Middle right quad Vertices[3] = Vertices[0]; Vertices[2] = Vertices[1]; Vertices[1] = INDEX_NONE; Vertices[0] = IslandBottomRight; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(IslandBounds.Max.X, IslandBounds.Min.Y, 0), FVector3d(OceanBounds.Max.X, IslandBounds.Max.Y, 0), Vertices); // Bottom right quad Vertices[3] = Vertices[0]; Vertices[2] = Vertices[1]; Vertices[1] = INDEX_NONE; Vertices[0] = INDEX_NONE; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(IslandBounds.Max.X, OceanBounds.Min.Y, 0), FVector3d(OceanBounds.Max.X, IslandBounds.Min.Y, 0), Vertices); // Middle bottom quad Vertices[1] = Vertices[0]; Vertices[2] = Vertices[3]; Vertices[0] = FirstQuadB; Vertices[3] = FirstQuadC; AddAABBQuadToDynamicMesh(OutMesh, FVector3d(IslandBounds.Min.X, OceanBounds.Min.Y, 0), FVector3d(IslandBounds.Max.X, IslandBounds.Min.Y, 0), Vertices); } if (ShapeDilation > 0.f && OutDilatedMesh) { TRACE_CPUPROFILER_EVENT_SCOPE(DilateOceanMesh); FPolygon2d OceanBoundingPoly; OceanBoundingPoly.AppendVertex(FVector2D(OceanBounds.Min)); OceanBoundingPoly.AppendVertex(FVector2D(OceanBounds.Min.X, OceanBounds.Max.Y)); OceanBoundingPoly.AppendVertex(FVector2D(OceanBounds.Max)); OceanBoundingPoly.AppendVertex(FVector2D(OceanBounds.Max.X, OceanBounds.Min.Y)); FGeneralPolygon2d OceanPoly(OceanBoundingPoly); if (Island.IsClockwise()) { Island.Reverse(); } OceanPoly.AddHole(Island); TArray OffsetPolys; UE::Geometry::PolygonsOffset( ShapeDilation / 2.f, { OceanPoly }, OffsetPolys, false, 2.0); FConstrainedDelaunay2d DilationTriangulation; DilationTriangulation.FillRule = FConstrainedDelaunay2d::EFillRule::Positive; for (const FGeneralPolygon2d& Poly : OffsetPolys) { if (Poly.SignedArea() <= 0.) { UE_LOG(LogWater, Warning, TEXT("Failed to apply offset for shape dilation (%s"), *GetOwner()->GetActorNameOrLabel()); continue; } DilationTriangulation.Add(Poly); } if (!DilationTriangulation.Triangulate()) { UE_LOG(LogWater, Warning, TEXT("Failed to triangulate dilated ocean mesh (%s"), *GetOwner()->GetActorNameOrLabel()); return false; } if (DilationTriangulation.Triangles.Num() == 0) { return false; } FDynamicMeshColorOverlay* DilatedColorOverlay = OutDilatedMesh->Attributes()->PrimaryColors(); FDynamicMeshNormalOverlay* DilatedNormalOverlay = OutDilatedMesh->Attributes()->PrimaryNormals(); for (const FVector2d& Vertex : DilationTriangulation.Vertices) { FVertexInfo MeshVertex(FVector3d(Vertex.X, Vertex.Y, 0.)); OutDilatedMesh->AppendVertex(MeshVertex); DilatedColorOverlay->AppendElement(FVector4f(0.f)); DilatedNormalOverlay->AppendElement(FVector3f(0., 0., 1.)); } for (const FIndex3i& Triangle : DilationTriangulation.Triangles) { const int TriangleID = OutDilatedMesh->AppendTriangle(Triangle); DilatedColorOverlay->SetTriangle(TriangleID, Triangle); DilatedNormalOverlay->SetTriangle(TriangleID, Triangle); } } return true; } void UWaterBodyOceanComponent::Reset() { for (UBoxComponent* Component : CollisionBoxes) { if (Component) { Component->DestroyComponent(); } } CollisionBoxes.Reset(); for (UOceanCollisionComponent* Component : CollisionHullSets) { if (Component) { Component->DestroyComponent(); } } CollisionHullSets.Reset(); } void UWaterBodyOceanComponent::PostLoad() { Super::PostLoad(); } void UWaterBodyOceanComponent::OnPostRegisterAllComponents() { Super::OnPostRegisterAllComponents(); #if WITH_EDITOR // Only run the fixup code when the object was loaded. This is required as any newly created and not-yet-saved objects will have no linker and therefore return an invalid custom version. if (HasAnyFlags(RF_WasLoaded)) { // In this version the ocean was changed to not be strongly coupled to the water zone and instead rely on the user to keep the mesh extent in sync with the zone extent if (GetLinkerCustomVersion(FFortniteMainBranchObjectVersion::GUID) < FFortniteMainBranchObjectVersion::WaterBodyStaticMeshComponents) { // Trigger a rebuild of the render data manually since this doesn't occur on load anymore. // The mesh extent will be out of sync for old oceans so ensure they rebuild now: FillWaterZoneWithOcean(); } } #endif // WITH_EDITOR } FBoxSphereBounds UWaterBodyOceanComponent::CalcBounds(const FTransform& LocalToWorld) const { const FTransform& ComponentTransform = GetComponentTransform(); FVector RelativeLocationToZone = (bCenterOnWaterZone && !OwningWaterZone.IsNull()) ? ComponentTransform.InverseTransformPosition(FVector(SavedZoneLocation, 0.0)) : FVector::ZeroVector; RelativeLocationToZone.Z = 0; const FVector2D OceanExtentScaled = (OceanExtents / FVector2D(GetComponentScale())) / 2.; return FBoxSphereBounds(RelativeLocationToZone, FVector(OceanExtentScaled.X, OceanExtentScaled.Y, GetChannelDepth()), FMath::Max(OceanExtentScaled.X, OceanExtentScaled.Y)).TransformBy(LocalToWorld); } void UWaterBodyOceanComponent::OnPostActorCreated() { Super::OnPostActorCreated(); #if WITH_EDITOR if (UWorld* World = GetWorld(); World && World->IsGameWorld() == false) { UpdateWaterZones(); FillWaterZoneWithOcean(); } #endif // WITH_EDITOR } #if WITH_EDITOR void UWaterBodyOceanComponent::OnWaterBodyRenderDataUpdated() { Super::OnWaterBodyRenderDataUpdated(); // Store the location of the zone when the ocean render data is created so we can validate this later and check for inconsistencies: if (const AWaterZone* WaterZone = GetWaterZone()) { SavedZoneLocation = FVector2D(WaterZone->GetActorLocation()); } } TArray> UWaterBodyOceanComponent::CheckWaterBodyStatus() { TArray> Result = Super::CheckWaterBodyStatus(); if (const AWaterZone* WaterZone = GetWaterZone()) { auto DisableWarnOnMismatchExtent = []() { if (UWaterRuntimeSettings* WaterRuntimeSettings = GetMutableDefault()) { WaterRuntimeSettings->SetShouldWarnOnMismatchOceanExtent(false); } }; const UWaterRuntimeSettings* WaterRuntimeSettings = GetDefault(); check(WaterRuntimeSettings); if (WaterRuntimeSettings->ShouldWarnOnMismatchOceanExtent()) { if ((WaterZone->GetZoneExtent() != OceanExtents) || (FVector2D(WaterZone->GetActorLocation()) != SavedZoneLocation)) { Result.Add(FTokenizedMessage::Create(EMessageSeverity::Warning) ->AddToken(FUObjectToken::Create(this)) ->AddToken(FTextToken::Create(FText::Format( LOCTEXT("MapCheck_Message_MismatchedOceanExtent", "WaterBodyOcean ({0}) has a serialized mesh which does not match the WaterZone it belongs to ({1})."), FText::FromString(*GetNameSafe(this)), FText::FromString(*GetNameSafe(WaterZone))))) ->AddToken(FActionToken::Create(LOCTEXT("MapCheck_MessageAction_SaveOcean", "Click here to fill the zone with ocean."), FText(), FOnActionTokenExecuted::CreateUObject(this, &UWaterBodyOceanComponent::FillWaterZoneWithOcean))) ->AddToken(FActionToken::Create(LOCTEXT("MapCheck_MessageAction_DisableOceanZoneMismatchWarning", "If this is desired behavior, click here to disable this warning."), FText(), FOnActionTokenExecuted::CreateLambda(DisableWarnOnMismatchExtent))) ); } } } return Result; } #endif // WITH_EDITOR void UWaterBodyOceanComponent::OnUpdateBody(bool bWithExclusionVolumes) { AActor* OwnerActor = GetOwner(); check(OwnerActor); if (GetCollisionEnabled() != ECollisionEnabled::NoCollision) { FVector OceanCollisionExtents = GetCollisionExtents(); OceanCollisionExtents.Z += CollisionHeightOffset / 2; // The volume's top is located on the ocean actor's height + the additional ocean level + the collision height offset: // and the volume's bottom is deeper by a value == OceanCollisionExtents.Z : FVector OceanBoxLocation = FVector(0, 0, GetHeightOffset() - OceanCollisionExtents.Z + CollisionHeightOffset); // No matter the scale, OceanCollisionExtents is always specified in world-space : FVector OceanBoxExtent = OceanCollisionExtents; // get our box information and exclusion volumes FTransform ComponentTransform = GetComponentTransform(); FBoxSphereBounds WorldBounds; WorldBounds.Origin = ComponentTransform.TransformPositionNoScale(OceanBoxLocation); WorldBounds.BoxExtent = OceanBoxExtent; TArray Exclusions = bWithExclusionVolumes ? GetExclusionVolumes() : TArray(); // Calculate a set of boxes and meshes that are Difference(Box, Union(ExclusionVolumes)) // Output is calculated in World space and then transformed into Actor space, ie by inverse of ActorTransform TArray Boxes; TArray> ConvexSets; double WorldMeshBufferWidth = 1000.0; // extra space left around exclusion meshes double WorldBoxOverlap = 10.0; // output boxes overlap each other and meshes by this amount FWaterBooleanUtils::BuildOceanCollisionComponents(WorldBounds, ComponentTransform, Exclusions, Boxes, ConvexSets, WorldMeshBufferWidth, WorldBoxOverlap); // Don't delete components unless we have to : this generates determinism issues because UOceanCollisionComponent has a UBodySetup with a GUID : if ((CollisionBoxes.Num() != Boxes.Num()) || (CollisionHullSets.Num() != ConvexSets.Num())) { Reset(); } // create the box components for (int32 i = 0; i < Boxes.Num(); ++i) { const FBoxSphereBounds& Box = Boxes[i]; // We want a deterministic name within this water body component's outer to avoid non-deterministic cook issues but we also want to avoid reusing a component that might have been deleted // prior to that (in order to avoid potentially stalls caused by the primitive component not having been FinishDestroy-ed) (because OnUpdateBody runs 2 times in a row, // once with bWithExclusionVolumes == false, once with bWithExclusionVolumes == true) so we use MakeUniqueObjectName for the name here : FName Name = MakeUniqueObjectName(OwnerActor, UOceanCollisionComponent::StaticClass(), *FString::Printf(TEXT("OceanCollisionBoxComponent_%d"), i)); UOceanBoxCollisionComponent* BoxComponent = nullptr; if (CollisionBoxes.IsValidIndex(i) && (CollisionBoxes[i] != nullptr)) { BoxComponent = CollisionBoxes[i]; } else { BoxComponent = NewObject(OwnerActor, Name, RF_Transactional); BoxComponent->SetupAttachment(this); CollisionBoxes.Add(BoxComponent); } BoxComponent->SetNetAddressable(); // it's deterministically named so it's addressable over network (needed for collision) BoxComponent->bDrawOnlyIfSelected = true; BoxComponent->SetRelativeLocation(FVector::ZeroVector); CopySharedCollisionSettingsToComponent(BoxComponent); CopySharedNavigationSettingsToComponent(BoxComponent); FVector RelativePosition = Box.Origin; // boxes are calculated in space of actor BoxComponent->SetRelativeLocation(RelativePosition); BoxComponent->SetBoxExtent(Box.BoxExtent); // Registers as the last step to reduce amount of updates required when modifying an already registered component if (!BoxComponent->IsRegistered()) { BoxComponent->RegisterComponent(); } } // create the convex-hull components for (int32 i = 0; i < ConvexSets.Num(); ++i) { const TArray& ConvexSet = ConvexSets[i]; // We want a deterministic name within this water body component's outer to avoid non-deterministic cook issues but we also want to avoid reusing a component that might have been deleted // prior to that (in order to avoid potentially stalls caused by the primitive component not having been FinishDestroy-ed) (because OnUpdateBody runs 2 times in a row, // once with bWithExclusionVolumes == false, once with bWithExclusionVolumes == true) so we use MakeUniqueObjectName for the name here : FName Name = MakeUniqueObjectName(OwnerActor, UOceanCollisionComponent::StaticClass(), *FString::Printf(TEXT("OceanCollisionComponent_%d"), i)); UOceanCollisionComponent* CollisionComponent = nullptr; if (CollisionHullSets.IsValidIndex(i) && (CollisionHullSets[i] != nullptr)) { CollisionComponent = CollisionHullSets[i]; } else { CollisionComponent = NewObject(OwnerActor, Name, RF_Transactional); CollisionComponent->SetupAttachment(this); CollisionHullSets.Add(CollisionComponent); } CollisionComponent->SetNetAddressable(); // it's deterministically named so it's addressable over network (needed for collision) CollisionComponent->SetRelativeLocation(FVector::ZeroVector); CopySharedCollisionSettingsToComponent(CollisionComponent); CopySharedNavigationSettingsToComponent(CollisionComponent); CollisionComponent->InitializeFromConvexElements(ConvexSet); // Registers as the last step to reduce amount of updates required when modifying an already registered component if (!CollisionComponent->IsRegistered()) { CollisionComponent->RegisterComponent(); } } } else { // clear existing Reset(); } } #undef LOCTEXT_NAMESPACE