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.Teamon 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.
-- 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
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
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.
| Property | What it does |
|---|---|
Team.TeamColor | The team's colour. It must be different for each team: it also links SpawnLocations to the team. |
Team.AutoAssignable | On: new players are put on whichever auto-assignable team has the fewest players. Off: only a script puts players on it. |
SpawnLocation.Neutral | On: anyone spawns there. Off: only the team with the matching TeamColor. |
SpawnLocation.TeamColor | Which team spawns there, when Neutral is off. |
Step 1: Add the teams and their spawns
- In the Explorer, hover over Teams, click + and insert a Team. Name it
Redand set its TeamColor to a red such as Bright red. - Add a second Team named
Bluewith a blue TeamColor such as Bright blue. - 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.
- Insert a SpawnLocation in each team's base. Untick Neutral and set its TeamColor to the same colour as the team.
- 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.
-- 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.
-- 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.
- Make the door part, tag it
TeamDoor, and add a String attribute namedTeamwith the team's name, such asRed. - Insert a LocalScript named
TeamDoorsin StarterPlayerScripts:
-- 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
- Start a 2-player test (Test tab, Clients and Servers, 2 players, Start).
- On Player1, press Red: Player1 respawns at the red spawn and the button shows (1).
- On Player2, press Red too: nothing happens, because Red would be 2 against 0. Press Blue: it works.
- 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.
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.
- 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
- Roblox Creator Docs: Implement teams. adding teams, auto-assign and team spawn locations
- Roblox Creator Docs: Team. AutoAssignable, TeamColor and the player events
- Roblox Creator Docs: SpawnLocation. Neutral and TeamColor
- Roblox Creator Docs: Network ownership. why a client can change its own character's collisions
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.