Can't override mass of class derived from ProceduralMeshComponent, causes erratic physics

I’m trying to make a game wherein players build ships out of “surfaces” on a 3d integer grid. This naturally lends itself to representing ships with something like a ProceduralMeshComponent, which is exactly what I’ve done. However, I’ve run into a problem in that I can’t seem to override the mass of the component. I call “SetMassOverrideInKg()” from both the component and its parent actor, and it even shows up in the editor as the right value, but when I enter “Game” mode it behaves as if it has very little mass and having it print GetMass() to the log it confirms that it is operating with a mass of 1Kg (I tried to set it to 500). Specifically, a static mesh cube with the same buoyancy formula floats as expected while the procedural one gets flung hundreds of meters into the air as soon as it touches the water.

Collisions also don’t work with the procedural mesh, and I get a warning that says:


Warning Trying to simulate physics on ''/Temp/UEDPIE_0_Untitled_1.Untitled_1:PersistentLevel.CustomVesselPawn_1.RootComponent'' but it has ComplexAsSimple collision.


While this is certainly a problem I have to deal with down the road, I don’t see how it would affect my ability to override the mass of the component, nor how it would affect the buoyancy formula which behaves normally for a static mesh of the exact same size. However, I thought I’d include it in case the two problems turn out to be connected. Can anyone see what the problem is? I hate asking vague questions like this, but I’ve been tearing my hair out over this problem for days and finally decided it’s time to ask for help.

Alternatively, are there any alternative ways to make this system work without using ProceduralMeshComponent? For example, surfaces cannot be created or destroyed in the game world (only in a separate ship editor screen), so it might be possible to use a static mesh generated when a ship enters the world. However, as far as I can tell, custom static meshes cannot be created through pure code without appropriating source from the editor itself.

Constructor of CustomVesselPawn.cpp:


// Sets default values
ACustomVesselPawn::ACustomVesselPawn()
{
 	// Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	SetActorEnableCollision(true);

	FSurfaceArray NewSurfaces;

	FIntVector v0(1, 1, 1); //+++
	FIntVector v1(-1, 1, 1); //-++
	FIntVector v2(-1, -1, 1); //--+
	FIntVector v3(-1, -1, -1); //---
	FIntVector v4(1, -1, -1); //+--
	FIntVector v5(1, 1, -1); //++-
	FIntVector v6(-1, 1, -1); //-+-
	FIntVector v7(1, -1, 1); //+-+

	NewSurfaces.Add(FSurface(v0, v5, v6, 10));
	NewSurfaces.Add(FSurface(v0, v1, v6, 10));

	NewSurfaces.Add(FSurface(v1, v2, v3, 10));
	NewSurfaces.Add(FSurface(v1, v6, v3, 10));

	NewSurfaces.Add(FSurface(v2, v7, v0, 10));
	NewSurfaces.Add(FSurface(v2, v1, v0, 10));

	NewSurfaces.Add(FSurface(v2, v3, v4, 10));
	NewSurfaces.Add(FSurface(v2, v7, v4, 10));

	NewSurfaces.Add(FSurface(v7, v0, v5, 10));
	NewSurfaces.Add(FSurface(v7, v4, v5, 10));

	NewSurfaces.Add(FSurface(v3, v4, v5, 10));
	NewSurfaces.Add(FSurface(v3, v6, v5, 10));

	UBuoyantProceduralMeshComponent* MeshComponent = CreateDefaultSubobject<UBuoyantProceduralMeshComponent>(TEXT("RootComponent"));
	RootComponent = MeshComponent;

	MeshComponent->SetSurfaceData(NewSurfaces);
	MeshComponent->SetCollisionProfileName(TEXT("Pawn"));
	MeshComponent->SetMassOverrideInKg(NAME_None, 500.0F, true);
	MeshComponent->SetSimulatePhysics(true);
	MeshComponent->SetEnableGravity(true);
	MeshComponent->RegisterComponent();
}

BuoyantProceduralmeshComponent.h:


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ProceduralMeshComponent.h"
#include "BoatGameUtils.h"
#include "BuoyantProceduralMeshComponent.generated.h"

/**
 * 
 */
USTRUCT()
struct BOATGAME_API FSurface {

	GENERATED_BODY()

	UPROPERTY()
		FIntVector v0;

	UPROPERTY()
		FIntVector v1;

	UPROPERTY()
		FIntVector v2;

	UPROPERTY()
		int Thickness;

	FSurface(FIntVector av0 = FIntVector(0, 0, 0), FIntVector av1 = FIntVector(0, 0, 0), FIntVector av2 = FIntVector(0, 0, 0), int aThickness = 10) {
		v0 = av0;
		v1 = av1;
		v2 = av2;
		Thickness = aThickness;
	}

	operator FTriangle() {
		return { (FVector)(v0 * 50), (FVector)(v1 * 50), (FVector)(v2 * 50) };
	}
};

typedef TArray<FSurface> FSurfaceArray;

UCLASS()
class BOATGAME_API UBuoyantProceduralMeshComponent : public UProceduralMeshComponent
{
	GENERATED_BODY()
	
public:
	UBuoyantProceduralMeshComponent();

	virtual void BeginPlay() override;

	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction *ThisTickFunction) override;

	static const float WATER_DENSITY;

	void SetSurfaceData(FSurfaceArray NewSurfaces);

private:
	FSurfaceArray Surfaces;

	TArray<FTriIndices> Triangles;
	TArray<FVector> Verts;
	TArray<FVector> Normals;

	void SortTriangleVerticesByDepth(FVector* Vertices, float* Depths);

	FVector SurfaceToUUCoords(FIntVector& SurfaceCoords);
	
};


BuoyantProceduralMeshComponent.cpp:


// Fill out your copyright notice in the Description page of Project Settings.

#include "BoatGame.h"
#include "BuoyantProceduralMeshComponent.h"

const float UBuoyantProceduralMeshComponent::WATER_DENSITY = 0.001F;

UBuoyantProceduralMeshComponent::UBuoyantProceduralMeshComponent() {
	this->bAutoActivate = true;
	PrimaryComponentTick.bCanEverTick = true;
}

void UBuoyantProceduralMeshComponent::BeginPlay() {
	Super::BeginPlay();
	UpdateCollisionProfile();
}

void UBuoyantProceduralMeshComponent::TickComponent(float DeltaTime, enum ELevelTick TickType,
	FActorComponentTickFunction *ThisTickFunction) {

	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	AActor* Owner = GetOwner();

	FTriangle* SubTris = new FTriangle[Surfaces.Num() * 2];
	unsigned TriCount = 0;

	const int H = 0;
	const int M = 1;
	const int L = 2;

	const float WorldTime = Owner->GetWorld()->GetTimeSeconds();

	const FVector OwnerPos = Owner->GetActorLocation();
	const FTransform OwnerTransform = Owner->GetTransform();

	FVector COM = GetCenterOfMass();

	for (int i = 0; i < Triangles.Num(); i++) {
		FTriIndices Tri = Triangles*;
		FVector VerticesSorted] = { Verts[Tri.v0], Verts[Tri.v1], Verts[Tri.v2] };
		float Depth] = {
			BoatGameUtils::VectorHeightAboveWater(OwnerPos + OwnerTransform.TransformVector(VerticesSorted[0]), WorldTime),
			BoatGameUtils::VectorHeightAboveWater(OwnerPos + OwnerTransform.TransformVector(VerticesSorted[1]), WorldTime),
			BoatGameUtils::VectorHeightAboveWater(OwnerPos + OwnerTransform.TransformVector(VerticesSorted[2]), WorldTime)
		};

		SortTriangleVerticesByDepth(VerticesSorted, Depth);

		if (Depth[L] > 0) continue;	//skip this triangle if the lowest point is above water

		FVector normal = OwnerTransform.GetRotation().RotateVector(Normals*);

		if (Depth[H] < 0) {		//if all points are below water, calculation is very easy
			SubTris[TriCount] = { VerticesSorted[H], VerticesSorted[M], VerticesSorted[L], normal };
			TriCount++;
		}
		else if (Depth[M] > 0) {		//if only lowest point is submerged
			float tM = -Depth[L] / (Depth[M] - Depth[L]);
			float tH = -Depth[L] / (Depth[H] - Depth[L]);
			FVector JM = VerticesSorted[L] + tM * (VerticesSorted[M] - VerticesSorted[L]);
			FVector JH = VerticesSorted[L] + tM * (VerticesSorted[H] - VerticesSorted[L]);
			SubTris[TriCount] = { JH, JM, VerticesSorted[L], normal };
			TriCount++;
		}
		else {							//if lowest two points are submerged
			float tM = -Depth[M] / (Depth[H] - Depth[M]);
			float tL = -Depth[L] / (Depth[H] - Depth[L]);
			FVector IM = VerticesSorted[M] + tL * (VerticesSorted[H] - VerticesSorted[M]);
			FVector IL = VerticesSorted[L] + tL * (VerticesSorted[H] - VerticesSorted[L]);
			SubTris[TriCount] = { VerticesSorted[M], VerticesSorted[L], IM, normal };
			TriCount++;
			SubTris[TriCount] = { VerticesSorted[L], IM, IL, normal };
			TriCount++;
		}
		//UE_LOG(LogTemp, Log, TEXT("Triangle %i has vertex depths %f > %f > %f"), i, Depth[0], Depth[1], Depth[2]);
	}

	for (int unsigned i = 0; i < TriCount; i++) {
		FTriangle Tri = SubTris*;
		FVector Center = OwnerPos + OwnerTransform.TransformVector((Tri.v0 + Tri.v1 + Tri.v2) / 3.0f);
		float CDepth = BoatGameUtils::VectorHeightAboveWater(Center, WorldTime);

		float SurfaceArea = FVector::CrossProduct(Tri.v1 - Tri.v0, Tri.v2 - Tri.v0).Size() / 2.0F;

		FVector F = -WATER_DENSITY * Owner->GetWorldSettings()->GetGravityZ() * SurfaceArea * CDepth * Tri.n;
		F.X = 0;
		F.Y = 0;
		AddForceAtLocation(F, Center);
		UE_LOG(LogTemp, Log, TEXT("(BUOYANT PROCEDURAL MESH) Relative Buoyant Force on Triangle %i = %f; SA=%f; Depth=%f; Mass=%f; MassOverride=%i"),
			i, F.Size() / SurfaceArea, SurfaceArea, CDepth, GetMass(), GetBodyInstance()->bOverrideMass);
	}

}

void UBuoyantProceduralMeshComponent::SortTriangleVerticesByDepth(FVector* Vertices, float* Depths) {
	if (Depths[1] > Depths[0]) {
		BoatGameUtils::SwapIndices<FVector>(Vertices, 0, 1);
		BoatGameUtils::SwapIndices<float>(Depths, 0, 1);
	}

	if (Depths[2] > Depths[1]) {
		BoatGameUtils::SwapIndices<FVector>(Vertices, 1, 2);
		BoatGameUtils::SwapIndices<float>(Depths, 1, 2);
	}

	if (Depths[1] > Depths[0]) {
		BoatGameUtils::SwapIndices<FVector>(Vertices, 0, 1);
		BoatGameUtils::SwapIndices<float>(Depths, 0, 1);
	}
}

void UBuoyantProceduralMeshComponent::SetSurfaceData(FSurfaceArray NewSurfaces) {
	Surfaces = NewSurfaces;
	
	Triangles.Empty();
	Verts.Empty();
	Normals.Empty();

	TArray<int32> TriIndices;
	TArray<FVector> VertNormals;
	TArray<FVector2D> UV0;
	TArray<FColor> Colors;
	TArray<FProcMeshTangent> Tangents;

	for (int i = 0; i < Surfaces.Num(); i++) {
		FSurface Surface = Surfaces*;
		FTriIndices tri;

		FVector v0, v1, v2;

		v0 = SurfaceToUUCoords(Surface.v0);
		tri.v0 = Verts.Add(v0);

		v1 = SurfaceToUUCoords(Surface.v1);
		tri.v1 = Verts.Add(v1);

		v2 = SurfaceToUUCoords(Surface.v2);
		tri.v2 = Verts.Add(v2);

		Triangles.Add(tri);

		TriIndices.Add(tri.v0);
		TriIndices.Add(tri.v1);
		TriIndices.Add(tri.v2);

		UV0.Add(FVector2D(0, 0));
		UV0.Add(FVector2D(0, 10));
		UV0.Add(FVector2D(10, 10));

		Colors.Add(FColor(100, 100, 100, 100));
		Colors.Add(FColor(100, 100, 100, 100));
		Colors.Add(FColor(100, 100, 100, 100));

		Tangents.Add(FProcMeshTangent(1, 1, 1));
		Tangents.Add(FProcMeshTangent(1, 1, 1));
		Tangents.Add(FProcMeshTangent(1, 1, 1));
	}

	for (int i = 0; i < Triangles.Num(); i++) {
		FTriIndices tri = Triangles*;
		FVector v0 = Verts[tri.v0];
		FVector v1 = Verts[tri.v1];
		FVector v2 = Verts[tri.v2];

		FVector center = (v0 + v1 + v2) / 3.0F;
		FVector n = FVector::CrossProduct(v1 - v0, v2 - v0);
		n.Normalize();
		if (BoatGameUtils::CountIntersections(center, center + n, i, Triangles, Verts) % 2 == 1) n *= -1;
		Normals.Add(n);

		VertNormals.Add(n);
		VertNormals.Add(n);
		VertNormals.Add(n);
	}

	CreateMeshSection(0, Verts, TriIndices, VertNormals, UV0, Colors, Tangents, true);

	UpdateCollisionProfile();
}

FVector UBuoyantProceduralMeshComponent::SurfaceToUUCoords(FIntVector& SurfaceCoords) {
	
	return (FVector)(SurfaceCoords * 50);
}