Project Status | Completed |
Softwares Used | Unreal Engine 4.27 |
Languages Used | C++ |
I applied for a 'Junior Game Programmer' position in a game company. They asked me to develop the 'Dash' mechanic, the gravity gun in Half Life, and 1 gameplay mechanic to choose from the list they gave me, based on the first person template in the latest version of Unreal Engine 4. There are no visual effects at all, as the main focus on the mission is programming. That may be why it feels like wood.
You can review and download this project on Github!
What they wanted from me was that the player was thrown in the direction of movement when the 'Shift' key was pressed. They wanted to be able to dynamically adjust dash speed and distance.
I've decided that the most convenient way to do this is to use Timeline. I created my variables for this.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dash", meta=(ClampMin=1, UIMin=1))
float DashSpeed = 200.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Dash", meta=(ClampMin=1, UIMin=1))
float DashDistance = 1000.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Dash")
bool bIsDashing = false;
FVector DashStartLocation;
FVector DashEndLocation;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Dash")
class UCurveFloat* DashCurve;
FTimeline DashTimeline;
Then I did the binding operations in the BeginPlay() method.
// Binding dash functions
if(DashCurve)
{
FOnTimelineFloat TimelineUpdateCallback;
FOnTimelineEventStatic TimelineFinishedCallback;
TimelineUpdateCallback.BindUFunction(this, FName("DashUpdate"));
TimelineFinishedCallback.BindUFunction(this, FName("DashFinished"));
DashTimeline.AddInterpFloat(DashCurve, TimelineUpdateCallback);
DashTimeline.SetTimelineFinishedFunc(TimelineFinishedCallback);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("DashCurve not assigned in character BP. Please assign it!"));
}
By the way, don't forget to call this function in Tick():
DashTimeline.TickTimeline(DeltaSeconds);
When the 'Shift' key is pressed, the Dash() function is called. First, some necessary checks are made. Dashs direction is determined. The target location is determined in this direction and according to the speed and distance information given. Time is calculated from the formula distance = speed * time and the speed of the Timeline is adjusted.
// Dash implementation
void ASTGameProgrammerTaskCharacter::Dash()
{
if(bIsDashing) return;
if(GetVelocity().Size() <= 0.f) return;
if(DashCurve == nullptr) return;
FVector DashDirection = GetVelocity();
DashDirection.Z = 0.f;
DashDirection.Normalize();
DashStartLocation = GetActorLocation();
DashEndLocation = DashStartLocation + DashDirection * DashDistance;
DashTimeline.SetPlayRate(1 / (DashDistance / DashSpeed));
DashTimeline.PlayFromStart();
bIsDashing = true;
}
//Function called while running the timeline
void ASTGameProgrammerTaskCharacter::DashUpdate()
{
const float TimelineValue = DashTimeline.GetPlaybackPosition();
const float CurveFloatValue = DashCurve->GetFloatValue(TimelineValue);
const FVector TargetLocation = FMath::Lerp(DashStartLocation, DashEndLocation, CurveFloatValue);
SetActorLocation(TargetLocation, true);
}
void ASTGameProgrammerTaskCharacter::DashFinished()
{
bIsDashing = false;
}
Another mechanic they wanted from me was the Gravity Gun in Half-Life. A weapon that you can carry and throw objects around.
I will need to Line Trace to select the object for pickup, so I define the variables I need. In addition, I created a SceneComponent that I will attach to the Weapon mesh in order to understand where the object I will carry will appear more easily.
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Gravity Gun")
class USceneComponent* GrabbedObjectLocation;
UPROPERTY()
class UPrimitiveComponent* GrabbedObject;
UFUNCTION()
void SetGrabbedObject(UPrimitiveComponent* ObjectToGrab);
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Gravity Gun")
float PickupTraceRange = 5000.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Gravity Gun")
float PickupTraceRadius = 20.f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Gravity Gun")
float FiringForce = 5000.f;
//Calling on Left-Mouse-Button press
void OnFire();
// Calling on LMB release
void EndFire();
The logic is very simple. Trace forward from camera. if the hit component is simulating physics then turn off the physics and connect it to the SceneComponent that we've created.
void ASTGameProgrammerTaskCharacter::OnFire()
{
const FVector PickupTraceStart = GetFirstPersonCameraComponent()->GetComponentLocation();
const FVector PickupTraceEnd = PickupTraceStart + GetFirstPersonCameraComponent()->GetForwardVector() * PickupTraceRange;
FHitResult PickupTraceHitResult;
const bool bHit = UKismetSystemLibrary::SphereTraceSingle(this, PickupTraceStart, PickupTraceEnd, PickupTraceRadius, UEngineTypes::ConvertToTraceType(ECC_Visibility),
false, {this}, EDrawDebugTrace::None, PickupTraceHitResult, true);
if(bHit)
{
// If the Hit Component is using physics simulation, we grap it.
if(UPrimitiveComponent* Prim = PickupTraceHitResult.GetComponent())
{
if(Prim->IsSimulatingPhysics())
{
SetGrabbedObject(Prim);
}
}
}
}
void ASTGameProgrammerTaskCharacter::SetGrabbedObject(UPrimitiveComponent* ObjectToGrab)
{
GrabbedObject = ObjectToGrab;
if(GrabbedObject)
{
GrabbedObject->SetSimulatePhysics(false);
GrabbedObject->AttachToComponent(GrabbedObjectLocation, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
}
}
Throwing is even simpler than grabbing. Detach from the SceneComponent we connected, turn physics back on and apply force.
void ASTGameProgrammerTaskCharacter::EndFire()
{
// Releasing the grabbed object if there is one.
if(GrabbedObject)
{
const FVector ShootVelocity = GetFirstPersonCameraComponent()->GetForwardVector() * FiringForce;
GrabbedObject->DetachFromComponent(FDetachmentTransformRules::KeepWorldTransform);
GrabbedObject->SetSimulatePhysics(true);
GrabbedObject->AddImpulse(ShootVelocity, NAME_None, true);
SetGrabbedObject(nullptr);
}
}
As a final task, they asked me to pick a mechanic from a list and do it. I chose the jetpack because I thought it would be compatible with Dash.
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Jetpack")
float JetpackMaxTime = 2.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Jetpack")
float JetpackTime = JetpackMaxTime;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Jetpack")
float JetpackBoostForce = 120.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Jetpack")
bool bIsJetpackActive = false;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="Jetpack")
class UCurveFloat* JetpackBoostCurve;
void ToggleJetpack(const bool bReset, const bool bActivate);
void UpdateJetpack(const float DeltaSeconds);
When the 'Space' button is pressed, I jump or open the jetpack depending on the movement mode.
void ASTGameProgrammerTaskCharacter::JumpButtonPressed()
{
if(GetCharacterMovement())
{
switch (GetCharacterMovement()->MovementMode)
{
case EMovementMode::MOVE_Falling:
ToggleJetpack(false, true);
case EMovementMode::MOVE_Walking:
Jump();
default: ;
}
}
}
void ASTGameProgrammerTaskCharacter::JumpButtonReleased()
{
StopJumping();
ToggleJetpack(false, false);
}
I open and close the jetpack in the 'ToggleJetpack()' function. If the 'bReset' parameter is true, you can think of it as refueling. If the jetpack is open I increase Air Control.
void ASTGameProgrammerTaskCharacter::ToggleJetpack(const bool bReset, const bool bActivate)
{
if(bReset)
{
JetpackTime = JetpackMaxTime;
}
bIsJetpackActive = bActivate;
if(GetCharacterMovement())
{
GetCharacterMovement()->AirControl = bIsJetpackActive ? 5.f : 1.f;
}
}
Working logic is not difficult. We check whether jetpack are used in each frame(by calling UpdateJetpack() in Tick function. It's not showing here). If it is used, we apply force to the character on the Z axis according to the curve and reduce the JetpackTime. When the character hits the ground, we equalize the time to the maximum again.
void ASTGameProgrammerTaskCharacter::UpdateJetpack(const float DeltaSeconds)
{
if(bIsJetpackActive)
{
JetpackTime -= DeltaSeconds;
if(JetpackBoostCurve)
{
const float BoostAmount = JetpackBoostCurve->GetFloatValue(UKismetMathLibrary::NormalizeToRange(JetpackMaxTime - JetpackTime, 0.f, JetpackMaxTime)) * JetpackBoostForce;
LaunchCharacter(FVector(0.f, 0.f, BoostAmount), false, true);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("JetpackBoostCurve is not assigned in character BP!"));
}
if(JetpackTime <= 0.f)
{
ToggleJetpack(false, false);
}
}
}
void ASTGameProgrammerTaskCharacter::Landed(const FHitResult& Hit)
{
Super::Landed(Hit);
ToggleJetpack(true, false);
}