Create a Checkpoint System

Checkpoints for obbies that only require scripting one time.

by MooMooManager

Author Avatar

This is a tutorial on how to create a simple to use checkpoint system. When finished, creating new checkpoints should be very easy, and all of your code will be in accessible locations for modification!


Setting up the place

To start off, we'll need to create a few things inside of the game so that our scripts can work properly.

First, inside of workspace, create a folder named Checkpoints. This folder will store all of the checkpoints in one place so that the scripts can access it more easily.

Next, for the checkpoints themselves. As for building them, the only requirement is that they are an anchored part. Otherwise, you can customize their other properties however you like. All the checkpoints should be placed inside of the checkpoints folder, and they should be named based on which stage number they will represent. This means that the first checkpoint should be named 1, the second checkpoint should be named 2, and so on.

Finally, add a RemoteEvent named updateStage inside of ReplicatedStorage. If you don't know what a remote event is, it's an object that allows the server and the players to send specific signals to each other, which will be useful for when we want the player's stage number to be updated.

Now, your place should look something like this:

image|1024x1024

Studio tip: You can use the keyboard shortcut Ctrl + D to duplicate an object. This can be useful for making new checkpoints because the duplicated object will stay in the same folder, unlike with copy/pasting!

Creating the scripts

In order for this system to work, we'll have to program different scripts to do accomplish different tasks. Namely:

The first two tasks can easily be done in one script. We can use Players.PlayerAdded to detect when to make the stage number for a new player, and then use Player.CharacterAdded to move their character when respawning.

In ServerScriptService, create a script with an appropriate name like playerSetup, and add this code:

local players = game:GetService("Players")
-- 'game:GetService("Players")' and 'game.Players' are both acceptable.

-- This function fires whenever a player joins
players.PlayerAdded:Connect(function(newPlayer)
	
	-- Creating a folder to display statistics on the leaderboard
	local leaderstatsFolder = Instance.new("Folder")
	leaderstatsFolder.Name = "leaderstats"
	leaderstatsFolder.Parent = newPlayer
	-- Creating a integer value to keep track of the player's stage
	local stage = Instance.new("IntValue")
	stage.Name = "Stage"
	stage.Value = 1 -- Setting the value here so the default is stage 1
	stage.Parent = leaderstatsFolder
	
	-- Now we create a function which fires every time the player spawns
	-- The purpose of this function is to send the player back to their respective checkpoint
	newPlayer.CharacterAdded:Connect(function(character)
		
		-- Converting their stage number to use as a string
		local stageNumberString = tostring(stage.Value)
		-- Getting the correct checkpoint block inside of the checkpoints folder
		local checkpoint: BasePart = workspace.Checkpoints:FindFirstChild(stageNumberString)
		
		-- Wait for a bit, otherwise the character might not be loaded
		task.wait(0.5)
		
		-- Finding the "Head" part of the character.
		-- The head is one of the only parts which is in both R15 and R6 characters.
		-- Also set the timeout parameter of WaitForChild to 3 seconds, just to be sure it loads
		local head: BasePart = character:WaitForChild("Head", 3)
		-- Moving the player's head to the checkpoint by changing the CFrame.
		-- Note that a bit of Y height is added so the player isn't moved inside of the block
		head.CFrame = checkpoint.CFrame + Vector3.new(0, 4.5 + checkpoint.Size.Y / 2, 0)
		
	end)
end)

A couple of extra details about this script:

Now, onto the third task. If we want to be efficient, we'll actually have to split the task into two different scripts. The first script will be controlled by the player and will detect when the player should change stages, and the second script will be controlled by the server and will validate the player's stage change.

We'll use the remote event created earlier to control these scripts, and we'll use Humanoid.Touched to detect checkpoint collisions on the player's side.

Inside of StarterPlayer -> StarterCharacterScripts, create a LocalScript with an appropriate name like detectCheckpoints, and add this code:

local replicatedStorage = game:GetService("ReplicatedStorage")
-- 'game:GetService("ReplicatedStorage")' and 'game.ReplicatedStorage' are both acceptable.

local localPlayer = game.Players.LocalPlayer
local humanoid: Humanoid = script.Parent:WaitForChild("Humanoid")

humanoid.Touched:Connect(function(otherPart)
	
	-- Ignore touched events from parts outside of the checkpoint folder, or if the player is dead
	if otherPart.Parent ~= workspace.Checkpoints or humanoid.Health <= 0 then return end
	
	-- Convert the checkpoint part's name to a number
	local checkpointNumber = tonumber(otherPart.Name)
	-- Get the stage number of the player running this script
	local currentCheckpoint = localPlayer.leaderstats.Stage.Value
	
	-- Check if the checkpoint number is one more than the player's stage number
	if checkpointNumber == currentCheckpoint + 1 then
		-- Send a request to the server update the player's stage number to the next one
		replicatedStorage.updateStage:FireServer(checkpointNumber)
	end
	
end)

A few extra details about this script:

The second script for the third task will be made to respond to stage change requests. For security purposes, it'll also re-verify some of the things that the player checked before sending the change request.

In ServerScriptService, create a script with an appropriate name like stageHandler, and add this code:

local replicatedStorage = game:GetService("ReplicatedStorage")

replicatedStorage.updateStage.OnServerEvent:Connect(function(player: Player, checkpointNumber: number)
	
	-- Getting the player's character and the checkpoint from the number given
	local character = player.Character
	local checkpoint: BasePart = workspace.Checkpoints:FindFirstChild(tostring(checkpointNumber))
	
	-- Verify that the character exists, the checkpoint exists and the character is alive before proceeding
	if character and checkpoint and character:FindFirstChild("Humanoid").Health > 0 then
		
		-- Change the player's stage number if it's one lower than the checkpoint number
		if checkpointNumber == player.leaderstats.Stage.Value + 1 then
			player.leaderstats.Stage.Value = checkpointNumber
		end
	end
end)

All done!

By this point, the checkpoints should work properly and be able to respawn the player on the correct checkpoint. If something doesn't work, make sure everything is in the correct location and has the correct name (uppercase letters will matter for names).

Also remember to use the shortcut Ctrl + D to duplicate checkpoints, which makes it easier to make new ones since you don't need to move them into the checkpoint folder.

Hopefully you found this tutorial useful!

image|1024x1024


Appendix: Who controls touch detection?

This appendix only discusses a design choice made when creating the checkpoint system. Nothing is added to the system in this section, so feel free to stop reading.

When making a checkpoint system based around collisions, how do you manage collision checking? Do you add touch detection to the checkpoints and wait for a player to touch it, or do you add touch detection to the player and wait for them to touch a checkpoint?

If you had asked me this question back in 2020, I would've answered with adding touch detection to the checkpoints. Now, however, I would say to add touch detection to the player (hence this tutorial). What's the difference?

Well, as it turns out, it takes a bit of processing power to have a touched event. This means that the more touched events you have, the slower your game becomes. This is unnoticeable with a small amount of them (maybe less than 100, I would say), but it can certainly become problematic with thousands of them.

...This of course leads to the idea of having a 5000 stage easy obby with a touched event for each checkpoint. That doesn't sound good. With only around 10 body parts per player, having player based touched events seems more appealing.

Okay, so now you're planning to have player based touched events. But here's another question: Do you let the server handle them, or the player?

You might decide that controlling them via the server is better, since you don't have to further complicate your scripts by adding player to server communication. That's certainly justified! But in my opinion, controlling it on the player's side is better since it reduces the server's workload. Sometimes, you want to ensure that the server is running quickly and properly at the expense of making the players do some of the work. Which is why, in the end, I chose to make this tutorial use client-sided touch detection.


This tutorial was created in August 2020. This tutorial was last edited in October 2023.

View in-game to comment, award, and more!