Roblox EP5 Lava Rising: build the level in code, test the rules outside Studio
The series' second game: a round game where lava rises and whoever reaches the top scores. The level is built from code with Rojo, and Lune checks every jump is possible.
- A level built from code changes everywhere when you change one number, and its history lives in Git
- Rules written as functions that never touch Roblox objects can be tested for fairness outside Studio
- Almost every round game has the same shape, wait → play → judge → wait again, and the server always judges
In EP1-4 we placed floors and steps by hand in Studio, then wrote code to make the parts behave. This episode builds a second game, Lava Rising, a round game you can play with friends, and changes the method: the whole level is built from code, and the game’s rules are tested outside Studio before anyone presses Play. That is how real teams work once a game starts to grow.
This one is a step up. If you skipped EP3 on the server and players’ devices, read it first.
The game
- Players spawn on a lobby island with a 15-second countdown
- Everyone is teleported around the foot of a spiral tower of 24 steps
- Five seconds later the lava starts to rise, in waves: slow, then a surge
- Feet in the lava means death and back to the lobby
- After 75 seconds, everyone standing on the golden summit gets Wins +1, and a new round starts
On the way up there are two special steps: blue ones that bob up and down, and brown ones that shake and crumble when you land.
Why build the level in code
| Placed by hand in Studio | Built from code | |
|---|---|---|
| Make every step taller | edit 24 parts one by one | change one number |
| Keep a history of changes | hard: the place is one big file | text files, Git works |
| Check every jump is possible | play it yourself every time | write a test that calculates it |
| See the level while editing | yes | no, press Play |
| Hand-sculpted, artistic levels | a good fit | a poor fit |
This game is a repeated shape from a formula, steps spiralling around a pillar at equal spacing, so code suits it well. A level that needs careful placement corner by corner is better done by hand. Many games mix both.
Two tools
- Rojo connects code files on your computer with Studio. You write plain
.luaufiles, and Rojo builds a.rbxlxplace file that Studio opens - Lune runs Luau outside Roblox. We use it to run the rules tests on our own machine without opening Studio
Both are open source, downloaded from the Releases page of each project on GitHub. This episode uses Rojo 7.7.1 and Lune 0.11.0 on Windows.
Where each file lands in Studio:
| File | In Studio | Type |
|---|---|---|
src/shared/Config.luau | ReplicatedStorage › Shared | ModuleScript, every number |
src/shared/RoundLogic.luau | ReplicatedStorage › Shared | ModuleScript, the rules |
src/server/MapBuilder.luau | ServerScriptService › Server | ModuleScript, builds the level |
src/server/Main.server.luau | ServerScriptService › Server | Script, the game loop |
src/client/StatusUI.client.luau | StarterPlayerScripts › Client | LocalScript, the status bar |
Rojo reads the file endings: .server.luau becomes a Script, .client.luau a LocalScript, anything else a ModuleScript. default.project.json maps folders to places in Studio:
{
"name": "lava-rising",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": { "Shared": { "$path": "src/shared" } },
"ServerScriptService": { "Server": { "$path": "src/server" } },
"StarterPlayer": {
"StarterPlayerScripts": { "Client": { "$path": "src/client" } }
}
}
}
One command builds the place, then double-click the result to open it in Studio:
rojo build -o build/lava-rising.rbxlx
Every number in one file
Config.luau holds every number that shapes the game’s look and difficulty. An excerpt:
local Config = {
INTERMISSION = 15, -- seconds in the lobby
ROUND_TIME = 75, -- round length
LAVA_DELAY = 5, -- seconds before the lava moves
TOWER_STEPS = 24, -- number of steps
STEP_RISE = 5, -- height per step (a normal jump reaches about 7)
STEP_ANGLE = 30, -- degrees turned per step
TOWER_RADIUS = 22, -- distance from the pillar to the steps
PLATFORM_SIZE = 8,
SURGE_WAVES = 3, -- the lava rises in waves, this many per round
SURGE_STRENGTH = 0.6 -- 0 = steady, 0.6 = between 0.4x and 1.6x the average speed
}
return Config
Want it harder? Lower ROUND_TIME. A taller tower? Raise TOWER_STEPS. The whole level follows.
The rules as plain functions
The key idea of this episode is separating the game’s rules from Roblox objects. RoundLogic.luau takes numbers in and gives numbers back. It never touches a Part or a player. For example, where each step goes:
function RoundLogic.towerLayout(cfg)
local points = {}
for i = 1, cfg.TOWER_STEPS do
local a = math.rad(i * cfg.STEP_ANGLE)
table.insert(points, {
x = cfg.TOWER_RADIUS * math.cos(a),
y = i * cfg.STEP_RISE,
z = cfg.TOWER_RADIUS * math.sin(a),
})
end
return points
end
Step i turns i × 30 degrees and rises i × 5 studs: a spiral around the pillar. MapBuilder then turns these positions into real Parts.
The rule for who burns is short too. What we can measure is the HumanoidRootPart, which sits about 3 studs above the feet:
function RoundLogic.isBurned(rootY, lavaTop, cfg)
return rootY - cfg.BURN_MARGIN <= lavaTop -- BURN_MARGIN = 3
end
The lava doesn’t rise in a straight line but in waves: slow stretches to breathe, surges to panic. The formula is built so the lava only ever goes up, and reaches its highest point exactly when time runs out, 4 studs under the summit, so whoever is on top always survives.
Testing the rules outside Studio
Because the rules never touch Roblox, Lune can run them. tests/round_logic.luau asks questions that calculation can answer:
check("a step is jumpable (rise under 7)", Config.STEP_RISE < 7)
local gap = RoundLogic.stepGap(Config)
check("gap between steps is positive and short (0-6 studs)", gap > 0 and gap < 6)
-- even at the fastest surge, there must be at least 1.5 seconds per step
local secondsPerStep = Config.STEP_RISE / RoundLogic.lavaPeakSpeed(Config)
check("lava gives at least 1.5 s per step, even in a surge", secondsPerStep >= 1.5)
check("standing on the summit at the end is safe", not RoundLogic.isBurned(summitTop + 3, endY, Config))
Run it on your machine:
lune run tests/round_logic
lava
ok still during the head start
ok lava only ever goes up
...
climb time
ok lava gives at least 1.5 s per step, even in a surge
...
34 passed, 0 failed
A worked example, the gap between steps: two step centres 30 degrees apart on a circle of radius 22 are 2 × 22 × sin 15° ≈ 11.4 studs apart. Take away the 8-stud step width and the real gap is ≈ 3.4 studs, an easy jump.
The real payoff: the day you change STEP_RISE to 8 for a taller tower, the tests tell you at once that the jump is impossible, before any player finds out.
Test what code can calculate. Save real play for what numbers can’t measure: is it fun?
The game loop: wait, play, judge
Main.server.luau is the one server Script that runs the whole game. Its shape:
while true do
intermission() -- countdown in the lobby
playRound() -- teleport, lava rises, judge
end
Three things in playRound are worth a look.
1. Track who is alive. At teleport, each player’s name goes into an alive table, and we listen for their Died:
alive[player.Name] = true
humanoid.Died:Connect(function()
alive[player.Name] = false
end)
2. Move the lava and check every frame, with RunService.Heartbeat, which runs every frame on the server:
RunService.Heartbeat:Connect(function()
lavaTop = RoundLogic.lavaHeight(os.clock() - started, Config)
MapBuilder.setLava(lava, lavaTop)
for _, player in Players:GetPlayers() do
local root = if alive[player.Name] then rootOf(player) else nil
if root and RoundLogic.isBurned(root.Position.Y, lavaTop, Config) then
player.Character:FindFirstChildWhichIsA("Humanoid").Health = 0
end
end
end)
Why not Touched, like the lava in EP2? This lava block moves every frame and covers the whole arena. Comparing heights directly is more certain and easier to read.
The if alive[player.Name] line comes from a real bug caught before the first play: the lava climbs higher than the lobby island, so checking everyone in the server would burn people waiting in the lobby, nowhere near the lava.
3. Judge at the end. Everyone still in alive gets Wins +1 and goes back to the lobby. All of this happens on the server; players’ devices only show text, following the EP3 rule: never trust the client.
The status bar uses the simplest method there is: the server writes into a StringValue called Status in ReplicatedStorage, and the player’s LocalScript just reads and shows it. No RemoteEvent needed.
Making it more fun
The first version worked but felt flat: climb around and around. The second version added three things:
- Lava in waves, three per round. During a surge the status bar turns red and reads
LAVA SURGE! - Special steps in a pattern, normal › moving › normal › crumbling, repeating up the tower. Moving steps bob with
TweenService; crumbling steps shake for 0.6 seconds, then vanish for 3 - A height-above-lava meter on the player’s screen, and a red glow that grows as the lava gets close. Display only, on the player’s device; it decides nothing
Fun must not cost fairness, so the tests gained new rules, and all of them must pass before the place is built:
- The first 3 steps are always normal, so everyone gets a calm start
- Never two special steps in a row
- The last step before the summit is normal
- A moving step plus its bob is still under 7 studs high
Play it on your computer
- Build the place with
rojo build, then double-clickbuild/lava-rising.rbxlx - Studio opens to an empty sky. That’s expected
- Press Play (the blue ▶ at the top left) or F5. Solo works, because
MIN_PLAYERS = 1 - For several players: Test › Clients and Servers, set 2-3 players, Start
- Keep the Output window open from the Window menu to see errors
To open the file from inside Studio instead, use File › Open from File.
Common mistakes
- Checking the lava against every player in the server, so people in the lobby burn too
- Not disconnecting at the end of a round, so the next round runs two lava checkers. Keep every connection in a table and
Disconnectthem all at the end - Letting the lava reach the summit, so nobody can ever win. It must stop below the top
- Changing a number in Config without running the tests, and shipping a jump nobody can make
- A LocalScript in the wrong place, which, as EP3 explained, silently never runs
Next episode
Right now Wins vanish whenever you leave. EP6 saves player data between visits with DataStore, handles saves that fail, and publishes the game to Roblox, which DataStore needs first.
FAQ
Do I need Rojo, or can I do this in Studio alone?
Studio alone works. Every file in this episode can be pasted into a Script or ModuleScript in the place shown in the table. Rojo pays off as a project grows, because you write code in the editor you like, use Git, and let AI help edit the files.
Why do I only see an empty sky when I open the place?
The level is built when the server starts. Press Play and it appears. That is the trade-off of building levels in code.
What is Lune, and is it safe?
Lune runs the Luau language outside Roblox. It is open source and downloaded from the project's GitHub. Here it only runs the rules tests on your own machine; nothing is sent to Roblox.
Commands and settings were tested on sample systems. Try them on a test machine before production.