Obby
An obby is a classic obstacle game in Roblox. This project focuses on coding certain common components of an Obby from scratch.
List of Common Mechanics
Some common mechanics include
- Damage Brick - Kill Brick, Fire Brick
- Path Pattern - A correct path is illuminated before disappearing for a couple of seconds
- Stages System - Spawn locations
- Progress Saving - Saving The stage the player is on through DataStore.
Damage Brick
When the player touches a damage brick, their health will decrease. We’re going to be using the Touched event for both this and two different bricks.
Prerequisites
Knowledge of functions is required as well as conditionals.
Kill Brick
Kill Brick simply sets the health of the player to zero when the player touches it.
Instructions
- Create a
scriptinside the kill brick. - Create a variable for your kill brick.
- Create an empty named function
- Call the
Connectfunction of theTouchedevent inside the kill brick - Implement the empty named function
Implementing the empty named function
Here are the specific sub-steps of step 5.
- Make sure the part touching our kill brick is a body part.
- Set the
Healthproperty of the character model’sHumanoid.
- Create a
Implementation Hints, Solutions
Step 1 - Activity Setup
Simply create a Part in
workspaceand insert ascriptinside the Part.Script or Local Script
In Roblox, we only use a
local scriptfor graphical interface elements, things the player should only see or experience, and the events of the character’s humanoid.
Step 2 - Making Variable For Kill Brick
We’re creating a
variableso think of a name. e.g.killbrick,part.Remember how variable initialization/assignment work in Lua. On the right side of
=, how can you access the kill brick? Remember that the script we’re writing in is inside the kill brick.Solution
workspace/KillBrick/ScriptkillBrick = script.Parent
Step 3 - Making an Empty Function
Choose a name for your
function. Look at the documentation reference page for theTouchedevent, what parameter(s) do we need?Solution
workspace/KillBrick/Scriptfunction processKill(otherPart: BasePart) end
Step 4 - Passing Empty Function to Connect Function
Access the
Touchedevent inside theKillBrick, then call theConnectfunction and pass in our empty function in as argument.Solution
workspace/KillBrick/ScriptkillBrick = script.Parent -- From Step 2 -- Step 3 function processKill(otherPart: BasePart) end killBrick.Touched:Connect(processKill)
Step 5 - Implementing the Empty Function
First, we have to make sure that the argument we received is an actual body part because the
Touchedevent occurs for any base part that moved to touch. In order to do this, we check if there’s aHumanoidinside theParentof the body part. If there isn’t, thenreturnfrom the function early.Parent of Body Part
The parent of every body part is the character model.
Use FindFirstChild
Use the function
FindFirstChildin order to safely access achild(nested object) of another object.FindFirstChildrequires astringas argument which is the name of the child that should be attempted to access.FindFirstChildreturnsnilif it could not find the child of given name or it gives the actualchildback.Check out the reference documentation page on
FindFirstChild.Solution - Making Sure It’s a Body Part
workspace/KillBrick/Scriptfunction processKill(otherPart: BasePart) local potentialCharacter = otherPart.Parent local potentialHumanoid = potentialCharacter:FindFirstChild("Humanoid") if not potentialHumanoid then return end -- Equivalent: if potentialHumanoid == nil then return end endThen, we simply set the
Healthproperty of the character’shumanoidto 0.Solution - Setting Property Health
workspace/KillBrick/Scriptfunction processKill(otherPart: BasePart) local potentialCharacter = otherPart.Parent local potentialHumanoid = potentialCharacter:FindFirstChild("Humanoid") if not potentialHumanoid then return end -- Equivalent: if potentialHumanoid == nil then return end potentialHumanoid.Health = 0 end
Complete Solution
Before you look at the complete solution, make sure to struggle first.
Solution
workspace/KillBrick/Scriptpart = script.Parent function burn(otherPart) -- First, we confirm if the otherPart is a body part local character = otherPart.Parent local humanoid = character:FindFirstChild("Humanoid") if not humanoid then return end humanoid.Health = 0 end part.Touched:Connect(burn)
Fire Brick
Unlike the Kill Brick, the fire brick is much more complicated because we’re dealing with gradual damage over time. Coming from the finished code of the Kill Brick, instead of setting Health to zero, you might have thought that instead of setting the Health to 0, you could just decrease it.
Finish KillBrick First
This section assumes that you have the code for the
KillBrickalready. Don’t rush it and sacrifice your learning experience.
Naive Solution
workspace/FireBrick/ScriptfirePart = script.Parent
function burn(otherPart: BasePart)
-- making sure it's a body part
local pCharacter = otherPart.Parent
local pHumanoid = pCharacter:FindFirstChild("Humanoid")
if not pHumanoid then return end
-- Old: pHumanoid.Health = 0
pHumanoid.Health -= 5
end
firePart.Touched:Connect(processDamage)You’ll quickly see that when you touch the part, there might be more than just 5 damages. This is because body part engaged the event and so the burn function is called multiple times. Then, worse is the fact that there’s no burning effect which we expected.
Part of the engineering process, you want to list out your problems and think about a potential solution for each of them. Ask yourself what you would like this piece of code to do instead. A strategy is to list out things you think Roblox (might) have which you can use.
Solution - Engineering Sub-activity
Problems - Potential Solution
- Multiple body-parts triggering event - A
debouncemechanic is needed. - Doesn’t burn until body part stops touching - Use
TouchEndedevent and something to process characters still touching the fire brick
This guide assumes that you have reached a conclusion similar to the solution above. In turn, I expect the student to know these concepts:
- Functions
- Conditionals
- Maps (also called dictionaries)
charsTouching - Our Table (Map)
How to Approach the Guide
Try to implement the code on your own first before looking at the solutions.
First, create a variable and assign an empty table to it. This guide is going to refer to this table as
charsTouching. Expanded, it’s characters touching.Solution - Making an empty table
workspace/FireBrick/ScriptcharsTouching = {}Our table is going to hold keys which are going to the characters’ name. The name of every player’s character model is the same as the player’s username. The value for each key is going to be table that holds (1) the player character and (2) the last time the burn effect was applied which we get from the function os.time.
Solution - Table Entry Example
Pseudo-codecharsTouching = { ["player1"] = {<Character>, <number>} }
Setting Up the Code Skeleton
Code Skeletonis no way an official term in computer science. We first though write a simple structure of our code which includes two functions for the eventsTouched,TouchEnded, and as well Heartbeat of RunService.Solution - Code Skeleton
workspace/FireBrick/ScriptRunService = game:GetService("RunService") fireBrick = script.Parent charsTouching = {} function subscribe(otherPart: BasePart) end function unsubscribe(otherPart: BasePart) end function processBurn(deltaTime: number) -- We won't use the parameter deltaTime. end fireBrick.Touched:Connect(subscribe) fireBrick.TouchEnded:Connect(unsubscribe) RunService.Heartbeat:Connect(processBurn)
Complete Solution
Solution
charactersTouching = { -- ["Player1"] = {12312512, true, <Character>}, -- touching is still occuring. -- ["Player2"] = {123451243, false, <Character>}, -- touch stops about to be removed from the table -- ["Player3"] = nil -- not actually in the table anymore } part = script.Parent function isCharacter(otherPart: BasePart) local potentialCharacter = otherPart.Parent local humanoid = potentialCharacter:FindFirstChild("Humanoid") if humanoid then return potentialCharacter end return false end function startBurn(otherPart: BasePart) local character = isCharacter(otherPart) if not character then return end if charactersTouching[otherPart] then return end charactersTouching[otherPart] = {os.time(), true, character} end function stopBurn(otherPart: BasePart) local character = isCharacter(otherPart) if not character then return end charactersTouching[character.Name][2] = false end function burnOnStandBy() for characterName, data in charactersTouching do if not data[2] or not data[3] then charactersTouching[characterName] = nil continue end if os.time() - data[1] > 1 then data[3].Humanoid.Health -= 5 end end end part.Touched:Connect(startBurn) part.Touched:Connect(stopBurn) part.Heartbeat:Connect(burnOnStandby)
Custom Menu
In preparation to make our custom stages, we’re customizing the way the player respawns and make a game menu with a play button which we’re placing inside ReplicatedFirst.
Refer to the documentation to create GUI
Here are some
GuiObjectsthat you should look at to create your menu.
Your menu needs a local script and as well as a script. The script handles respawning players on the server while the local script should use the event Activated of a TextButton or ImageButton.
Create a remote event and then use the function FireServer without any extra arguments inside the function that you’ll connect to the Activated event of your button object.
Refer to the documentation
Here are some resources you look at for you
local script.
- Remote events
- Remote events - FireServer
- GuiButton (Works For ImageButton and TextButton) - Activated
Solution - Local Script
menuButton = script.Parent
spawnRemoteEvent = ReplicatedStorage:WaitForChild("SpawnRemoteEvent")
function requestSpawning()
spawnRemoteEvent:FireServer()
end
menuButton.Activated:Connect(requestSpawning)Then, for the server script. First, we need to manually code the way the player should spawn in to the game and respawn. We’ll be using the property called CharacterAutoLoads. We’ll also grab the example code in the page.
ServerScriptService/PlayerSpawninglocal Players = game:GetService("Players")
-- Set CharacterAutoLoads to false
Players.CharacterAutoLoads = false
-- Remove player's character from workspace on death
Players.PlayerAdded:Connect(function(player)
while true do
local char = player.CharacterAdded:Wait()
char.Humanoid.Died:Connect(function()
char:Destroy()
end)
end
end)
-- Respawn all dead players once every 10 seconds
while true do
local players = Players:GetChildren()
-- Check if each player is dead by checking if they have no character, if dead load that player's character
for _, player in pairs(players) do
if not workspace:FindFirstChild(player.Name) then
player:LoadCharacterAsync()
end
end
-- Wait 10 seconds until next respawn check
task.wait(10)
endAll we need to do now is work with OnServerEvent on the server script. We’re going to create a table of strings which we’re going to use as the whitelist that allow players to spawn. The function that you’re passing to the OnServerEvent event should include code that inserts a player name into the table. In order to get the player from the player’s name, pass the player character to the function GetPlayerFromCharacter of player.
Solution - Full
ServerScriptService/PlayerSpawningPlayers = game:GetService("Players")
ReplicatedStorage = game:GetService("ReplicatedStorage")
spawnRemoteEvent = ReplicatedStorage:WaitForChild("SpawnRemotEvent")
-- Whitelist
spawnWhitelist = {}
spawnRemoteEvent.OnServerEvent:Connect(function(player: Player)
table.insert(player.Name)
end)
-- Set CharacterAutoLoads to false
Players.CharacterAutoLoads = false
-- Remove player's character from workspace on death
Players.PlayerAdded:Connect(function(player)
while true do
local char = player.CharacterAdded:Wait()
char.Humanoid.Died:Connect(function()
char:Destroy()
end)
end
end)
-- Respawn all dead players once every 10 seconds
while true do
local players = Players:GetChildren()
-- Check if each player is dead by checking if they have no character, if dead load that player's character
for _, playerName in pairs(spawnWhitelist) do
if not workspace:FindFirstChild(playerName) then
local player = Players:FindFirstChild(playerName)
player:LoadCharacterAsync()
end
end
-- Wait 10 seconds until next respawn check
task.wait(10)
endData - Module Script
We’re making a custom spawn location which are going to be assigned to different stages. We’re also going to save the player’s progress later. So this begs the question of how we’re going to be managing the player’s data. We’re going to do this with a module script.
ServerScriptService/PlayerDataDataStore = game:GetService("DataStoreService")
Players = game:GetService("Players")
Data = {
playerData = {
["Player1"] = {
stage = 10,
coins = 10000
}
["Player1-Loaded"] = true
},
dataStoreNames = {
stage = "ObbyProgress",
-- coins = "Coins"
}
}
--[[
Gets the data inside the game not synced to data store
playerName: name of player
dataName: data key
Returns the value requested or nil if it doesn't exist.
]]
function Data:get(playerName: string, dataName: string): any
assert(Players:FindFirstChild(playerName), "Name of player do not exist")
if self.playerData[playerName][dataName] == nil then
return nil
end
if self.playerData[playerName][dataName] == nil then
return nil
end
return self.playerData[playerName][dataName]
end
--[[
Waits for player data to be loaded into the game and returns that data
playerName: name of player
dataName: name of data being loaded
attempts: number of times to try to wait for data
Returns data waited for and whether getting the data is successful
]]
function Data:waitForPlayerData(playerName: string, dataName: string, attempts: number): (any, boolean)
local currentAttempt = 0
while not self:get(playerName, dataName) and currentAttempt < attempts do
warn(`Waiting for data {dataName} of player {playerName}`)
task.wait(1)
end
return self:get(playerName, dataName), currentAttempt ~= attempts
end
function Data:save(playerName: string, dataName: string): boolean
if not Players:FindFirstChild(playerName) then
return false
end
-- 1. Does player data even exist? If not we return early.
---> Look inside playerData (it's inside Data),
---- See if playerData keying player's name exists. If not, return early.
if not self.playerData[playerName] then
return false
end
-- 2. Does the specific player data of name exist? If not we return early
-- self.playerData[playerName][dataName]
if self.playerData[playerName][dataName] == nil then
return false
end
-- 3. Saves data to data store
local dataStore = DataStore:GetDataStore(self.dataStoreNames[dataName][1])
local success, data = pcall(function()
dataStore:UpdateAsync(Players[playerName].UserId, function()
return self.playerData[playerName][dataName]
end)
end)
end
--[[
Requests data from the data store
playerName: name of player
dataName: name of the data being loaded from data store
Returns the data loaded from the data store, and whether the data load is successful
]]
function Data:request(playerName: string, dataName: string): (any, boolean)
local player = Players:FindFirstChild(playerName)
assert(player, "Name of player do not exist")
assert(self.dataStoreNames[dataName], "Data store information for dataName do not exist")
assert(#self.dataStoreNames[dataName] == 2, "Missing configured data store information")
local dataStore = DataStore:GetDataStore(self.dataStoreNames[dataName][1])
local success, value = pcall(function()
return dataStore:GetAsync(player.UserId)
end)
if not success then
warn(value)
return nil, false
end
if data == nil then
return self.dataStoreNames[dataName][2], true
end
return data, true
end
--[[
Sets up the events PlayerAdded and PlayerRemoving to get and save the player's data
]]
function Data:listenPlayerEvents(): nil
Players.PlayerAdded:Connect(function(player: Player)
self.playerData[player.Name] = {}
for dataName, _ in self.dataStoreNames do
-- Loads the data of this specific data store
-- Kick the player if the data loading fails (player:Kick)
-- If data loading completes add PlayerName-Loaded = false
local data, success = self:request(player.Name, dataName)
if not success then
player:Kick(`{dataName} not loaded`)
return
end
self.playerData[player.Name][dataName] = data
end
self.playerData[player.Name .. "-Loaded"] = true
end)
Players.PlayerRemoving:Connect(function(player: Player, reason: Enum.PlayerExitReason)
if reason == Enum.PlayerExitReason.CreatorKick and not self.playerData[player.Name.."-Loaded"] then
-- returns early if player data is not completedly loaded
return
end
for dataName, _ in self.dataStoreNames do
self:save(player.Name, dataName)
end
end)
end
return Data