Maps

On this page

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:

Luau
-- 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())
end

Spawns#

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.

Luau
-- 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
end

Use 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.

Luau
-- 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
  1. Publish your place (File > Publish to Roblox). DataStores only work in a published place.
  2. To test saving in Studio, open Game Settings > Security and turn on Enable Studio Access to API Services.
  3. 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.

Luau
-- 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 Label when 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.

Luau
-- 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
end

Put 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.

Luau
-- 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
end

In 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.

© 2026 BloxMaps · bloxmaps.com · Terms · Privacy · Contact

BloxMaps is not affiliated with, endorsed by or sponsored by Roblox Corporation. Roblox and Roblox Studio are trademarks of Roblox Corporation.