Pitfalls in UE4 - WXGopher/GameDevGems GitHub Wiki

Pitfalls in UE4

Here is a collection of random pitfalls (they are feature not bugs) I've encountered.

//TOC here

ApplyWorldOffset

If we have several levels with offset in a game, after unloading and reloading levels you may find some level components not set up properly.

void USceneComponent::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift)
{
	Super::ApplyWorldOffset(InOffset, bWorldShift);
	
	// Calculate current ComponentToWorld transform
	// We do this because at level load/duplication ComponentToWorld is uninitialized
	{
		ComponentToWorld = CalcNewComponentToWorld(GetRelativeTransform());
	}

	// Update bounds
	Bounds.Origin+= InOffset;

	// Update component location
	if (GetAttachParent() == nullptr || IsUsingAbsoluteLocation())
	{
		SetRelativeLocation_Direct(GetComponentLocation() + InOffset);
		
		// Calculate the new ComponentToWorld transform
		ComponentToWorld = CalcNewComponentToWorld(GetRelativeTransform());
	}

	// Physics move is skipped if physics state is not created or physics scene supports origin shifting
	// We still need to send transform to physics scene to "transform back" actors which should ignore origin shifting
	// (such actors receive Zero offset)
	const bool bSkipPhysicsTransform = (!bPhysicsStateCreated || (bWorldShift && FPhysScene::SupportsOriginShifting() && !InOffset.IsZero()));
	OnUpdateTransform(SkipPhysicsToEnum(bSkipPhysicsTransform));
	
	// We still need to send transform to RT to "transform back" primitives which should ignore origin shifting
	// (such primitives receive Zero offset)
	if (!bWorldShift || InOffset.IsZero())
	{
		MarkRenderTransformDirty();
	}

	// Update physics volume if desired	
	if (bShouldUpdatePhysicsVolume && !bWorldShift)
	{
		UpdatePhysicsVolume(true);
	}

	// Update children
	for (USceneComponent* ChildComp : GetAttachChildren())
	{
		if(ChildComp != nullptr)
		{
			ChildComp->ApplyWorldOffset(InOffset, bWorldShift);
		}
	}
}

How unload works is the actor to be unloaded traversing through inheritance tree and executing ApplyWorldOverset (as seen in // Update children above). This happens recursively, and all children components may have spatial data offset by InOffset (in their ApplyWorldOffset).

This causes problem on load. The root component still calls ApplyWorldOffset on children components; however, by the time this gets called, children components are not initialized yet (ChildComp is nullptr), hence spatial data offset by InOffset in unloading process may not get compensated, resulting weirdly shifted components.

How UE internally solved this problem is in each components' OnRegister, spatially offset data are recalculated. Take UCableComponent for example:

void UCableComponent::OnRegister()
{
	Super::OnRegister();

	const int32 NumParticles = NumSegments+1;

	Particles.Reset();
	Particles.AddUninitialized(NumParticles);

	FVector CableStart, CableEnd;
	GetEndPositions(CableStart, CableEnd);

	const FVector Delta = CableEnd - CableStart;

	for(int32 ParticleIdx=0; ParticleIdx<NumParticles; ParticleIdx++)
	{
		FCableParticle& Particle = Particles[ParticleIdx];

		const float Alpha = (float)ParticleIdx/(float)NumSegments;
		const FVector InitialPosition = CableStart + (Alpha * Delta);

		Particle.Position = InitialPosition;
		Particle.OldPosition = InitialPosition;
		Particle.bFree = true; // default to free, will be fixed if desired in TickComponent
	}
}

As shown above, cable data are recalculated using CableStart and CableEnd, which are recalculated and do not depend on last saved status.