Luau scripting is what turns a Roblox map into a functioning game. Roblox Studio provides the tools for creating worlds, interfaces, characters, and assets, but scripts are responsible for the gameplay logic connecting everything together. A few well-structured scripts can handle currencies, hazards, movement, doors, checkpoints, saving, abilities, and entire game loops.
Below are practical Luau examples you can adapt for Obbys, simulators, RPGs, battlers, survival games, and other Roblox projects.
| IMPORTANT | These are not client-side exploit scripts. Scripts intended to manipulate someone else’s game or bypass its protections are against Roblox Terms and Conditions. Your own Studio scripts should run on the appropriate side of the client-server boundary, with the server responsible for anything that affects game state. |
1. Leaderstats and Persistent Currency
A leaderstats folder creates values that Roblox displays in the default player leaderboard. This is useful for Coins, Cash, Wins, Level, or other visible statistics. For an actual game, however, you also need to save that data. Instead of maintaining separate scripts that create the same leaderboard values, you can combine the two systems into one basic server-side player-data script.
- Script Type: Script (Server)
- Location: ServerScriptService
— Leaderstats + DataStore
— Place in ServerScriptService
local Players = game:GetService(“Players”)
local DataStoreService = game:GetService(“DataStoreService”)
local coinStore = DataStoreService:GetDataStore(“PlayerData_v1”)
local STARTING_COINS = 100
local STARTING_LEVEL = 1
local isShuttingDown = false
local savingPlayers = {}
local loadedPlayers = {}
local activeSaves = 0
local savesFinished = Instance.new(“BindableEvent”)
local function getKey(player)
return “Player_” .. player.UserId
end
local function createStats(player)
local leaderstats = Instance.new(“Folder”)
leaderstats.Name = “leaderstats”
leaderstats.Parent = player
local coins = Instance.new(“IntValue”)
coins.Name = “Coins”
coins.Value = STARTING_COINS
coins.Parent = leaderstats
local level = Instance.new(“IntValue”)
level.Name = “Level”
level.Value = STARTING_LEVEL
level.Parent = leaderstats
return coins, level
end
local function loadPlayerData(player)
local coins, level = createStats(player)
local success, data = pcall(function()
return coinStore:GetAsync(getKey(player))
end)
if success then
if type(data) == “table” then
coins.Value = data.Coins or STARTING_COINS
level.Value = data.Level or STARTING_LEVEL
end
loadedPlayers[player] = true
else
warn(“Failed to load data for”, player.Name)
end
end
local function savePlayerData(player)
— Prevent two save operations for the same player
— from running at the same time.
if savingPlayers[player] then
return false
end
— Never overwrite stored data with default values
— if the player’s data never finished loading.
if not loadedPlayers[player] then
warn(“Skipping save because data was not loaded for”, player.Name)
return false
end
local leaderstats = player:FindFirstChild(“leaderstats”)
if not leaderstats then
return false
end
local coins = leaderstats:FindFirstChild(“Coins”)
local level = leaderstats:FindFirstChild(“Level”)
if not coins or not level then
return false
end
savingPlayers[player] = true
activeSaves += 1
local data = {
Coins = coins.Value,
Level = level.Value,
}
local success, err = pcall(function()
coinStore:UpdateAsync(getKey(player), function()
return data
end)
end)
savingPlayers[player] = nil
activeSaves -= 1
if activeSaves == 0 then
savesFinished:Fire()
end
if not success then
warn(“Failed to save data for”, player.Name, err)
end
return success
end
Players.PlayerAdded:Connect(function(player)
loadPlayerData(player)
end)
Players.PlayerRemoving:Connect(function(player)
— During shutdown, BindToClose handles the remaining players.
if not isShuttingDown then
savePlayerData(player)
end
loadedPlayers[player] = nil
end)
game:BindToClose(function()
— Stop new PlayerRemoving handlers from starting
— independent saves while shutdown saves are running.
isShuttingDown = true
— Save all players still connected.
for _, player in Players:GetPlayers() do
savePlayerData(player)
end
— Wait for any save that was already running when
— shutdown began.
while activeSaves > 0 do
savesFinished.Event:Wait()
end
end)
Important: DataStore code must run on the server, not in a LocalScript. (create.roblox.com)
2. Classic Kill Brick
Every Obby needs hazards. A kill brick is one of the simplest Roblox scripts: when a character touches the designated Part, its Humanoid is reduced to zero health.
- Script Type: Script (Server)
- Location: Inside the hazard Part in Workspace
— Kill Brick Script
local hazard = script.Parent
local cooldown = {}
local function onTouched(otherPart)
local character = otherPart.Parent
if not character then
return
end
local humanoid = character:FindFirstChildOfClass(“Humanoid”)
if not humanoid or humanoid.Health <= 0 then
return
end
if cooldown[character] then
return
end
cooldown[character] = true
humanoid.Health = 0
task.delay(0.5, function()
cooldown[character] = nil
end)
end
hazard.Touched:Connect(onTouched)
The small debounce table prevents multiple touch events from firing repeatedly for the same character. You can adapt the same pattern for lava, lasers, falling platforms, traps, spikes, or environmental damage. For gradual damage instead of an instant kill, replace the Health = 0 line with a fixed amount of damage.
3. Shift-to-Sprint
A sprint system is useful in Obbys, RPGs, survival games, and open-world experiences. The following LocalScript changes the player’s WalkSpeed while Left Shift is held.
- Script Type: LocalScript (Client)
- Location: StarterPlayer > StarterPlayerScripts
— Shift-to-Sprint LocalScript
local UserInputService = game:GetService(“UserInputService”)
local Players = game:GetService(“Players”)
local player = Players.LocalPlayer
local DEFAULT_SPEED = 16
local SPRINT_SPEED = 28
local function setSpeed(speed)
local character = player.Character
if not character then
return
end
local humanoid = character:FindFirstChildOfClass(“Humanoid”)
if humanoid then
humanoid.WalkSpeed = speed
end
end
local function onCharacterAdded(character)
local humanoid = character:WaitForChild(“Humanoid”)
humanoid.WalkSpeed = DEFAULT_SPEED
end
player.CharacterAdded:Connect(onCharacterAdded)
UserInputService.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then
return
end
if input.KeyCode == Enum.KeyCode.LeftShift then
setSpeed(SPRINT_SPEED)
end
end)
UserInputService.InputEnded:Connect(function(input)
if input.KeyCode == Enum.KeyCode.LeftShift then
setSpeed(DEFAULT_SPEED)
end
end)
Roblox’s default Humanoid WalkSpeed is 16 studs per second, so 28 provides a noticeable but manageable sprint increase.
4. Interactive Door with ProximityPrompt
ProximityPrompt is the modern way to create nearby interactions such as doors, switches, NPCs, shops, and pickup points. It automatically provides appropriate interaction input for supported devices. First, create a Part named Door. Insert a ProximityPrompt inside it. Then, insert a normal Script inside the same Part.
- Script Type: Script (Server)
- Location: Inside the Door Part
— ProximityPrompt Door
local TweenService = game:GetService(“TweenService”)
local door = script.Parent
local prompt = door:FindFirstChildOfClass(“ProximityPrompt”)
if not prompt then
warn(“No ProximityPrompt found inside door”)
return
end
local closedCFrame = door.CFrame
local openCFrame = closedCFrame * CFrame.new(0, 6, 0)
local tweenInfo = TweenInfo.new(
0.5,
Enum.EasingStyle.Quad,
Enum.EasingDirection.Out
)
local isOpen = false
local busy = false
local function setDoor(open)
if busy then
return
end
busy = true
prompt.Enabled = false
local targetCFrame = open and openCFrame or closedCFrame
local tween = TweenService:Create(door, tweenInfo, {
CFrame = targetCFrame
})
tween:Play()
tween.Completed:Wait()
isOpen = open
prompt.ActionText = open and “Close” or “Open”
prompt.Enabled = true
busy = false
end
prompt.ActionText = “Open”
prompt.Triggered:Connect(function()
setDoor(not isOpen)
end)
5. Coin Pickup
If your game uses a currency system, you can make physical Coins that players collect by touching them.
- Script Type: Script (Server)
- Location: Inside each Coin Part
— Coin Pickup
local Players = game:GetService(“Players”)
local coin = script.Parent
local VALUE = 10
local collected = false
local function onTouched(otherPart)
if collected then
return
end
local character = otherPart.Parent
if not character then
return
end
local player = Players:GetPlayerFromCharacter(character)
if not player then
return
end
local leaderstats = player:FindFirstChild(“leaderstats”)
if not leaderstats then
return
end
local coins = leaderstats:FindFirstChild(“Coins”)
if not coins then
return
end
collected = true
coins.Value += VALUE
coin:Destroy()
end
coin.Touched:Connect(onTouched)
Because the server awards the currency, players cannot grant themselves Coins merely by changing a LocalScript. You can turn this into a reusable system by storing Coin values in Attributes rather than hard-coding 10 into every script.
6. Checkpoint System
Checkpoints are essential for Obbies and can also work for survival or adventure games. A simple scalable approach is to tag checkpoint Parts with the Checkpoint tag and use one server script to handle all of them. Start by creating your checkpoint Parts in Workspace. Open CollectionService through Roblox Studio’s Tag Editor and add the tag Checkpoint to each one. Place the following Script in ServerScriptService.
- Script Type: Script (Server)
- Location: ServerScriptService
— Checkpoint System
local Players = game:GetService(“Players”)
local CollectionService = game:GetService(“CollectionService”)
local checkpoints = {}
local function connectCheckpoint(checkpoint)
if not checkpoint:IsA(“BasePart”) then
return
end
checkpoint.Touched:Connect(function(otherPart)
local character = otherPart.Parent
if not character then
return
end
local player = Players:GetPlayerFromCharacter(character)
if not player then
return
end
checkpoints[player.UserId] = checkpoint.CFrame
player:SetAttribute(“CheckpointName”, checkpoint.Name)
end)
end
for _, checkpoint in CollectionService:GetTagged(“Checkpoint”) do
connectCheckpoint(checkpoint)
end
CollectionService:GetInstanceAddedSignal(“Checkpoint”):Connect(connectCheckpoint)
local function applyCheckpoint(player, character)
local checkpointCFrame = checkpoints[player.UserId]
if not checkpointCFrame then
return
end
local rootPart = character:WaitForChild(“HumanoidRootPart”)
rootPart.CFrame = checkpointCFrame + Vector3.new(0, 3, 0)
end
local function setupPlayer(player)
player.CharacterAdded:Connect(function(character)
applyCheckpoint(player, character)
end)
end
for _, player in Players:GetPlayers() do
setupPlayer(player)
end
Players.PlayerAdded:Connect(setupPlayer)
Players.PlayerRemoving:Connect(function(player)
checkpoints[player.UserId] = nil
end)
This script keeps checkpoint data in the current server session. For persistent checkpoints, store the checkpoint identifier in your DataStore and restore it when the player joins.

7. Simple Round System
A basic round loop gives you the foundation for survival games, minigames, battle arenas, and other multiplayer experiences.
- Script Type: Script (Server)
- Location: ServerScriptService
— Simple Round System
local Players = game:GetService(“Players”)
local ReplicatedStorage = game:GetService(“ReplicatedStorage”)
local INTERMISSION_TIME = 15
local ROUND_TIME = 120
local gameState = Instance.new(“StringValue”)
gameState.Name = “GameState”
gameState.Value = “Waiting”
gameState.Parent = ReplicatedStorage
local function setState(state, duration)
gameState.Value = state
print(state)
for remaining = duration, 1, -1 do
task.wait(1)
end
end
while true do
if #Players:GetPlayers() < 1 then
gameState.Value = “Waiting for players”
task.wait(2)
else
setState(“Intermission”, INTERMISSION_TIME)
gameState.Value = “Round Active”
for remaining = ROUND_TIME, 1, -1 do
gameState:SetAttribute(“TimeLeft”, remaining)
task.wait(1)
end
setState(“Round Over”, 5)
end
end
The same pattern can later be expanded with team assignment, spawn points, round winners, maps, elimination conditions, and a countdown UI. For more complex games, avoid creating dozens of independent loops that all manage time separately. A centralized game-state system is much easier to maintain.
8. Simple NPC Damage Handler
Combat games often need server-side damage logic for NPCs. This example damages a Humanoid when a tool or other server-controlled system calls a function.
- Script Type: ModuleScript
- Location: ServerScriptService or a dedicated Modules folder
— DamageModule
local DamageModule = {}
function DamageModule.DealDamage(targetModel, amount)
if typeof(amount) ~= “number” or amount <= 0 then
return false
end
if not targetModel or not targetModel:IsA(“Model”) then
return false
end
local humanoid = targetModel:FindFirstChildOfClass(“Humanoid”)
if not humanoid or humanoid.Health <= 0 then
return false
end
humanoid:TakeDamage(amount)
return true
end
return DamageModule
A shared ModuleScript like this prevents you from rewriting the same damage-validation logic across every weapon and ability. It also gives you one place to add future rules such as armor, shields, critical hits, invulnerability, or PvP restrictions.
9. Useful Utility: Timed Respawn
Another small script that fits many games is an automatic respawn handler.
- Script Type: Script (Server)
- Location: ServerScriptService
— Timed Respawn
local Players = game:GetService(“Players”)
local RESPAWN_DELAY = 3
Players.CharacterAutoLoads = false
Players.PlayerAdded:Connect(function(player)
local function spawnCharacter()
if player.Parent then
player:LoadCharacter()
end
end
spawnCharacter()
while player.Parent do
local character = player.Character
if character then
local humanoid = character:FindFirstChildOfClass(“Humanoid”)
if humanoid then
humanoid.Died:Wait()
task.wait(RESPAWN_DELAY)
spawnCharacter()
end
end
task.wait()
end
end)
This gives you direct control over respawn timing, which is useful for Obbys, round-based games, and survival experiences where the default behavior is not enough.
Essential Roblox Development Libraries
Once your project grows beyond a collection of small scripts, reusable libraries become increasingly valuable.
| LIBRARY | MAIN PURPOSE | GOOD FOR |
| ProfileStore | Player-data management with saving and session locking | Persistent inventories, currencies, progression |
| Knit | Service and controller architecture | Larger client-server projects |
| Promise | Asynchronous task handling | Data loading, networking, chained operations |
| Roact | Declarative UI architecture | Large, component-based interfaces |
| Fusion | Reactive UI and state management | Dynamic HUDs, menus, inventories |
The Most Important Roblox Scripting Rule
The easiest way to remember Roblox security is simple: 1) The client asks. 2) The server decides. A player pressing a button is a client input. Receiving Coins, dealing damage, purchasing an item, completing a quest, entering a restricted zone, or changing an inventory should be decided by the server.
Do not put sensitive game logic in a LocalScript simply because it is easier. Likewise, do not assume a ProximityPrompt is more secure because Roblox displays it as an interaction prompt. Client-triggered interactions still need appropriate server-side validation. Roblox specifically warns that replicated ProximityPrompts can be manipulated by exploiters.

Roblox scripting becomes much easier when you stop thinking about individual code snippets and start thinking in terms of systems. A kill brick is one small system. So is a coin pickup, sprint mechanic, checkpoint, door, combat ability, or round manager. Once you understand how those pieces communicate, you can combine them into much more ambitious games.
Start with simple scripts, keep important game logic on the server, and turn repeated logic into ModuleScripts as your project grows. You do not need hundreds of scripts to build a good Roblox game. You need a small number of reliable systems that communicate cleanly.
Read More
Looking for more game strategies and guides? Explore our full collection of tips & game guides to level up your gameplay!