Projectile Curve for predetermined target

Hi,

I want to launch projectiles from a magic spell in a 2D plane (top down view, no gravity) and make them hit a predetermined target (moving actor or static location). My only requirement is that I can guarantee the target location and the moment in time the projectiles hit the target. For example 3 projectiles could spawn from the caster towards the target where:

  • 1st launches 20° to the left side, makes a right arc and hits the target after 2 seconds
  • 2nd goes in a straight line towards the target and hits after 1.5 seconds
  • 3rd launches 20° to the right side, makes a left arc and hits the target after 2 seconds

I could program this manually probably, but I was wondering if there is anything in-engine that I could use to describe those arcs? Splines maybe?

Thanks in advance

1 Like

Yeah, I think your best bet to define the paths mathematically will be splines, driving the movement over them with a timeline

You need to calculate the inter:

Thanks, I tinkered with splines a bit and it works well so far. I haven’t really figured out how to calculate the tangents programmatically to make it a smooth arc but I am confident it can be done, I need to invest some more time. I also added cubic easing in C++ to make the flight feel a bit more natural and I figured I should be able to use a Curve component instead to make this even more flexible, so I plan to work on that next.

1 Like

Nice! Mind sharing your cubic easing? I’d be really interested to see how that looks!

No problem, these are standard functions for easing movement:

float AAbilityProjectile::EasingIn(float t, float b, float c, float d)
{
	t /= d;
	return c * t * t * t + b;
}

float AAbilityProjectile::EasingOut(float t, float b, float c, float d)
{
	t /= d;
	t--;
	return c * (t * t * t + 1) + b;
}

So these are basically just translating your [0, 1] interval into a different [0, 1] interval. You can remove one of the * ts to have quadratic easing, which is less agressive. You would use them like this, where t is the current time passed since moving on the spline started, b is 0 (no offset), c is 1 (the multiplier) and d is the duration (the total time moving on the spline from start to end)

void AAbilityProjectile::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	TimePassed += DeltaTime;

	float Tween = EasingIn(TimePassed, 0, 1, TimeTotal);

	float Dist = Tween * Spline->GetSplineLength();

	FVector Location = Spline->GetWorldLocationAtDistanceAlongSpline(Dist);
	FRotator Rotation = Spline->GetWorldRotationAtDistanceAlongSpline(Dist);

	MyComponent->SetWorldLocation(Location);
	MyComponent->SetWorldRotation(Rotation);

Again, I plan to replace those functions with a Curve component, this way the designer can decide what easing he wants. I havent started working on it but I don’t see why it shouldn’t work.

Okay I’m done and really happy with the results. I’ll put them here for reference:

First, I use a UCurveFloat in C++ which can be set in blueprint and is used for easing the movement, as discussed before:

UPROPERTY(EditDefaultsOnly, BlueprintReadWrite)
UCurveFloat* EasingCurve;

I also calculated nice tangents which work well for my magic projectiles:

	FOccluderVertexArray Points;
	Points.Add(Start);
	Points.Add(TargetLocation);

	FVector Tangent1, Tangent2;
	Tangent1 = Tangent2 = TargetLocation - Start;

	Tangent1 = Tangent1.RotateAngleAxis(Angle, FVector(0, 0, 1));
	Tangent1 *= 2;

	Tangent2 = Tangent2.RotateAngleAxis(-Angle, FVector(0, 0, 1));
	Tangent2 *= 1;

	Spline->SetSplineWorldPoints(Points);
	Spline->SetTangentsAtSplinePoint(0, Tangent1, Tangent1, ESplineCoordinateSpace::World);
	Spline->SetTangentsAtSplinePoint(1, Tangent2, Tangent2, ESplineCoordinateSpace::World);

	TimeTotal = Spline->GetSplineLength() / Speed;

Ticking is almost the same as before:

void AAbilityProjectile::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (IsValid(TargetActor))
	{
		TargetLocation = TargetActor->GetActorLocation();
		Spline->SetLocationAtSplinePoint(1, TargetLocation, ESplineCoordinateSpace::World);
	}

	TimePassed += DeltaTime;

	float Tween = EasingCurve->GetFloatValue(TimePassed / TimeTotal);
		
	float Dist = Tween * Spline->GetSplineLength();

	FVector Location = Spline->GetWorldLocationAtDistanceAlongSpline(Dist);
	FRotator Rotation = Spline->GetWorldRotationAtDistanceAlongSpline(Dist);

	PaperFlipbookComponent->SetWorldLocation(Location);
	PaperFlipbookComponent->SetWorldRotation(Rotation + FRotator(0,0,-90));


	if (TimePassed >= TimeTotal)
	{
		if (GetWorld()->IsServer())
		{
			HandleTargetHit();
		}
		Destroy();
	}
}

TimeTotal is now based on the spline length which means that from my 3 projectiles, the one with Angle == 0 arrives first. Visually that looks a bit odd so I am thinking to base TimeTotal on the length between start and end vector instead of the spline length but that is an easy change.

Thanks again for your inputs!

1 Like