Character Wing Suit Controller
Overview Author: 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 integra...
Overview
Author:
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:
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.
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.
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.
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(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(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.
Set the CharacterMovement variable's Movement Capabilities to support flying:
Add the following events to the ThirdPersonCharacter blueprint graph
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
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.
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!
Another thing you may want to do is invert the camera pitch when flying. Optional.
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).
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.
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).
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.
Implementation
Rotation (Pitch & Yaw)
This part is very simple. Use MoveUpdatedComponent with the CharacterMovement to assign the GetControlRotation to the Pitch and Yaw
Velocity
Check that our curves aren't missing, and print a warning if they are!
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:
And finally, we apply our velocity based on the direction the character is facing with the magnitude set to our movement speed.
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.
Completed Blueprint
Here is the completed Blueprint
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
Import the animation to the UE4 Mannequinn with the Animation Length set to Animated Time (Not Exported Time)
Open up the ThirdPerson_AnimBP Blueprint. Add a boolean variable IsFlying and set it in Event Blueprint Update Animation as so:
Then in the Default Anim State Machine, add a flying State with transitions like so:
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:
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.
Project Files
You can download the completed project here