Spawned obstacles are invisible? Unable to assign mesh upon creation?

So, I have a spawner object, whose job it is to spawn Fence obstacles that move toward the player along the X axis. Through debug messages, I have confirmed that the Fence objects do indeed spawn, but I have yet to see one. Debug messages assure me that given their coordinates, they should pass through the view. I would assume that the meshes, even if they don’t have a material applied, would at least be somewhat visible in my window as a translucent figure, right?

Below is my code for the Spawner’s “tick”, as well as the Fence’s constructor and AssignMesh script. And for reference, the spawner’s state is automatically set to SPAWN_PLAY in its constructor.

AObstacleSpawner::Tick

// Called every frame
void AObstacleSpawner::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	switch (state) {
		case SPAWN_PLAY:
			//Get a reference to the game world so we can spawn new objects as needed
			UWorld* World = GetWorld();
			if (World) {
				FActorSpawnParameters SpawnParams;
				SpawnParams.Owner = this;
				SpawnParams.Instigator = GetInstigator();

				//Increment/Decrement timers, spawn objects
				if (SpawnTimer <= 0.0f) {
					//Spawn a new fence obstacle with appropriate height
					int FenceHeight = FMath::RandRange(1, 8);
					AFenceObstacle* NewFence = World->SpawnActor<AFenceObstacle>(SpawnParams);
					FVector SpawnLocation = FVector(300.0f, 0.0f, 0.0f);
					NewFence->SetActorLocation(SpawnLocation);
					NewFence->ObstacleSpeed = ObstacleSpeed;
					NewFence->HeightMarker = FenceHeight;
					NewFence->AssignMesh();

					//DEBUG: Show a prompt indicating that a new fence has been created
					FVector loc = NewFence->GetActorLocation();
					auto const debug_msg = FString::Printf(TEXT("New fence %i spawned. Height = %i. Spd = %f. X = %f. Y = %f. Z = %f"), FenceIndex, FenceHeight, ObstacleSpeed, loc.X, loc.Y, loc.Z);
					GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, debug_msg);
					//GUBED

					Fences[FenceIndex] = NewFence;
					FenceIndex++;
					if (FenceIndex >= 4) {
						FenceIndex = 0;
					}
					
					SpawnTimer = 5.0f;	//<-- This will be modified later to depend on TimeElapsed
				}
				else {
					SpawnTimer -= DeltaTime;
				}

				int ii;
				FVector CurrentLocation;
				if (TimeToAccel <= 0.0f) {
					ObstacleSpeed += ObstacleAccel;

					//Set the speeds of the fences
					for (ii = 0; ii < 4; ii++) {
						if (Fences[ii] != nullptr) {
							Fences[ii]->ObstacleSpeed = ObstacleSpeed;
							CurrentLocation = Fences[ii]->GetActorLocation();
							if (CurrentLocation.X < -1000.0) {
								Fences[ii]->Destroy();
								Fences[ii] = nullptr;
							}
						}
					}

					TimeToAccel = 5.0f;	//<-- This will be modified later to depend on TimeElapsed
				}
				else {
					TimeToAccel -= DeltaTime;
				}

				TimeElapsed += DeltaTime;
		}
	
	}
}

AFenceObstacle::AFenceObstacle()

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

	//Create the box component for collisions
	BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox"));
	check(BoxComponent != nullptr);

	//Create a visible mesh for the fence. CreateDefaultSubobject cannot be called outside of constructors
	if (!VisibleMesh) {
		VisibleMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisibleMesh"));
	}
}

AFenceObstacle::AssignMesh()

//Assigns the appropriate mesh after creation (called externally)
void AFenceObstacle::AssignMesh() {
	if (HeightMarker > 0 && HeightMarker < 9) {
		//Assign the appropriate mesh to the fence based on the assigned HeightMarker
		auto const Path = FString::Printf(TEXT("/Game/Meshes/sm_obstacle_fence_size_%i.sm_obstacle_fence_size_%i"), HeightMarker, HeightMarker);
		auto Mesh = LoadObject<UStaticMesh>(nullptr, TEXT("FenceMesh"), *Path);
		if (Mesh) {
			VisibleMesh->SetStaticMesh(Mesh);
		}
	}
	else {
		//If an erroneous HeightMarker value was assigned, show that in a debug message
		check(GEngine != nullptr);
		//Display a debug message for five seconds
		//The -1 "Key" vale argument prevents the message from being updated or refreshed.
		auto const err_msg = FString::Printf(TEXT("AssignMesh: Invalid fence height value: %i"), HeightMarker);
		GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, err_msg);
	}
}

Check that you have attached your components to the root/each other, I’m not seeing that in your constructor.

Can you elaborate on what that code would look like?

It’s been a few days, so I wanted to elaborate on some code. I am very unfamiliar with what necessitates SetupAttachment() for various components. I tried setting the RootComponent of the Fence obstacle to its mesh, but still no sign of anything.

What more do I need to do to make the mesh visible? Even if it’s translucent, I need to see SOMETHING.

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

	//Create the box component for collisions
	BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox"));
	check(BoxComponent != nullptr);

	//Create a visible mesh for the fence. CreateDefaultSubobject cannot be called outside of constructors
	if (!VisibleMesh) {
		VisibleMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisibleMesh"));
		RootComponent = VisibleMesh;	//Added 2022/02/06 in attempt to make fences visible.
	}
}

You can use this for attaching components to each other.

MyComp->AttachToComponent(GetAttachmentRoot(), FAttachmentTransformRules::KeepRelativeTransform);

You can set the Mesh and its Material as follows:
these should be properties you set in your blueprint.

if(MyMesh){
  VisibleMesh->SetStaticMesh(MyMesh);
}

if(MyMaterial){
  VisibleMesh->SetMaterial(0, MyMaterial);
}

So I haven’t bothered setting the Material yet. I just want to make sure the fences actually spawn and move properly. So far, nothing I have done has made them visible.

Here are the constructor and AssignMesh methods for the Fence obstacle. Note that the spawner object instantiates new fence objects, and calls the AssignMesh() method for each immediately after creating them.

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

	//Create the box component for collisions
	BoxComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("CollisionBox"));
	check(BoxComponent != nullptr);

	//Create a visible mesh for the fence. CreateDefaultSubobject cannot be called outside of constructors
	VisibleMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("VisibleMesh"));
	if (VisibleMesh) {
		VisibleMesh->SetupAttachment(GetRootComponent());
	}
}

//Assigns the appropriate mesh after creation (called externally)
void AFenceObstacle::AssignMesh() {
	if (HeightMarker > 0 && HeightMarker < 9) {
		//Assign the appropriate mesh to the fence based on the assigned HeightMarker
		auto const Path = FString::Printf(TEXT("/Game/Meshes/sm_obstacle_fence_size_%i.sm_obstacle_fence_size_%i"), HeightMarker, HeightMarker);
		auto Mesh = LoadObject<UStaticMesh>(nullptr, TEXT("FenceMesh"), *Path);
		if (Mesh) {
			VisibleMesh->SetStaticMesh(Mesh);
		}
	}
	else {
		//If an erroneous HeightMarker value was assigned, show that in a debug message
		check(GEngine != nullptr);
		//Display a debug message for five seconds
		//The -1 "Key" vale argument prevents the message from being updated or refreshed.
		auto const err_msg = FString::Printf(TEXT("AssignMesh: Invalid fence height value: %i"), HeightMarker);
		GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, err_msg);
	}
}

For reference, this is the relevant code in the spawner object.

//Increment/Decrement timers, spawn objects
				if (SpawnTimer <= 0.0f) {
					//Spawn a new fence obstacle with appropriate height
					int FenceHeight = FMath::RandRange(1, 8);
					AFenceObstacle* NewFence = World->SpawnActor<AFenceObstacle>(SpawnParams);
					FVector SpawnLocation = FVector(300.0f, 0.0f, 0.0f);
					NewFence->SetActorLocation(SpawnLocation);
					NewFence->ObstacleSpeed = ObstacleSpeed;
					NewFence->HeightMarker = FenceHeight;
					NewFence->AssignMesh();

					//DEBUG: Show a prompt indicating that a new fence has been created
					FVector loc = NewFence->GetActorLocation();
					auto const debug_msg = FString::Printf(TEXT("New fence %i spawned. Height = %i. Spd = %f. X = %f. Y = %f. Z = %f"), FenceIndex, FenceHeight, ObstacleSpeed, loc.X, loc.Y, loc.Z);
					GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, debug_msg);
					//GUBED

					Fences[FenceIndex] = NewFence;
					FenceIndex++;
					if (FenceIndex >= 4) {
						FenceIndex = 0;
					}
					
					SpawnTimer = 5.0f;	//<-- This will be modified later to depend on TimeElapsed
				}
				else {
					SpawnTimer -= DeltaTime;
				}

Ignore what I said regarding attaching, I see you don’t need that with how you have designed it.
At what scale is your mesh and actor spawning?
Try setting the scale to 10 to test

Didn’t know that was a parameter, and not sure how to change it, so I assume scale = 1.

The bird’s mesh is 70x205x46 and the shortest fence is 9x294x140.

[EDIT] I should also make it clear that the fences are spawning offscreen. If the meshes are working correctly, they are not moving with the object.

[EDIT 2] Confirmed that the meshes are not visible regardless by adjusting spawn location.

In that case you should try setting a basic material. I’m not sure if the default gets rendered without setting a material. Just do a material with a base color, set it in the mesh component just to rule that out.

So, I modified my AssignMesh() method to include the assignment of a generic material (no texture yet.) Still, the fences are invisible. And yes, I added the appropriate pointer in the *.h file, so everything compiles just fine.

//Assigns the appropriate mesh after creation (called externally)
void AFenceObstacle::AssignMesh() {
	if (HeightMarker > 0 && HeightMarker < 9) {
		//Assign the appropriate mesh to the fence based on the assigned HeightMarker
		auto const Path = FString::Printf(TEXT("/Game/Meshes/sm_obstacle_fence_size_%i.sm_obstacle_fence_size_%i"), HeightMarker, HeightMarker);
		auto Mesh = LoadObject<UStaticMesh>(nullptr, TEXT("FenceMesh"), *Path);
		if (Mesh) {
			VisibleMesh->SetStaticMesh(Mesh);

			static ConstructorHelpers::FObjectFinder<UMaterial>Material(TEXT("'/Game/Materials/wood.wood'"));
			if (Material.Succeeded()) {
				MaterialInstance = UMaterialInstanceDynamic::Create(Material.Object, VisibleMesh);
			}
			VisibleMesh->SetMaterial(0, MaterialInstance);
		}
	}
	else {
		//If an erroneous HeightMarker value was assigned, show that in a debug message
		check(GEngine != nullptr);
		//Display a debug message for five seconds
		//The -1 "Key" vale argument prevents the message from being updated or refreshed.
		auto const err_msg = FString::Printf(TEXT("AssignMesh: Invalid fence height value: %i"), HeightMarker);
		GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Red, err_msg);
	}
}