Deaths on the map example
Record every death with its position and cause, then read hotspots on your map.
This example records where players die, why, and which stage they were on. It also logs stage starts and checkpoints, so the Spatial page can show deaths per attempt and where sessions end.
What you will see
- A marker for every death at its position, grouped into hotspots ranked by count.
- The cause of a death when you select its marker.
- Per stage: attempts, deaths, unique players, completion rate and quit after death.
Set up the parts in Studio
Tag parts in View → Tag Editor and add the attributes below.
| Tag | Attributes | What it does |
|---|---|---|
StageStart | StageId (string) | The first touch records stage_started and makes that stage the player's default. |
Checkpoint | StageId (string) | Records checkpoint_reached once per player. |
KillPart | Cause (string), for example Lava | Kills the player on touch and remembers why. |
The script
Create a server Script in ServerScriptService. It assumes you followed the Quick start.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")
local RoStructure = require(ServerScriptService.RoStructure)
local secrets = require(ServerStorage.RoStructureSecrets)
local analytics = RoStructure.init({
projectId = "my_game",
ingestionKey = secrets.ingestionKey,
buildId = "v" .. game.PlaceVersion,
endpoint = "https://api.rostructure.com",
-- We record deaths ourselves below, so each one can carry a cause.
autoTrack = { deaths = false },
})
local currentStage: { [Player]: string } = {}
local reachedCheckpoints: { [Player]: { [Instance]: boolean } } = {}
local function playerFromHit(hit: BasePart): (Player?, Model?)
local model = hit:FindFirstAncestorOfClass("Model")
local player = model and Players:GetPlayerFromCharacter(model)
return player, model
end
-- Parts tagged "StageStart" (attribute StageId) mark where a stage begins.
local function bindStageStart(part: Instance)
local stageId = part:GetAttribute("StageId")
if not part:IsA("BasePart") or type(stageId) ~= "string" then
return
end
part.Touched:Connect(function(hit)
local player = playerFromHit(hit)
if not player or currentStage[player] == stageId then
return
end
currentStage[player] = stageId
analytics:setStage(player, stageId)
analytics:track(player, "stage_started", { stageId = stageId, position = true })
end)
end
-- Parts tagged "Checkpoint" (attribute StageId) are recorded once per player.
local function bindCheckpoint(part: Instance)
local stageId = part:GetAttribute("StageId")
if not part:IsA("BasePart") or type(stageId) ~= "string" then
return
end
part.Touched:Connect(function(hit)
local player = playerFromHit(hit)
local reached = player and reachedCheckpoints[player]
if not player or not reached or reached[part] then
return
end
reached[part] = true
analytics:track(player, "checkpoint_reached", { stageId = stageId, position = true })
end)
end
-- Parts tagged "KillPart" (attribute Cause, for example "Lava") kill on touch.
local function bindKillPart(part: Instance)
if not part:IsA("BasePart") then
return
end
local cause = part:GetAttribute("Cause")
part.Touched:Connect(function(hit)
local _, model = playerFromHit(hit)
local humanoid = model and model:FindFirstChildOfClass("Humanoid")
if humanoid and humanoid.Health > 0 and model then
model:SetAttribute("LastCause", if type(cause) == "string" then cause else part.Name)
humanoid.Health = 0
end
end)
end
local function bindTagged(tag: string, bind: (Instance) -> ())
for _, instance in CollectionService:GetTagged(tag) do
bind(instance)
end
CollectionService:GetInstanceAddedSignal(tag):Connect(bind)
end
bindTagged("StageStart", bindStageStart)
bindTagged("Checkpoint", bindCheckpoint)
bindTagged("KillPart", bindKillPart)
-- One death event per life, with the position and the cause.
local function onCharacter(player: Player, character: Model)
local humanoid = character:WaitForChild("Humanoid", 10)
if not humanoid or not humanoid:IsA("Humanoid") then
return
end
humanoid.Died:Once(function()
local cause = character:GetAttribute("LastCause")
analytics:track(player, "player_died", {
position = true,
metadata = { cause = if type(cause) == "string" then cause else "Unknown" },
})
end)
end
local function onPlayerAdded(player: Player)
reachedCheckpoints[player] = {}
player.CharacterAdded:Connect(function(character)
onCharacter(player, character)
end)
if player.Character then
task.spawn(onCharacter, player, player.Character)
end
end
Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
onPlayerAdded(player)
end
Players.PlayerRemoving:Connect(function(player)
currentStage[player] = nil
reachedCheckpoints[player] = nil
end)Read the results
- Open Spatial and pick a stage. The 3D view shows deaths as markers and clusters them into hotspots.
- Select a marker to see its cause, stage and device. A red ring marks a death after which the player left the game.
- Choose Explore these sessions to send the visible sessions to Funnels.
- To draw your level under the markers, publish a map export for the same
buildId. See Spatial analytics.
Notes
- Why deaths are turned off in
autoTrack. The SDK's own death event has no cause. Recording deaths yourself, once per life, lets you attach one. - One event per life.
Died:Oncefires a single time for each character. - Falling into the void. The position is where the character was when it died, so the height can be very low, but the horizontal position still shows where the fall began.
- Keep causes short and consistent (for example
Lava, notTouched the lava at 4:32) so they are easy to compare.