Maps
Scripting a map
Your map's markers are invisible parts with tags and attributes. A server script finds them with CollectionService and hooks your game to the map. Here are working scripts for spawns, saved checkpoints, loot chests, zones and the map's edges.
Before you start#
- Put your scripts in ServerScriptService. In the Explorer, hover ServerScriptService, press + and pick Script. Don't put them inside
Workspace.BloxMap: building a new map replaces that folder. - Find markers by tag. Every marker has the tag
BloxMap+ its kind (BloxMapSpawn,BloxMapChest...). Tags stay the same after every build, and they work for every section of a world too. - Read the details from attributes. Click a marker in the Explorer to see its attributes in the Properties window. The full list is on Markers and spawn points.
- Press Play to test. Watch the Output window for messages.
This is how you find markers:
-- ServerScriptService > a Script
local CollectionService = game:GetService("CollectionService")
-- every chest marker in the map
for _, chest in CollectionService:GetTagged("BloxMapChest") do
print(chest.Name, chest.Position, chest:GetAttribute("Label"))
end
-- every marker of any kind: the BloxMapMarker attribute says which kind it is
for _, marker in CollectionService:GetTagged("BloxMapMarker") do
print(marker:GetAttribute("BloxMapMarker"), marker:GetFullName())
endSpawns#
Spawn markers are real SpawnLocations, so players already spawn on them without any script. This script sends players back to a spawn whenever you want, for example at the end of a round. It picks a spawn of the player's team, or a neutral one.
-- ServerScriptService > MapSpawns (a Script)
-- Sends players back to one of the map's spawn points (for example at the end of a round).
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
-- the spawns this player may use: their team's spawns, or the neutral ones
local function spawnsFor(player: Player): { SpawnLocation }
local list = {}
for _, spawn in CollectionService:GetTagged("BloxMapSpawn") do
if spawn:IsA("SpawnLocation") then
if spawn.Neutral or (player.Team ~= nil and spawn.TeamColor == player.TeamColor) then
table.insert(list, spawn)
end
end
end
return list
end
local function sendToSpawn(player: Player)
local character = player.Character
local list = spawnsFor(player)
if character == nil or #list == 0 then
return
end
local spawn = list[math.random(1, #list)]
-- the spawn pad is invisible: stand the player just above it
character:PivotTo(spawn.CFrame + Vector3.new(0, 4, 0))
end
-- Example: every 2 minutes, everyone goes back to spawn
while true do
task.wait(120)
for _, player in Players:GetPlayers() do
sendToSpawn(player)
end
endUse sendToSpawn(player) from your own round code instead of the 2-minute loop at the bottom.
Checkpoints with saving#
This script remembers the highest checkpoint each player reached, shows it as Stage on the leaderboard, saves it in a DataStore and starts the player there next time they join. It uses the BloxMapCheckpoint markers and their Index attribute (1, 2, 3...), which obbies, mountain and volcano climbs, race tracks and scenic drives have.
-- ServerScriptService > SavedCheckpoints (a Script)
-- Remembers the highest checkpoint each player reached, saves it, and starts them there next time.
local CollectionService = game:GetService("CollectionService")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("MapCheckpoints")
local REACH = 8 -- studs: this close to a checkpoint counts as reaching it
-- Index -> the checkpoint marker (checkpoints without an Index are skipped)
local checkpoints: { [number]: BasePart } = {}
for _, marker in CollectionService:GetTagged("BloxMapCheckpoint") do
local index = marker:GetAttribute("Index")
if marker:IsA("BasePart") and type(index) == "number" then
checkpoints[index] = marker
end
end
local best: { [Player]: number } = {} -- highest Index reached
local loaded: { [Player]: boolean } = {} -- only save what loaded fine
-- the Stage number on the leaderboard (made when missing)
local function stageValue(player: Player): IntValue
local stats = player:FindFirstChild("leaderstats")
if stats == nil then
stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
end
local value = stats:FindFirstChild("Stage")
if value == nil then
value = Instance.new("IntValue")
value.Name = "Stage"
value.Parent = stats
end
return value :: IntValue
end
local function reach(player: Player, index: number)
if index > (best[player] or 0) then
best[player] = index
stageValue(player).Value = index
end
end
-- start each life on the best checkpoint
local function onCharacter(player: Player, character: Model)
local checkpoint = checkpoints[best[player] or 0]
if checkpoint == nil then
return
end
character:WaitForChild("HumanoidRootPart", 10)
task.wait()
if character.Parent then
character:PivotTo(checkpoint.CFrame + Vector3.new(0, 4, 0))
end
end
local function save(player: Player)
if loaded[player] then
local key = "player_" .. player.UserId
local ok, err = pcall(function()
store:SetAsync(key, best[player] or 0)
end)
if not ok then
warn("Could not save checkpoint for " .. player.Name .. ": " .. tostring(err))
end
end
end
Players.PlayerAdded:Connect(function(player)
stageValue(player)
local ok, saved = pcall(function()
return store:GetAsync("player_" .. player.UserId)
end)
loaded[player] = ok
best[player] = math.max(best[player] or 0, if ok and type(saved) == "number" then saved else 0)
stageValue(player).Value = best[player]
player.CharacterAdded:Connect(function(character)
onCharacter(player, character)
end)
if player.Character then
onCharacter(player, player.Character)
end
end)
Players.PlayerRemoving:Connect(function(player)
save(player)
best[player] = nil
loaded[player] = nil
end)
game:BindToClose(function()
for _, player in Players:GetPlayers() do
save(player)
end
end)
-- check who is standing near a checkpoint, 4 times a second
while true do
task.wait(0.25)
for _, player in Players:GetPlayers() do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if root and root:IsA("BasePart") then
for index, checkpoint in checkpoints do
if (root.Position - checkpoint.Position).Magnitude <= REACH then
reach(player, index)
end
end
end
end
end- Publish your place (File > Publish to Roblox). DataStores only work in a published place.
- To test saving in Studio, open Game Settings > Security and turn on Enable Studio Access to API Services.
- Press Play, reach a few checkpoints, stop, and press Play again. You start on your last checkpoint.
Loot chests#
This script puts an Open prompt on every chest marker. Opening a chest gives Coins, and a player can open the same chest again after 5 minutes. Chests with a number Tier give more.
-- ServerScriptService > MapChests (a Script)
-- Adds an "Open" prompt to every chest marker. Opening one gives Coins; each player can open it again after a while.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local COINS = 25 -- coins per chest
local COOLDOWN = 300 -- seconds before the same player can open the same chest again
local function coinsValue(player: Player): IntValue
local stats = player:FindFirstChild("leaderstats")
if stats == nil then
stats = Instance.new("Folder")
stats.Name = "leaderstats"
stats.Parent = player
end
local value = stats:FindFirstChild("Coins")
if value == nil then
value = Instance.new("IntValue")
value.Name = "Coins"
value.Parent = stats
end
return value :: IntValue
end
local function setUpChest(chest: Instance)
if not chest:IsA("BasePart") then
return
end
local label = chest:GetAttribute("Label") -- the chest's name, when the map gave it one
local prompt = Instance.new("ProximityPrompt")
prompt.ActionText = "Open"
prompt.ObjectText = if type(label) == "string" then label else "Chest"
prompt.HoldDuration = 0.5
prompt.MaxActivationDistance = 10
prompt.RequiresLineOfSight = false -- the chest model itself must not hide the prompt
prompt.Parent = chest
local openedAt: { [Player]: number } = {}
prompt.Triggered:Connect(function(player)
local last = openedAt[player]
if last and os.clock() - last < COOLDOWN then
return
end
openedAt[player] = os.clock()
-- some maps give a Tier number (1, 2, 3): better chests, more coins
local tier = chest:GetAttribute("Tier")
local coins = if type(tier) == "number" then COINS * tier else COINS
coinsValue(player).Value += coins
end)
end
-- Loot markers (battle royale, horror...) work the same way: add "BloxMapLoot" to this list
for _, tag in { "BloxMapChest" } do
for _, chest in CollectionService:GetTagged(tag) do
setUpChest(chest)
end
end
Players.PlayerAdded:Connect(coinsValue)- The prompt shows the chest's
Labelwhen it has one (like "Royal Treasury"). - Loot spots on battle royale and horror maps have the tag
BloxMapLoot. Add it to the list near the bottom to give them prompts too. - The Coins here are only for this example. If your game already has a Coins value, use yours.
Zones#
Zone markers mark areas: simulator zones, a village square, fields. Round zones have a Radius attribute; box zones use the marker's size. This script checks twice a second which zone each player is in, sets the player attribute Zone to its name and calls onEnter and onLeave.
-- ServerScriptService > MapZones (a Script)
-- Knows which zone every player is in and tells you when it changes.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
-- is this point inside the zone? Round zones have a Radius, box zones use the marker's size
local function inside(zone: BasePart, point: Vector3): boolean
local radius = zone:GetAttribute("Radius")
if type(radius) == "number" then
local flat = Vector3.new(point.X - zone.Position.X, 0, point.Z - zone.Position.Z)
return flat.Magnitude <= radius
end
local localPoint = zone.CFrame:PointToObjectSpace(point)
local half = zone.Size / 2
return math.abs(localPoint.X) <= half.X and math.abs(localPoint.Z) <= half.Z
end
-- how big a zone is (to pick the smaller one when two overlap)
local function extent(zone: BasePart): number
local radius = zone:GetAttribute("Radius")
if type(radius) == "number" then
return radius
end
return math.max(zone.Size.X, zone.Size.Z) / 2
end
local function zoneName(zone: BasePart): string
local label = zone:GetAttribute("Label")
return if type(label) == "string" then label else zone.Name
end
local function onEnter(player: Player, zone: BasePart)
print(player.Name .. " entered " .. zoneName(zone))
-- Simulator zones also carry Index, Cost and Multiplier: check them here
end
local function onLeave(player: Player, zone: BasePart)
print(player.Name .. " left " .. zoneName(zone))
end
local current: { [Player]: BasePart? } = {}
Players.PlayerRemoving:Connect(function(player)
current[player] = nil
end)
while true do
task.wait(0.5)
local zones = CollectionService:GetTagged("BloxMapZone")
for _, player in Players:GetPlayers() do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
local found: BasePart? = nil
if root and root:IsA("BasePart") then
for _, zone in zones do
if zone:IsA("BasePart") and inside(zone, root.Position) then
-- zones can overlap (a village square inside the village): the smallest one wins
if found == nil or extent(zone) < extent(found) then
found = zone
end
end
end
end
local before = current[player]
if found ~= before then
if before then
onLeave(player, before)
end
if found then
onEnter(player, found)
end
current[player] = found
player:SetAttribute("Zone", if found then zoneName(found) else nil)
end
end
endPut your own code in onEnter: show the zone's name, play music, or check a simulator zone's Cost before the player may go in. A LocalScript can read the player's Zone attribute to show it on screen.
Map bounds#
The Bounds marker is a Model with an invisible box called Area (the play area) and four invisible walls that keep players on the map. This script is a safety net: if a player still ends up outside the area (a glitch, a fling, an exploit), it puts them back on a spawn inside the map.
-- ServerScriptService > MapBounds (a Script)
-- The map's invisible walls keep players in. This catches anyone who still gets out
-- (a glitch, a fling, a speed hack) and puts them back on a spawn.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
-- every play area: Bounds > Area (a world has one around all of its sections)
local areas: { BasePart } = {}
for _, bounds in CollectionService:GetTagged("BloxMapBounds") do
local area = bounds:FindFirstChild("Area")
if area and area:IsA("BasePart") then
table.insert(areas, area)
end
end
-- inside the area's square seen from above (sky spawns can start higher than the box)
local function insideAny(point: Vector3): boolean
for _, area in areas do
local localPoint = area.CFrame:PointToObjectSpace(point)
local half = area.Size / 2
if math.abs(localPoint.X) <= half.X and math.abs(localPoint.Z) <= half.Z then
return true
end
end
return false
end
-- a spawn inside the play area, of the player's team or a neutral one
local function backToSpawn(player: Player, character: Model)
local list = {}
for _, spawn in CollectionService:GetTagged("BloxMapSpawn") do
local mine = spawn:IsA("SpawnLocation") and (spawn.Neutral or spawn.TeamColor == player.TeamColor)
if mine and insideAny(spawn.Position) then
table.insert(list, spawn)
end
end
if #list > 0 then
local spawn = list[math.random(1, #list)]
character:PivotTo(spawn.CFrame + Vector3.new(0, 4, 0))
end
end
if #areas > 0 then
while true do
task.wait(1)
for _, player in Players:GetPlayers() do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
if character and root and root:IsA("BasePart") and not insideAny(root.Position) then
backToSpawn(player, character)
end
end
end
endIn a world, every section keeps its Area box and the world has one Bounds around everything, so the same script works there too.
Next steps#
- See every marker kind and attribute on Markers and spawn points.
- Give your game a UI with coins, shops and saving: Scripting your UI.