Character Wing Suit Controller

From Epic Wiki
Jump to: navigation, search

Overview

Author: Vaei

This tutorial will walk you through how to create a wing suit flight system for the character controller with multiplayer support. We will be observing proper practices and integrate it fully into the CharacterMovementComponent to leverage the systems that it is built around.

This system does not exclude the initial functionality of a character, but rather it is integrated into it.

To keep things simple, when you walk off a ledge or reach the apex of a jump up it will enter flight mode automatically.

Here is a video of what we will be making:

https://www.youtube.com/watch?v=wIXDQx0GqC8

Requirements

This is an intermediate level tutorial.

This tutorial is largely based in blueprint but requires some C++ setup to give us the entry points we want for Blueprint. If you would prefer to use C++ it should be immediately obvious how to do so, assuming you are experienced enough, however this tutorial is BP focused and wont cover a C++ implementation.

  • Be familiar with the editor and blueprints
  • Have Visual Studio setup as there is some minimal C++ involved due to CharacterMovementComponent being based in C++
  • Due to proper integration you will not need to know or do any networking, however to customize the system to your own needs you will likely want some familiarity for when things no longer work as expected.


Project Setup

Create the Project

Note: I will be using UE4.18 and Visual Studio 2017 and can't guarantee consistency in the future (or past versions)

Launch the engine and create a Blueprint project based on the Third Person template. I have named the project WingSuit for this tutorial. From here-on out, any class or project names I used will be used to refer to our own classes or blueprints.

UE4Editor 2018-01-25 13-20-24.png


Initial Setup

Delete the character that is present in the ThirdPersonExampleMap from the level

Create two new C++ classes, one based on Character and another based on CharacterMovementComponent.

For my purposes I have named these WSCharacter and WSCharacterMovementComponent respectively.

2018-01-25 12-25-39.gif

For CharacterMovementComponent you will need to check ``Show All Classes`` and type the name.


Basis of Design

Don't skip this part if you want to have any idea of why we're doing it this way

CMC (CharacterMovementComponent) uses a physics loop for any movement. This helps to eliminate inaccuracies, particularly at varied and low frame rates.

For example, if you were to trace downwards each frame to find the floor, you could move to a position before you hit it on frame 1 and then on frame 2 you could be past it, so there was no frame where the trace would succeed. CMC combats this by checking back over each frame.

By placing our own flight movement code into CMC physics loop we get the additional accuracy - flight feels smoother, more responsive, accurate.

Another aspect we will achieve is to use the systems that are already in place, such as acceleration and velocity, which are not only replicated but built into CMC's prediction system which means it works with the client-side prediction and server reconciliation that allows for responsive movement while still remaining server authoritative (and therefore not prone to being hacked). Because we will only modify these variables there should be no additional work required for multiplayer; it will work out of the box.


C++ Setup

Close the editor! - When it's time to build the code having the editor open will cause it to hot reload instead which will cause problems.

Open the solution if it isn't already (WingSuit.sln if you used my naming). Expand the project to view the classes we added in the previous step.

Devenv 2018-01-25 12-40-26.png


WSCharacter.h

We will start with the setup for WSCharacter so double click on WSCharacter.h. Empty out the added code so we are left with a shell:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "WSCharacter.generated.h"

UCLASS()
class WINGSUIT_API AWSCharacter : public ACharacter
{
	GENERATED_BODY()
public:
};

There are two basic things that we need to be able to access from Blueprint to create a flight system. We need an entry point to handle the movement, and we need to know when we colliding with something or landed.

We will add a tick event that we can override in Blueprint to provide the flying functionality. And we will add another event that is called whenever we impact another object (and it tells us if we impacted a walkable floor).

Also, we need to add a constructor so that we can tell WSCharacter to use the WSCharacterMovementComponent that we made.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "WSCharacter.generated.h"

UCLASS()
class WINGSUIT_API AWSCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Constructor required to assign our own CharacterMovementComponent
	AWSCharacter(const FObjectInitializer& OI);

	/**
	* --------------------------------------------
	* Entry point for handling of flying movement.
	* --------------------------------------------
	* Added a "FlyingTick" into CharacterMovementComponent because calculating the movement
	* from within the physics loop results in high accuracy that wont cause significant 
	* indiscrepancies at varied frame rates.
	*/
	UFUNCTION(BlueprintNativeEvent, Category = "Character Movement: Flying")
	void FlyingTick(float DeltaTime);

	/**
	* Called by WSCharacterMovementComponent to notify that we have impacted another
	* object while flying.
	* @param Hit : The hit result for the impacted object
	* @param bIsWalkableFloor : True if the impacted object can be walked on
	*/
	UFUNCTION(BlueprintImplementableEvent, Category = "Character Movement: Flying")
	void OnImpactDuringFlying(const FHitResult& Hit, bool bIsWalkableFloor);
};


WSCharacter.cpp

Delete the auto generated classes so we are left with just the include line.

Include WSCharacterMovementComponent and add the definition for constructor as follows:

#include "WSCharacter.h"
#include "WSCharacterMovementComponent.h"

AWSCharacter::AWSCharacter(const FObjectInitializer& OI)
	: Super(OI.SetDefaultSubobjectClass<UWSCharacterMovementComponent>(AWSCharacter::CharacterMovementComponentName))
{

}

The OI.SetDefaultSubobjectClass tells it to use our UWSCharacterMovementComponent for the CharacterMovementComponent.

WSCharacterMovementComponent.h

All we need to do here is override the flying physics. Your code should look like this:

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "WSCharacterMovementComponent.generated.h"

/**
 * 
 */
UCLASS()
class WINGSUIT_API UWSCharacterMovementComponent : public UCharacterMovementComponent
{
	GENERATED_BODY()
	
public:
	virtual void PhysFlying(float deltaTime, int32 Iterations) override;
};


WSCharacterMovementComponent.cpp

As previously mentioned, CMC provides a physics loop for each movement mode. In the header we added the override for the flying physics loop.

To construct this, I copied the default definition from UCharacterMovementComponent, then I added a while loop (the same used for PhysWalking and PhysFalling).

From there I cast the character to our own and added the two calls to the blueprint events in our WSCharacter.

The code here looks very complex, but like with most things becomes basic when you break it down. However that is out of the context of this tutorial, for now the important thing is that the flying is substepped and we have it calling the relevant blueprint events and there is basic collision handling.

This is the resulting WSCharacterMovementComponent.cpp:

#include "WSCharacterMovementComponent.h"
#include "WSCharacter.h"

// ==============================================================================
// * Copied from UCharacterMovementComponent::PhysFlying
// * Added a "FlyingTick" because calculating the movement
// * from within the physics loop results in high accuracy
// * that wont cause significant indiscrepencies at varied
// * frame rates.
// ==============================================================================
void UWSCharacterMovementComponent::PhysFlying(float deltaTime, int32 Iterations)
{
	if (deltaTime < MIN_TICK_TIME)
	{
		return;
	}

	if (!CharacterOwner)
	{
		return;
	}

	float remainingTime = deltaTime;
	while ((remainingTime >= MIN_TICK_TIME) && (Iterations < MaxSimulationIterations))
	{
		Iterations++;
		const float timeTick = GetSimulationTimeStep(remainingTime, Iterations);
		remainingTime -= timeTick;

		RestorePreAdditiveRootMotionVelocity();

		AWSCharacter* WSCharacter = Cast<AWSCharacter>(CharacterOwner);
		if (!WSCharacter)
		{
			return;
		}

		if (!HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
		{
			if (bCheatFlying && Acceleration.IsZero())
			{
				Velocity = FVector::ZeroVector;
			}

			WSCharacter->FlyingTick(timeTick);
		}

		ApplyRootMotionToVelocity(deltaTime);

		Iterations++;
		bJustTeleported = false;

		FVector OldLocation = UpdatedComponent->GetComponentLocation();
		const FVector Adjusted = Velocity * deltaTime;
		FHitResult Hit(1.f);
		SafeMoveUpdatedComponent(Adjusted, UpdatedComponent->GetComponentQuat(), true, Hit);

		if (Hit.Time < 1.f)
		{
			WSCharacter->OnImpactDuringFlying(Hit, IsWalkable(Hit));

			const FVector GravDir = FVector(0.f, 0.f, -1.f);
			const FVector VelDir = Velocity.GetSafeNormal();
			const float UpDown = GravDir | VelDir;

			bool bSteppedUp = false;
			if ((FMath::Abs(Hit.ImpactNormal.Z) < 0.2f) && (UpDown < 0.5f) && (UpDown > -0.2f) && CanStepUp(Hit))
			{
				float stepZ = UpdatedComponent->GetComponentLocation().Z;
				bSteppedUp = StepUp(GravDir, Adjusted * (1.f - Hit.Time), Hit);
				if (bSteppedUp)
				{
					OldLocation.Z = UpdatedComponent->GetComponentLocation().Z + (OldLocation.Z - stepZ);
				}
			}

			if (!bSteppedUp)
			{
				//adjust and try again
				HandleImpact(Hit, deltaTime, Adjusted);
				SlideAlongSurface(Adjusted, (1.f - Hit.Time), Hit.Normal, Hit, true);
			}
		}

		if (!bJustTeleported && !HasAnimRootMotion() && !CurrentRootMotion.HasOverrideVelocity())
		{
			Velocity = (UpdatedComponent->GetComponentLocation() - OldLocation) / deltaTime;
		}
	}
}

Compile Your Code!

The editor MUST be closed!

Right click on the project and press Build. It should say something along the lines of Build: 2 succeeded, 0 failed, 0 up-to-date, 0 skipped

If anything failed, check that the code matches my own. Copy/paste if in doubt.

Blueprint Setup

Open the editor.

Open up ThirdPersonCharacter and reparent the blueprint to our own.

2018-01-25 13-39-19.gif

Set the CharacterMovement variable's Movement Capabilities to support flying:

UE4Editor 2018-01-25 14-42-11.png

Add the following events to the ThirdPersonCharacter blueprint graph

UE4Editor 2018-01-25 13-45-12.png

Entering & Exiting Flying Mode

We want to be in wing suit mode when we walk off a ledge or when we reach the apex of a jump - basically any time we're in the air and not jumping upwards. You may have different requirements for your own game, but for the purpose of this tutorial I want to keep it simple.

And when we land on a walkable floor, we want to return to walking.

There is an event for when the character reaches the apex of a jump that we can bind to. We need to tell it that we want to be notified whenever we jump

UE4Editor 2018-01-25 15-16-46.png

Changing Things Up When Flying!

There are a few things you may want to happen when entering or exiting flying mode and we do this in the OnMovementModeChanged event. For the purposes of this tutorial we will add some camera lag while flying, for regular movement this feels unresponsive but when flying it can give a nice feel to it.

In the CameraBoom settings temporarily check the Enable Camera Rotation Lag so you can set Camera Rotation Lag Speed' to 5. A lower speed means a smoother/less responsive camera. Disable Enable Camera Rotation Lag.

Also set Target Arm Length to 800 (or whatever distance from the character you desire.

Add a float variable called FlightSpeed. Default 0. Add another called InitialFlightSpeed. Default 500.

When we enter flight mode we will enable camera position and rotation lag and also set the flight speed to a higher initial value, so that we don't start out falling! And provide a boost to the velocity to help us in the same regard.

UE4Editor 2018-01-25 13-59-03.png

When flying the pitch of the character is modified, when landing we want to reset the pitch as it isn't used for walking. Also time to disable the camera lag!

UE4Editor 2018-01-25 14-02-55.png

Another thing you may want to do is invert the camera pitch when flying. Optional.

UE4Editor 2018-01-25 14-06-04.png UE4Editor 2018-01-25 14-51-49.png

Make sure to mark GetFlyingPitch as Pure in the Details panel.

Flying Movement

Now we get to the juicy part. Making him actually fly!

Overview

There are three main components to observe here:

Pitch - Based off the controller rotation (provided by mouse/gamepad input, also turns camera) Velocity - The speed we move at, influenced by the pitch Gravity - The speed we fall at, influenced by our velocity

When designing this system, I wanted it to be highly customize-able and I achieved this using curves.

There are three curves:

1. FlightSpeed What this curve implies is that if we have a pitch of -45 (pointing down) then the speed should be 700, a pitch of 0 should be 400 (pointing ahead), pitch of 45 (pointing up) should be -100.

This isn't the absolute speed, it is the influence on the speed. If we are pointing up then speed is deducted at a rate of 100 (or added at a rate of -100).

UE4Editor 2018-01-25 14-17-40.png

2. FlightGravity This curve implies that if we are moving at a speed of 0 then apply a gravity multiplier of 90, at speed of 225 apply 24.5, at speed of 700 apply 0.75.

This equates to a faster speed applying less downwards force so maintaining your speed allows you to maintain your height.

UE4Editor 2018-01-25 14-17-17.png

3. FlightRate FlightRate is the interpolation rate applied based on our pitch, essentially affecting how fast our speed changes based on our pitch.

At a pitch of -45 the interpolation rate is 200 (very fast change to reward facing downwards), at a pitch of 0 the rate is 10, allowing for very gradual changes when oriented straight ahead, and at a pitch of 45 the change is moderately fast, allowing us to aim up a bit without too drastic a change (but also punishing this decision if maintained).

UE4Editor 2018-01-25 14-20-17.png

Parameters

Add three parameters of type Curve Float named FlightSpeedCurve, FlightRateCurve, and FlightGravityCurve. Assign the curves outlined in the previous step to these variables.

Add a float parameter called FlightGravity. This is used to determine the gravity while flying. Default 980.

UE4Editor 2018-01-25 14-47-15.png

Implementation

Rotation (Pitch & Yaw)

This part is very simple. Use MoveUpdatedComponent with the CharacterMovement to assign the GetControlRotation to the Pitch and Yaw

UE4Editor 2018-01-25 14-26-33.png

Velocity

Check that our curves aren't missing, and print a warning if they are!

UE4Editor 2018-01-25 14-30-33.png

Now we set the Flight Speed based on the interpolated result of our current Flight Speed -> the flight speed based on our rotation taken from the curves, as so:

UE4Editor 2018-01-25 14-33-18.png

And finally, we apply our velocity based on the direction the character is facing with the magnitude set to our movement speed.

File:UE4Editor 2018-01-25 14-35-37.png

Gravity

Check the curve is valid, again.

Essentially what we are doing for the gravity is reducing the velocity based on the up vector multiplied by a gravity value. The amount of gravity is influenced by how fast we are moving - essentially causing a greater downward pull when moving at lower speeds.

UE4Editor 2018-01-25 14-58-48.png

Completed Blueprint

Here is the completed Blueprint

UE4Editor 2018-01-25 15-00-10.png

Animation Graph

I have included a very quick/basic and ugly (lets be honest) animation of a person flying, this is actually just a modified walk cycle with the joint rotation changed.

Grab it here File:ThirdPersonFly.zip

Import the animation to the UE4 Mannequinn with the Animation Length set to Animated Time (Not Exported Time)

2018-01-25 15-21-46.gif

Open up the ThirdPerson_AnimBP Blueprint. Add a boolean variable IsFlying and set it in Event Blueprint Update Animation as so:

UE4Editor 2018-01-25 15-28-38.png

Then in the Default Anim State Machine, add a flying State with transitions like so:

UE4Editor 2018-01-25 15-31-23.png

I recommend a transition duration of 1 second into flying and 0.4 seconds out of flying with this animation.

Inside our flying state add the animation:

UE4Editor 2018-01-25 15-33-00.png

For the transitions into flying add the condition Is Flying and for the transitions out of flying add the inverse.

End Result

Preview Map

To really get a feel for it you will want to modify the ThirdPersonExampleMap. Here I added a bunch of objects, extra start positions (for multiplayer) and placed objects high up for jumping off.

UE4Editor 2018-01-25 15-35-20.png

Project Files

You can download the completed project here

https://github.com/Vaei/WingSuitTutorial