Building Timestop: Freezing a Roblox Fight Convincingly
How the Timestop ability in Bard’s Fighting Arena works: a server-owned timestop state, a Glass mesh that freezes the screen, animations that ease to a stop, and a color grade.
Timestop is an ability in Bard’s Fighting Arena that freezes the fight around the player who uses it. Most of the effect is presentation: making a frozen moment feel frozen.
A freeze mesh in front of the camera
The freeze effect comes from a mesh with the Glass material, placed half a stud in front of the camera and sized to cover the whole view. It relies on an engine bug: looking through a Glass mesh “freezes” the screen, so everything behind it stops updating and holds perfectly still.
The code below keeps it filling the screen at any field of view or aspect ratio:
local FreezePart do
local Distance = 0.5
local FreezePartOffset = CFrame.new(0, 0, -Distance)
FreezePart = function(GarbageCollector)
local FrozenPart = Instance.fromExisting(script.FreezePart) do
local Viewport, Height = Camera.ViewportSize, 2*math.tan(math.rad(Camera.FieldOfView)/2)*Distance
FrozenPart.Size = Vector3.new(Height*(Viewport.X/Viewport.Y), Height, 0)
FrozenPart.CFrame = Camera.CFrame*FreezePartOffset
FrozenPart.Parent = workspace.Visuals
end
GarbageCollector[#GarbageCollector+1] = FrozenPart
end
end
The height comes from basic camera math: at distance d, a vertical field of view fov covers 2 · tan(fov / 2) · d studs. Multiplying by the viewport’s aspect ratio gives the width.
Easing animations to a stop
Cutting animations to speed zero looks like a bug. Instead, each playing track slows down along a cubic ease-out curve:
local SlowDownAnimation do
local Cubic, Out = Enum.EasingStyle.Cubic, Enum.EasingDirection.Out
SlowDownAnimation = function(AnimationTrack : AnimationTrack)
local Speed = math.floor(AnimationTrack.Speed*1000)/1000
local PreviousSpeed = Speed
local Elapsed = 0; repeat
local DeltaTime = task.wait()
local CurrentSpeed = Elapsed == 0 and Speed or math.floor(AnimationTrack.Speed*1000)/1000
if PreviousSpeed ~= CurrentSpeed then
break
end
Elapsed += DeltaTime
local NewSpeed = Speed*(1-TweenService:GetValue(Elapsed, Cubic, Out))
if NewSpeed <= 0.01 or AnimationTrack.TimePosition+AnimationTrack.Speed*DeltaTime*2 > AnimationTrack.Length then
AnimationTrack:AdjustSpeed(0)
break
else
AnimationTrack:AdjustSpeed(NewSpeed)
PreviousSpeed = math.floor(NewSpeed*1000)/1000
end
until not AnimationTrack.IsPlaying or AnimationTrack.Speed <= 0 do
if PreviousSpeed == math.floor(AnimationTrack.Speed*1000)/1000 then
AnimationTrack:AdjustSpeed(Speed)
end
end
end
end
It samples TweenService:GetValue without creating a tween. If something else changes the track’s speed partway through, the loop notices the mismatch and stops interfering.
The color grade
The whole scene shifts to full grayscale with raised contrast over half a second, then eases back over a quarter second:
local VolumeTInfo, VolumeProp = TweenInfo.new(0.25), {Volume = 0}
local ColorCorrectionTInfo, ColorCorrectionProp = TweenInfo.new(0.5, Enum.EasingStyle.Exponential), {Brightness = 0.1, Contrast = 1, Saturation = -1, TintColor = Color3.fromRGB(227, 227, 227)}
local ColorCorrectionTInfo2, ColorCorrectionProp2 = TweenInfo.new(0.25, Enum.EasingStyle.Sine), {Brightness = 0, Contrast = 0, Saturation = 0, TintColor = Color3.fromRGB(255, 255, 255)}
Keeping it in sync
Everything that matters about a timestop is decided on the server. When someone uses it, the server creates an ObjectValue on their character that describes the timestop, and schedules it to destroy itself when the duration runs out:
local IsTimestopped = Instance.new("ObjectValue") do
IsTimestopped.Name = "Timestopped"
IsTimestopped.Value = Character
if Timeskip then
IsTimestopped:SetAttribute("Timeskip", true)
end
IsTimestopped:SetAttribute("Duration", Length)
IsTimestopped:SetAttribute("CreatedTime", Globals.Time+(os.clock()-Globals.TimeElapsed))
IsTimestopped:SetAttribute("Resistant", true)
IsTimestopped:SetAttribute("GUID", HttpService:GenerateGUID(false))
IsTimestopped.Parent = Character.States
task.delay(Length or 2, game.Destroy, IsTimestopped)
end
Because it is a plain instance, replication delivers it to every client with no extra remote calls. Clients can read who stopped time, how long it lasts, and when it started. The GUID tells separate timestops apart, and destroying the instance is the single signal that time is moving again.
Who gets frozen
A bubble grows out from the user to the timestop’s range. Every frame, the server asks which hurtboxes are inside it:
local SpacialQuery = task.spawn(function()
while true do
local CurrentVictims = table.clone(TimestopProfiles)
for Index, Hurtbox in next, workspace:GetPartsInPart(TimestopBubble.PrimaryPart, Globals.HitboxOParams) do
local Attachment : Weld = Hurtbox:FindFirstChild("Weld")
local Parent = Attachment and Attachment.Part1
local Entity : Globals.CharacterType = Parent and Parent.Parent
if Entity and Entity.Parent == workspace.Entities and Entity ~= Character then
CurrentVictims[Entity] = nil
if TimestopProfiles[Entity] then
continue
end
StunVictim(TimestopProfiles, IsTimestopped, Entity)
end
end
for Victim, Profile in next, CurrentVictims do
UnstunVictim(TimestopProfiles, Victim, Profile)
end
table.clear(CurrentVictims); CurrentVictims = nil
task.wait()
end
end)
Anyone who enters the bubble is frozen, and anyone who leaves is released. Freezing a character on the server means:
- Stopping motion. A
BodyVelocitytakes over and eases the character’s velocity to zero over three quarters of a second, so a character frozen mid-jump hangs in the air instead of dropping. - Taking network ownership. The server takes ownership of player characters for the duration, so a frozen player’s own client cannot move them.
- Pausing everything alive about them. Animations, sounds, and particle emitters slow to zero. Each one’s original speed is saved in a
TimestopSpeedattribute, and new animations that start while frozen are caught and paused as well. - Locking actions. State folders like
TimestopFrozen,Stunned,NoMove, andNoRotatego on the character, so the rest of the combat system treats them as unable to act.
Releasing a character reverses each step using the saved speeds, then cleans up everything in one pass.
The TimestopFrozen state also reaches the client-side character sync from my Parallel Luau post. That loop skips frozen characters entirely, so their poses do not drift while time is stopped.