Firewall Runner is all about building speed and maintaining it. Giving players the rush of going fast and rewarding them for it is the main priority.
Though Firewall Runner has some intended routes, there are ways to bypass certain obstacles with clever uses of mechanics.
Moving from obstacle to obstacle should feel like a breeze. The smoother the player can transition between obstacles, the more they should be rewarded.
Goals
Create a proof of concept
Practice 3D math skills
Create a fun and compelling movement system
Experiment with level design
Learn more about how audio systems work in Unreal Engine
Explore and implement Unreal Engine materials using 3D math
Scope
A tutorial and one level that showcases some interesting obstacles that can be made with only simple movement
Main menu with settings for both audio and controls
Pause menu that can change the settings, restart the current level, and return to the main menu.
Highlights
Movement system flows very well and has a very high skill ceiling, while still having a fairly accessible skill floor
Implemented 3D math in a way that is both consistent, and doesn’t get in the way of player experience
Level design of the level allows for continuous flow through all obstacles, and mastery of the geometry feels satisfying
Tutorial teaches players by doing, and not just listing controls
Teaches players the building blocks of movement
Teaches players that the building blocks can be combined
Lessons Learned
Found plenty of features to add for a better player experience
Reset to previous checkpoint (implemented)
Resetting to previous checkpoint should reset the time when the checkpoint is the beginning of the level (not implemented)
Something to indicate rope tension (implemented)
Bounce mechanic needed a cooldown (implemented)
HUD element indicating how long is left on the cooldown (implemented)
Grapple range indicator needed (implemented)
Crosshair changes colour when valid terrain is in range (implemented)
Tutorial would make it easier to grasp concepts (implemented)
Attempted to make a second level
Creating unique level elements that flow well is very challenging
Scrapped the second level
What I’d do differently
More consistent grappling on some surfaces. Using cubes as poles can create some unexpected trajectories when swinging is calculated from the surface of the terrain
Could create pole objects that connect the grapple to the core of the cylinder. More consistent swings, but could be confusing.
Create better tools to design levels
Having to change the collision and material of each object after creating the mesh made it easy to make mistakes
Create editor tool to change the material and the collision at the same time with the press of a button
Angled blocks were challenging to make, and took more time than they should
Create some default static meshes of angled blocks to be used
void AFR_Character_Player::CalculateSwingPhysics(float DeltaTime)
{
if(IsRetracting && GrappleLength > 250.0f)
{
GrappleLength = GrappleLength >= RetractSpeed * DeltaTime ? GrappleLength - RetractSpeed * DeltaTime : 250.0f;
}
FVector delta = GrapplePoint - GetActorLocation();
float distToGrapple = delta.Size();
FVector deltaNormal = delta.GetSafeNormal();
if (GrappleLength <= distToGrapple)
{
FVector gravityForce, parallelForce, currentVelocity, retractForce;
currentVelocity = GetCharacterMovement()->Velocity;
gravityForce = FVector(0.0f, 0.0f, GetCharacterMovement()->GetGravityZ());
// Force Parallel to rope
parallelForce = -gravityForce.ProjectOnTo(deltaNormal);
parallelForce -= (currentVelocity.ProjectOnTo(deltaNormal) / DeltaTime) * GetCharacterMovement()->Mass;
if (parallelForce.GetSignVector() == deltaNormal.GetSignVector())
{
GetCharacterMovement()->AddForce(parallelForce);
if (GrappleLength < distToGrapple)
{
retractForce = (deltaNormal / DeltaTime) * (distToGrapple - GrappleLength) * GetCharacterMovement()->Mass;
GetCharacterMovement()->AddForce(retractForce);
}
}
}
float ropeEndProximityMod = 0.2f;
if (distToGrapple > GrappleLength * 0.8f)
{
ropeEndProximityMod = FMath::Clamp((distToGrapple - (GrappleLength * 0.8f)) / (GrappleLength * 0.2f), 0.0f, 1.0f);
ropeEndProximityMod = 0.2f + ropeEndProximityMod * 1.3f;
}
GrappleRope->DynamicMat->SetScalarParameterValue("RopeEndProximityMod", ropeEndProximityMod);
}
The main movement mechanic of Firewall Runner is swinging on a grappling tether with adjustable length. In order to get the feel of a rope that can go slack, I wrote a system that uses basic pendulum physics.
In my first attempt, I added a force that negated the player's velocity along a vector parallel to the rope. This worked, to keep the player tethered correctly, but the player couldn't gain momentum by falling. To make the movement more fluid, and to conserve player velocity, I took the force of gravity and projected it parallel to the rope.
After some further testing, and bumping into a lot of things that I didn't intend to, I decided that the rope should be able to be retracted during a swing. To do this, I had to add the force of the rope retracting to the player as well. I added a variable to track the expected distance of the rope and compare it to the distance between the player and the grapple point. I also added a minimum length for the rope in order to avoid any side effects of rapid collision if the player pulled the rope in too short.
void AFR_Character_Player::OnHitDetected(AActor* SelfActor, AActor* OtherActor, FVector NormalImpulse, const FHitResult& Hit)
{
if (IsBounceEnabled && BounceTimer >= 1.0f)
{
AudioComp->SetSound(BounceSoundEffect);
AudioComp->Play();
GetCharacterMovement()->Velocity = PrevVelocity.MirrorByVector(Hit.Normal) * BounceModifier;
BounceTimer = 0.0f;
}
}
The bounce mechanic to compliments the swing mechanic, and to adds an intuitive way to interact with level geometry. In order to achieve the effect I wanted, I mirrored the velocity of the player along the surface of the object they hit, and added a modifier to tune the strength of the bounce.
Adding a cooldown to the bounce prevents any unintended speed ramping when the modifier can be greater than one. This opens up a lot of possibilities in map design, and makes the movement flow better.
There was one hitch in the implementation that caused some issues. Since player velocity ticks with physics, by the time OnHitDetected is run, the velocity has already updated with the physics tick. This caused the bounce velocity to be calculated on a much smaller velocity than expected. In order to fix this, the velocity from the previous tick is tracked by the player charter.
void AFR_Character_Player::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
if (BounceTimer < 1.0f) BounceTimer += DeltaTime;
if (IsGrappling)
{
CalculateSwingPhysics(DeltaTime);
}
else
{
FHitResult hitResult;
static FName TraceTag = TEXT("TraceTag");
FCollisionQueryParams queryParams(TraceTag);
FVector endLocation = FVector(CharacterCamera->GetComponentLocation() + (CharacterCamera->GetComponentRotation().Vector() * 1000.0f));
InRange = GetWorld()->LineTraceSingleByChannel(
hitResult,
CharacterCamera->GetComponentLocation(),
endLocation,
ECollisionChannel::ECC_Visibility,
queryParams
)
&& hitResult.GetComponent()->GetCollisionObjectType() == ECollisionChannel::GRAPPLE_OBJECT;
}
PrevVelocity = GetCharacterMovement()->Velocity;
}