Tutorial · Gameplay

How to make teams in Roblox Studio

Teams split players into sides with their own colour, spawn point and name list. Roblox does most of it with no code: add Team objects and lock SpawnLocations to them. This guide sets that up, then adds what games usually want next: a picker so players choose their side, a server check that keeps the teams even, and doors only one team can pass.

Updated 23 October 20266 min readBeginnerLuau

In short

Insert Team objects into the Teams service and give each a name and a unique TeamColor. For each team, add a SpawnLocation, untick Neutral and set its TeamColor to the team's. Players then spawn at their team's spawn. To let players choose, untick AutoAssignable and set player.Team from a server Script when they press a button.

  • With AutoAssignable on, Roblox puts each new player on the auto-assignable team with the fewest players.
  • Change player.Team on the server, then respawn the player so they move to their new spawn.
  • A SpawnLocation's TeamColor is what links it to a team, not its BrickColor.

Get the script

Team picker server script with balancing

A Script in ServerScriptService that lets players pick a team, refuses picks that would make teams uneven, and respawns them at their team spawn.

Script in ServerScriptService named TeamManager
-- Script in ServerScriptService named TeamManager.
-- Lets players pick a team from the Teams service, keeps teams even, and respawns
-- the player at their team's SpawnLocation.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Teams = game:GetService("Teams")

local MAX_DIFFERENCE = 1 -- a team may have at most this many more players than the smallest
local SWITCH_COOLDOWN = 5 -- seconds between team changes for one player

local chooseTeam = Instance.new("RemoteEvent")
chooseTeam.Name = "ChooseTeam"
chooseTeam.Parent = ReplicatedStorage

local lastSwitch: { [Player]: number } = {}

-- Only teams with AutoAssignable off are offered in the picker
local function pickableTeams(): { Team }
	local list: { Team } = {}
	for _, child in Teams:GetChildren() do
		if child:IsA("Team") and not child.AutoAssignable then
			table.insert(list, child)
		end
	end
	return list
end

local function wouldUnbalance(team: Team, player: Player): boolean
	local smallest = math.huge
	for _, other in pickableTeams() do
		local count = #other:GetPlayers()
		if other == player.Team then
			count -= 1 -- the player is leaving that team
		end
		smallest = math.min(smallest, count)
	end
	return #team:GetPlayers() + 1 - smallest > MAX_DIFFERENCE
end

chooseTeam.OnServerEvent:Connect(function(player: Player, teamName: unknown)
	if typeof(teamName) ~= "string" then
		return
	end
	local team = Teams:FindFirstChild(teamName)
	if not (team and team:IsA("Team")) or team.AutoAssignable or player.Team == team then
		return
	end
	local now = os.clock()
	if now - (lastSwitch[player] or -math.huge) < SWITCH_COOLDOWN then
		return
	end
	if wouldUnbalance(team, player) then
		return
	end
	lastSwitch[player] = now
	player.Team = team
	player.Neutral = false
	-- Respawn so the player appears at a SpawnLocation with their team's TeamColor
	player:LoadCharacterAsync()
end)

Players.PlayerRemoving:Connect(function(player: Player)
	lastSwitch[player] = nil
end)

RoCode builds it in the place you have open in Roblox Studio, on Windows or macOS with a desktop browser; on a phone the button sends the prompt and the link to your computer. Free to start, with free credits every day. More Roblox Studio scripts

Where to put it

Explorer: two teams, the server script and the picker
Teams
  Red Team (AutoAssignable off)
  Blue Team (AutoAssignable off)
ServerScriptService
  TeamManager Script new
StarterPlayer
  StarterPlayerScripts
    TeamPicker LocalScript new

Settings you can change

MAX_DIFFERENCEIn the script
How many more players a team may have than the smallest one before it refuses new players. Default: 1.
SWITCH_COOLDOWNIn the script
Seconds a player must wait between team changes. Default: 5.

Use it with the TeamPicker LocalScript from Step 3, which draws a button for each team.

What a cheater can and cannot doThe server checks the team exists, is pickable, is not full and that the player is not switching too often. A cheater can send any team name, but only these checks decide.

Type-checked with luau-lsp strict against Roblox's API definitions on 23 October 2026. Not playtested in Studio yet: press Play and watch the Output window before you publish.

Prompt for RoCode

Add a team system: Red and Blue teams with their own SpawnLocations, a team picker at the top of the screen with a button per team showing the player count, and a server Script that only allows a switch when it keeps teams within 1 player of each other, then respawns the player.

On this page
  1. Get the script
  2. How it works
  3. 1. Teams and spawns
  4. 2. Server
  5. 3. Picker
  6. 4. Team doors
  7. Common errors
  8. With RoCode
  9. Questions
  10. Sources

How teams work in Roblox

The Teams service is empty in a new place. Each Team you put in it appears in the player list as a heading in its colour, and players on it get a name tag in that colour. player.Team says which team a player is on; player.Neutral is true when they are on none.

The properties that matter
PropertyWhat it does
Team.TeamColorThe team's colour. It must be different for each team: it also links SpawnLocations to the team.
Team.AutoAssignableOn: new players are put on whichever auto-assignable team has the fewest players. Off: only a script puts players on it.
SpawnLocation.NeutralOn: anyone spawns there. Off: only the team with the matching TeamColor.
SpawnLocation.TeamColorWhich team spawns there, when Neutral is off.

Step 1: Add the teams and their spawns

  1. In the Explorer, hover over Teams, click + and insert a Team. Name it Red and set its TeamColor to a red such as Bright red.
  2. Add a second Team named Blue with a blue TeamColor such as Bright blue.
  3. To let Roblox share players out, leave AutoAssignable ticked and you are done with this step. To let players choose (Steps 2 and 3), untick AutoAssignable on both.
  4. Insert a SpawnLocation in each team's base. Untick Neutral and set its TeamColor to the same colour as the team.
  5. Keep one neutral SpawnLocation in a lobby for players who have not picked yet.

Press Play now with AutoAssignable on, and you spawn on a team's spawn with your name in its colour.

Step 2: Let players pick, on the server

The choice has to be made on the server, because player.Team set by a LocalScript only changes that player's own screen. Insert a Script in ServerScriptService named TeamManager:

Script in ServerScriptService named TeamManager
-- Script in ServerScriptService named TeamManager.
-- Lets players pick a team from the Teams service, keeps teams even, and respawns
-- the player at their team's SpawnLocation.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Teams = game:GetService("Teams")

local MAX_DIFFERENCE = 1 -- a team may have at most this many more players than the smallest
local SWITCH_COOLDOWN = 5 -- seconds between team changes for one player

local chooseTeam = Instance.new("RemoteEvent")
chooseTeam.Name = "ChooseTeam"
chooseTeam.Parent = ReplicatedStorage

local lastSwitch: { [Player]: number } = {}

-- Only teams with AutoAssignable off are offered in the picker
local function pickableTeams(): { Team }
	local list: { Team } = {}
	for _, child in Teams:GetChildren() do
		if child:IsA("Team") and not child.AutoAssignable then
			table.insert(list, child)
		end
	end
	return list
end

local function wouldUnbalance(team: Team, player: Player): boolean
	local smallest = math.huge
	for _, other in pickableTeams() do
		local count = #other:GetPlayers()
		if other == player.Team then
			count -= 1 -- the player is leaving that team
		end
		smallest = math.min(smallest, count)
	end
	return #team:GetPlayers() + 1 - smallest > MAX_DIFFERENCE
end

chooseTeam.OnServerEvent:Connect(function(player: Player, teamName: unknown)
	if typeof(teamName) ~= "string" then
		return
	end
	local team = Teams:FindFirstChild(teamName)
	if not (team and team:IsA("Team")) or team.AutoAssignable or player.Team == team then
		return
	end
	local now = os.clock()
	if now - (lastSwitch[player] or -math.huge) < SWITCH_COOLDOWN then
		return
	end
	if wouldUnbalance(team, player) then
		return
	end
	lastSwitch[player] = now
	player.Team = team
	player.Neutral = false
	-- Respawn so the player appears at a SpawnLocation with their team's TeamColor
	player:LoadCharacterAsync()
end)

Players.PlayerRemoving:Connect(function(player: Player)
	lastSwitch[player] = nil
end)

The script creates a RemoteEvent named ChooseTeam for the picker to fire. Every request is checked: the name must be a team that is not auto-assignable, the player must not have switched in the last 5 seconds, and joining must not leave that team more than MAX_DIFFERENCE players bigger than the smallest. If it passes, the server sets the team and calls LoadCharacterAsync, which respawns the player at their new team's spawn.

Step 3: The team picker buttons

Insert a LocalScript in StarterPlayer > StarterPlayerScripts named TeamPicker. It draws one button per pickable team, in the team's colour, with the current player count:

LocalScript in StarterPlayerScripts named TeamPicker
-- LocalScript in StarterPlayerScripts named TeamPicker.
-- One button per pickable team, in the team's colour, with a live player count.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Teams = game:GetService("Teams")

local chooseTeam = ReplicatedStorage:WaitForChild("ChooseTeam") :: RemoteEvent
local player = Players.LocalPlayer

local gui = Instance.new("ScreenGui")
gui.Name = "TeamPicker"
gui.ResetOnSpawn = false
local bar = Instance.new("Frame")
bar.AnchorPoint = Vector2.new(0.5, 0)
bar.Position = UDim2.new(0.5, 0, 0, 12)
bar.Size = UDim2.fromOffset(0, 44)
bar.AutomaticSize = Enum.AutomaticSize.X
bar.BackgroundTransparency = 1
bar.Parent = gui
local layout = Instance.new("UIListLayout")
layout.FillDirection = Enum.FillDirection.Horizontal
layout.Padding = UDim.new(0, 8)
layout.Parent = bar
gui.Parent = player:WaitForChild("PlayerGui")

local buttons: { [Team]: TextButton } = {}

local function refresh()
	for team, button in buttons do
		local count = #team:GetPlayers()
		local mine = player.Team == team
		button.Text = string.format("%s%s (%d)", if mine then "> " else "", team.Name, count)
	end
end

local function addButton(team: Instance)
	if not team:IsA("Team") or team.AutoAssignable then
		return
	end
	local button = Instance.new("TextButton")
	button.Size = UDim2.fromOffset(150, 44)
	button.BackgroundColor3 = team.TeamColor.Color
	button.TextColor3 = Color3.new(1, 1, 1)
	button.Font = Enum.Font.GothamBold
	button.TextSize = 18
	local corner = Instance.new("UICorner")
	corner.CornerRadius = UDim.new(0, 10)
	corner.Parent = button
	button.Activated:Connect(function()
		chooseTeam:FireServer(team.Name)
	end)
	button.Parent = bar
	buttons[team] = button
	team.PlayerAdded:Connect(refresh)
	team.PlayerRemoved:Connect(refresh)
	refresh()
end

for _, team in Teams:GetChildren() do
	addButton(team)
end
Teams.ChildAdded:Connect(addButton)
player:GetPropertyChangedSignal("Team"):Connect(refresh)

The counts update from each team's PlayerAdded and PlayerRemoved events, and a > marks your own team. To restyle the buttons, change the properties in addButton, or see the UI animations guide to make them grow on hover.

Step 4: Doors only one team can pass

A team base often has a door only its own team can walk through. Each player's device simulates their own character, so a LocalScript that turns off a door's collision on that device lets that player through, while the door stays solid for everyone else.

  1. Make the door part, tag it TeamDoor, and add a String attribute named Team with the team's name, such as Red.
  2. Insert a LocalScript named TeamDoors in StarterPlayerScripts:
LocalScript in StarterPlayerScripts named TeamDoors
-- LocalScript in StarterPlayerScripts named TeamDoors.
-- Parts tagged "TeamDoor" with a "Team" attribute let only that team walk through.
local CollectionService = game:GetService("CollectionService")
local Players = game:GetService("Players")

local player = Players.LocalPlayer

local function update(door: Instance)
	if not door:IsA("BasePart") then
		return
	end
	local allowed = door:GetAttribute("Team")
	local mine = player.Team and player.Team.Name
	-- Each device simulates its own character, so turning collision off here
	-- lets this player through while the door stays solid for everyone else
	door.CanCollide = not (mine ~= nil and allowed == mine)
end

local function updateAll()
	for _, door in CollectionService:GetTagged("TeamDoor") do
		update(door)
	end
end

CollectionService:GetInstanceAddedSignal("TeamDoor"):Connect(update)
player:GetPropertyChangedSignal("Team"):Connect(updateAll)
updateAll()

Test it

  1. Start a 2-player test (Test tab, Clients and Servers, 2 players, Start).
  2. On Player1, press Red: Player1 respawns at the red spawn and the button shows (1).
  3. On Player2, press Red too: nothing happens, because Red would be 2 against 0. Press Blue: it works.
  4. Walk Player1 into the blue door: it is solid. Walk into the red door: you pass.

Common errors and fixes

Check the Output window on the Server view first.

Players do not spawn at their team's spawn

The SpawnLocation's Neutral must be off and its TeamColor must match the team's TeamColor exactly. BrickColor and Color do not count.

The picker shows no buttons

TeamPicker only shows teams with AutoAssignable off. Untick it on the teams players should choose.

Pressing a team button does nothing

The switch would make teams uneven, or you switched less than 5 seconds ago. In a solo test there is no one on the other team, so the first pick always works.

Two teams share a colour

Every team needs its own TeamColor; spawns and name colours use it to tell teams apart.

Build it with RoCode

If you would rather not set this up by hand, RoCode can build it in the place you have open. RoCode is an independent AI agent for Roblox Studio, not made by Roblox: you describe the change in a web chat, and its Studio plugin creates the teams, spawns and scripts in your Explorer.

You type

Add Red and Blue teams with a spawn in each base, a team picker at the top of the screen that keeps teams even, and a door at each base that only that team can walk through.

RoCode does
  • Creates the Team objects, team SpawnLocations and door parts with their tags and attributes
  • Writes the server script and both LocalScripts, and is told to compile-check each with Check Script
  • Sets undo points around each batch of changes

After a build, RoCode can run its test robot: it plays your game in Studio, clicks buttons and walks into parts, then reports PASS or FAIL. It checks only what it chose to test and plays as one player, so check the balancing with a 2-player test yourself. How RoCode connects to Studio.

Questions

How do I make teams in Roblox Studio?

Insert Team objects into the Teams service, give each a name and a unique TeamColor, and add a SpawnLocation per team with Neutral off and a matching TeamColor.

How do I change a player's team with a script?

On the server, set player.Team = Teams.Red. Then respawn them with LoadCharacterAsync if they should move to the new team's spawn.

How do I stop friendly fire?

In your damage script, skip the hit when attacker.Team == target.Team, using Players:GetPlayerFromCharacter to find both players. The combat system guide marks where that check goes.

Why does everyone end up on the same team?

Only one team has AutoAssignable on, or players are switching to the bigger team. Turn AutoAssignable on for all teams, or use the balancing check in Step 2.

Sources and further reading

How the code was checked: every script on this page passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 23 October 2026). A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.

Try RoCode on your own place.

Free every day, no card required. Install the plugin, describe the feature, and review what lands in Studio.