[WIP - Beta] Ultimate Replay 3.0 - Next generation state based replay system (Killcam, Ghost, +More)

Ultimate Replay 3.0 is currently in open beta with ongoing devlopment!
Follow this thread to receive news and progress updates.

8999035--1239595--Icon.png

Coming soon to the Unity asset store!
Releasing: 15th September 2023

Support / Suggestions | Discord | Documentation | Samples | Asset Store | Asset Store (Trial) | Ultimate Replay 2.0 (Previous Version)

Ultimate Replay 3.0 Open Beta Is Now Available!
Ultimate Replay 3.0 is now available as an open beta. You can find the latest version over on github (closed source): Ultimate Replay 3.0 - Beta

Ultimate Replay 3.0 Is Suitable For:
Killcam’s or Deathcam’s, Action or Instant Replays, Ghost Cars, Highlights Reel, Sports Montage, Rewind Time, Second Chance Gameplay, Plus many more…

Ultimate Replay 3.0 is the next iteration of our popular Ultimate Replay state based replay system, and aims to make in-game replays, killcams and ghost vehicles easier than ever. We have received some really good feedback and suggestions over the lifecycle of version 2.0, some which we could implement and some which would cause breaking changes. Now we are starting once again with a clean slate to support as many of these features and workflows as possible, while making ease of use a top priority.

If you are not familiar with our Ultimate Replay series, the asset is designed to be a complete replay system for your game to make it easy to add action replays, killcams, ghost cars and much more with minimal effort. It works by capturing a snapshot of the scene at regular intervals containing information such as object postion, rotation, plus other data specified by replay components. The replay system then uses that information to reconstruct a smooth replay using interpolation in the Unity scene which can be rendered by any active camera. For that reason, it is possible to view the replay from any angle and even fly the camera around while the replay is occuring. Also since the replays are rendered in realtime, the replay system can run on any render pipeline with any camera syetem, post effects etc.

Existing Users (Ultimate Replay 2.0)
If you are a previous user of Ultimate Replay 2.0, you may wonder about the future plans for version 2.0 and what will happen when version 3.0 releases. There is no set release date for version 3.0 for the moment, however when it does become available, existing Ultimate Replay 2.0 customers will be entitled to a heavily reduced upgrade price for version 3.0 which will be handled by Unity asset store upgrade system. The final upgrade price is yet to be confirmed, however typically it will be in the area of 25% of the full asset price as with our previous asset upgrades.
When version 3.0 does release, Ultimate Replay 2.0 will remain available on the asset store for long term support, and will continue to receive bug fixes in that time, but note that new feature will likley not be added. Also note that version 2.0 will be marked as legacy at that time and we would encourage users to update to version 3.0 if possible. Feel free to contact us if there are any question.

Features
Most existing features from Ultimate Replay 2.0 have been brought forward and either updated or improved in some way. However based on the great feedback we have received from existing users, we have also refectored many systems and started work on new features that have been requested. Here are some features that are in various states of completion:

  • Complete - Refactor main replay API to be simpler to use and more powerful.
  • Complete - Support for multiple simultaneous record and replay operations.
  • Complete - Highly optimized memory/file storage requirements (More that 2x improvement over the previous version in some cases).
  • Complete - Large improvements to runtime performance in terms of memory usage and CPU usage.
  • Complete - Massive improvements to replay state API and serialization speed to allow more data ond objects to be recorded on less performant devices.
  • Complete - (Highly requested) Support custom metadata stored with a replay. Includes common values like scene name and developer name by default, but can easily be expanded to support any custom data specific to your game.
  • Complete - Improvements to replay file streaming performance to avoid buffering issues on slower devices.
  • Complete - Add support for operations such as copying or appending to replay storage targets making it much easier to combine replays.
  • Complete - Redesign replay component editor inspectors for better usability and dispaly of information.
  • Complete - Offer more fine grain control when needed for updating the replay system manually. It is possible to manually update the replay system with a single call to ReplayManager.ReplayTick(float dt)
  • Complete - Support tokenising replay data to support annotateded output formats such as json which expects Key/Value pairs.
  • Complete - (Highly Requested) Support using interpolation when seeking. No more snapping to keyframes when dragging the slider, but now you get buttery smooth seeking.
  • Complete- Improved compression algorithm to support much smaller file sizes with minimal impact on performance.
  • Partially Completed - Single highly optimized replay component to record skeletal animation, IK or ragdoll. The previous version used a ReplayTansform on each bone which is fine, but does come with extra overhead per bone. The new system eliminates that additional overhead, and can also implement some further optimizations. Automatic setup support for humanoid and legacy rigs.
  • Completed - (Highly requested) Support streaming to and from json. Writing to json file is working but readin is currently WIP.
  • Completed - Introduce lifecycle providers for better pooling support, and to allow replay prefabs to be instantiated from resouces, asset bundles or other sources. The current prefab system in version 2.0 holds a reference to a prefab asset which is not ideal since it forces all prefabs to be loaded at startup, and also does not support loading from arbitrary locations such as resources, or addressables.
  • WIP - Support for combining multiple separate replays into a single replay montage such as a hilights reel for example.
  • Completed - Support async operations for potentially lengthy tasks such as copying storage targets.
  • Completed - Support for fully loading replay files upfront where streaming operations are not desirable.
  • WIP - Scripting reference, samples repo and user guide documentation.

Feedback
Missing a feature? Let us know what you would like to see worked on/added either though one of the support channles, or by posting in this thread.
Any feedback or criticismis welcome as it will help us build a better final product to power your games.

Showcase
Simple ghost vehicle example for a racing game:
8999035--1289112--GhostReplayGif.gif

Simple action replay montage for a racing game showing highlights of the lap when the race has finished:
8999035--1289115--ActionReplayGif.gif

3 Likes

More features have been completed since the last update, and we have also started working on the documentation and samples. Some basic code samples are already completed and are accessible on github samples repo, but they will be subject to change as development continues.

For now there has been progress on the following features:

  • Now Complete - Improved compression algorithm to support much smaller file sizes with minimal impact on performance. We have implemented a much more efficient segment compression algorighm for reducing the size of a series of replay snapshots in a segment. On top of that we have added support for block compression when streaming to and from file, and the file size results look very promising. More info on that in a future update.
  • Now Complete- Introduce lifecycle providers for better pooling support, and to allow replay prefabs to be instantiated from resouces, asset bundles or other sources. This feature is now complete and allows prefabs to be added by reference or by resource path by default. Both methods have built in support for pooling using UnityEngine.Pool implementation that can be enabled or disabled with a simple toggle (Enabled by default). It is also now very easy to create your own lifecycle provider if you wanted to use replay prefabs from an asset bundle for example. More examples for that will follow.
  • Further work on json file reading, although there is still much more to do until that is completed. Writing json files is working well and some example output files will be showcased in the near future.

Just another quick update about the progress made over the last couple of weeks.
More work has been completed on replay components for recording and replaying skeletal animations, ragdoll, IK, etc. Also some work on the main API and async operations has been completed.

  • Partially Complete - Single highly optimized replay component to record skeletal animation, IK or ragdoll. We have now finished work on support for fully recording generic rigs (Works for humanoid too but we are working on another dedicated component for humanoid rigs that can offer even better performance) with a highly optimized dedicated replay component. It is ideal for recording skeletal animation, ragdolls, IK and any other form of animation or effect that works by modifying the transforms of bones. Support for humanoid rigs is partially implemented and will be completed soon.
  • Completed - Support async operations for potentially lengthy tasks such as copying storage targets. Finished and it is now possible to stream memory storage targets to file at a later time without blocking the main thread.

More work to come soon and we will also be planning to release a beta version over the next few weeks for existing Ultimate Replay users to try out. More info will be posted here when that becomes available.

I am pleased to announce that Ultimate Replay 3.0 is now in open beta! :smile:
We decided to move forward with an open beta to collect a wider range of feedback to help make a better asset, so anyone is welcome to check out the beta over on github (closed source): Ultimate Replay 3.0 - Beta

Feedback, bug reports and suggestions are all welcome either in this thread, over on discord or by creating an issue on github.Any feedback is highly appreciated.

Disclaimer: Currently Ultimate Replay 3.0 is considered early access as features and bugs are being activley worked on.

Beta version 3.0.1p1 is now avialble to try out here. This version fixes some bug fixes, includes extra code comments in preparation for documentation, and samples repo has been updated with extra code examples. There is now also support for loading streams and files upfront (Default behaviour is to stream on demand via background thread, but is not always the best solution):

  • Completed - Support for fully loading replay files upfront where streaming operations are not desirable. It is now possible to fully load replay streams and files upfront during a loading screen for example to avoid possible buffering issues on slow devices. There are also async versions available that can load without blocking the main thread
1 Like

Looking forward to the full release! Keep up the great work :slight_smile:

Hey, thanks for the support :slight_smile:
Yes we are working hard to get a full release ready. Not too long now hopefully but I will keep posting updates as progress is made.

An exciting update this week!
The highly requested JSON file/stream support is now fully implemented and working so you can now save and load your replay files in plain text if required. Using the custom binary format (Default) is still recommended for most cases since a comparible JSON file is more than 20x larger (10kb vs 230kb for a simple test case - size could be reduced by not using formatted text (tab, new line, etc.)), but the option is now there and performance also looks very reasonable so far with no obvious issues for realtime streaming.
Just a little more testing required before it can be considered fully complete, but hopefully there should be another beta version with json support available by the end of the week.

  • Completed - (Highly requested) Support streaming to and from json. Writing to json file is working but reading is currently WIP. Completed pending further testing - Support for both reading and writing json replay files.

As an example you can find a json file created by Ultimate Replay 3.0 here to give an idea of the structure used. If you are interested it uses pretty much the same prinicples and structure as the binary format with 5 main sections, only in text form:

  • Fixed Size Header - Contains info about file contents, duration etc. (JSON format uses hex values for header so that they are fixed size and can be overwritten with correct values when the recording ends).
  • Replay Data - Contains all replay segments that have been recorded which contains the actual recorded data for replay objects in the form of snapshots at regular intervals (A segment is the built in chunking system which improves streaming performance and file size by using chunk compression - for binary format only).
  • Segment table - A lookup table that provides information about all segments that were recorded and their location in the replay file for quick access.
  • Persistent Data - Contains any information stored using the persistent data api, for example: information about newly spawned replay objects such as position and parent.
  • Metadata (New for v3.0) - Additional metadata that can be set by the user (Can also be expanded to support custom fields) to give more information about the replay such as a name, the scene used, or anything else that might be needed. Previously another acompanying file would need to be created to store such data which was far from ideal.

With that major step complete we are only left with a few more minor features to implement, bug fixes, testing and then finish work on the documentation in preparation for release. With that in mind I can now say that we will aim for an official release for around september, although that could change especially with the current state of asset store verification queue times. We will do our best though :slight_smile:

Beta version 3.0.2 is now available which adds the previously mentioned json read/write support for replay files and streams, as well as many other improvements and bug fixes. You can find the latest version here (closed source).

A source version is also available for existing Ultimate Replay 2.0 customers via discord or support email. You will just need your invoice number to gain access.

Any feedback is welcome :slight_smile:

Beta version 3.0.3 is now available which includes a number of bug fixes and improvements. You can try out the latest beta version here.

Looks like things are going well and we are on track to release in September, so I can now confirm that the release date for Ultimate Replay 3.0 onto the Unity asset store will be 15th September 2023. More updates to follow as further progress is made :slight_smile:

1 Like

Ultimate Replay open beta version 3.0.4 is now available for testing. This update includes the following :

  • Implement new ReplayAnimator with support for IK
  • Performance and allocation improvements.
  • Storage size improvements.
  • Support further performance improvements using unsafe code: Requires enabling unsafe code and defining ULTIMATEREPLAY_UNSAFE (source version only).
  • Further testing and work on demos/samples.
  • Bug fixes.

Try the beta version here (Source version available via discord for existing 2.0 users).

Still on track for the Sep 15th release :slight_smile:

Further progress has been made on demos showcasing some of the common use cases and how they can be implemented with Ultimate Replay 3.0. In particular we have completed demos for ghost vehicles for racing games or similar, as well as action replays which can apply to a wide variety of racing, sports or really any game to show gameplay highlights at the end of a round for example.

Ghost Vehicle:
We have remastered the ghost car demo from Ultimate Replay 2.0 and updated it to use the newer and easier to work with API’s. The result is a smooth and accurate ghost replay file implemented by attaching a few replay components and a little bit of basic coding (See source code below).

Demo Source Code

using System;
using System.IO;
using UltimateReplay.Storage;
using UnityEngine;
using UnityEngine.UI;

namespace UltimateReplay.Demo
{
    public class GhostCarReplay : MonoBehaviour
    {
        // Private
        private const string prefsKeyBestLap = "ultimatereplay.ghostcar.bestlap";
        private const string replayFileCurrent = "current.replay";
        private const string replayFileBest = "best.replay";

        private ReplayStorage recordStorage = null;
        private ReplayStorage playbackStorage = null;
        private ReplayRecordOperation recordOp = null;
        private ReplayPlaybackOperation playbackOp = null;

        private bool lapStarted = false;
        private float lapStartTime = 0;
        private float lapBestTime = -1f;

        // Public
        public Text timer;
        public Text bestTime;
        public ReplayObject playerCar;
        public ReplayObject ghostCar;

        // Methods
        [ContextMenu("Reset Best Lap Time")]
        public void ResetBestLapTime()
        {
            PlayerPrefs.SetFloat(prefsKeyBestLap, -1f);
        }

        public void Start()
        {
            // Load best time
            lapBestTime = PlayerPrefs.GetFloat(prefsKeyBestLap, -1f);
        }

        public void OnDestroy()
        {
            // Release record storage
            if (recordStorage != null)
                recordStorage.Dispose();

            // Release playback storage
            if (playbackStorage != null)
                playbackStorage.Dispose();           
        }

        public void Update()
        {
            if (lapStarted == true)
            {
                TimeSpan raceTime = TimeSpan.FromSeconds(Time.time - lapStartTime);
                timer.text = string.Format("{0:00}:{1:00}:{2:00}", raceTime.Minutes, raceTime.Seconds, raceTime.Milliseconds);
            }

            // Update best time
            if(lapBestTime >= 0f)
            {
                TimeSpan bestRaceTime = TimeSpan.FromSeconds(lapBestTime);
                bestTime.text = string.Format("Best: {0:00}:{1:00}:{2:00}", bestRaceTime.Minutes, bestRaceTime.Seconds, bestRaceTime.Milliseconds);
            }
            else
            {
                bestTime.text = "Best: --:--:---";
            }
        }

        public void OnTriggerEnter(Collider other)
        {
            // Make sure a car is triggering
            if (other.GetComponentInParent<CarController>() == null)
                return;

            bool betterLap = false;
            float currentLapTime = Time.time - lapStartTime;

            // Check for improved lap time
            if(lapStarted == true && (lapBestTime < 0f || currentLapTime < lapBestTime))
            {
                betterLap = true;
                lapBestTime = currentLapTime;

                // Save best lap
                PlayerPrefs.SetFloat(prefsKeyBestLap, lapBestTime);

                Debug.Log("New best lap: " + bestTime.text);
            }

            lapStartTime = Time.time;

            // Stop replaying ghost
            if(playbackOp != null)
            {
                playbackOp.StopPlayback();
                playbackOp = null;

                if (playbackStorage != null)
                {
                    playbackStorage.Dispose();
                    playbackStorage = null;
                }
            }

            // Stop recording player
            if(recordOp != null)
            {
                recordOp.StopRecording();
                recordOp = null;

                recordStorage.Dispose();
                recordStorage = null;

                // Check for better lap
                if(betterLap == true)
                {
                    if(File.Exists(replayFileBest) == true)
                        File.Delete(replayFileBest);

                    // Save new file
                    File.Move(replayFileCurrent, replayFileBest);
                }
            }

            // Start replaying ghost car if available
            if(lapBestTime >= 0f && File.Exists(replayFileBest) == true)
            {
                // Enable the ghost car
                ghostCar.gameObject.SetActive(true);

                // Clone identities
                ReplayObject.CloneReplayObjectIdentity(playerCar, ghostCar);

                // Load ghost replay
                playbackStorage = ReplayFileStorage.FromFile(replayFileBest);

                // Start replaying - Pass in the ghost replay scene since we only want to replay the ghost car
                playbackOp = ReplayManager.BeginPlayback(playbackStorage, playerCar, ghostCar);

                // Add end playback listener
                playbackOp.OnPlaybackEnd.AddListener(OnGhostPlaybackEnd);
            }

            // Create storage for current player lap
            recordStorage = ReplayFileStorage.FromFile(replayFileCurrent);

            // Start recording - Pass in the player replay scene since we only want to record the player car
            recordOp = ReplayManager.BeginRecording(recordStorage, playerCar);

            // Set lap started flag - player has crossed the line once
            lapStarted = true;
        }

        private void OnGhostPlaybackEnd()
        {
            // Hide ghost car
            ghostCar.gameObject.SetActive(false);

            // Cleanup playback
            playbackStorage.Dispose();
            playbackStorage = null;

            Debug.Log("Ghost car finished");
        }
    }
}

Action Replay:
For the action replay demo we have reused the driving scene to keep assets and overall package size to a minimum, but instead of a ghost replay you will see an action replay montage once the race is finished from a number of camera angles. All cameras rendered in realtime as the replay occurs, and most cameras are even moving with the car or rotating to track the car.

Demo Source Code

using System;
using UltimateReplay.StatePreparation;
using UltimateReplay.Storage;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;

namespace UltimateReplay.Demo
{
    public class CarActionReplay : MonoBehaviour
    {
        // Type
        /// <summary>
        /// Custom preparer that does not do any preparation to the replay objects.
        /// Required because we need to keep the colliders and rigid bodies enabled during playback so that triggers are detected to activate and deactivate replay cameras.
        /// </summary>
        private class CustomPreparer : IReplayPreparer
        {
            // Methods
            public void PrepareForPlayback(ReplayObject replayObject) { }
            public void PrepareForGameplay(ReplayObject replayObject) { }
        }

        // Private
        private ReplayStorage storage = null;
        private ReplayRecordOperation recordOp = null;
        private ReplayPlaybackOperation playbackOp = null;
        private bool lapStarted = false;
        private bool lapFinished = false;
        private float lapStartTime = 0;

        private Vector3 carStartPosition = Vector3.zero;
        private Quaternion carStartRotation = Quaternion.identity;

        // Public
        public Text timer;
        public GameObject uiRacing;
        public GameObject uiFinishedRace;
        public ReplayObject playerCar;
        public Camera chaseCamera;
        public TriggerCamera[] triggerCameras;
        public Camera[] carCameras;

        // Methods
        public void Start()
        {
            // Get car starting position
            carStartPosition = playerCar.transform.position;
            carStartRotation = playerCar.transform.rotation;

            // Add listeners
            foreach (TriggerCamera cam in triggerCameras)
                cam.OnTriggerStateChanged.AddListener(UpdateActionReplayCamera);
        }

        public void OnDestroy()
        {
            // Release the replay storage
            if(storage != null)
                storage.Dispose();
        }

        public void Update()
        {
            if (lapStarted == true && lapFinished == false)
            {
                TimeSpan raceTime = TimeSpan.FromSeconds(Time.time - lapStartTime);
                timer.text = string.Format("{0:00}:{1:00}:{2:00}", raceTime.Minutes, raceTime.Seconds, raceTime.Milliseconds);
            }

            // Check for restart
            if(lapFinished == true && Input.GetKeyDown(KeyCode.R) == true)
            {
                // Start racing again
                RestartRace();
            }
        }

        public void OnTriggerEnter(Collider other)
        {
            // Make sure a car is triggering
            if (other.GetComponentInParent<CarController>() == null)
                return;

            // Set finished flag
            if (lapStarted == true)
            {
                lapFinished = true;

                // Stop recording
                if (recordOp != null)
                {
                    recordOp.StopRecording();
                    recordOp = null;
                }

                // Start our action replay
                StartActionReplay();
            }
            else
            {
                lapStartTime = Time.time;
            }

            // Record the player car
            if(lapFinished == false)
            {
                // Create our storage
                storage = new ReplayMemoryStorage();

                // Start recording
                recordOp = ReplayManager.BeginRecording(storage, playerCar);
            }

            // Set lap started flag - player has crossed the line once
            lapStarted = true;
        }

        private void StartActionReplay()
        {
            // Start replay of player car
            playbackOp = ReplayManager.BeginPlayback(storage, playerCar, new CustomPreparer());

            // Disable main camera
            chaseCamera.gameObject.SetActive(false);

            // Switch UIs
            uiFinishedRace.SetActive(true);
            uiRacing.SetActive(false);

            // Activate replay cameras
            UpdateActionReplayCamera();
        }

        private void UpdateActionReplayCamera()
        {
            // Only update when the race has finished
            if (lapFinished == false)
                return;

            TriggerCamera activeCamera = null;

            // Check if any trigger cameras are available
            foreach (TriggerCamera triggerCam in triggerCameras)
            {
                triggerCam.cam.gameObject.SetActive(false);

                if (triggerCam.HasTriggerObjects == true)
                {
                    activeCamera = triggerCam;
                    triggerCam.cam.gameObject.SetActive(true);
                }
            }


            // Select random car camera
            int carCamIndex = (activeCamera == null) ? Random.Range(0, carCameras.Length) : -1;
           
            // Disable all car cameras
            for(int i = 0; i < carCameras.Length; i++)
            {
                carCameras[i].gameObject.SetActive(carCamIndex == i);
            }
        }

        private void RestartRace()
        {
            // Stop active replay
            if(playbackOp != null)
            {
                playbackOp.StopPlayback();
                playbackOp = null;
            }

            // Enable UI
            uiRacing.SetActive(true);
            uiFinishedRace.SetActive(false);

            // Disable all trigger cameras
            foreach (TriggerCamera triggerCam in triggerCameras)
                triggerCam.cam.gameObject.SetActive(false);

            // Disable all car cameras
            foreach(Camera cam in carCameras)
                cam.gameObject.SetActive(false);

            // Enable chase cam
            chaseCamera.gameObject.SetActive(true);

            // Reset state
            lapStarted = false;
            lapFinished = false;
            lapStartTime = Time.time;

            // Reset forces
            playerCar.GetComponent<Rigidbody>().velocity = Vector3.zero;
            playerCar.GetComponent<Rigidbody>().angularVelocity = Vector3.zero;

            timer.text = "00:00:000";

            // Reset player car
            playerCar.transform.position = carStartPosition;
            playerCar.transform.rotation = carStartRotation;
        }
    }
}
2 Likes

Ultimate Replay open beta version 3.0.6 is now available for testing. This update includes the following :

  • Optimize memory allocations during recording and playback.
  • Optimize some replay components to record less data.
  • Fix issue with json file streaming.
  • Fix issue with replay formatter id’s causing incompatibilities between versions.
  • Add API’s for converting replay streams to and from byte arrays.
  • Add extra async API’s for potentially slow calls.
  • Further work on demo scenes.
  • Work on user guide documentation in preparation for release.
  • Other minor bug fixes.

Try the beta version here (Source version available via discord for existing 2.0 users).

Ultimate Replay open beta version 3.0.8 is now available for testing. This update includes the following:

  • Fix an issue where record fps could be ignored in some cases.
  • Reduced allocations when reading json replay stream data.
  • Improved performance when reading json replay stream data.
  • Minor bug fixes.

Try the beta version here (Source version available via discord for existing 2.0 users).

Only 1 week to go now until official release :sunglasses:

Check out the official support and marketing thread here .

I am happy to anounce that Ultimate Replay 3.0 has now released on the Unity asset store :smile:.

Check it out on the asset store and grab a copy at 30% off as part of out initial release sale. Existing v2.0 users can also upgrade via the asset store for the upgrade price of just $10, simply login to see the discount!.

4 Likes

Hey, I guess you are talking about moving particle systems which are not replayed correctly by the default ReplayParticles component due to how it works?
In that case there is an additional ReplayParticlesV2 component available which should work ok with moving particle systems, although does not simulate 100 accuratley. It is not possible to add it via the tools menu though so you would need to AddComponent -> ReplayParticlesV2 to set that up. Hopefully it should give better results for this particular use case but let me know if not.

Hope that helps. Let me know if there is anything else.

Hi.
The Replay Transform component strangely updates the Replay Identity. For example, if you enter the editing mode of any Prefab and go back. As a result, the CloneReplayObjectIdentity function does not work correctly. Is this how it should be?

I think there is a problem with PlaybackDirection. This does not work in the test case.

The error that often occurs is very annoying: Objects are trying to be loaded during a domain backup. This is not allowed as it will lead to undefined behaviour! (Assets/Ultimate Replay 3.0/Scripts/Runtime/Replay Object.cs:771)