UTimelineComponent for multiple actors and performance

I am currently in the early stages of programming my space game. I have managed to code each ship to have a spline component which is used for navigation and traversal. I then use a UTimelineComponent with a UFloatCurve to traverse the spline.

I have noticed though that I am struggling with performance. I currently have about 100 ships and notice that when all are moving my FPS hits about 30-40fps in the editor. To alleviate I have reduced the Components tick rate but that comes obviously with a lack of smoothness when the ships are moving.

Is there something else I should be doing to improve performance (ships and surroundings are VERY low poly (whole scene probably 10,000 polys).

Should timelines be used in this way? or are they known to be performance heavy?

Should I just use tick for ship movement down the spline?

Any suggestions would be greatly appreciated.

I would have thought Unreal should be able to handle thousands of moving objects so was quite shocked to see such a frame rate dip for only 100 ships moving.

Kind regards and thanks in advance for anyone willing to help or guide me on the right path!

How many Scene Components are in each of them? Each of them consumes resources when moving.

Do you call the move function once per frame for each ship, or multiple times for the same one? Are both the move and the turn done by one function? Each of these nuances multiplies your movement costs.

The CPU is responsible for movement. You can move 10,000 invisible Scene Components and that’s where your performance ends.

But if you move one mesh (Scene Component) with 10,000 polygons, your GPU will handle it just fine.

Each ship has its’ own scene component (singular). The move function is called once which invokes the timeline and ucurvefloat to be constructed on the fly only once per navigation order. Each ship only receives one navigation order at the moment. So no looping. I then just invoke the timelinecompnent to PlayFromStart(); I let the UTimelineComponent deal with the tick and update the actors position with FVector ShipsSplineLocation = SplineComponent->GetLocationAtDistanceAlongSpline(TimeLength, ESplineCoordinateSpace::World); (Timelength is the Timeline Value (delta) * the length of the spline.)

Don’t hesitate to ask any further questions if you need more clarity on my mess of code :wink:

Below is attached my code for the ships navigation computer:


#include "CoreObjects/Spaceships/ShipComponents/ShipNavigationSystem.h"
#include "Components/SplineComponent.h"
#include "Curves/CurveFloat.h"
#include"Components/TimelineComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h"
#include "CoreObjects/Spaceships/SpaceshipBase.h"


// Sets default values for this component's properties
UShipNavigationSystem::UShipNavigationSystem()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = false;
	PrimaryComponentTick.TickInterval = 0.5f;
	SplineComponent = CreateDefaultSubobject<USplineComponent>(TEXT("SplineComp"));
	SplineComponent->SetVisibility(true, true); 
	SplineComponent->ToggleVisibility(true);

	//TimelineComponent setup
	FlightTimeline = CreateDefaultSubobject<UTimelineComponent>(TEXT("FlightTimeline"));


	//Cast to reference the spaceship itself
	DASpaceship = Cast<ASpaceshipBase>(this->GetOwner());
	if (DASpaceship)
	{
		ShipMovementSpeed = DASpaceship->GetSpeed();
	}	
}

// Called when the game starts
void UShipNavigationSystem::BeginPlay()
{
	Super::BeginPlay();


}

// Called every frame
void UShipNavigationSystem::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	GEngine->AddOnScreenDebugMessage(5, 0.2f, FColor::White, FString::Printf(TEXT("DeltaTime: %f"), DeltaTime));
}

//Navigation Call - Start/End points - obstacle check and reroute if needed
void UShipNavigationSystem::NavigateTo(const FVector EndLocation)
{

	DrawDebugSphere(GetWorld(), EndLocation, 50.f, 20, FColor::Yellow);

	//LineTrace to be short of hitting its destination - otherwise it will calculate around our destination object 
	FVector LineTraceLength;
	LineTraceLength.X = EndLocation.X - 1000.f; //Shorter trace
	LineTraceLength.Y = EndLocation.Y;
	LineTraceLength.Z = EndLocation.Z;

	//RayTrace Start/Finish setup
	FVector ShipLocation = GetOwner()->GetActorLocation();
	FVector RayTraceStart = FVector(ShipLocation.X + 50.f, ShipLocation.Y, ShipLocation.Z);
	ShipLocation.X = ShipLocation.X + 50.f; /* WARNING: ShipLocation is not accurate to the ship (+50) for collision purposes */

	//Obstacle Check - Line Trace
	FHitResult HitResult;	
	bool bHit = GetWorld()->LineTraceSingleByChannel(
		HitResult,
		RayTraceStart,
		LineTraceLength,
		ECC_Visibility //channel
	);

	//Obstacle hit
	if (bHit)
	{
		//debug data
		const float LineThickness = 0.5f;
		const float Time = 2.0f;

		//Which way round the obstacle
		FVector VectorToObstacle = FVector(0.f);
		FVector AvoidancePoint = FVector(0.f);
		FVector AvoidanceDirection = FVector(0.f);

		//LineTrace debug
		UE_LOG(LogTemp, Warning, TEXT("ObjectHit: %s at %s"), *HitResult.GetActor()->GetName(), *HitResult.Location.ToString());
		DrawDebugLine(GetWorld(), ShipLocation, LineTraceLength, FColor::Red, false, Time, 0, LineThickness);

		//Show impact point debug
		DrawDebugSphere(GetWorld(), HitResult.Location, 50.0f, 5, FColor::Red, false, 1.4f, 0, 1.2f);

		//vector from ship to obstacle
		VectorToObstacle = HitResult.Location - ShipLocation;
		 
		//which way to go round (up/down)		
		if (FMath::Abs(HitResult.ImpactNormal.Z) > 0.5f)
		{			
			AvoidanceDirection = ((HitResult.ImpactNormal.Z > 0.0f) ? FVector::UpVector : -FVector::UpVector);			
		}
		else //which way to go round (left/right)
		{
			FVector RightVector = FVector::CrossProduct(VectorToObstacle.GetSafeNormal(), FVector::UpVector);
			AvoidanceDirection = (FVector::DotProduct(RightVector, VectorToObstacle) > 0) ? RightVector : -RightVector;			
		}

		//ObstacleSize and buffer zone
		FVector Origin, Extent;
		float ObstacleRadius = 0.0f;
		const float AvoidanceBuffer = 300.0f;
		float AvoidanceDistance = 0.0f;

		//Calculate avoidance point in regard to size of object hit
		TSoftObjectPtr<AActor> HitActor;
		HitActor = HitResult.GetActor();
		HitActor->GetActorBounds(true, Origin, Extent);
		ObstacleRadius = Extent.Size();
		AvoidanceDistance = ObstacleRadius + AvoidanceBuffer;
		AvoidancePoint = HitResult.Location + AvoidanceDirection * AvoidanceDistance;
		DrawDebugSphere(GetWorld(), AvoidancePoint, 50.0f, 5, FColor::Magenta, false, 2.5f, 0, 1.2f);	

		//Build Navigation 3 point		
		BuildNavigationPath(AvoidancePoint, EndLocation);
	}
	else //no object in path
	{
		BuildNavigationPath(FVector(0.f), EndLocation);
	}
}

//Build the navigation path (spline)
void UShipNavigationSystem::BuildNavigationPath(const FVector AvoidanceLocation, const FVector ShipDestination)
{
	FVector CurrentShipLocation = GetOwner()->GetActorLocation();

	//Clear any previous spline points created
	SplineComponent->ClearSplinePoints();
	SplineComponent->UpdateSpline();

	//If no defined middle point - then just A->B and add middle of two points
	if (AvoidanceLocation == FVector(0.f))
	{ 
		//Halfway between A -> B
		FVector MiddlePoint = (CurrentShipLocation + ShipDestination) * 0.5f;

		NavigationPoints.Empty();

		//Starting Point/MiddlePoint/Destination
		NavigationPoints.Add(CurrentShipLocation);		
		NavigationPoints.Add(MiddlePoint);
		NavigationPoints.Add(ShipDestination);

		//Create the spline navigation route
		for (const FVector& point : NavigationPoints)
		{
			SplineComponent->AddSplinePoint(point, ESplineCoordinateSpace::World);
		}
	}
	else //Something is in the way... use the avoidancePoint given
	{		
		NavigationPoints.Empty();
		NavigationPoints.Add(CurrentShipLocation);
		NavigationPoints.Add(AvoidanceLocation);
		NavigationPoints.Add(ShipDestination);
		
		for (const FVector& point : NavigationPoints)
		{
			SplineComponent->AddSplinePoint(point, ESplineCoordinateSpace::World);			
		}		
	}

	SplineComponent->UpdateSpline();

//Start Moving----------------------------------------------------------------

	SetupTravelTimeline();
	FlightTimeline->PlayFromStart();

	

	
}


//FTimeline for Travel
void UShipNavigationSystem::SetupTravelTimeline()
{
	
	float Distance = SplineComponent->GetSplineLength();
	UCurveFloat* ShipMovementCurve = CreateCurveFloat(Distance);

	//create and add the curve to the timeline
	if (ShipMovementCurve)
	{
		//Bind timeline update and finished functions
		FOnTimelineFloat MoveShipFunction;
		MoveShipFunction.BindUFunction(this, FName("OnTimelineUpdate"));

		FOnTimelineEvent FinishedFunction;
		FinishedFunction.BindUFunction(this, FName("OnTimelineFinished"));
				
		FlightTimeline->SetTimelineLengthMode(ETimelineLengthMode::TL_LastKeyFrame);
		FlightTimeline->SetLooping(false);
		FlightTimeline->AddInterpFloat(ShipMovementCurve, MoveShipFunction);
		FlightTimeline->SetTimelineFinishedFunc(FinishedFunction);		
	}
	else { UE_LOG(LogTemp, Warning, TEXT("NO TIMELINE FOUND")); }	
}



//work on a timer - clear nav points and test for collision again - create new routing
void UShipNavigationSystem::NavigationUpdateCheck()
{	
	SplineComponent->ClearSplinePoints();
	NavigateTo(GetDestination());
	
}

//Timeline update call
void UShipNavigationSystem::OnTimelineUpdate(float TimeValue)
{
	float SplineLength = SplineComponent->GetSplineLength();
	float TimeLength = TimeValue * SplineLength;

	if (SplineComponent)
	{		
		if (TimeValue == 0.5f)
		{
			//update navigation on a timer.
			float TimeToWait = 25.f, Delay = 25.f;
			if (!bHasReachedDestination)
			{
				if (!GetWorld()->GetTimerManager().IsTimerActive(TimerHandler))
				{
					GetWorld()->GetTimerManager().SetTimer(TimerHandler, this, &UShipNavigationSystem::NavigationUpdateCheck, TimeToWait, true, Delay);
				}
			}
			else
			{
				GetWorld()->GetTimerManager().ClearTimer(TimerHandler);
				UE_LOG(LogTemp, Warning, TEXT("NavigationTimer Cancelled. Destination reached"));
			}
		}
		//Get the location and rotation of the spline with reference to the time of our curve (float Value)
		FVector ShipsSplineLocation = SplineComponent->GetLocationAtDistanceAlongSpline(TimeLength, ESplineCoordinateSpace::World);
		FRotator ShipsSplineRotation = SplineComponent->GetRotationAtDistanceAlongSpline(TimeLength, ESplineCoordinateSpace::World);

		//Move the actor
		AActor* Ship = GetOwner();
		if (Ship)
		{
			Ship->SetActorLocation(ShipsSplineLocation);
			Ship->SetActorRotation(ShipsSplineRotation);
			UE_LOG(LogTemp, Warning, TEXT("ShipLocation_ %s"), *ShipsSplineLocation.ToString());
		}		

		if (TimeValue == 1.0f)
		{
			FlightTimeline->Stop();
		}		
	}
}

//Timeline finish
void UShipNavigationSystem::OnTimelineFinished()
{
	//here we can flag for more tasking or job harvesting once at destination. need to deter what we are doing 
	UE_LOG(LogTemp, Warning, TEXT("FINISHED SPLINE MOVEMENT"));
	bHasReachedDestination = true; 
	FlightTimeline->Stop();

}

FVector UShipNavigationSystem::GetDestination()
{
	return Destination;
}

void UShipNavigationSystem::SetDestination(FVector EndPoint)
{
	Destination = EndPoint;
}


//Create Curve Floats for flight
UCurveFloat* UShipNavigationSystem::CreateCurveFloat(const float TravelDistance)
{
	//traveldistance  add to ship mileage counter
	// 
	// Time  = distance / speed

	float TimeToTravel = TravelDistance / DASpaceship->GetSpeed();

	//Ships Curve Float
	UCurveFloat* ShipCurveFloat = NewObject<UCurveFloat>(UCurveFloat::StaticClass());

	//Curve Float Keys (in time /in value (length)) 
	FKeyHandle KeyPoint1 = ShipCurveFloat->FloatCurve.AddKey(0.0f, 0.0f); //beginning
	FKeyHandle KeyPoint2 = ShipCurveFloat->FloatCurve.AddKey(TimeToTravel / 2.f, 0.5f);
	FKeyHandle KeyPoint3 = ShipCurveFloat->FloatCurve.AddKey(TimeToTravel, 1.0f); //ease in
	//Add the keypoints to the curve

	ShipCurveFloat->FloatCurve.SetKeyTangentMode(KeyPoint1, ERichCurveTangentMode::RCTM_Auto);
	ShipCurveFloat->FloatCurve.SetKeyTangentMode(KeyPoint2, ERichCurveTangentMode::RCTM_Auto);
	ShipCurveFloat->FloatCurve.SetKeyTangentMode(KeyPoint3, ERichCurveTangentMode::RCTM_Auto);

	return ShipCurveFloat;
}

Can you disable DrawDebugsomething and AddOnScreenDebugMessage? :kissing:

I will get back to you on that… I currently have an issue in my code and things are not working as intended… I will let you know the results of removing he debug messages and draw calls as soon as I get everything back to running again.

Yes, by removing the DrawDebug info and the OnScreenDebug messages have improved performance considerably. It still isn’t amazing performance but it is considerably better. I never knew or thought this would have much of an impact on performance.

My original question still stands if I am approaching this correctly (Navigation with splines) or would I be better of with a NavigationVolume. All ships in the game are AI based. Just looking for advice. My only concern with doing things with a navmesh is scale. I am building solar systems… Maybe too much for it?

Another thing I have heard of is pathfinding. I am not sure what this is and if it would be appropriate for what I want. (Large amounts of ships flying in the solar system)

I fear my current system of Timelines on splines may become very heavy on unreal.

Any more advice would be helpful and thank you for any previous replies or new replies.

Can you do just call Add Actor Offset on the tick?. Perhaps (more likely) the problem is in finding the path, not in the movement itself.

Did some testing today… I’m starting to think it may be timeline related. Each ship having it’s own timeline and spline, then moving in either timeline tick or the actors tick both have terrible performance. I have seen hoards of AI in unreal before so must be something with they way I am doing things. I might take a good look navmesh movement. My only concern with that is I feel the scale may be a performance issue for me too… also I am unaware if navmeshes can work in a 3d plane not just 2d…

Try turning off tick component and moving it’s logic to the MoveShipFunction function. It will update when moving and not cause any extra costs.

You could dynamically create the timeline via the newObject command. Right now you have timelines on each ship costing you memory.

You could just spawn it and bind the function on navigation start and once the navigation is over you can destroy the timeline.

You can also try running unreal insights to check if the timeline is the performance problem.

Thanks for your continued help on this. So the last couple of days I totally reworked my code and now have done away with timelines and ucurvefloats. I now do all position updates in tick and initial spline setup and guidance collision checks before giving the order to move. I am at the same fps. I also checked insights but I think I need a lot more practice to try and remove any usable data from it. Very daunting and have never used it before.

I have noticed that when this is running it is in the editor window. I tried using standalone and my fps returned to 120+. I guess the issue is the editor (tried PIE as well and that sucked too). So after all of the above it may just be an editor issue? I tried changing all the settings in the editor to medium, it made no difference… any guidance or help with that would be very useful to me… I really didn’t expect the editor to be so poor performing!

Rather a feature…

Additional open editor windows - can also significantly reduce fps.
Strictly speaking - you should always do a final test of anything in the Shipping Build.

PIE may also behave strangely in multiplayer mode, so if something doesn’t work as it should, it’s worth checking in the build, or trying in Standalone Game.

Perhaps PIE holds other secrets for us. :grimacing:

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.