Rocket Rider
Rocket riding, Combat, Platforming
Overview
Project Duration
9 Weeks at PlaygroundSquad
Game Engine
Unreal Engine 5.1
Platforms
PS5 & PC
Responsibilities
Character Controller
Movement
Rail grinding
Platforming
DualSense
Trigger effects
Audio Haptics
Lightbar
This is a showcase of what I made during the project
This nine-week project, developed with a team of 17 members (5 programmers, 4 designers, and 8 artists), was my first experience with Unreal Engine.
I appreciated the larger team size, which allowed me to focus on areas I enjoy, such as player controllers and overall feedback.
Targeting the PlayStation 5 platform, I had the opportunity to write code for key console features, particularly the DualSense controller.
I developed modular DualSense code, enabling designers to implement features through blueprints without needing to access any C++ classes.
In addition to coding, I dedicated significant time to optimizing the game, ensuring well a optimized game and great performance despite the demands of raytracing and lumen.
I hope you enjoy exploring this project, which was a significant part of my journey with PlaygroundSquad!
Rocket Movement
A specialized constant forward velocity character controller was required for the game
HandleRocketMovement(const FVector& MovementDirection)
Smoothly adjusts and normalizes the movement direction towards the desired direction based on time and speed.
LastMovementDirection = FMath::VInterpTo(LastMovementDirection,
MovementDirection.GetSafeNormal(),
GetWorld()->GetDeltaSeconds(),
RocketDirectionalChangeSpeed);
LastMovementDirection = LastMovementDirection.GetSafeNormal();
Adjusts the movement direction away from the opposite direction if the angle between them exceeds the threshold,
rotating slightly and interpolating towards the new direction.
if (FVector::DotProduct(MovementDirection.GetSafeNormal(), LastMovementDirection) < -AngleThreshold)
{
const FVector AdjustmentVector = LastMovementDirection.RotateAngleAxis(1.0f, FVector::UpVector);
LastMovementDirection = FMath::VInterpTo(LastMovementDirection,
AdjustmentVector,
GetWorld()->GetDeltaSeconds(),
RocketDirectionalChangeSpeed);
}
Move(const FInputActionValue& InputValue)
Calculates movement direction based on normalized input and controller's yaw rotation.
MoveAxisValue = InputValue.Get<FVector2D>().GetSafeNormal();
const FRotator YawRotation(0, Controller->GetControlRotation().Yaw, 0);
const FVector ForwardDir = FRotationMatrix(YawRotation).GetScaledAxis(EAxis::X);
const FVector RightDir = FRotationMatrix(YawRotation).GetScaledAxis(EAxis::Y);
const FVector MovementDirection = ForwardDir * MoveAxisValue.Y + RightDir * MoveAxisValue.X;
Updates rocket movement and decays the last movement direction, clamping its size to a maximum of 1.0.
HandleRocketMovement(MovementDirection);
LastMovementDirection -= LastMovementDirection * RocketDirectionalChangeSpeed * GetWorld()->GetDeltaSeconds();
LastMovementDirection = LastMovementDirection.GetClampedToMaxSize(1.0f);
Rail Grinding
Predetermined splines in the world allows you to grind, which the magnet helps to make it more forgiving while jumping
RailGrindingMagnet(float DeltaTime)
if (!bIsRailSplineCollided && !GetCharacterMovement()->IsMovingOnGround())
{
if (GetWorld()->SweepMultiByChannel(HitResults,
TraceStart,
TraceEnd,
FQuat::Identity,
ECC_Pawn,
FCollisionShape::MakeSphere(100.0f), Params))
{
float MinDistanceSquared = MAX_FLT;
FVector ClosestPointOnSpline = FVector::ZeroVector;
for (const FHitResult& HitResult : HitResults)
{
if (HitResult.GetActor() && HitResult.GetComponent())
{
AActor* HitActor = HitResult.GetActor();
TArray<FName> ActorTags = HitActor->Tags;
Detects nearby spline actors with the tag "Rail", finds the closest point on their spline,
and smoothly moves the player toward that point if closer than previous points.
if (HitActor->ActorHasTag("Rail"))
{
USplineComponent* SplineComponent =
Cast<USplineComponent>(HitActor->FindComponentByClass<USplineComponent>());
if (SplineComponent)
{
FVector ClosestPoint = SplineComponent->FindLocationClosestToWorldLocation(HitResult.Location,
ESplineCoordinateSpace::World);
float DistanceSquared = FVector::DistSquared(TraceStart, ClosestPoint);
if (DistanceSquared < MinDistanceSquared)
{
MinDistanceSquared = DistanceSquared;
ClosestPointOnSpline = ClosestPoint;
}
FVector NewLocation = FMath::VInterpTo(GetActorLocation(), ClosestPointOnSpline, DeltaTime, 18.5f);
SetActorLocation(NewLocation);
}
}
}
}
}
}
MoveAlongRail(float DeltaTime)
if ( RailSpline && bIsRailSplineCollided )
{
Updates position along the spline, adjusts actor's location,
and smoothly interpolates the mesh's vertical position based on a cosine wave.
TimeAlongSpline += DeltaTime;
SplineLocation = RailSpline->GetLocationAtDistanceAlongSpline(DistanceAlongSpline, ESplineCoordinateSpace::World);
RailTangent = RailSpline->GetTangentAtDistanceAlongSpline(DistanceAlongSpline, ESplineCoordinateSpace::World);
if(!SetSplineDirection)
SetRailVariables();
SetActorLocation(SplineLocation + FVector(0.f, 0.f, GetCapsuleComponent()->GetScaledCapsuleHalfHeight()));
FVector NewMeshLocation = FVector(0, 0, FMath::Lerp(-79.5f, -80.5f, FMath::Cos(CurveFrequency * GetWorld()->TimeSeconds)));
FVector InterpolatedLocation = FMath::VInterpTo(GetMesh()->GetRelativeLocation(), NewMeshLocation, DeltaTime, 5.0f);
GetMesh()->SetRelativeLocation(InterpolatedLocation);
Calculates movement direction along the spline,
smoothly rotates the actor towards the spline's direction,
and updates the last movement direction.
SplineMoveDirection = ( SplineLocation - LastPlayerPos );
const FVector RotationDirection = ( SplineLocation - PreviousRailLocation ).GetSafeNormal();
FRotator TargetRotation = RotationDirection.Rotation();
float InterpSpeed = 5.0f;
FRotator NewRotation = FMath::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, InterpSpeed);
SetActorRotation(NewRotation);
LastMovementDirection = SplineMoveDirection;
Moves along the spline based on direction and speed, handles boundary conditions to trigger exit, and updates position.
bool bMovingForward = SplineDot > 0.0f;
if ( !bIsSprinting )
SplineMoveDistance = RailGrindSpeed * DeltaTime;
else
SplineMoveDistance = RailGrindSpeed * ( RocketSpeedMultiplier / 1.5f ) * DeltaTime;
if ( !bMovingForward )
SplineMoveDistance = -SplineMoveDistance;
if ( ( DistanceAlongSpline <= 0.01f || DistanceAlongSpline >= SplineLength ) && TimeAlongSpline > 0.2f )
{
SplineExit();
}
DistanceAlongSpline = FMath::Clamp( DistanceAlongSpline + SplineMoveDistance, 0.f, SplineLength );
PreviousRailLocation = SplineLocation;
}
Challenges
Movement
You lost your rocket after attacking or being attacked, leading to gameplay where you only had to back off because you didn't have any other form of offense/defense abilities, which wasn't intuitive gameplay.
Rail Grinding
Initially rail grinding was frustrating due to the size of the players collider making it difficult to jump on the rails
Solutions & Outcome
Movement
After playtesting, we removed the option to exit the rocket to streamline gameplay.
This adjustment focused the player’s movement on the rocket mechanics, enhancing overall game coherence and simplicity.
Rail Grinding
Added a magnet feature that activates when the player is jumping. This helps to grab the rails without making the game easy.
The magnet improved the gameplay experience by reducing frustration while maintaining the game's challenge.