Runtime water body (River) creation, C++

So I’m aware that all this water stuff is still experimental, even here in UE 5.5.1, but let’s see where this leads.

In its current experimental form, a lot of this stuff (PCG, Water Bodies) seems almost actively hostile to terrains generated at runtime. In particular, I’m not using a Landscape as my landform, because so far as I can tell their fundamental geometry/heightmap isn’t something that can be created or altered at runtime. So my base landform is a procedural mesh, into which I’m attempting to create water bodies (initially, a river, I’ll eventually need lakes and ocean).

First off, I verified that this is even theoretically possible:

  • Create a new Third-person project (note that the default map is NOT Landscape-based).
  • Turn on all the water plugins and restart.
  • Drag in a Water Body River (which also implicitly creates a Water Zone).

And there it is, in all its glory. It doesn’t make physical sense, basically being the “water” part of a river floating in the air, but it clearly works. So I’m not entirely crazy to believe I might be able to do this myself.

And I’m close, but I’m hitting a couple significant snags.

I’m trying to create a WaterBodyRiver actor at runtime, as part of a large, tiled, entirely runtime-generated landscape. Since I don’t get the automatic channel-cutting that rivers and the like would give you in the editor, I determine the river’s spline path ahead of time, and manually carve a channel in my terrain for it to “drop into.”

Then I instantiate the actor itself:

		riverBody = GetWorld()->SpawnActor<AWaterBodyRiver>(RiverBodyActorClass, GetActorLocation(), FRotator(0.0f, 0.0f, 0.0f));
		UWaterSplineComponent* spline = Cast<UWaterSplineComponent>(riverBody->GetDefaultSubobjectByName(FName("WaterSpline")));
		if (!spline)
		{
			UE_LOG(LogTemp, Error, TEXT("Could not find spline subcomponent"));
		}
		else
		{
			spline->ClearSplinePoints();
			spline->AddPoints(RiverSplinePoints);
		}

		waterZone = GetWorld()->SpawnActor<AWaterZone>(AWaterZone::StaticClass(), GetActorLocation(), FRotator(0.0f, 0.0f, 0.0f));
		waterZone->SetZoneExtent(FVector2D(PatchSize * 200.0f, PatchSize * 200.0f));

(“RiverBodyActorClass” is a blueprint subclass of AWaterBodyRiver so that I can set up some additional debugging and/or have more control, but the code works exactly the same if you replace it with AWaterBodyRiver::StaticClass().)

At runtime, the SpawnActor() call for the River body itself will produce some warnings:

UEDPIE_0_RunelandsMap.RunelandsMap:PersistentLevel.BP_WaterBodyRiver_C_0.SplineMeshComponent_0’ but Mobility is Static.
PIE: Warning: Calling SetStaticMesh on ‘/Game/Maps/UEDPIE_0_RunelandsMap.RunelandsMap:PersistentLevel.BP_WaterBodyRiver_C_0.SplineMeshComponent_1’ but Mobility is Static.
PIE: Warning: Calling SetStaticMesh on ‘/Game/Maps/UEDPIE_0_RunelandsMap.RunelandsMap:PersistentLevel.BP_WaterBodyRiver_C_0.SplineMeshComponent_0’ but Mobility is Static.
PIE: Warning: Calling SetStaticMesh on ‘/Game/Maps/UEDPIE_0_RunelandsMap.RunelandsMap:PersistentLevel.BP_WaterBodyRiver_C_0.SplineMeshComponent_1’ but Mobility is Static.
PIE: Warning: Calling SetStaticMesh on ‘/Game/Maps/UEDPIE_0_RunelandsMap.RunelandsMap:PersistentLevel.BP_WaterBodyRiver_C_0.SplineMeshComponent_0’ but Mobility is Static.
PIE: Warning: Calling SetStaticMesh on ‘/Game/Maps/UEDPIE_0_RunelandsMap.RunelandsMap:PersistentLevel.BP_WaterBodyRiver_C_0.SplineMeshComponent_1’ but Mobility is Static.

Those warnings appear whether or not I actually try to set the location, everything in the actor class is already set to “Movable,” and the Spline Mesh components are created by the actor itself during its initialization, so I have no ability to access them, anyway.

Using ninja-level skills developed over 40 years of software development, I ignored those warnings.

Anyway, back to the code. A few seconds into gameplay (because the water zone apparently needs a little time to make meshes), I try to hook everything up:

		UWaterBodyRiverComponent* rivComp = Cast<UWaterBodyRiverComponent>(
			riverBody->GetDefaultSubobjectByName(FName("WaterBodyRiverComponent")));
			if (!rivComp)
			{
			       rivComp->SetMobility(EComponentMobility::Movable);
				rivComp->SetWaterZoneOverride(waterZone);
			}

And…nothing happens, except sometimes another couple of those warnings.

But at this point, if I pause the game in the editor, and make basically any change at all to the WaterBodyRiver actor’s details (e.g. move it’s location up a centimeter, or even just change the “Player Can Step Up On” collision setting), suddenly we get a glorious–albeit geographically improbable–river!

Attempts to modify those same parameters from code have no effect, however. It has to be done manually in the editor; something about that process triggers it to build itself.

So I guess there are two questions:

  • Any way to get the spawned water body actor not to complain about being static?
  • More importantly – how do I trigger the river to actually appear, from code?.

At this point I’m not too worried about physics or post-process effects (although the latter appears to work fine once the river is present; I can drop underwater and see the effects), just the visual appearance of the river.

I prefer the compactness of C++ for my own code, but if anybody has gotten this working from Blueprints, I’d be happy to see that, too.

Hey @TimeWinder2 ,

I ran into the same issue. After changing the Transform on the object at runtime, the water suddenly appeared. Then I added the “UpdateAll” method call and it loaded the water correctly at runtime. Hope this helps.

    UE::Geometry::FDynamicMesh3 WaterMeshRef;
    UE::Geometry::FDynamicMesh3 DilatedMeshRef;

WaterBody = World->SpawnActor<ABellwetherWaterBody>(ABellwetherWaterBody::StaticClass(), GetActorLocation(), GetActorRotation());
    WaterBody->InitializeBody();
    WaterBody->SetActorScale3D(FVector(5.0f, 5.0f, 5.0f));
    WaterBody->SetActorLocation(FVector(0.0f, 0.0f, 0.0f));
    
    WaterBody->MarkComponentsRenderStateDirty();
    WaterBody->UpdateComponentTransforms();

    UGerstnerWaterWaves* Waves = NewObject<UGerstnerWaterWaves>(WaterBody, UGerstnerWaterWaves::StaticClass(), TEXT("WaterWaves"));
    UGerstnerWaterWaveGeneratorSimple* WaveGenerator = NewObject<UGerstnerWaterWaveGeneratorSimple>(Waves, UGerstnerWaterWaveGeneratorSimple::StaticClass(), TEXT("GerstnerWaterWaveGeneratorSimple"));

    Waves->GerstnerWaveGenerator = WaveGenerator;
    WaterBody->SetWaterWaves(Waves);

 UWaterBodyComponent* WaterBodyComponent = WaterBody->GetWaterBodyComponent();
    WaterBodyComponent->SetMobility(EComponentMobility::Movable);
    UWaterBodyLakeComponent* LakeComponent = Cast<UWaterBodyLakeComponent>(WaterBodyComponent);
    LakeComponent->SetMobility(EComponentMobility::Movable);
    WaterBodyManager->AddWaterBodyComponent(WaterBodyComponent);
    WaterBodyComponent->ShouldGenerateWaterMeshTile();
    WaterBodyComponent->UpdateWaterZones();
    AWaterZone* WaterZone = WaterBodyComponent->GetWaterZone();
    WaterBodyComponent->SetWaterZoneOverride(WaterZone);
    WaterBodyComponent->WaterMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Water/Materials/WaterSurface/Water_Material_Lake.Water_Material_Lake"));

FWaterBodyHeightmapSettings* HeightmapSettings = new FWaterBodyHeightmapSettings();
    HeightmapSettings->BlendMode = EWaterBrushBlendType::AlphaBlend;

    FWaterCurveSettings* WaterCurveSettings = new FWaterCurveSettings();
    WaterCurveSettings->ChannelDepth = 500.0f;
    WaterCurveSettings->bUseCurveChannel = true;
    WaterCurveSettings->ChannelEdgeOffset = 0.0f;
    WaterCurveSettings->CurveRampWidth = 2000.0f;

    WaterCurveSettings->ElevationCurveAsset = LoadObject<UCurveFloat>(nullptr, TEXT("/Water/Curves/FloatCurve.FloatCurve"));

    if (!WaterCurveSettings->ElevationCurveAsset)
    {
        UE_LOG(LogTemp, Warning, TEXT("Failed to load ElevationCurveAsset! Check the path."));
    }
    WaterBodyComponent->CurveSettings = *WaterCurveSettings;
    WaterBodyComponent->WaterHeightmapSettings = *HeightmapSettings;
    WaterBodyComponent->WaterHLODMaterial = LoadObject<UMaterialInterface>(nullptr, TEXT("/Water/Materials/HLOD/HLODWater.HLODWater"));
    
    UDynamicMeshComponent* WaterMeshComp = NewObject<UDynamicMeshComponent>(this);
    WaterMeshComp->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
    WaterMeshComp->RegisterComponent();
    WaterMeshComp->SetCastShadow(false);
    WaterMeshComp->GetDynamicMesh()->EditMesh([&](FDynamicMesh3& MeshRef)
    {
        MeshRef.EnableAttributes();
        MeshRef.Attributes()->EnablePrimaryColors();
        MeshRef.Attributes()->EnableTangents();
    }, EDynamicMeshChangeType::GeneralEdit);
    UE_LOG(LogTemp, Log, TEXT("Assigning material to mesh component."));
    WaterMeshComp->SetMaterial(0, WaterMaterial);
    if (UBodySetup* BodySetup = WaterMeshComp->GetBodySetup())
    {
        BodySetup->InvalidatePhysicsData();
        BodySetup->CollisionTraceFlag = CTF_UseComplexAsSimple;
        BodySetup->CreatePhysicsMeshes();
        WaterMeshComp->RecreatePhysicsState();
    }
    WaterMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    WaterMeshComp->SetCollisionResponseToAllChannels(ECR_Block);
    WaterMeshComp->SetCollisionObjectType(ECC_WorldStatic);
    WaterMeshComp->EnableComplexAsSimpleCollision();
    WaterMeshComp->UpdateComponentToWorld();
    WaterMeshComp->MarkRenderStateDirty();
    WaterMeshComp->MarkRenderDynamicDataDirty();
    WaterMeshComp->SetMobility(EComponentMobility::Movable);

    UDynamicMeshComponent* DilatedWaterMeshComp = NewObject<UDynamicMeshComponent>(this);
    DilatedWaterMeshComp->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepRelativeTransform);
    DilatedWaterMeshComp->RegisterComponent();
    DilatedWaterMeshComp->SetCastShadow(false); // clearly disable shadows on water mesh
    DilatedWaterMeshComp->GetDynamicMesh()->EditMesh([&](FDynamicMesh3& MeshRef)
    {
        MeshRef.EnableAttributes();
        MeshRef.Attributes()->EnablePrimaryColors();
        MeshRef.Attributes()->EnableTangents();
    }, EDynamicMeshChangeType::GeneralEdit);
    UE_LOG(LogTemp, Log, TEXT("Assigning material to mesh component."));
    DilatedWaterMeshComp->SetMaterial(0, WaterMaterial);
    if (UBodySetup* BodySetup = DilatedWaterMeshComp->GetBodySetup())
    {
        BodySetup->InvalidatePhysicsData();
        BodySetup->CollisionTraceFlag = CTF_UseComplexAsSimple;
        BodySetup->CreatePhysicsMeshes();
        DilatedWaterMeshComp->RecreatePhysicsState();
    }
    DilatedWaterMeshComp->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    DilatedWaterMeshComp->SetCollisionResponseToAllChannels(ECR_Block);
    DilatedWaterMeshComp->SetCollisionObjectType(ECC_WorldStatic);
    DilatedWaterMeshComp->EnableComplexAsSimpleCollision();
    DilatedWaterMeshComp->UpdateComponentToWorld();
    DilatedWaterMeshComp->MarkRenderStateDirty();
    DilatedWaterMeshComp->MarkRenderDynamicDataDirty();
    DilatedWaterMeshComp->SetMobility(EComponentMobility::Movable);

    // Extract Water Mesh
    WaterMeshComp->GetDynamicMesh()->ProcessMesh([&](const FDynamicMesh3& Mesh)
    {
        WaterMeshRef = Mesh; // Copy actual mesh data
    });

    // Extract Dilated Mesh (if available)
    if (DilatedWaterMeshComp)
    {
        DilatedWaterMeshComp->GetDynamicMesh()->ProcessMesh([&](const FDynamicMesh3& Mesh)
        {
            DilatedMeshRef = Mesh; // Copy actual mesh data
        });
    }

    WaterBodyComponent->SetWaterBodyStaticMeshEnabled(true);
    FOnWaterBodyChangedParams OnWaterBodyChangedParams;
    OnWaterBodyChangedParams.bShapeOrPositionChanged = true;
    OnWaterBodyChangedParams.bUserTriggered = true;
    OnWaterBodyChangedParams.bWeightmapSettingsChanged = true;
    WaterBodyComponent->UpdateAll(OnWaterBodyChangedParams);
    WaterBodyComponent->GenerateWaterBodyMesh(WaterMeshRef, DilatedWaterMeshComp ? &DilatedMeshRef : nullptr);

    WaterSubsystem->MarkAllWaterZonesForRebuild(
        EWaterZoneRebuildFlags::All,
        nullptr
    ); 

    // delete FalloffSettings;
    delete HeightmapSettings;
    delete WaterCurveSettings;
1 Like

@Mayour_McCheese

Somehow, I missed this until now. Thank you so much for your response.

I’m impressed by the amount of effort you appear to have put into this based on your code – my scenario is (at least thus far), much simpler, but it’s nice to know that others have blazed the trail once my water starts being more than just cosmetic eye candy to the levels (my next step is to try and fit it with a physics volume, so that one of my third-party components can identify it as water and trigger “swimming” behavior).

But in any case, you correctly identified the solution:

 FOnWaterBodyChangedParams OnWaterBodyChangedParams;
    OnWaterBodyChangedParams.bShapeOrPositionChanged = true;
    OnWaterBodyChangedParams.bUserTriggered = true;
 WaterBodyComponent->UpdateAll(OnWaterBodyChangedParams);

does in fact cause the water to appear at runtime. For anyone reading this in their flying cars of the future – it may require a restart of the engine before it begins behaving. When I added the code, I got a sort of refraction effect that looked a little bit like the “video camera taking video of its own output” until I shut everything down and relaunched.

There still seems to be a lot of “bugs” / “interesting challenges” in these runtime scenarios. For example, the rest of my terrain is filled in by PCG – except when it isn’t. If the river is long/large enough, it appears to take too much time to render or something, and the runtime PCG sometimes happens and sometimes doesn’t. Both Water Body and PCG stuff clearly merit their “experimental” labels at this point. But it’s also clear that this stuff is going to dramatically simplify the sort of procedural “I need this space filled in and I’m not picky about the exact details” sort of needs of random levels in the future.

Hmm, may have spoken too soon. It’s definitely better (the water would never draw before adding that code), but it still fails to draw about one time in five in the editor, and 100% of the time in packaged builds. So I may need to incorporate more of the stuff you’re doing in your code, or keep looking for a different solution.

I’m finding that both water and PCG in general are incredibly intermittent at this stage of development. Both will work some to most of the time, and just completely fail to fire other times, usually without producing any actual errors, and with no code changes between runs. It reminds me of race conditions – whether it works or not seems to depend on something timing related that I haven’t been able to identify.

1 Like