Now lets use what you’ve learned to create a spiral stair challenge with steps that disappear at timed intervals. We’ll use CFrames to build the spiral stairs. Then use a loop to hide the steps at some interval.
We can break the the steps down to series of blocks arranged in a circle and rotated based on their angle in the circle.
Then each subsequent step is raise by a set amount and the depth the each step should be deep enough so that there is not a gap at the outer edges.
Next, we need a way to hide the steps and then unhide them. Or more accurately, hide them for some amount of time and then unhide them for some other amount.
We won’t hide them randomly, however. You might set that as a challenge once you’ve built it. Instead, we will create a cascade of hidden stairs that travel up the stairs to the top.
So this means we’ll use an array to iterate through a list of parts and hide/unhide them at the correct time.
Let’s start with the circle. We learned earlier that we could create a rotated CFrame and then multiply that with another CFrame to arrive at a final CFrame with an offset and orientation.
So if we create a rotated CFrame and multiply it with a CFrame that is shifted n studs along the X axis, that will give us a CFrame oriented in the correct direction at radius n.
local part = Instance.new("Part")
part.CFrame = CFrame.Angles(0, math.rad(30), 0) * CFrame.new(10, 0, 0)
part.Parent = game.Workspace
But that’s just for a single part and it would be tedious if we had to manually script each part and input each angle. So let’s use a loop to do it for us.
To create m number of blocks per a circle, the circle needs to be divided into m sections. More accurately, 360 degrees divided by m sections.
local folder = Instance.new("Folder")
folder.Parent = game.Workspace
local numberOfSteps = 36
local degreeIncrement = 360 / numberOfSteps
for i = 1, numberOfSteps do
local part = Instance.new("Part")
part.CFrame = CFrame.Angles(0, math.rad(i * degreeIncrement), 0) * CFrame.new(10, 0, 0)
part.Size = Vector3.new(4, 1, 2)
part.Parent = folder
end
From numberOfSteps we can calculate the angle between each step. Then multiplying that by the iterator get us the correct angle for each CFrame.
Now we need to figure out the dimensions for each step.
In the example, dimensions are “hardcoded.” This means that if we wanted to change the dimensions of the spiral or the steps in the future, we would have to go in and figure out the dimensions again.
What we need is a way to automatically calculate those dimensions.
We could set the dimensions based on the radius of the spiral. But that is also hardcoded in. And because the radius is measured to the center of the step, this will leave a gap at the outer edge between steps. For a wider spiral, that gap gets exacerbated.
Let’s fix this.
Instead of directly setting radius, we’ll set a target minor diameter. By dividing the minor diameter by some target step depth, it gives us the required number of steps per rotation. And then dividing that by the major circumference gives the required full depth of each step.
local folder = Instance.new("Folder")
folder.Parent = game.Workspace
local stepDepthTarget = 2
local stepWidth = 10
local minorDiameter = 45
local radius = minorDiameter / 2-- + stepWidth / 2
local minorCircumference = math.pi * minorDiameter --circumference=2*pi*r
local majorCircumference = math.pi * (minorDiameter + stepWidth)
local stepsPerCircle = minorCircumference / stepDepthTarget
local angleIncrement = 360 / stepsPerCircle
local stepDepth = majorCircumference / stepsPerCircle
local stepSize = Vector3.new(stepWidth, 1, stepDepth)
for i = 1, stepsPerCircle do
local part = Instance.new("Part")
part.CFrame = CFrame.Angles(0, math.rad(i * angleIncrement), 0) * CFrame.new(radius, 0, 0)
part.Size = stepSize
part.Parent = folder
end
The circle is missing the “0th” step but that’s okay. Subsequent rotations properly place it.
Height of each step is constant and rise per step is constant. So a step’s Y position is just it’s iterator times the step height.
...
local stepHeight = 1
...
...
...
local stepSize = Vector3.new(stepWidth, stepHeight, stepDepth)
for i = 1, stepsPerCircle do
...
part.CFrame = CFrame.Angles(0, math.rad(i * angleIncrement), 0) * CFrame.new(radius, stepHeight * i, 0)
...
...
end
This is usable but there are still some limitations.
It’s limited to one full rotation. Sure we can add more rotations but to get a desired height, we would need to know the number of rotations or partial rotations, which itself already depends on other dimensions.
Let’s modify it so that that number of steps is automatically calculated based on a target height.
If you have a target height and know the step height, then the number of steps to reach that height is given by (target height)/(step height). And that number can be used for the iterator max.
local folder = Instance.new("Folder")
folder.Parent = game.Workspace
local stepDepthTarget = 2
local stepWidth = 10
local stepHeight = 1
local targetHeight = 1000
local minorDiameter = 45
local radius = minorDiameter / 2-- + stepWidth / 2
local minorCircumference = math.pi * minorDiameter --circumference=2*pi*r
local majorCircumference = math.pi * (minorDiameter + stepWidth)
local stepsPerCircle = minorCircumference / stepDepthTarget
local angleIncrement = 360 / stepsPerCircle
local stepDepth = majorCircumference / stepsPerCircle
local stepSize = Vector3.new(stepWidth, stepHeight, stepDepth)
local numberOfSteps = targetHeight / stepHeight
for i = 1, numberOfSteps do
local part = Instance.new("Part")
part.CFrame = CFrame.Angles(0, math.rad(i * angleIncrement), 0) * CFrame.new(radius, stepHeight * i, 0)
part.Size = stepSize
part.Parent = folder
end
And if you did all of that correctly, you should now have the set of spiral stairs.
Now let’s work on the loop that will hide/unhide the stair steps.
The most barebones to do this is to simply create two variables that countdown how long a part has been hidden/shown and then toggle it when the time runs out.
local part = Instance.new("Part")
part.Anchored = true
part.Parent = game.Workspace
local hideTime = 1
local showTime = 2
local countdown = showTime
local lastTick = tick()
while true do
local currentTick = tick()
local et = currentTick - lastTick
countdown = countdown - et
if countdown <= 0 then
if part.CanCollide then
part.CanCollide = false
part.Transparency = 1.0
countdown = hideTime
else
part.CanCollide = true
part.Transparency = 0.0
countdown = showTime
end
end
lastTick = currentTick
task.wait()
end
‘tick‘ is a Roblox global function (just like print) that returns the amount of time elapsed since the “UNIX epoch time” of 00:00:00 on January 1st, 1970.
Don’t worry what that means, all that matters is that it gives us an accurate way to measure time. If you record one tick and then record another one second later, the time difference will be very close to 1 second.
In the example, tick one time around the loop minus the previous tick yields the elapsed time. This is then subtracted from the countdown timer. Once the timers reach zero, reset the time and toggle collision/visibility of the part.
The example works for a single part but obviously entirely inadequate for potentially hundreds or more of parts.
Variables are only suitable for addressing single objects so a table will likely be needed. Plus a for loop for iterating through an array of elements.
And because we need to track a large number of part and also timers for those parts, we’ll need two arrays.
One way you certainly could do this is to create two separate arrays, one for the parts and another for timers. Then just be sure the index of each part in the parts list corresponds with the same timer in the timer array.
This results in two tables we need to keep track of though. It’s trivial at the moment, but as a project gets bigger, this can get disorganized quickly. Ideally, we should keep this related data together.
Fortunately, we can implement that using a table.
Recall that an array can store nested arrays. If we create a two-element array that couples the part with its timer, we’ll only need to track one array and use the loop to access the elements of the other.
local part1 = Instance.new("Part")
local part2 = Instance.new("Part")
local part3 = Instance.new("Part")
part1.Anchored = true
part2.Anchored = true
part3.Anchored = true
part1.Position = Vector3.new(0, 1, 0)
part2.Position = Vector3.new(5, 1, 0)
part3.Position = Vector3.new(10, 1, 0)
part1.Parent = game.Workspace
part2.Parent = game.Workspace
part3.Parent = game.Workspace
local PART_INDEX = 1
local TIMER_INDEX = 2
local hideTime = 1
local showTime = 2
local partsList = {{part1, showTime}, {part2, showTime}, {part3, showTime}}
local lastTick = tick()
while true do
local currentTick = tick()
local et = currentTick - lastTick
for _, partAndTimer in ipairs(partsList) do
partAndTimer[TIMER_INDEX] = partAndTimer[TIMER_INDEX] - et
local timeLeft = partAndTimer[TIMER_INDEX]
local part = partAndTimer[PART_INDEX]
if timeLeft <= 0 then
if part.CanCollide then
part.CanCollide = false
part.Transparency = 1.0
partAndTimer[2] = hideTime
else
part.CanCollide = true
part.Transparency = 0.0
partAndTimer[TIMER_INDEX] = showTime
end
end
end
lastTick = currentTick
task.wait()
end
Three parts are created, each stored in their own nested array. The part resides at index 1 of that nested array, while the timer is index 2.
PART_INDEX and TIMER_INDEX represent the indices for the nested lists.
You might encounter variables written in this manner using all capital letters. This is a convention to indicate that these variables are intended to be constants—meaning they won’t change throughout their lifetime. Lua doesn’t technically have constant variables but this is consistent with coding convention only. It is up to you how you wish to style this.
During the for loop, the elapsed time is subtracted from each timer and once the timer has run out a branch statement toggles the visibility and resets it to the proper time.
The only problem now is that all the parts appear/disappear at the same time. We don’t want the full stairway to disappear, just a moving cascade of steps. If each timer is given some constant offset that is dependent on the index of its order, then the cascading effect can be achieved.
You can get the effect by modifying this line:
local partsList = {{part1, showTime}, {part2, showTime}, {part3, showTime}}
To:
local partsList = {{part1, showTime+0.2}, {part2, showTime+0.4}, {part3, showTime+0.6}}
Once you’ve added that, you now have everything you need to create the full script. Before viewing the full script, see if you can integrate the two.
I also added an ‘origin’ variable, which will allow the steps to placed at some location other than (0, 0, 0) and then cleaned up the code a bit.
local stepsList = {}
local folder = Instance.new("Folder")
folder.Parent = game.Workspace
local origin = Vector3.new()
local targetHeight = 1000
local stepDepthTarget = 2
local stepWidth = 10
local stepHeight = 1
local minorDiameter = 45
local radius = minorDiameter / 2
local showLength = 8
local hideLength = 1.5
local hideDelay = 0.075
local minorCircumference = math.pi * minorDiameter --circumference=2*pi*r
local majorCircumference = math.pi * (minorDiameter + stepWidth)
local stepsPerCircle = minorCircumference / stepDepthTarget
local angleIncrement = 360 / stepsPerCircle
local stepDepth = majorCircumference / stepsPerCircle
local stepSize = Vector3.new(stepWidth, stepHeight, stepDepth)
local numberOfSteps = targetHeight / stepHeight
for i = 1, numberOfSteps do
local part = Instance.new("Part")
part.CFrame = CFrame.Angles(0, math.rad(i * angleIncrement), 0) * CFrame.new(radius, stepHeight * i, 0) + origin
part.Size = stepSize
part.Anchored = true
part.BottomSurface = Enum.SurfaceType.Smooth
part.TopSurface = Enum.SurfaceType.Smooth
part.BrickColor = BrickColor.random()
part.Parent = folder
stepsList[i] = {part, i * hideDelay + showLength}
end
local lastTick = tick()
local PART_INDEX = 1
local TIMER_INDEX = 2
while true do
local et = tick() - lastTick
lastTick = tick()
for _, list in ipairs(stepsList) do
local timeLeft = list[TIMER_INDEX] - et
local part = list[PART_INDEX]
list[TIMER_INDEX] = timeLeft
if list[TIMER_INDEX] <= 0 then
part.CanCollide = not part.CanCollide
if part.CanCollide then
list[TIMER_INDEX] = showLength
part.Transparency = 0.0
else
list[TIMER_INDEX] = hideLength
part.Transparency = 1.0
end
end
end
task.wait()
end
You can modify the variables to see how each affect the stairs and game.
Challenge
As it currently is written, the steps toggle on/off at regular intervals. As a challenge, modify the code such that during the cascade, some parts remain visible so players can utilize unaffected steps to keep from falling back down.
-
- Expand Your Knowledge
- Tables and Arrays
- Key-Value Pairs
- The for Loop: Numeric
- The for Loop: Generic
- CFrames
- Game: Stairway to Heaven
- Functions
- Expand Your Knowledge