Hey,
Attached below is a video of the problem:
The “synchronous playback” is the playback right after the replay is recorded and the “asynchronous playback” is the playback retrieved from persistent storage (datastore, stringvalue, etc.)
In the video, the synchronous replay works as expected; but when the player clicks on the asynchronous replay button (the button that says play), it just skips to the end.
The script is based off of @boatbomber’s VPF Replay Module:
VPF Replay Module - Resources / Community Resources - Developer Forum | Roblox
Here is the script:
local ipairs,pairs = ipairs,pairs
local UIS = game:GetService("UserInputService")
local RS = game:GetService("RunService")
local Mouse = game:GetService("Players").LocalPlayer:GetMouse()
local https = game:GetService("HttpService")
local DEBUG = true --RS:IsStudio()
local Module = {}
local RenderStepped = RS.RenderStepped
local serializer = require(script.Parent.RBXSerialize)
local function splitString(str, sep)
local result = {}
local index = 1
while true do
local start, finish = string.find(str, sep, index)
if not start then
break
end
local part = string.sub(str, index, start - 1)
table.insert(result, part)
index = finish + 1
end
local part = string.sub(str, index)
table.insert(result, part)
return result
end
local function Serialize(frames)
return https:JSONEncode(frames)
end
local function Deserialize(data)
return https:JSONDecode(data)
end
local CleanChildren = {["Decal"] = true;["Texture"] = true;["SpecialMesh"] = true;["BlockMesh"] = true;}
local function CleanClone(Object,CharClone)
for i,v in pairs(game.ReplicatedStorage.Assets:GetDescendants()) do
if v:IsA("BasePart") then
if v.Name == Object.Name then
local CleanObject = v:Clone()
for _, c in ipairs(CleanObject:GetChildren()) do
if not CleanChildren[c.ClassName] then
c:Destroy()
end
end
CleanObject.Anchored = true
CleanObject.CanCollide = false
CleanObject.CanQuery = false
CleanObject.CanTouch = false
CleanObject.CFrame = CleanObject.CFrame+Vector3.new(150,-200,0)
CleanObject.Parent = CharClone
return CleanObject
end
end
end
end
function Module.new(Settings)
Settings = Settings or {}
local RecordingStopped = Instance.new("BindableEvent")
local RecordingStarted = Instance.new("BindableEvent")
local RegistrationCompleted = Instance.new("BindableEvent")
local FrameChanged = Instance.new("BindableEvent")
local Replay = {
-- States
Playing = false;
Recording = false;
Recorded = false;
-- Main
Registers = {};
StaticRegisters = {};
CloneIndex = {};
RegisteredObjects = {};
NameRegister = {};
Frames = {};
FrameTimes = {};
FrameCount = 0;
RecordingTime = 0;
LastSnapshotTick = 0;
FPSDelay = 1/(Settings.FPS or 30);
-- Objects
VPF = Instance.new("Folder");
-- Connections (written for reference and autofill)
RecordConnection = nil;
PlayConnection = nil;
-- Events
RecordingStarted = RecordingStarted.Event;
RecordingStopped = RecordingStopped.Event;
RegistrationCompleted = RegistrationCompleted.Event;
FrameChanged = FrameChanged.Event;
}
-- Setup
Replay.VPF.Name = "ReplayFolder"
Replay.VPF.Parent = workspace
-- Primary functions
function Replay:Register(Object, IgnoreDescendants)
task.defer(function() -- Run async due to possible yielding
if Replay.Recording then
warn("Cannot register new objects while recording is in progress")
return
end
if typeof(Object) ~= "Instance" then return end
if DEBUG then
print("Register:",Object)
end
table.insert(Replay.NameRegister,Object.Name)
local IsHumanoid = false
if Object:IsA("Model") and Object:FindFirstChildWhichIsA("Humanoid") then
IsHumanoid = true
if DEBUG then
print("Character detected:",Object)
end
while Object:FindFirstChildWhichIsA("BasePart") == nil do
task.wait(0.05) -- .CharacterAdded is fired before the character loads, so this wait is needed or the model has no basepart children yet
end
local CharClone = Instance.new("Model")
CharClone.Name = Object.Name
for _,Child in ipairs(Object:GetDescendants()) do
if Child:IsA("BasePart") then
if not Replay.RegisteredObjects[Child] then -- Avoid duplication
if DEBUG then
print(" Valid character part:",Child)
end
Replay.RegisteredObjects[Child] = true
local Clone = CleanClone(Child,CharClone)
Replay.CloneIndex[Child] = Clone
Replay.Registers[#Replay.Registers+1] = {Mirror = Clone; Original = Child;}
end
elseif Child:IsA("CharacterMesh") then
Child:Clone().Parent = CharClone
end
end
local Shirt,Pants,Tee = Object:FindFirstChildWhichIsA("Shirt"),Object:FindFirstChildWhichIsA("Pants"),Object:FindFirstChildWhichIsA("ShirtGraphic")
do -- Handles clothing in a `do end` just for easy code folding, not really about scope or anything
if Shirt then
if DEBUG then
print(" Shirt registered:")
end
local ShirtClone = Shirt:Clone()
ShirtClone.Parent = CharClone
Shirt.Changed:Connect(function(Prop)
ShirtClone[Prop] = Shirt[Prop]
end)
end
if Pants then
if DEBUG then
print(" Pants registered:")
end
local PantsClone = Pants:Clone()
PantsClone.Parent = CharClone
Pants.Changed:Connect(function(Prop)
PantsClone[Prop] = Pants[Prop]
end)
end
if Tee then
if DEBUG then
print(" Tee registered:")
end
local TeeClone = Tee:Clone()
TeeClone.Parent = CharClone
Tee.Changed:Connect(function(Prop)
TeeClone[Prop] = Tee[Prop]
end)
end
end
local StatelessHumanoid = Instance.new("Humanoid")
do -- Again, the `do end` block is just so I can fold the code
StatelessHumanoid.DisplayDistanceType = Enum.HumanoidDisplayDistanceType.None
for _, enum in next, Enum.HumanoidStateType:GetEnumItems() do
if (enum ~= Enum.HumanoidStateType.None) then
StatelessHumanoid:SetStateEnabled(enum, false)
end
end
StatelessHumanoid:SetStateEnabled(Enum.HumanoidStateType.RunningNoPhysics,true)
StatelessHumanoid.RigType = Object:FindFirstChildWhichIsA("Humanoid").RigType
end
StatelessHumanoid.Parent = CharClone
CharClone.Parent = Replay.VPF
end
if Object:IsA("BasePart") and Object.ClassName ~= "Terrain" and Object.Archivable then
if not Replay.RegisteredObjects[Object] then -- Avoid duplication
if DEBUG then
print(" Valid register")
end
Replay.RegisteredObjects[Object] = true
local Clone = CleanClone(Object)
Replay.CloneIndex[Object] = Clone
Clone.CFrame = Clone.CFrame+Vector3.new(150,-200,0)
Replay.Registers[#Replay.Registers+1] = {Mirror = Clone; Original = Object;}
Clone.Parent = Replay.VPF
end
end
if not IsHumanoid then
if not IgnoreDescendants then
for _,Child in ipairs(Object:GetChildren()) do
Replay:Register(Child)
end
end
end
RegistrationCompleted:Fire()
end)
end
function Replay:StartRecording(MaxRecordingTime)
-- Check if there is a previous recording, if yes, warn and exit the function
if Replay.Recorded then
warn("Cannot start recording until previous recording is cleared")
return
end
-- Check if recording is already in progress, if yes, warn and exit the function
if Replay.Recording then
warn("Cannot start recording since recording is already in progress")
return
end
-- Set the maximum recording time to 300000 milliseconds (5 minutes) if not provided
MaxRecordingTime = MaxRecordingTime or 300000
-- Print "Start Recording" if DEBUG is true (used for debugging purposes)
if DEBUG then
print("Start Recording")
end
-- Fire the RecordingStarted event to signal the start of recording
RecordingStarted:Fire()
-- Set the Recording flag to true, indicating that recording is in progress
Replay.Recording = true
-- Create a connection to the RenderStepped event to capture frame data
Replay.RecordConnection = RenderStepped:Connect(function(DeltaTime)
-- If the recording time exceeds the maximum recording time, stop recording
if Replay.RecordingTime + DeltaTime > MaxRecordingTime then
Replay:StopRecording()
return
end
-- Update the recording time with the time since the last frame
Replay.RecordingTime = Replay.RecordingTime + DeltaTime
-- Calculate the time difference between the current frame and the last snapshot
local FrameDelta = tick() - Replay.LastSnapshotTick
-- Check if enough time has elapsed to take a new snapshot
if FrameDelta >= Replay.FPSDelay then
if DEBUG then
print("Snapshotting")
end
-- Increment the frame count
Replay.FrameCount = Replay.FrameCount + 1
-- Update the last snapshot tick to the current tick
Replay.LastSnapshotTick = tick()
-- Store the recording time for this frame
Replay.FrameTimes[Replay.FrameCount] = Replay.RecordingTime
-- Create a table to store the data of each registered object for this frame
local ObjectData = {}
-- Loop through the registered objects and store their data
for _, Object in ipairs(Replay.Registers) do
local mir, og = Object.Mirror, Object.Original
if og and og:IsDescendantOf(game) then
if og.ClassName == "ParticleEmitter" then
-- For ParticleEmitter, we currently do nothing (can be customized)
else
-- Store the CFrame, Color, and Transparency of the object for this frame
ObjectData[Object.Mirror] = {
["CFrame"] = serializer.Encode(og.CFrame);
["Color"] = serializer.Encode(og.Color);
["Transparency"] = og.Transparency;
}
end
else
-- If the object is not present in the game, mark it as destroyed
ObjectData[Object.Mirror] = {
["Destroyed"] = true;
}
end
end
-- Set the frame length for the previous frame (the time between snapshots)
if Replay.Frames[Replay.FrameCount - 1] then
Replay.Frames[Replay.FrameCount - 1].FrameLength = FrameDelta
end
-- Store the frame data for this frame
Replay.Frames[Replay.FrameCount] = {
ID = Replay.FrameCount;
Time = Replay.RecordingTime;
CameraCF = workspace.CurrentCamera.CFrame;
ObjectData = ObjectData;
}
end
end)
end
function Replay:StopRecording(calledByScript)
if Replay.Recorded then
warn("Cannot stop recording since it has already stopped")
return
end
if not Replay.Recording then
warn("Cannot stop recording since no recording is in progress")
return
end
if DEBUG then
print("Stop Recording")
end
Replay.RecordConnection:Disconnect()
Replay.Recording = false
Replay.Recorded = true
RecordingStopped:Fire()
local ObjectNames = {}
for _, Object in ipairs(Replay.NameRegister) do
table.insert(ObjectNames, Object)
end
local framesData = Serialize(ObjectNames).."UNIQUESEPERATOR"..Serialize(Replay.Frames)
script.Parent.ServerFrames.Value = tostring(framesData)
end
function Replay:ClearRecording()
if not Replay.Recorded then
warn("Cannot clear nonexistent recording")
return
end
if Replay.RecordConnection then
Replay.RecordConnection:Disconnect()
end
if Replay.PlayConnection then
Replay.PlayConnection:Disconnect()
end
Replay.FrameCount = 0
Replay.Frames = {}
Replay.FrameTimes = {}
Replay.RecordingTime = 0
Replay.Recorded = false
Replay.Recording = false
Replay.Playing = false
end
function Replay:Destroy()
RecordingStopped:Destroy()
RecordingStarted:Destroy()
FrameChanged:Destroy()
Replay.VPF:Destroy()
if Replay.RecordConnection then
Replay.RecordConnection:Disconnect()
end
if Replay.PlayConnection then
Replay.PlayConnection:Disconnect()
end
for _,Object in pairs(Replay.CloneIndex) do
Object:Destroy()
end
Replay = nil
end
local function FindFrame(f,t)
if not f then return end
t = t or 0
local FrameDepth = t-(f.Time or 0)
if (FrameDepth or 0) <= (f.FrameLength or 0) then
return Replay:GoToFrame(f.ID + (FrameDepth/(f.FrameLength or 0)))
else
return FindFrame(Replay.Frames[f.ID+1],t)
end
end
function Replay:Stop()
if not Replay.Playing then
warn("Cannot stop playback since playback isn't in progress")
return
end
Replay.Playing = false
Replay.PlayConnection:Disconnect()
end
function Replay:GoToPercent(Percent)
if not Replay.Recorded then
warn("Cannot go to percent since there is no recording")
return
end
if DEBUG then
print("GoToPercent:",Percent)
end
return Replay:GoToTime(Replay.RecordingTime*math.clamp(Percent,0,1))
end
function Replay:GoToTime(Time)
if not Replay.Recorded then
warn("Cannot go to time since there is no recording")
return
end
if DEBUG then
print("GoToTime:",Time)
end
-- Find frame
for f,t in ipairs(Replay.FrameTimes) do
if t == Time then
return Replay:GoToFrame(f)
else
local FrameLength = Replay.Frames[f].FrameLength or 0
if t + FrameLength >= Time then
return Replay:GoToFrame( (f)+ ((Time-t)/FrameLength) )
end
end
end
end
function Replay:GoToFrame(Frame)
if not Replay.Recorded then
warn("Cannot go to frame since there is no recording")
return
end
if DEBUG then
print("GoToFrame:",Frame)
end
Frame = math.clamp(Frame,1,Replay.FrameCount)
local StartFrameData = Replay.Frames[math.floor(Frame)]
local EndFrameData = Replay.Frames[math.ceil(Frame)]
local FrameDelta = Frame-math.floor(Frame)
local Time = StartFrameData.Time+(FrameDelta*(StartFrameData.FrameLength or 0))
FrameChanged:Fire(
Frame,
Time,
Time/Replay.RecordingTime
)
for Object, Data in pairs(StartFrameData.ObjectData) do
local NextData = EndFrameData.ObjectData[Object]
if Data.Destroyed then
Object.Transparency = 1
else
if NextData.Destroyed then
Object.CFrame = serializer.Decode(Data.CFrame)
Object.Color = serializer.Decode(Data.Color)
Object.Transparency = Data.Transparency
else
Object.CFrame = serializer.Decode(Data.CFrame):lerp(serializer.Decode(NextData.CFrame),FrameDelta)
Object.Color = serializer.Decode(Data.Color):lerp(serializer.Decode(NextData.Color),FrameDelta)
Object.Transparency = Data.Transparency + ((NextData.Transparency - Data.Transparency) * FrameDelta)
end
end
end
return StartFrameData
end
function Replay:Play(PlaySpeed,StartTime,Override)
Replay.RegisteredObjects = {}
if not Replay.Recorded then
warn("Cannot play nonexistent recording")
return
end
if Replay.Playing and not Override then
warn("Cannot play recording since playback is already in progress")
return
end
if Replay.PlayConnection then
Replay.PlayConnection:Disconnect()
end
PlaySpeed = math.clamp(PlaySpeed or 1,0.02,999)
Replay.Playing = true
print(Replay.RecordingTime)
local Timer = math.clamp(StartTime or 0,0,Replay.RecordingTime)
local Frame = Replay:GoToTime(Timer)
Replay.PlayConnection = RenderStepped:Connect(function(DeltaTime)
Timer = Timer+(DeltaTime*PlaySpeed)
Frame = FindFrame(Frame,Timer)
if Timer>Replay.RecordingTime or not Frame then
Replay:Stop()
script.Parent:WaitForChild("End"):Invoke()
print("end")
return
end
end)
end
function Replay:PlayReplayAsync(framesData)
if Replay then
-- Split the framesData into object names and frames data using the "UNIQUESEPERATOR"
local separatorIndex = "UNIQUESEPERATOR"
local objectNamesData = framesData:split(separatorIndex)[1]
local framesDataOnly = framesData:split(separatorIndex)[2]
-- Deserialize the object names and frames data
local objectNames = Deserialize(objectNamesData)
local frames = Deserialize(framesDataOnly)
-- Clear any previous registration before playing the replay
Replay:ClearRecording()
print(objectNames)
-- Register all the objects from the object names list
for _, name in ipairs(objectNames) do
print(name)
local object = game:GetService("Workspace"):FindFirstChild(name, true)
if object then
Replay:Register(object)
end
end
Replay.Recorded = true
Replay.Frames = frames
Replay.RecordingTime = Replay.Frames[#Replay.Frames].Time
wait(0.5)
Replay:Play(1)
end
end
return Replay
end
return Module