While our game may be running without any issues in the editor or even in…
Recreating Sniper Elite’s killshot cameras
In this post we’re going to recreate Sniper Elite’s killshot cameras. This post is written using Unreal Engine 4.12.5 so your implementation might differ in case you’re working with a different version of the engine.
Before we start, here is the end result:
Before we start recreating this mechanic, let’s take a step back and analyze how everything works. The mechanic is triggered when the character that we’re shooting at is going to die. For the sake of simplicity, we’re going to assume that every enemy character dies with one shot only. For the rest of this post, a successful shot will be considered a shot that hits an enemy target.
Assuming that our character fired a successful shot, our system’s logic is divided into the following steps:
- We dilate the world’s time by a specified value
- We make a transition from a First Person Camera view to a Third Person Camera view in order to see the character that fired the shot.
- After a specified time, we transition from a Third Person Camera view to a camera that is attached to the fired projectile.
- When the projectile is within a specified range from the enemy character we transition from the projectile’s camera to a camera that is attached to the enemy character
- The moment that the enemy character hits the ground, we transition from the active camera to First Person Camera view again.
For this project, I’m using the First Person C++ Template that comes with UE4. Before we achieve the end result shown in the video above, we will modify some functions and properties that come with the c++ template. This means that we’re going to strip down some functionality and add our own.
Step 1. Configuring World’s Time dilation
In order to dilate the World’s Time, expose the following property in your charater’s header file:
1 2 3 4 |
protected: /*The time dilation multiplier*/ UPROPERTY(EditAnywhere) float TimeDilationMultiplier = 0.05f; |
Then, navigate on the OnFire() function in your source file and add the following line right after the Projectile’s spawn to dilate the time:
UGameplayStatics::SetGlobalTimeDilation(World, TimeDilationMultiplier);To clarify, here is the whole OnFire function at this step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void ASELethalShotCharacter::OnFire() { // try and fire a projectile if (ProjectileClass != NULL) { const FRotator SpawnRotation = GetControlRotation(); // MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset); UWorld* const World = GetWorld(); if (World != NULL) { // spawn the projectile at the muzzle World->SpawnActor<ASELethalShotProjectile>(ProjectileClass, SpawnLocation, SpawnRotation); //Dilate the time UGameplayStatics::SetGlobalTimeDilation(World, TimeDilationMultiplier); } } // try and play the sound if specified if (FireSound != NULL) { UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation()); } // try and play a firing animation if specified if (FireAnimation != NULL) { // Get the animation object for the arms mesh UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance(); if (AnimInstance != NULL) { AnimInstance->Montage_Play(FireAnimation, 1.f); } } } |
Step 2. Transition from First Person to Third Person View
In order to switch a perspective, we’re going to use a new camera that is attached to a spring arm component which is attached to our character’s root component.
Having said that, add the following components and properties to your character’s header file:
1 2 3 4 5 6 7 8 9 10 11 |
protected: /*The time dilation multiplier*/ UPROPERTY(EditAnywhere) float TimeDilationMultiplier = 0.2f; UPROPERTY(VisibleAnywhere) USpringArmComponent* ThirdPersonSpringArmComp; UPROPERTY(VisibleAnywhere) UCameraComponent* ThirdPersonCameraComp; |
Then, navigate in your character’s constructor and type in the following code (in order to create our components):
1 2 3 4 5 6 7 8 9 10 11 |
//Create the spring arm comp ThirdPersonSpringArmComp = CreateDefaultSubobject<USpringArmComponent>(FName("ThirdPersonSpringArmComp")); //Attach it to the character's root component ThirdPersonSpringArmComp->SetupAttachment(GetRootComponent()); //Create the third person camera comp ThirdPersonCameraComp = CreateDefaultSubobject<UCameraComponent>(FName("ThirdPersonCameraComp")); //Attach it our spring arm ThirdPersonCameraComp->SetupAttachment(ThirdPersonSpringArmComp); |
Moreover, make sure to change the option SetOnlyOwnerSee from true to false on Mesh1P and FP_Gun. Here is my complete constructor at this stage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
ASELethalShotCharacter::ASELethalShotCharacter() { // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(55.f, 96.0f); // set our turn rates for input BaseTurnRate = 45.f; BaseLookUpRate = 45.f; // Create a CameraComponent FirstPersonCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("FirstPersonCamera")); FirstPersonCameraComponent->SetupAttachment(GetCapsuleComponent()); FirstPersonCameraComponent->RelativeLocation = FVector(-39.56f, 1.75f, 64.f); // Position the camera FirstPersonCameraComponent->bUsePawnControlRotation = true; // Create a mesh component that will be used when being viewed from a '1st person' view (when controlling this pawn) Mesh1P = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("CharacterMesh1P")); Mesh1P->SetOnlyOwnerSee(false); Mesh1P->SetupAttachment(FirstPersonCameraComponent); Mesh1P->bCastDynamicShadow = false; Mesh1P->CastShadow = false; Mesh1P->RelativeRotation = FRotator(1.9f, -19.19f, 5.2f); Mesh1P->RelativeLocation = FVector(-0.5f, -4.4f, -155.7f); // Create a gun mesh component FP_Gun = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("FP_Gun")); FP_Gun->SetOnlyOwnerSee(false); // only the owning player will see this mesh FP_Gun->bCastDynamicShadow = false; FP_Gun->CastShadow = false; // FP_Gun->SetupAttachment(Mesh1P, TEXT("GripPoint")); FP_MuzzleLocation = CreateDefaultSubobject<USceneComponent>(TEXT("MuzzleLocation")); FP_MuzzleLocation->SetupAttachment(FP_Gun); FP_MuzzleLocation->SetRelativeLocation(FVector(0.2f, 48.4f, -10.6f)); // Default offset from the character location for projectiles to spawn GunOffset = FVector(100.0f, 30.0f, 10.0f); //Create the spring arm comp ThirdPersonSpringArmComp = CreateDefaultSubobject<USpringArmComponent>(FName("ThirdPersonSpringArmComp")); //Attach it to the character's root component ThirdPersonSpringArmComp->SetupAttachment(GetRootComponent()); //Create the third person camera comp ThirdPersonCameraComp = CreateDefaultSubobject<UCameraComponent>(FName("ThirdPersonCameraComp")); //Attach it our spring arm ThirdPersonCameraComp->SetupAttachment(ThirdPersonSpringArmComp); // Note: The ProjectileClass and the skeletal mesh/anim blueprints for Mesh1P are set in the // derived blueprint asset named MyCharacter (to avoid direct content references in C++) } |
Then in order to switch from the first person to third person camera, declare the following function in the header file of your character:
1 2 3 |
private: /*De-activates the default camera and activates the third person camera*/ void ActivateThirdPersonCamera(); |
In your character’s source file type in the following implementation of the above function:
1 2 3 4 5 6 7 8 |
void ASELethalShotCharacter::ActivateThirdPersonCamera() { //Deactivates the first person camera FirstPersonCameraComponent->Deactivate(); //Activates the third person camera ThirdPersonCameraComp->Activate(); } |
When you’re done with that, right after the Time Dilation on the OnFire() function, call the above function. Here is the complete OnFire function at this step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
void ASELethalShotCharacter::OnFire() { // try and fire a projectile if (ProjectileClass != NULL) { const FRotator SpawnRotation = GetControlRotation(); // MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset); UWorld* const World = GetWorld(); if (World != NULL) { // spawn the projectile at the muzzle World->SpawnActor<ASELethalShotProjectile>(ProjectileClass, SpawnLocation, SpawnRotation); //Dilate the time UGameplayStatics::SetGlobalTimeDilation(World, TimeDilationMultiplier); //Change the activate camera ActivateThirdPersonCamera(); } } // try and play the sound if specified if (FireSound != NULL) { UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation()); } // try and play a firing animation if specified if (FireAnimation != NULL) { // Get the animation object for the arms mesh UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance(); if (AnimInstance != NULL) { AnimInstance->Montage_Play(FireAnimation, 1.f); } } } |
Save and compile your code. Then, open up the Character’s Blueprint and assign the following options in your SpringArmComponent:
In case you’re wondering how I came up with these values it was just by trial and error.
At this point, the active camera will change when the character fires his weapon. However you won’t be able to see the fired projectile just yet. This is because the projectile that comes with the First Person C++ Template contains an initial speed. This means that by the time you switch the active camera the projectile will have moved “a bit” forward.
Step 3. Transition from a Third Person Camera to the camera that is attached to the fired projectile
Before we modify the projectile class that comes with the demo to suits our needs, we need to inform the character’s controller that in case we attempt to set a new view target, the controller will search the new target’s hierarchy until it finds a valid camera. Then, it automatically activates that camera.
To do that, inside the BeginPlay function in your character’s source file type in the following code:
GetController()->bFindCameraComponentWhenViewTarget = true;Here is my complete BeginPlay function at this point:
1 2 3 4 5 6 7 8 9 10 |
void ASELethalShotCharacter::BeginPlay() { // Call the base class Super::BeginPlay(); FP_Gun->AttachToComponent(Mesh1P, FAttachmentTransformRules(EAttachmentRule::SnapToTarget, true), TEXT("GripPoint")); //Attach gun mesh component to Skeleton, doing it here because the skelton is not yet created in the constructor //Tells the controller to search for an owned camera componenet to view through when used as a view target GetController()->bFindCameraComponentWhenViewTarget = true; } |
We’re now ready to modify the projectile class. Specifically we need to:
- Create a Spring Arm Component attached to the root component.
- Create a Camera Component attached to the aforementioned component.
- Since the World’s time is dilated we need to apply a velocity multiplier in order to adjust the movement speed of our projectile. This is done because we need to find a “sweetspot”, meaning that the projectiles should not travel too fast or too slow either.
Let’s get started then. In your projectile’s header file, type in the following declarations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
protected: /*The velocity multiplier that is applied when we slow the world's time*/ UPROPERTY(EditAnywhere) float VelocityMultiplier = 5.f; UPROPERTY(VisibleAnywhere) UCameraComponent* ProjectileCameraComp; UPROPERTY(VisibleAnywhere) USpringArmComponent* ProjectileSpringArmComp; public: /** Applies the velocity multiplier to the existing velocity of the projectile */ void ApplyVelocityMultiplier(); |
Then, open up the source file of your projectile’s class and inside the constructor:
- Adjust the projectile’s max speed to 0.f
- Disable it’s bounce effect
- Adjust the initial life span to 0.f. This is done because we want the projectile to exist until it hits somethings. Please note that we’re not going to modify the OnHit function just yet!
Here is the needed code to apply all the menionted changes, plus the implementation of the ApplyVelocityMultiplier function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
void ASELethalShotProjectile::ApplyVelocityMultiplier() { ProjectileMovement->Velocity *= VelocityMultiplier; //We need to explicitely tell the projectile movement to update it's velocity after we adjust it's value ProjectileMovement->UpdateComponentVelocity(); } ASELethalShotProjectile::ASELethalShotProjectile() { // Use a sphere as a simple collision representation CollisionComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComp")); CollisionComp->InitSphereRadius(5.0f); CollisionComp->BodyInstance.SetCollisionProfileName("Projectile"); CollisionComp->OnComponentHit.AddDynamic(this, &ASELethalShotProjectile::OnHit); // set up a notification for when this component hits something blocking // Players can't walk on it CollisionComp->SetWalkableSlopeOverride(FWalkableSlopeOverride(WalkableSlope_Unwalkable, 0.f)); CollisionComp->CanCharacterStepUpOn = ECB_No; // Set as root component RootComponent = CollisionComp; // Use a ProjectileMovementComponent to govern this projectile's movement ProjectileMovement = CreateDefaultSubobject<UProjectileMovementComponent>(TEXT("ProjectileComp")); ProjectileMovement->UpdatedComponent = CollisionComp; ProjectileMovement->InitialSpeed = 5000.f; //Don't clamp the max speed of the projectile ProjectileMovement->MaxSpeed = 0.f; ProjectileMovement->bRotationFollowsVelocity = true; //Disable the bounce effect on projectiles ProjectileMovement->bShouldBounce = false; //We want this projectile to exist until it hits something so don't set an initial life span InitialLifeSpan = 0.f; //Create the spring arm component ProjectileSpringArmComp = CreateDefaultSubobject<USpringArmComponent>(FName("ProjectileSpringArmComp")); //Attach it to our root component ProjectileSpringArmComp->SetupAttachment(GetRootComponent()); //Create the camera component ProjectileCameraComp = CreateDefaultSubobject<UCameraComponent>(FName("ProjectileCameraComp")); //Attach it to our spring arm component ProjectileCameraComp->SetupAttachment(ProjectileSpringArmComp); } |
Save and compile your code. Then, assign the following values to the projectile’s spring arm component:
At this point, we need to modify our character’s class again in order to transition from it’s third person camera to the camera that is attached to the fired projectile. Before we do that, make sure to include the projectile’s header file right before the .generated.h file. Then, type in the following declarations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private: /*De-activates the third person camera and activates the camera on the given projectile*/ UFUNCTION() void ActivateProjectileCamera(ASELethalShotProjectile* Projectile); protected: /*The time in seconds that we will transition from the third person camera to the projectile's camera*/ UPROPERTY(EditAnywhere) float ThirdPersonCameraToProjectileCameraBlendTime = 0.05f; /*The delay in seconds that we will activate the projectile's camera*/ UPROPERTY(EditAnywhere) float ThirdPersonToProjectileTransitionDelay = 0.005f; |
Then, open up your character’s source file and type in the following implementation for the ActivateProjectileCamera function:
1 2 3 4 5 |
void ASELethalShotCharacter::ActivateProjectileCamera(ASELethalShotProjectile* Projectile) { //Change the active camera based on the assigned blend time from within our editor Cast<APlayerController>(GetController())->SetViewTargetWithBlend(Projectile, ThirdPersonCameraToProjectileCameraBlendTime); } |
When you’re done with that, navigate to the OnFire function which resides inside the character’s source file and:
- Grab a reference of the spawned projectile
- Execute the ActivateProjectileCamera function after the ThirdPersonToProjectileTransitionDelay seconds.
To execute a function after a specified time in UE4 we’re going to use a timer. Here is my OnFire function at this point (which implements the mentioned steps):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
void ASELethalShotCharacter::OnFire() { // try and fire a projectile if (ProjectileClass != NULL) { const FRotator SpawnRotation = GetControlRotation(); // MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset); UWorld* const World = GetWorld(); if (World != NULL) { // spawn the projectile at the muzzle ASELethalShotProjectile* SpawnedProjectile = World->SpawnActor<ASELethalShotProjectile>(ProjectileClass, SpawnLocation, SpawnRotation); //Dilate the time UGameplayStatics::SetGlobalTimeDilation(World, TimeDilationMultiplier); //Change the activate camera ActivateThirdPersonCamera(); //Create a timer handle and a timer delegate FTimerHandle TimerHandle; FTimerDelegate TimerDel; //Assign the corresponding UFUNCTION to the timer delegate TimerDel.BindUFunction(this, FName("ActivateProjectileCamera"), SpawnedProjectile); //Fire the delegate after she specified delay World->GetTimerManager().SetTimer(TimerHandle, TimerDel, ThirdPersonToProjectileTransitionDelay, false); } } // try and play the sound if specified if (FireSound != NULL) { UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation()); } // try and play a firing animation if specified if (FireAnimation != NULL) { // Get the animation object for the arms mesh UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance(); if (AnimInstance != NULL) { AnimInstance->Montage_Play(FireAnimation, 1.f); } } } |
At this point, if we fire a projectile, the cameras will get activated with the following order:
- Third Person Camera
- Projectile Camera
Let’s continue on Step 4.
When the projectile is within a specified range from the enemy character we transition from the projectile’s camera to a camera that is attached to the enemy character
Step 4. Transition from the projectile’s camera to a camera attached to an enemy character
In order to complete this step we’re going to divide it into two sub steps:
- Create an Enemy Character
- Create the camera transition (this step includes 2 more sub-steps).
Step 4.1. Creating an Enemy Character
In this post, I’m going to use the skeletal mesh with the animations that reside in the Third Person Template Project. So, before we dive into our code, create the mentioned template project, and migrate those assets to your project. When you’re done with that, create a new C++ class which inherits from the Character class (in my case this class is named DummyEnemyCharacter). Then, we’re going to create a SpringArmComponent and a CameraComponent (just like in our projectile’s class) in order to achieve a camera transition.
Open up the header file of your DummyEnemyCharacter and add the following declarations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
protected: /*The blend time in seconds, from the projectile's camera to corresponding death camera*/ UPROPERTY(EditAnywhere) float DeathCameraBlendTime = 0.05f; UPROPERTY(VisibleAnywhere) USpringArmComponent* DeathSpringArmComp; UPROPERTY(VisibleAnywhere) UCameraComponent* DeathCameraComp; public: /*Transitions the active camera to the corresponding death camera*/ void EnableCameraTransition(); |
Then, type in the following implementations in the source file of the character:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void ADummyEnemyCharacter::EnableCameraTransition() { UGameplayStatics::GetPlayerController(GetWorld(), 0)->SetViewTargetWithBlend(this, DeathCameraBlendTime); } // Sets default values ADummyEnemyCharacter::ADummyEnemyCharacter() { // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; DeathSpringArmComp = CreateDefaultSubobject<USpringArmComponent>(FName("DeathSpringArmComp")); DeathSpringArmComp->SetupAttachment(GetRootComponent()); DeathCameraComp = CreateDefaultSubobject<UCameraComponent>(FName("DeathCameraComp")); DeathCameraComp->SetupAttachment(DeathSpringArmComp); } |
Save and compile your code. Then:
- Create a Blueprint based on the above class
- Assign the ThirdPerson Static Mesh from the Third Person Template project to the Mesh component
- Assign the Pre-Built Anim_BP to the above mesh
Steps 2 and 3 can be seen below:
Moreover, configure the following options for the enemy character’s spring arm component:
Step 4.2. Creating the transition from the projectile’s camera to the enemy’s camera
In order to create this transition, we need to know two things:
- The distance between the projectile and the enemy in order to create a smooth transition.
- That the projectile will hit an enemy. Up until now, we’re transition from the character’s camera to the projectile’s camera even if we don’t hit an enemy.
Step 4.2.1 Determining the distance between the projectile and the enemy
In case our spawned projectile hits an enemy, we’re going to store a reference to that enemy before the actual hit, so we can transition to his camera and see the hit result from that point of view. This reference will come from the Step 4.2.2 which is written below. For now we’re going to assume that we have a valid reference and focus on calling the camera transition.
To do that, go to your projectile’s header file and when you include the DummyEnemyCharacter.h (right before the .generated.h file), type in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private: /*The enemy that this projectile is going to kill*/ ADummyEnemyCharacter* EnemyToKill = nullptr; /*True when the transition from the projectile's camera to the enemy's camera has been activated*/ bool bActivatedTransition = false; protected: /*The distance threshold that the death camera transition will occur*/ UPROPERTY(EditAnywhere) float DeathCameraTransitionDistance = 300.f; public: virtual void Tick(float DeltaSeconds) override; /*Sets the enemy that is going to be killed*/ FORCEINLINE void SetEnemyToKill(ADummyEnemyCharacter* Enemy) { EnemyToKill = Enemy; } |
As you might notice, we need to override the Tick function (which isn’t overriden by default). This is happening because we need to check the distance between the EnemyToKill and the projectile in order to activate our camera transition. Please note that the Projectile provided in the First Person Template project does not tick by default. With that said, we need to tell the engine that we want our projectiles to call the tick function. To do that, open up the constructor of your projectile class and add the following line:
PrimaryActorTick.bCanEverTick = true;Once you have included the above line of code, here is the implementation of the tick function:
1 2 3 4 5 6 7 8 9 10 11 |
void ASELethalShotProjectile::Tick(float DeltaSeconds) { Super::Tick(DeltaSeconds); if (!bActivatedTransition && EnemyToKill && (EnemyToKill->GetActorLocation() - GetActorLocation()).Size() <= DeathCameraTransitionDistance) { //We're near the enemy, enable the camera transition EnemyToKill->EnableCameraTransition(); bActivatedTransition = true; } } |
Save and compile your code. Then, let’s move to the next step.
Step 4.2.2 Making sure that the spawned projectile hits an enemy
To make sure that the spawned projectile hits an enemy, we’re going to implement a line raycast. If we have a hit, we’re going to store a reference to our projectile. Before we implement the actual raycast, make sure to include the DummyEnemyCharacter.h file in your Character’s header files right before the .generated.h file. Then, type the following declarations:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private: /*Returns true if the projectile hits an enemy - false otherwise*/ bool HitsAnEnemy(ASELethalShotProjectile* Projectile, ADummyEnemyCharacter*& HitEnemy); /*Performs a raycast and returns the hit actor - if any*/ AActor* GetHitActor(ASELethalShotProjectile* Projectile); protected: /*The raycast length*/ UPROPERTY(EditAnywhere) float RaycastLength = 2000.f; |
Open up your character’s source file and type in the following implementations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
bool ASELethalShotCharacter::HitsAnEnemy(ASELethalShotProjectile* Projectile, ADummyEnemyCharacter*& HitEnemy) { HitEnemy = Cast<ADummyEnemyCharacter>(GetHitActor(Projectile)); return (HitEnemy) ? true : false; } AActor* ASELethalShotCharacter::GetHitActor(ASELethalShotProjectile* Projectile) { FHitResult HitResult; FVector StartLocation = Projectile->GetActorLocation(); FVector EndLocation = StartLocation + (Projectile->GetActorForwardVector() * RaycastLength); FCollisionQueryParams CollisionParams; //Ignore the character and the projectile CollisionParams.AddIgnoredActor(this); CollisionParams.AddIgnoredActor(Projectile); //Perform a raycast - search for pawns only GetWorld()->LineTraceSingleByChannel(HitResult, StartLocation, EndLocation, ECollisionChannel::ECC_Pawn, CollisionParams); return HitResult.GetActor(); } |
When you’re done with that, navigate to the OnFire function and make the following changes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
void ASELethalShotCharacter::OnFire() { // try and fire a projectile if (ProjectileClass != NULL) { const FRotator SpawnRotation = GetControlRotation(); // MuzzleOffset is in camera space, so transform it to world space before offsetting from the character location to find the final muzzle position const FVector SpawnLocation = ((FP_MuzzleLocation != nullptr) ? FP_MuzzleLocation->GetComponentLocation() : GetActorLocation()) + SpawnRotation.RotateVector(GunOffset); UWorld* const World = GetWorld(); if (World != NULL) { // spawn the projectile at the muzzle ASELethalShotProjectile* SpawnedProjectile = World->SpawnActor<ASELethalShotProjectile>(ProjectileClass, SpawnLocation, SpawnRotation); ADummyEnemyCharacter* EnemyToBeKilled = nullptr; //If the projectile is going to hit an enemy - dilate the time and assign the enemy that we're going to kill if (HitsAnEnemy(SpawnedProjectile, EnemyToBeKilled)) { SpawnedProjectile->SetEnemyToKill(EnemyToBeKilled); //Dilate the time UGameplayStatics::SetGlobalTimeDilation(World, TimeDilationMultiplier); //Change the activate camera ActivateThirdPersonCamera(); //Create a timer handle and a timer delegate FTimerHandle TimerHandle; FTimerDelegate TimerDel; //Assign the corresponding UFUNCTION to the timer delegate TimerDel.BindUFunction(this, FName("ActivateProjectileCamera"), SpawnedProjectile); //Fire the delegate after she specified delay World->GetTimerManager().SetTimer(TimerHandle, TimerDel, ThirdPersonToProjectileTransitionDelay, false); } } } // try and play the sound if specified if (FireSound != NULL) { UGameplayStatics::PlaySoundAtLocation(this, FireSound, GetActorLocation()); } // try and play a firing animation if specified if (FireAnimation != NULL) { // Get the animation object for the arms mesh UAnimInstance* AnimInstance = Mesh1P->GetAnimInstance(); if (AnimInstance != NULL) { AnimInstance->Montage_Play(FireAnimation, 1.f); } } } |
At this point, if you place a DummyEnemyCharacter blueprint in your level and shoot him every camera transition will work. However, the projectile will stop the moment it hits the enemy. Don’t worry about that, we’re going to fix this in Step 5 which is written below. Moreover, at this point, only the projectiles that are lethal slow the world’s time.
Step 5. Configuring a Death for the DummyEnemyCharacter and resetting the cameras
Open up the header file of your main character and declare the following function:
1 2 3 4 |
public: /*Enables the FirstPersonCamera again*/ void ResetActiveCamera(); |
Then, type in the following function:
1 2 3 4 5 6 7 8 9 |
void ASELethalShotCharacter::ResetActiveCamera() { //Activate the first person camera FirstPersonCameraComponent->Activate(); ThirdPersonCameraComp->Deactivate(); //Transition to the first person camera Cast<APlayerController>(GetController())->SetViewTarget(this); } |
One you’re done with that, open up the header file of your DummyEnemyCharacter add declare the following function and property:
1 2 3 4 5 6 7 8 9 10 |
protected: /*The delay the camera will be reset right after the death of the enemy*/ UPROPERTY(EditAnywhere) float CameraResetDelay = 1.f; public: /*Kills the character and activates the FirstPerson camera again*/ void Die(); |
Then, open up the source file of the DummyEnemyCharacter and when you include the main character’s header file, provide the following logic for the Die function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void ADummyEnemyCharacter::Die() { USkeletalMeshComponent* CharSM = GetMesh(); //Enable ragdoll physics for our dummy enemy (to simulate a dying effect) CharSM->SetSimulatePhysics(true); CharSM->SetAllBodiesSimulatePhysics(true); CharSM->WakeAllRigidBodies(); CharSM->SetCollisionEnabled(ECollisionEnabled::PhysicsOnly); //Reset the global time to default UGameplayStatics::SetGlobalTimeDilation(GetWorld(), 1.f); FTimerHandle TimerHandle; FTimerDelegate TimerDel; TimerDel.BindLambda([&]() { //Deactive the death camera DeathCameraComp->Deactivate(); ASELethalShotCharacter* MainChar = Cast<ASELethalShotCharacter>(UGameplayStatics::GetPlayerCharacter(GetWorld(), 0)); MainChar->ResetActiveCamera(); }); GetWorld()->GetTimerManager().SetTimer(TimerHandle, TimerDel, CameraResetDelay, false); } |
The last step, is to modify the OnHit function inside the projectile’s class in order to match our needs. To do that, locate that function and type in the following logic:
1 2 3 4 5 6 7 8 9 10 11 |
void ASELethalShotProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, FVector NormalImpulse, const FHitResult& Hit) { if ((OtherActor != NULL) && (OtherActor != this)) { if (OtherActor->IsA<ADummyEnemyCharacter>()) { Cast<ADummyEnemyCharacter>(OtherActor)->Die(); } Destroy(); } } |
Save and compile your code. You have re-created the cameras transition successfully!
This Post Has 0 Comments