Finished Obstacle Assault!

Well hello there, fellow students!
I’ve shared my mid-point course before, but this finished course includes some exciting new additions worth sharing!

For starters, a new checkpoint system has been made. A fall to the void will now send the player to the last checkpoint they walked through.
I made an actor blueprint containing only a sphere collision and scattered it across my level.

Then I did this inside the player’s BP event graph:


Simply, if the player overlaps a checkpoint actor, the vector variable CheckpointLocation will update to that actor’s location. Every tick a branch node is triggered to check if the player has fallen under Z 200, if so it will teleport them slightly above last checkpoint they walked through.
If there’s a cleaner way to do this, please do let me know!

Then I worked on some new obstacles: a spinning wing and a wrecking ball!
For the spinning wing, I just followed the lectures, nothing special there, the wrecking ball however…
I wanted to attempt to make the rotation look smoother, so I did some research and decided to implement FMath::InterpSinInOut in this obstacle.

I created a RotateWreckingBall function with this code:

void AMovingPlatform::RotateWreckingBall(float DeltaTime)
{
const double RealtimeSeconds = UGameplayStatics::GetRealTimeSeconds(GetWorld());
SetActorRotation(FMath::InterpSinInOut(-1*RotationVelocity, RotationVelocity, RealtimeSeconds * DeltaTime * PlatformRange));
}

I had to do some Googling to find UGameplayStatics::GetRealTimeSeconds(GetWorld()), not sure why GetWorld() is there but it works so I’m not complaining.
RotationVelocity is the direction of the rotation. PlatformRange is how fast should it rotate, I find the sweet spot to be between 45 and 90.
It works, but I do notice some jittering sometimes. No idea how to fix that. I’d imagine it has something to do with GetRealTimeSeconds() but I’m not sure.

And with that, I felt somewhat satisfied with my Obstacle Assault course!

It’s definitely not perfect, and some parts look downright awful but I didn’t want to focus too much on designing lol.
I have to say, It’s been a lot of fun so far!

7 Likes

Hey @oSpace!

I really love your level designs - very creative! I also dug a bit deeper into your questions since they were good refreshers for myself and I wanted to be able to give you a couple different solutions to experiment with.

1. Kill Z
Checking if your actor has fallen under a World Z value that you set in Tick is fine, but it’s somewhat redundant because there is already code in the engine to handle this. If you open up World Settings from the Window menu and take a look at the World category, you’ll notice that there is a setting named Kill Z. This setting can be set to whatever value you want for the current level. For example:

image

If we were happy to just let any actors that fall below this threshold be destroyed, we’d be done - because that is exactly what happens by default. However, since we actually want our character to teleport to some location (like a checkpoint), we need to override the default behavior. This is a bit of an advanced topic for this point in the course, but it’s actually quite simple.

The function we want to override is called FellOutOfWorld which is a virtual member function inside AActor. This function is called by the engine when the actor falls below the Kill Z value we set in World Settings. In order to override the function, we need to create our own Character class. That may sound intimidating at first, but we’re only creating an ultra-thin child class of ACharacter so we can override the FellOutOfWorld function.

To create your own Character class, create a new C++ class just like we did for MovingPlatform. In the CHOOSE PARENT CLASS window, select Character as the parent class, click Next, then choose a name and click Create Class. I’m not very original, so I just called it MyCharacter. =]

Once your character class is created, you can delete all the generated functions and just add the code for FellOutOfWorld like so:

UCLASS()
class OBSTACLEASSAULT_API AMyCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	virtual void FellOutOfWorld(const class UDamageType& dmgType) override;
};

Since I didn’t create a fancy checkpoint system like you did, I just went with the simple option of teleporting the player to the first (and only) PlayerStart in my level:

void AMyCharacter::FellOutOfWorld(const class UDamageType& dmgType)
{
    TArray<AActor*> PlayerStarts;
    UGameplayStatics::GetAllActorsOfClass(GetWorld(), APlayerStart::StaticClass(), PlayerStarts);
    if (PlayerStarts.Num() > 0)
    {
        SetActorLocation(PlayerStarts[0]->GetActorLocation());
    }
}

You can of course modify FellOutOfWorld to use your own checkpoint system. Also, you might be wondering about the dmgType parameter - we need to include it in the function signature for the override but we’re not actually using it for anything so don’t worry about it for right now. If interested, you can read more about it in DamageType.h.

2. Wrecking Ball

Now this one was interesting. I love the creative use of InterpSinInOut - you’re on the right track! That said, I thought I might be able to help you out with the stuttering issue and offer a few alternative solutions.

The stuttering you are seeing is caused by spikes in your framerate. I actually saw the same problem on my machine using your code. My first thought was to actually log each of the variables out to see what was actually going inside the RotateWreckingBall function. Here’s the logging code:

UE_LOG(LogTemp, Warning, TEXT("%f * %f * %f = %f"), RealtimeSeconds, DeltaTime, PlatformRange,
		RealtimeSeconds * DeltaTime * PlatformRange);

The output below shows the log output for the first dozen frames or so:

Take a look at the last column - notice that the value being fed into InterpSinInOut is all over the place. After the first frame, the value for RealtimeSeconds is relatively stable, and PlatfomRange never changes. However, my DeltaTime is all over the place due to framerate spikes. Another issue is the fact that InterpSinInOut expects the last parameter, or the Alpha value to be in the range of 0-1 (a.k.a unit value). These two factors are why you see stuttering in your rotations.

Here are a couple solutions I came up with that you can feel free to experiment with:

Solution 1:
This solution also uses InterpSinInOut, but uses an alpha value that stays within the range (0-1) expected by the function:

SetActorRotation(FMath::InterpSinInOut(MinRotation, MaxRotation, CurrentAlpha));

CurrentAlpha += DeltaTime * AngularVelocity;

if (CurrentAlpha < 0.f || CurrentAlpha > 1.f)
{
	CurrentAlpha = FMath::Clamp(CurrentAlpha, 0.f, 1.f);
	AngularVelocity = -AngularVelocity;
}

Notice that I’m clamping the value of CurrentAlpha to prevent any under/overflow of our range in case there are large spikes in DeltaTime. Our relative error is also much less because of our limited range.

Feel free to experiment with MinRotation, MaxRotation, and AngularVelocity. You may want to expose them in the editor so you can play around with them, but I’ll leave that as an exercise for you. =]

Solution 2:
This solution trades InterpSinInOut for the SmoothStep function (also found in the FMath library). I like this solution because it achieves the same results, however it performs slightly better because it doesn’t call the relatively expensive Sin or Cos functions every frame.

const float CurrentAlpha = FMath::SmoothStep(MinRotation.Roll, MaxRotation.Roll, CurrentRoll);	
SetActorRotation(FMath::Lerp(MinRotation, MaxRotation, CurrentAlpha));

CurrentRoll += DeltaTime * AngularVelocity;
	
if (CurrentRoll < MinRotation.Roll || CurrentRoll > MaxRotation.Roll)
{
	CurrentRoll = FMath::Clamp(CurrentRoll, MinRotation.Roll, MaxRotation.Roll);
	AngularVelocity = -AngularVelocity;
}

Hopefully I’ve answered your questions and opened up some new areas for you to explore. Let me know if you have more questions and keep up the great work!

3 Likes

Privacy & Terms