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!