Skip to main content

Stage 3: Plank Walkway + low gravity

Course progressStage 3 of 10
~50 min
Before you start

Make sure you've finished Stage 2: Sphere Staircase + jump pads & comfort. Your VRController LocalScript should hold the climb code (Stage 1) and the comfort block (Stage 2), and a clean Play should show no errors.

Build

a narrow plank walkway over a gap, crossed under low gravity

Learn

that gravity is just a number you can change, and how to warn a VR player with touch

Ship

a floaty balance challenge whose controllers buzz when you step off the path

The big idea

Here's a secret that feels like a cheat code: gravity in Roblox is just a number. It lives at workspace.Gravity, normally 196.2. Turn it down and everything falls slowly — jumps float, mistakes are gentler, the whole world feels like the moon.

That's perfect for a plank walkway — a thin path over a drop. Normal gravity makes a narrow beam terrifying. Low gravity turns it into a slow, floaty balance challenge where a wobble gives you time to recover. You'll build the planks, then build two pads: one that turns gravity down as you step onto the walkway, and one that turns it back up when you reach the far side.

The VR half is about your other senses. On a screen you can see exactly where the plank ends. In a headset, looking down at a thin beam is disorienting. So we add haptics — a buzz in your controllers the instant you step off solid footing. Your hands feel the danger before your eyes sort it out. That's a real VR design idea: when sight is unreliable, talk to touch.

New words
workspace.Gravity
the global pull-down strength for the whole game; default 196.2
raycast
shooting an invisible line from a point in a direction to see what it hits first
RaycastParams
the settings for a raycast — like which objects to ignore (your own character)
HapticService
the service that vibrates controllers; how a VR game speaks to your hands
VibrationMotor
which motor to buzz — `LeftHand` and `RightHand` are the VR controllers

Build it

Step 1 — Build the plank walkway

Thin planks over a gap. Keep them narrow — the challenge is the width.

Build this part

Plank_1

Block
Open recipe
Size
3 × 1 × 10
Color
Brown
Material
Wood
Anchored
✓ Yes
Place
Just past the Stage 3 yellow pad, with a drop on both sides

The first Size number (3) is the width — keep it small. Length (10) runs along the path.

Build this part

Plank_2

Block
Open recipe
Size
3 × 1 × 10
Color
Brown
Material
Wood
Anchored
✓ Yes
Place
A small gap past Plank_1, in line with it
Build this part

Plank_3

Block
Open recipe
Size
3 × 1 × 10
Color
Brown
Material
Wood
Anchored
✓ Yes
Place
A small gap past Plank_2 — try offsetting it slightly left or right
Build this part

Plank_4

Block
Open recipe
Size
3 × 1 × 10
Color
Brown
Material
Wood
Anchored
✓ Yes
Place
The final plank, leading to where the next checkpoint will sit

Press ▶ Play and walk across at normal gravity. Notice how tense a thin plank feels. Now we make it floaty.

Step 2 — Build the low-gravity pads

Two pads. The first turns gravity down; the second turns it back up. You'll write both scripts from scratch.

2.1 The "gravity down" pad

Build this part

GravityDownPad

Block
Open recipe
Size
6 × 1 × 6
Color
Cyan
Material
Neon
Anchored
✓ Yes
Place
On solid ground right before Plank_1

Right-click GravityDownPadInsert ObjectScript (a server Script — gravity is the same for the whole world). Type:

local pad = script.Parent
local LOW_GRAVITY = 50 -- normal is 196.2; lower = floatier

pad.Touched:Connect(function(hit)
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if humanoid then
workspace.Gravity = LOW_GRAVITY
end
end)

2.2 The "gravity up" pad

Build this part

GravityUpPad

Block
Open recipe
Size
6 × 1 × 6
Color
Bright orange
Material
Neon
Anchored
✓ Yes
Place
On solid ground right after Plank_4

Insert a Script into GravityUpPad and type:

local pad = script.Parent
local NORMAL_GRAVITY = 196.2

pad.Touched:Connect(function(hit)
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if humanoid then
workspace.Gravity = NORMAL_GRAVITY
end
end)

Press ▶ Play. Step on the cyan pad — your next jump floats. Cross the planks slowly, then touch the orange pad to drop gravity back to normal. Tune LOW_GRAVITY until the float feels fun but still controllable. This whole mechanic is fully testable today, no headset needed.

Step 3 — Place the Stage 4 checkpoint

Same pattern as before:

  • Insert a SpawnLocation just past the orange pad. Size [6, 1, 6], anchored, a new color (try Lime green). Check AllowTeamChangeOnTouch, uncheck Neutral, set TeamColor to match.
  • Add a StageNumber attribute (number) = 4.
  • Add a Team named Stage 4, matching TeamColor, AutoAssignable unchecked.

Play, cross the floaty planks, touch the new pad, reset — you should respawn there.

Step 4 — Add the VR edge-warning buzz

Open VRController. First, make sure HapticService is required at the top of the file (add it if it isn't there yet):

local HapticService = game:GetService("HapticService")

Then paste this block at the bottom of VRController. It shoots a ray straight down each frame; if there's no plank under your feet, it buzzes both controllers.

-- ===== VR edge warning (added in Stage 3) =====
local FALL_CHECK = 12 -- studs of ground we expect beneath us
local warningOn = false

local function setBuzz(strength)
HapticService:SetMotor(Enum.UserInputType.Gamepad1, Enum.VibrationMotor.LeftHand, strength)
HapticService:SetMotor(Enum.UserInputType.Gamepad1, Enum.VibrationMotor.RightHand, strength)
end

RunService.RenderStepped:Connect(function()
if not VRService.VREnabled then return end
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if not root then return end

-- A ray pointing straight down, ignoring our own body.
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }

local result = workspace:Raycast(root.Position, Vector3.new(0, -FALL_CHECK, 0), params)

if not result and not warningOn then
warningOn = true -- nothing solid below: we stepped off
setBuzz(0.4)
elseif result and warningOn then
warningOn = false -- back on a plank: stop buzzing
setBuzz(0)
end
end)

Press ▶ Play on your laptop. Output stays clean and nothing buzzes — there's no headset to vibrate, and VREnabled is false. That clean run is today's pass. The buzz gets its first real test at the Stage 10 playtest.

In VR

Crossing the planks in a headset, the moment a foot drifts off the edge into open air, both controllers give a soft warning buzz — you feel "danger" in your hands before your eyes finish reading the drop. Step back onto wood and the buzz stops. It turns a scary look-down into a calm, guided balance.

Script anatomy

How the edge warning feels the floor with a ray

This is the same RenderStepped rhythm as the climb — check VR, read the world, react — but instead of moving the player it talks to their hands. It's a second connection in VRController, living happily beside the climb loop.

local FALL_CHECK = 12
local warningOn = false

local function setBuzz(strength)
HapticService:SetMotor(Enum.UserInputType.Gamepad1, Enum.VibrationMotor.LeftHand, strength)
HapticService:SetMotor(Enum.UserInputType.Gamepad1, Enum.VibrationMotor.RightHand, strength)
end

RunService.RenderStepped:Connect(function()
if not VRService.VREnabled then return end
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if not root then return end

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }

local result = workspace:Raycast(root.Position, Vector3.new(0, -FALL_CHECK, 0), params)

if not result and not warningOn then
warningOn = true
setBuzz(0.4)
elseif result and warningOn then
warningOn = false
setBuzz(0)
end
end)
  1. Lines 1–2How far down we look, and whether we're already warning.

    FALL_CHECK is how much solid ground we expect under our feet. warningOn remembers if the buzz is currently on, so we change the motors only when the situation flips — not 60 times a second.

  2. Lines 4–7One helper to buzz both hands.

    setBuzz takes a strength from 0 to 1 and sends it to both VR controllers. Wrapping the two SetMotor calls in one function keeps the loop below clean and means 'stop buzzing' is just setBuzz(0).

  3. Lines 16–18Aim the ray and ignore yourself.

    RaycastParams with FilterType Exclude tells the ray to skip our own character, otherwise it would hit our legs and always think there's ground. FilterDescendantsInstances is the list of things to ignore.

  4. Line 20Shoot the ray straight down.

    workspace:Raycast starts at the body, goes 12 studs down, and returns what it hits — or nil if it hits nothing. Over a plank you get a result; over the gap you get nil.

  5. Lines 22–27Buzz on the way off, silence on the way back.

    If the ray found nothing and we weren't already warning, start the buzz. If it found ground and we were warning, stop it. Comparing 'now' to warningOn is what keeps the motors from being spammed every frame.

Understand it

Low gravity works because workspace.Gravity is a single global number Roblox uses for every falling thing. Turning it down doesn't change your jump strength — it changes how slowly you come back down, so the same hop carries you much farther and gives you time to correct a wobble. We used two pads instead of one toggle because two one-way triggers are dead simple and reliable: enter → down, exit → up, no tricky "am I still inside?" bookkeeping. The trade-off is honest — because Gravity is global, in a real multiplayer game one player's low-gravity zone would affect everyone. (Fixing that per-player is the hard stretch.)

The edge warning shows what a raycast is really for: asking "what's directly over there?" We shoot a short ray down and read whether anything solid answers. The warningOn flag is the same trick as the jump pad's debounce and the climb's lastHandY — we act on the change, not the constant state. Buzzing on every frame would drain the controllers and feel like a constant rumble; buzzing only when you cross the edge makes it meaningful.

Both halves share one lesson: good feedback is about timing. The float gives you time; the buzz arrives exactly when you need it.

Try this

Learning beat

Try this

Three short experiments. Predict before you run, then test your guess.

Predict first

LOW_GRAVITY is 50. Predict how the walkway feels at 10 (almost no gravity) versus 150 (just a little floaty). One makes the planks too easy and one barely helps — decide which before testing, then pick the value that's fun but still a challenge.

Compare

Cross the planks once with the edge-warning idea in mind, then imagine the buzz firing. Would a warning that buzzed harder the closer you got to the edge feel better than the on/off buzz you built? What would you have to measure to do that? (Hint: not just "is there ground," but "how close to the edge.")

Connect

You raycast straight down to feel the floor. Stage 7 has boulders rolling at you. What direction would you raycast to feel a boulder coming — and how could a buzz warn the player to duck? Jot the idea down; you'll want it later.

Test your stage

  • Press ▶ Play and walk across the planks at normal gravity — feel how tense it is.
  • Step on the cyan pad; your next jump should clearly float.
  • Cross the planks under low gravity, then touch the orange pad — gravity returns to normal.
  • Touch the Stage 4 pad, reset, and confirm you respawn there.
  • Output is empty on a clean Play — no errors from either gravity Script or VRController.
  • Design check. Do the cyan and orange pads read as "something changes here"? In VR, color and Neon are how players know a pad matters. If they blend into the path, brighten them.

If it breaks

  • Gravity never goes back to normal. You touched the cyan pad but not the orange one — or the orange pad's Script has the wrong number. Confirm NORMAL_GRAVITY = 196.2 and that the orange pad is positioned where the player actually walks over it.
  • Gravity is stuck low after I fall and respawn. Known limitation of the two-pad design — respawning skips the orange pad. For a camp build it's fine; just step on the orange pad. (The hard stretch's per-player version avoids this.)
  • The float is uncontrollable. Raise LOW_GRAVITY toward 100. Too-low gravity makes the planks frustrating, not fun.
  • HapticService is underlined red in VRController. You didn't add local HapticService = game:GetService("HapticService") at the top of the file.
  • Nothing buzzes on my laptop. Expected — no headset means VREnabled is false and the whole loop is skipped. The buzz is verified at the Stage 10 playtest.
Coach notes

The "gravity is just a number" reveal is the hook of this stage — say it out loud and let a camper change LOW_GRAVITY live and watch the world react. It's one of the most empowering moments in the course.

Two common snags: campers put the gravity Scripts in VRController (they're server Scripts, one inside each pad), and campers forget the orange pad, leaving the whole game floaty. Walk the room after Step 2 and confirm each pad has its own Script and the right gravity number.

As always, the haptic buzz can't be felt today; the clean Play is the pass. Reinforce that the raycast edge-warning is the same RenderStepped pattern as Stage 1 — they're not learning a new structure, just pointing it at a new problem.