My first UE5 project
Role: Developer
Team size: 10
Engine: Unreal Engine 5
Duration: 4 weeks
Languages used: C++, UE5 Blueprint
In this game, the player's main "companion" is a retro muscle car with turret guns loaded at front. The player can freely roam in this word, either by foot, or by driving the car. Steering? No problem. Crouching for stealth passthrough enemies? The challenge is yours.
For one player controller in UE5, it may only control one active pawn (this game does not support local multiplayer). We designed two player pawns, one to be the FPS character, and one to be Car. When the car is active, player can "jump" out of the car at any moment with desired input. The car will not "disappear", but it should remain there. If and only if the player is adjacent to the vehicle and facing the desired direction, the player may enter the car, then the player pawn should not appear on the map.
To realize this, I choose to hide the player pawn deep down the map's ground. The player will remain unseen to the FPS camera, but can be brought back at any moment.
void ACPP_UnifiedCarPlayerController::SwitchToCar()
{
if (VehiclePawn)
{
if (FPSInputMappingContext)
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
if (Subsystem)
{
Subsystem->RemoveMappingContext(FPSInputMappingContext);
}
}
if (CarInputMappingContext)
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
if (Subsystem)
{
Subsystem->AddMappingContext(CarInputMappingContext, 0);
}
}
// Send back to "underground"
FPSPawn->SetActorLocation(FVector(0.0f, 0.0f, -100000.0f));
PossessPawn(VehiclePawn);
}
}
void ACPP_UnifiedCarPlayerController::SwitchToFPS()
{
if (FPSPawn)
{
if (CarInputMappingContext)
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
if (Subsystem)
{
Subsystem->RemoveMappingContext(CarInputMappingContext);
}
}
if (FPSInputMappingContext)
{
UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer());
if (Subsystem)
{
Subsystem->AddMappingContext(FPSInputMappingContext, 0);
}
}
if (FPSPawn && VehiclePawn->GetChaosVehicleMovement())
{
UChaosWheeledVehicleMovementComponent* Movement = VehiclePawn->GetChaosVehicleMovement();
Movement->SetThrottleInput(0.0f);
Movement->SetBrakeInput(1.0f); // apply full brake
Movement->SetSteeringInput(0.0f);
Movement->SetHandbrakeInput(true); // apply handbrake
Movement->StopMovementImmediately();
}
// flash to car
// part1: get car location, part 2: set a little bit to the left; part 3; a little bit above the ground to prevent stuck
FVector spawnLocation = VehiclePawn->GetActorLocation() + VehiclePawn->GetActorRightVector().GetSafeNormal2D() * -200.0f + FVector(0.0f, 0.0f, 100.0f);
FPSPawn->SetActorLocation(spawnLocation);
// Rotate FPSPawn
// Note: change VehiclePawn->GetActorForwardVector() to any direction you want.
// GetSafeNormal2D() means we only care about xOy plane
FVector FPSPawnForwardVector = FPSPawn->GetActorForwardVector();
FVector desiredFacingDirection = VehiclePawn->GetActorForwardVector().GetSafeNormal2D();
// degrees: how many degrees that FPSPawn needs to rotate
// rotationDirection: 1.0 means clockwise and -1.0 means counter-clockwise.
float degrees = FMath::Acos(FVector::DotProduct(FPSPawnForwardVector, desiredFacingDirection)) / PI * 180.0f;
float rotationDirection = FVector::DotProduct(FVector::CrossProduct(FPSPawnForwardVector, desiredFacingDirection), FVector::ZAxisVector) >= 0 ? 1.0f : -1.0f;
FRotator desiredRotation = FRotator(0.0f, degrees * rotationDirection, 0.0f);
FPSPawn->AddActorWorldRotation(desiredRotation);
PossessPawn(FPSPawn);
}
The player will infinitely "dropping" down to void. It will not affect any gameplay, though. Anyway, it will be very much reasonable to stop its physics and gravity and keep it there.
The player's flasing location will have to be sightly higher than ground, like what Counter-Strike did in general. Otherwise the player may stuck on the ground and not be able to move around.
Player is equipped with a gun, and it may shoot something, or someone. For simplicity, we use HITSCAN in this FPS game, and I realize this part by line tracing.
AActor* ACPP_BaseFPSCharacter::SingleShot(const float p_range, TEnumAsByte<ECollisionChannel>& p_traceChannelProperty, UWorld* p_world)
{
// isShotFired can be used to determine damage
FHitResult Hit;
// shoot from camera
FVector TraceStart = m_fpsCamera->GetComponentLocation();
FVector TraceEnd = GetActorLocation() + m_fpsCamera->GetForwardVector() * p_range;
// Don't block the trace
FCollisionQueryParams QueryParams;
QueryParams.AddIgnoredActor(this);
p_world->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, p_traceChannelProperty, QueryParams);
if (Hit.bBlockingHit && IsValid(Hit.GetActor()))
{
return Hit.GetActor();
ACPP_BaseCarPawn* carPawn = Cast<ACPP_BaseCarPawn>(Hit.GetActor());
if (IsValid(carPawn) && isShotFired == false)
{
// a car is in place
return HitAction::SwitchToCar;
}
}
else
{
UE_LOG(LogTemp, Log, TEXT("No Actors were hit"));
return nullptr;
}
return nullptr;
}
The player has the ability to aim down the iron sight, just as other FPS games usually do. For simplicity in this game, I did not actually move the location of camera, but to move the whole hand mesh. Since the game is first-person perspective and singleplayer only, it will be rather hard to detect any visual differences except the shadow. Also, an animation is used to make sure the transition is smooth and natural by looking.
It is my first UE5 project, which is quite challenging. UE5 has a lot of "advanced" feature than Unity, also it incorporates Blueprint in parallel to C++. I am not a mega fan of Blueprint; without proper experience, it will create a lot of diagrams looks like a bowl of spagetti: messy and hard to find any clue. Especially, debugging in UE5 is rather hard.
The ADS feature is done in a rather naive way; it should have better ways to do that. I am very interested to know how professional FPS developers like COD or BF developers do that.