Implementing Instant Replays (Killcam)
In this article we’ll learn how to implement a killcam.
Instant replays (or Killcams) can help the player understand how their character died during a game. They can be very useful in competitive scenarios to visualize the position of the instigator at the moment of your death, so they will play an important role in your game design if you decide to bring them in.
I’ve always wondered how to implement them properly in Unreal Engine, however, the
DemoNetDriver documentation was scarce and duplicating the whole
UWorld, like Unreal Tournament did, never convinced me. For that reason I’ve put this on hold for many years until the present day.
A year ago (at the time of writing), a tutorial written by Alex Koumandarakis surfaced in the Unreal Engine forums presenting a Sample Code for Implementing Instant Replays accompanied by a small description. The tutorial explains how they approached this problem in Paragon, and the technique employed to avoid duplicating the
UWorld. This piece of information and code has been critical to develop this article, and for that reason, I highly recommend everyone to read Alex’s post before proceeding here.
To follow this tutorial you’ll need the following resources:
- Lyra (at least 5.2) (optional): Download through the Epic Games launcher. Lyra is optional, but this article employs it to implement the feature.
- Killcam sample code by Epic (download here). Add the header and the cpp file to your project. Ensure you change the
PlayerStatereferences you’ll find in the source code so it compiles.
- Your IDE, like Visual Studio and… some patience.
With all the materials ready, we can start the implementation!
Note: The implementation provided in this article only supports Dedicated Servers and isn’t production ready. The provided code isn’t totally reliable given the current state of Game Features (UE 5.2), as they require some engine extra work to differ between the replay and game world; I expect future versions of the engine to improve this aspect, since to date, it is required to do some overengineering to control their execution scope. However, if you don’t use Game Features, most of the concepts provided in this article will work out of the box.
If you’ve gone through Alex’s article, you’ve learned that we are going to use level duplication to achieve our instant replays. For that, we have to override
UEngine::Experimental_ShouldPreDuplicateMap to return true for the appropriate levels.
Note however, that this feature is still experimental and enabling it comes with a risk.
Adding the Killcam manager in the Local Player
A good place to put our instant replay manager is in the
ULocalPlayer, as replays are totally client sided. To do that, we can use Lyra’s
Which we can create in Lyra’s Local Player constructor:
Record instant replay
The next step is to start recording in every respawn, we can do that by hooking locally to the
OnPossessedPawnChanged delegate in
ALyraPlayerController. Since it will get called every time we posses a different Pawn:
The in-memory killcam replay representation can free data that’s not needed to rewind beyond
CVarKillcamBufferTimeInSeconds. This means that every time we call
SetUpKillcam, we’ll fill the replay buffer up to
CVarKillcamBufferTimeInSeconds seconds and it will advance in a circular buffer manner. This value shall be adjusted accordingly given the Local Player-end memory budget determined for your project.
SetPawn won’t yield the same results, as
SetPawn doesn’t provide a mechanism to differ against the previous Pawn.
Play and stop instant replay
In order to stop recording and play the rewound recording we have to call
UKillcamPlayback, but for that, we have to determine first what will trigger our local Instant Replays. In this specific case, we’ll play the KillCam after our Pawn dies. For that, we can extend
ClientHandleOutOfHealth is a Reliable Client RPC that triggers the KillCam replay by calling
ALyraPlayerController. The timer inside
StartKillcam stops the instant replay by calling
FinishKillcam, which also notifies the server that the killcam stopped.
FinishKillcam can also be mapped to an input action to skip the instant replay.
ServerRequestKillcamStop can be used to notify the server that we are ready to respawn, however, to simplify our setup, we will rely on the
RespawnDelayDuration variable defined in the
GA_AutoRespawn Blueprint Ability.
For preview purposes, initialize these variables to the following values:
RespawnDelayDuration: 10.f - This variable is used in
GA_AutoRespawnto determine the time between the character death and its respawn through
KillcamDuration: 8.f - This variable is employed in
ALyraPlayerController::StartKillcamand determines the display time of the instant replay.
If everything is setup corrently, after our death, we should see the instant replay for 8 seconds while the respawn is in coldown (10 seconds).
Experiences and Game Features
If you are using Lyra to implement the Killcam, you have to deal with Game Features and their execution scope, which isn’t trivially simple…
First, we have to prevent the Experience to load in the replay world, to do that we need to skip the code called in
OnExperienceLoadComplete. We also need to do the same for unloading part in
EndPlay. The best way I found for this is by checking against the
DynamicDuplicatedLevels collection, however note that there might be better ways to do this, if you find out a better one, please let me know!
Besides culling the feature activation, we might find other issues, like animations not playing after the first death cam. However, I’m going to leave those issues to the reader as it goes a bit beyond the scope of the article.
Setting up the ViewTarget
If we run what we have programmed so far, we will notice that the camera stays in our death position.
This is because our ViewTarget is still our Character, as it only gets destroyed when the Delay
RespawnDelayDuration expires. In my implementation, I’d like to set the ViewTarget to the Pawn that killed us, so we can see how we got killed. To do that, we can extend
In the previous snippet we are assigning the ViewTarget to the killing Actor from the replay world, this is done through
SetViewTargetToKillingActor. However, due to how the Camera Manager of Lyra works, we have also to set the proper Camera Mode, for this we will reuse the
DefaultCameraMode contained in
PawnData (a third person camera). Note that I created an accesor to the Camera Component (
GetCameraComponent) to set the Camera Mode.
Interpolating the KillCam
If we implemented everything as stated in the article, we will notice that the KillCam is jittery. This is because of the recording rate and update frequency of the replay Actors, to solve this we will modify Lyra’s Third Person Camera mode:
With this change we fix the jitter by interpolating rotation and location snapshots. Note that for the replay rotation we use
GetBaseAimRotation() as the
TargetPawn has no Controller.
If you did everything accordingly you should be seeing something like this:
Note that at this point it’s just a matter to tweak the provided implementation to instantiate and remove the Instant Replay at the desired moment. Take also a look at all the available parameters for the rewinding in
Today we learned how to integrate Instant Replays in Lyra.
Note that this toy example uses a very simple setup for preview purposes and could be drastically improved by evolving
GA_AutoRespawn into a more convoluted Gameplay Ability that handles AI and Player Pawns separately to introduce Killcam instantiation and skip logic (could be another separate Ability). Also, the implementation provided doesn’t support Pawn swapping, so if the Pawn that killed you didn’t exist at the beginning of the replay rewind (because it didn’t spawn yet) your Instant Replay camera will stay in your death Pawn ViewTarget. However, I leave these challenges to the reader, I am sure that by playing with the system you will get a better understanding of how it all works.
You may run into problems (ie:
ensure checks) implementing the instant replays, but I would like you to understand why they occur and why they make sense in the way Lyra is programmed. With that said, I want to warn the reader that the implementation provided in this article is not production ready, so please, don’t blindly rely on articles you find on the internet and do your own debugging and profiling.
As always, feel free to contact me (and follow me! hehe) if you find any issues with the article or have any questions about the whole topic.