Rewind time with stacks. Rewind your character with the push of a button

After watching a video of Rewinder gameplay, I thought to myself, yes it can be covered with Stacks! If you haven’t seen my Stack tutorial maybe you can check it out? How to use Stacks. I won’t be covering what a stack is or how to make a stack class because the tutorial I linked tells you everything with more than 1000 words. Instead, I will show you how you can make a rewind function! This is a very interesting concept that I have never thought about. Instead, I gave a teleportation stack example that uses stacks to save teleported locations: Undo same place teleportation with clipboard using stacks. So the very first step Ill be doing in this tutorial is setting up the stack class. Again, my original tutorial tells you how to make it and all the methods it uses so I will just set up the class right now.

Finished Result:

1- Stack class
For simplicity’s sake, I will make the stack class on the client so It will be easier to connect to my button event. This is not a good idea for a real game for a few reasons but since this game will be 1 player only, it seems to make more sense to do this on the client. (in a 1 player game, you don’t really need to make a class and you can simply use a module script but I’m only using a 1 player game for this tutorial)

Stack = {}
Stack.__index = Stack
function Stack.new() return setmetatable({}, Stack) end

-- put a new object onto a stack
function Stack:push(input)
	self[#self+1] = input
end
-- take an object off a stack
function Stack:pop()
	assert(#self > 0, "Stack underflow")
	local output = self[#self]
	self[#self] = nil
	return output
end

function Stack:size()
	return #self
end

return Stack

This scenario is PERFECT for using stacks and is a great way to show off what it does.

Step 2-Saving CFrame
When you rewind a character, it is quite obvious that it is simply saving the CFrame of the character’s humanoid Root part. In our case, I will be using a simple way to constantly save the character’s frame. First, you want to create a new stack class using the method new. Next, you want to get the heartbeat of the game which runs per frame to get the character’s frame. Once you have the humanoidRootPart, you can simply PUSH into the stack the Cframe of the humanoidRootPart.

local player = game.Players.LocalPlayer
local stack = require(script.Parent.StackClass).new()
local character = player.Character or player.CharacterAdded:Wait()
game:GetService("RunService").Heartbeat:Connect(function(step)
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if humanoidRootPart then
		stack:push(humanoidRootPart.CFrame)
	end
end)

Step 3- A rewind button
In the video, the button was a keyboard button so I will do the same. The first thing I did was to create 2 events for input began and input ended. These events are part of the UserInputService. We need these 2 to determine if the key is being held down or not. In our functions, we are checking if the key is E on the keyboard. For each one, we will change our keyHeld variable respectively. If the variable is true, the loop will run until its not. That will be where we can start the rewind process.

local uis = game:GetService("UserInputService")
KeyHeld = false

function onKeyPress(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = true
		while KeyHeld do
			--do stuff
			wait()
		end
	end
end

function onKeyRelease(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = false
	end
end

uis.InputBegan:connect(onKeyPress)
uis.InputEnded:connect(onKeyRelease)

Step 4- Conditional for our event.
One problem here! When the key is being pressed, you want to make sure the character’s position isn’t being saved at that time. To solve this, we will add a conditional to our heartbeat event to stop pushing values when the key is being held! To do this, we will simply check if the variable is false. Altogether, our code is this:

local player = game.Players.LocalPlayer
local stack = require(script.Parent.StackClass).new()
local character = player.Character or player.CharacterAdded:Wait()
local KeyHeld = false

game:GetService("RunService").Heartbeat:Connect(function(step)
	if KeyHeld == false then
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if humanoidRootPart then
		stack:push(humanoidRootPart.CFrame)
		end
	end
end)

local uis = game:GetService("UserInputService")

function onKeyPress(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = true
		while KeyHeld do
			--do stuff
			wait()
		end
	end
end

function onKeyRelease(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = false
	end
end

uis.InputBegan:connect(onKeyPress)
uis.InputEnded:connect(onKeyRelease)

Step 5- Doing the rewind
The last step is to actually do the rewind action. How??? Well, we have everything else set up already so we only need to change things in the while loop. The first thing in the loop is pop from the stack. Remember this method also returns the popped value so this is perfect! The popped value is the last CFrame in the stack! Next, we can simply just set the humanoid root part’s cframe to that value!

		while KeyHeld do
			local lastCFrame = stack:pop()
			local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
			if humanoidRootPart and lastCFrame then
				humanoidRootPart.CFrame = lastCFrame
			end
			wait()
		end

Our code is currently:

local player = game.Players.LocalPlayer
local stack = require(script.Parent.StackClass).new()
local character = player.Character or player.CharacterAdded:Wait()
local KeyHeld = false

game:GetService("RunService").Heartbeat:Connect(function(step)
	if KeyHeld == false then
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if humanoidRootPart then
		stack:push(humanoidRootPart.CFrame)
		end
	end
end)

local uis = game:GetService("UserInputService")

local function onKeyPress(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = true
		while KeyHeld do
			local lastCFrame = stack:pop()
			local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
			if humanoidRootPart and lastCFrame then
				humanoidRootPart.CFrame = lastCFrame
			end
			wait()
		end
	end
end

local function onKeyRelease(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = false
	end
end

uis.InputBegan:connect(onKeyPress)
uis.InputEnded:connect(onKeyRelease)

Step 6- reverse the reverse?
If you were to test this, you would run into one problem: you can still move while reversing! Well there’s a solution to that. We simply need to stop the character from being able to move so lets disable their controls. PlayerModule is a module that contains a getControls method. This method creates a new class that lets you enable and disable. Let’s add this in our script!

local Controls = require(player.PlayerScripts.PlayerModule):GetControls()
Controls:Disable()

Our code is now:

local player = game.Players.LocalPlayer
local stack = require(script.Parent.StackClass).new()
local character = player.Character or player.CharacterAdded:Wait()
local KeyHeld = false
local Controls = require(player.PlayerScripts.PlayerModule):GetControls()

game:GetService("RunService").Heartbeat:Connect(function(step)
	if KeyHeld == false then
	local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
	if humanoidRootPart then
		stack:push(humanoidRootPart.CFrame)
		end
	end
end)

local uis = game:GetService("UserInputService")

local function onKeyPress(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		KeyHeld = true
		Controls:Disable()
		while KeyHeld do
			local lastCFrame = stack:pop()
			local humanoidRootPart = character:FindFirstChild("HumanoidRootPart")
			if humanoidRootPart and lastCFrame then
				humanoidRootPart.CFrame = lastCFrame
			end
			wait()
		end
	end
end

local function onKeyRelease(inputObject,gameProcessed)
	if inputObject.KeyCode == Enum.KeyCode.E then
		Controls:Enable()
		KeyHeld = false
	end
end

uis.InputBegan:connect(onKeyPress)
uis.InputEnded:connect(onKeyRelease)

TADA! This tutorial is now finished. If you would like the place file, feel free to get the uncopylocked game here: Rewind Devforum game example - Roblox

Side note
I completely forgot that physics still happens to the character so to fix this problem, you need to anchor the humanoid root part. In the uncopylocked game, I edited this appropriately so you can see what I did if you still don’t understant.

The Problem
The problem is very simple: as the stack expands, you will find yourself having difficulty handling the game’s performance. However, there are a few ways to solve this that can help prevent this from happening while following Stack principles. If you figured one of the methods you can help prevent this, leave a comment explaining your approach!

43 Likes

I love this! This is what i was looking for!

This is a fairly interesting concept, but I’d much rather use a doubly linked list over a stack for this, that way I can handle the data from both sides of the “list” in an efficient manner.

Although you can use arrays for stacks, I personally prefer using a sequential data structure, though arrays/dynamic arrays can also be more efficient depending on your use case.

I’m curious as to the methods you thought of to solve this issue, can you elaborate?

1 Like

To manage memory use you have to define the amount of time you want to save and use a circular buffer.

9 Likes

Could you elaborate on how the Circular Buffer would be applied to this solution? From what I’ve understood, In a sense, you circulate the data in the stack (that has a length of ‘X’) & then dispose of that data soon as it reaches the end?

Is it possible for it to affect objects also?

In this case, you don’t need to need to get both sides of the list. There is no need for traversing in a reverse way. This is simply redoing what you did before.

I don’t understand what you mean by

I personally prefer using a sequential data structure

A stack is a linear data structure.

I’m curious as to the methods you thought of to solve this issue, can you elaborate?

I meant methods like resetting on death, only pushing when the character is moving, add a maximum stack size, or adding a longer delay between pushing CFrame, but if you want to find a technical solution, go ahead like with what loleris has shown. Although it goes against a stack class, it removes the last few from the array when the array has reached the maximum size that you give it allowing you to continue with only a set amount of values.

yes its the same for objects. For example, if you wanted to reverse the entire workspace
not a good idea
you would make a dictionary with each key as the object and the value as the cframe

local table = {}

for _, v in ipairs(workspace:GetDescendants()) do
  if v:IsA("BasePart") then
    table[v] = v.CFrame
  end
end

stack.push(table)

But if you’re only controlling one object then you don’t need a table at all

1 Like

To create an array that represents a circular buffer, you simply do table.create(seconds * 60, CFrame.new()).

You should not use table.remove or change the length of the array in any way during runtime as that could cause a tremendous performance impact.

In a circular buffer you simply have a “head” (like a head on a vinyl record disk) value that holds the index of the value you’re going to overwrite with the next latest value of your position. You increment head by 1 every frame until you reach the lenght value of the array and then you reset head back to 1 and start overwriting values from the beginning of the buffer areay.

To read past data you simply go in the opposite direction of where the head was going - when you reach 1 while reading you hop your reader head to the end (length) of the circular buffer array and continue going back until where the writer head is at - that’s where your past data ends.

Circular buffers are ideal for embedded systems where memory is very limited but lots of past recent data needs to be pulled up periodically. I do use circular buffers myself for my anti-exploit systems where a history of past positions is necessary to make estimates.

5 Likes

There is, in fact, a need to get both sides of the list – memory management. You have a problem which requires a solution, thus imposing a need.

Arrays allow for random access.
LinkedLists allow for sequential access.

Arrays allow you to remove elements from the head in O(n) time.
LinkedLists allow you to remove elements from the head in O(1) time.

Both are linear data structures. One is a sequential data structure.

Stacks are O(1) you only move in one direction but I get what you mean.

Very nice :+1: i like it! I used to try to make things like that but never succeeded