Bugs related to AWaterZone actors

Hi,

I wanted to report some bugs we found in the Water plugin code and pass along some fixes we made:

  • In AWaterZone::GetZoneBounds() the return value is world space but the Z location of the actor isn’t added to the FBox it returns. This causes the renderer culling bounds of the water zone to be incorrect as well as the WP streaming bounds. The fixed code would be like this
FBox AWaterZone::GetZoneBounds() const
{
	const FBox2D ZoneBounds2D = GetZoneBounds2D();
	// #todo_water [roey]: Water zone doesn't have an explicit z bounds yet. For now just use the x or y.
	const double StreamingBoundsZ = FMath::Max(ZoneExtent.X, ZoneExtent.Y);
	const FVector ActorLocation = GetActorLocation();
	return FBox(FVector3d(ZoneBounds2D.Min, ActorLocation.Z - StreamingBoundsZ / 2.), FVector3d(ZoneBounds2D.Max, ActorLocation.Z + StreamingBoundsZ / 2.0));
}
  • FWaterQuadTree::Unlock() creates too short a FQueue which causes a crash in some situations with deep, but narrow quadtrees. It seems it needs to actually be:

FQueue Queue(GetMaxLeafCount()+TreeDepth);* UWaterSubsystem::FindWaterZone finds the wrong WaterZones when doing a LevelInstance Edit-In-Place. It incorrectly chooses actors from the parent umap which results in undesired map to map references. There are probably multiple ways to fix this, but we opted to filter the actors by the LevelInstance to only find sibling actors

TSoftObjectPtr<AWaterZone> UWaterSubsystem::FindWaterZone(const UWorld* World, const FBox2D& Bounds, const TSoftObjectPtr<const ULevel> PreferredLevel)
{
	if (!World)
	{
		return {};
	}
 
	//Only find zones which are in the same LI as the water body. 
	//PreferredLevel we are passed in is the result of GetLevel() on the AWaterBody so we can get the LI from that (if there is one)
	auto LevelInstanceSubsystem = World->GetSubsystem<ULevelInstanceSubsystem>();
	check(LevelInstanceSubsystem);
	ILevelInstanceInterface* LevelInstance = LevelInstanceSubsystem->GetOwningLevelInstance(PreferredLevel.Get());
 
	// Score each overlapping water zone and then pick the best.
	TMap<TSoftObjectPtr<AWaterZone>, int32> ViableZones;
 
#if WITH_EDITOR
	// Within the editor, we also want to check unloaded actors to ensure that the water body has serialized the best possible water zone, rather than just looking through what might be loaded now.
	if (GEditor && !World->IsGameWorld())
	{
		FActorContainerID ContainerID = LevelInstance ? LevelInstance->GetLevelInstanceID().GetContainerID() : FActorContainerID::GetMainContainerID();
		if (UWorldPartition* WorldPartition = World->GetWorldPartition())
		{
			const FBox Bounds3D(FVector(Bounds.Min.X, Bounds.Min.Y, -HALF_WORLD_MAX), FVector(Bounds.Max.X, Bounds.Max.Y, HALF_WORLD_MAX));
			FWorldPartitionHelpers::ForEachIntersectingActorDescInstance<AWaterZone>(WorldPartition, Bounds3D, [&Bounds, &ViableZones, ContainerID](const FWorldPartitionActorDescInstance* ActorDescInstance)
			{
				if (ActorDescInstance->GetContainerInstance()->GetContainerID() == ContainerID)
				{
					FWaterZoneActorDesc* WaterZoneActorDesc = (FWaterZoneActorDesc*)ActorDescInstance->GetActorDesc();
					ViableZones.Emplace(ActorDescInstance->GetActorSoftPath(), WaterZoneActorDesc->GetOverlapPriority());
				}
				return true;
			});
		}
	}
#endif // WITH_EDITOR
 
	for (AWaterZone* WaterZone : TActorRange<AWaterZone>(World, AWaterZone::StaticClass(), EActorIteratorFlags::SkipPendingKill))
	{
		const FBox2D WaterZoneBounds = WaterZone->GetZoneBounds2D();
 
		if (Bounds.Intersect(WaterZoneBounds) && LevelInstanceSubsystem->GetOwningLevelInstance(WaterZone->GetLevel()) == LevelInstance)
		{
			ViableZones.Emplace(WaterZone, WaterZone->GetOverlapPriority());
		}
	}
 
//The rest is the same as the original code...

<Continued in a reply since I hit the max character limit>

[Attachment Removed]

Steps to Reproduce
Create multiple AWaterZone actors, some in Level Instances and some not. Also move one of them away from the origin vertically.

[Attachment Removed]

<Continued…>

We also needed to change UWaterBodyComponent::OnRegister()

void UWaterBodyComponent::OnRegister()
{
        /// Unchanged code omitted...
 
	if (UWorld* World = GetWorld(); World && World->IsGameWorld())
	{
		if (!OwningWaterZone.IsNull())
		{
			//Pin this since we don't to reassign our zone automatically outside the editor viewport
			//This can result in incorrect assignments since the auto-assignment doesn't consider LI nesting once LIs are expanded
			//and AWaterZones trigger re-assignment on all water bodies when they stream in
			WaterZoneOverride = OwningWaterZone;
		}
	}
 
	if (AWaterZone* WaterZone = GetWaterZone())
	{
		WaterZone->AddWaterBodyComponent(this);
	}
}
  • There is a race condition depending on the order that AWaterBodies and AWaterZones stream in in WP. if the Zone streams in after (even during the same frame so there is no visual issue) the water body can end up not registered with the zone. The simplest fix was to modify UWaterBodyComponent::UpdateWaterZones like this to ensure the body is always added to OwnedWaterBodies even if it ends up assigned to the same zone.
if (OwningWaterZone != FoundZone || (FoundZone.IsValid() && !FoundZone->ContainsWaterBodyComponent(this))) //ContainsWaterBodyComponent is just "return OwnedWaterBodies.Contains(WaterBodyComponent);"
{
	if (AWaterZone* OldOwningZonePtr = OwningWaterZone.Get())
	{
		OldOwningZonePtr->RemoveWaterBodyComponent(this);
	}
 
	if (AWaterZone* FoundZonePtr = FoundZone.Get())
	{
		FoundZonePtr->AddWaterBodyComponent(this);
	}
 
	OwningWaterZone = FoundZone;
 
	UpdateMaterialInstances();
}

Anyway, passing these along incase it’s helpful since they were fairly simple fixes.

Thanks!

Lucas

[Attachment Removed]

Hey,

Thank you for forwarding us all of these fixes. We have integrated the first two and will look into solving the remaining issues as well.

Very much appreciated!

Roey

[Attachment Removed]

Right, yeah. An option could be to modify the code returning the zone bounds to expand the returned AABB to include the rotated bounds. I’ve logged an issue for this and we can look into a proper solution.

Thanks again for the reports!

[Attachment Removed]

Glad it helped! Another issue here is that WaterZones can’t be rotated or scaled without breaking the waterinfo texture bounds. Easy to avoid when placing them but harder to avoid when they are in LevelInstances that could be rotated. Not something we have solved since much of the water code assumes World -> UV mapping of the zone is axis-aligned.

[Attachment Removed]