GSmGpE6CwX2y9JjB25B8
We use cookies on this site to enhance your user experience

5 min

Keeping friends together and opposing teams ensures everyone has a fun, fair gameplay experience By writing code to balance teams, players can become friends over time, encouraging them to stay and play more. To implement the code in this article, we recommend knowledge of Lua tables and Roblox’s BindableEvent|BindableEvents.

Grouping up Friends

To keep track of groups of friends, write a script that checks if any friends are currently in-game. Use a table that will include lists of players who have at least one friend in common. Because this needs to be done each time a player joins the server, connect it to the Players.PlayerAdded event. A separate function named findMutualFriendGroups() will return that player’s groups of friends in the lobby.

local friendGroups = {}

-- Group up friends to keep them on the same team
local function groupFriends(player)
	-- Determines if we need to create another group table
	local groups = findMutualFriendGroups(player) 

end

-- When a player joins the server, try to group them with friends
game.Players.PlayerAdded:Connect(function (player)
	groupFriends(player)
end)

Finding Mutual Friend Groups

To find if the player has any friends in their game, call Player:IsFriendsWith() on every player currently in the lobby. You can do this by looking through players in every group within friendGroups. This function returns a table that will contain any groups of friends the player has on the server.

Add this code to the same script as the previous block of code.

-- Gather any groups who are friends with the player
local function findMutualFriendGroups(player)
	local mutualGroups = {}
	
	-- Iterate through friend groups
	for groupIndex, group in ipairs(friendGroups) do
		-- Iterate through friends found in groups
		for _, user in ipairs(group) do
			-- If the player is a mutual friend...
			if player:IsFriendsWith(user.UserId) then
				if (mutualGroups[group] == nil) then
					table.insert(mutualGroups, groupIndex)
				end
			end
		end
	end
	return mutualGroups
end

Having found and grouped friends, go back to work on groupFriends().

If the player has two unique groups of friends, combine them to form a single group of mutual friends by creating a new function named mergeGroups(). This new function will return the an index in friendGroups of the player’s mutual friend group that will be used to add the player to their friend group.

-- Group up friends to keep them on the same team
local function groupFriends(player)
	-- Determines if we need to create another group table
	local groups = findMutualFriendGroups(player)		
	
	if #groups > 0 then
		local groupIndex = groups[1]
		
		-- If the player has multiple friend groups, merge them together
		if #groups > 1 then
			<!groupIndex = mergeGroups(groups) !>
		end
     end
end

Merging Friend Groups

The mergeGroups() function goes through through groups of mutual friends and merges them into one group. Once combined, remove the old seperate groups and then return the new merged group back to the groupFriends() function.

-- Merge all mutual friend groups into one
local function mergeGroups(groups)
	-- Add other group members to the first group
	for i = 2, #groups do
		for _, user in pairs(friendGroups[groups[i]]) do
			table.insert(friendGroups[groups[1]], user)
		end
		-- Remove leftover groups that were merged
		table.remove(friendGroups, groups[i])
	end
	
	return groups[1]
end

Back in groupFriends(), if the player had a group of friends in the lobby, insert the player into the correct table. Otherwise, insert a new table containing only the player into friendGroups. To make sure the player’s team matches an existing group, include their group in the function call for assignInitialTeam(). This function will be written in the next section.

-- Group up friends to keep them on the same team 
local function groupFriends(player)
	-- Determines if we need to create another group table
	local groups = findMutualFriendGroups(player)		
	
	if #groups > 0 then
		local groupIndex = groups[1]
		
		-- If the player has multiple friend groups, merge them together
		if #groups > 1 then
			groupIndex = mergeGroups(groups)
		end

		<!table.insert(friendGroups[groupIndex], player) !>
		<!assignInitialTeam(player, friendGroups[groupIndex]) !>
	else
		<!table.insert(friendGroups, {player}) !>
		<!assignInitialTeam(player) !>
	end
end

Assigning Player Teams

If players are with friends, place them on the same team.

Start with a function named assignInitialTeam() with two parameters for the player and their potential group. If the function receives a group argument with a value, then the Player.Team property of the first entry in the friend group is used to assign the team. Otherwise, solo players will fill in the least populated team.

-- When players join the game, determine the correct team to join
local function assignInitialTeam(player, group)
	-- If the player belongs in a group
	if group then
		player.Team = group[1].Team
	else
		local teams = TeamsService:GetTeams()
	
	end
end

To interact with the game’s Teams, you will need to use TeamService. It’s recommended to keep services at the top of your scripts.

<!local TeamsService = game:GetService("Teams") !>

local friendGroups = {}

To find the least populated team, create a function near the top of the script called ascendingTeamSize. This will compare the Team:GetPlayers() values to sort the teams in ascending sizes.

-- Used instead of an anonymous function for Team sorting
local function ascendingTeamSize(a,b) 
	return #a:GetPlayers() < #b:GetPlayers() 
end

Now use the ascendingTeamSize sorting function to assign the player to the smallest team.

-- When players join the game, determine the correct team to join
local function assignInitialTeam(player, group)
	-- If the player belongs in a group
	if group then
		player.Team = group[1].Team
	else
		local teams = TeamsService:GetTeams()

		-- Sort the list of teams to have the smallest team first
		<!table.sort(teams, ascendingTeamSize) !>
		<!player.Team = teams[1] !>
	end
end

Handling Players Leaving

When a player leaves the game, you must make sure to remove them from the friendGroups table. Not doing so will cause the game to believe there are larger friend groups ingame than there actually is. In order to remove the player’s entry, iterate through the groups of friendGroups, searching for a user that matches the Player. Set the Player’s current index to nil. If the group the Player was in is now empty, set the group to nil as well.

Additionally, be sure to connect this function to the game.Players.PlayerRemoving event.

-- Clean up groups when a player leaves the game
local function removeFromGroup(player)
	-- Loop through the friend groups to find the player...
	for groupIndex, group in ipairs(friendGroups) do
		for userIndex, user in ipairs(group) do
			if user.Name == player.Name then
				-- ...remove them from whatever group they exist in
				friendGroups[groupIndex][userIndex] = nil
				
				-- If the group is empty, remove it
				if #friendGroups[groupIndex] == 0 then
					friendGroups[groupIndex] = nil
				end
			end
		end
	end
end

game.Players.PlayerRemoving:Connect(function (player)
	removeFromGroup(player)
end)

Balancing Teams

With friend groups already determined, let’s add a BindableEvent to be used when testing. Store the event in ReplicatedStorage. To keep up with Roblox conventions, add this code to the top of the script.

local TeamsService = game:GetService("Teams")
<!local ReplicatedStorage = game:GetService("ReplicatedStorage") !>

<!local balanceTeamsEvent = Instance.new("BindableEvent", ReplicatedStorage) !>
<!balanceTeamsEvent.Name = "BalanceTeamsEvent" !>

local friendGroups = {}

While you’ve balanced teams for groups of friends, it’s equally important to consider the game experience for individual players. To keep teams balanced with relatively equal sizes of friend groups, sort the friend groups before balancing in descending sizes. After each group is assigned a team, sort the teams table to find the team with the fewest players. The local function ascendingTeamSize can be used to sort teams this way.

When you’re iterating through groups of friends, if any groups are larger than half the game’s game.Players.MaxPlayers value, split up the friends into different teams. This breaks up massive teams that might cause a serious disadvantage for the other team. It also avoids having one team that would always be larger than the others.

--[[
	Occasionally teams need to be balanced. We don't want to break apart 
	friends on Roblox, so we'll try to equally balance groups of players 
	based on the size of the friend group.
--]]
local function balanceTeams()
	local teams = TeamsService:GetTeams()
	local function ascendingTeamSize(a,b) 
		return #a:GetPlayers() < #b:GetPlayers() end

	-- Sort the groups, so larger groups are first
	table.sort(friendGroups, function(a,b) return #a > #b end)

		-- Iterate through friendGroups (already sorted in ascending sizes)
	for i = 1, #friendGroups do
		if (#friendGroups[i] > #game.Players.MaxPlayers / 2) then
			for _, player in pairs(friendGroups[i]) do
				table.sort(teams, ascendingTeamSize)
				player.Team = teams[1]
			end
		else
			-- Sort the list of teams to have the smallest team first
			table.sort(teams, ascendingTeamSize)
			local groupTeam = teams[1]

			for _, player in pairs(friendGroups[i]) do
				player.Team = groupTeam
			end
		end
	end
end

Finally, connect the BalanceTeamEvent BindableEvent/Event|Event to the balanceTeams() function. This can be called when testing or to balance teams during an in-progress game. Write this at the bottom of your script.

balanceTeamsEvent.Event:Connect(balanceTeams)

In order to test the balanceTeams() function, you will need to BindableEvent/Fire|Fire the BindableEvent. It’s recommended to call this BindableEvent in between matches, but teams should decide on other conditions for when team balancing is required for their game.

Example Script

local TeamsService = game:GetService("Teams")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local balanceTeamsEvent = Instance.new("BindableEvent", ReplicatedStorage)
balanceTeamsEvent.Name = "BalanceTeamsEvent"

local friendGroups = {}

-- Used instead of an anonymous function for Team sorting
local function ascendingTeamSize(a,b) 
	return #a:GetPlayers() < #b:GetPlayers()
end

-- Gather any groups who are friends with the player
local function findMutualFriendGroups(player)
	local mutualGroups = {}
	
	-- Iterate through friend groups
	for groupIndex, group in ipairs(friendGroups) do
		-- Iterate through friends found in groups
		for _, user in ipairs(group) do
			-- If the player is a mutual friend...
			if player:IsFriendsWith(user.UserId) then
				if (mutualGroups[group] == nil) then
					table.insert(mutualGroups, groupIndex)
				end
			end
		end
	end
	return mutualGroups
end

-- Merge all mutual friend groups into one
local function mergeGroups(groups)
	-- Add other group members to the first group
	for i = 2, #groups do
		for _, user in pairs(friendGroups[groups[i]]) do
			table.insert(friendGroups[groups[1]], user)
		end
		-- Remove leftover groups that were merged
		table.remove(friendGroups, groups[i])
	end
	
	return groups[1]
end

-- When players join the game, determine the correct team to join
local function assignInitialTeam(player, group)
	-- If the player belongs in a group
	if group then
		player.Team = group[1].Team
	else
		local teams = TeamsService:GetTeams()
		-- Sort the list of teams to have the smallest team first
		table.sort(teams, ascendingTeamSize)
		player.Team = teams[1]
	end
end

-- Group up friends to keep them on the same team
local function groupFriends(player)
	-- Determines if we need to create another group table
	local groups = findMutualFriendGroups(player)		
	
	if #groups > 0 then
		local groupIndex = groups[1]
		
		-- If the player has multiple friend groups, merge them together
		if #groups > 1 then
			groupIndex = mergeGroups(groups)
		end

		table.insert(friendGroups[groupIndex], player)
		assignInitialTeam(player, friendGroups[groupIndex])
	else
		table.insert(friendGroups, {player})
		assignInitialTeam(player)
	end
end

-- Clean up groups when a player leaves the game
local function removeFromGroup(player)
	-- Loop through the friend groups to find the player...
	for groupIndex, group in ipairs(friendGroups) do
		for userIndex, user in ipairs(group) do
			if user.Name == player.Name then
				-- ...remove them from whatever group they exist in
				friendGroups[groupIndex][userIndex] = nil
				
				-- If the group is empty, remove it
				if #friendGroups[groupIndex] == 0 then
					friendGroups[groupIndex] = nil
				end
			end
		end
	end
end

--[[
	Occasionally teams need to be balanced. We don't want to break apart 
	friends on Roblox, so we'll try to equally balance groups of players 
	based on the size of the friend group.
--]]
local function balanceTeams()
	local teams = TeamsService:GetTeams()

	-- Sort the groups, so larger groups are first
	table.sort(friendGroups, function(a,b) return #a > #b end)
	
	-- Iterate through friendGroups (already sorted in ascending sizes)
	for i = 1, #friendGroups do
		if (#friendGroups[i] > game.Players.MaxPlayers / 2) then
			for _, player in pairs(friendGroups[i]) do
				table.sort(teams, ascendingTeamSize)
				player.Team = teams[1]
			end
		else
			-- Sort the list of teams to have the smallest team first
			table.sort(teams, ascendingTeamSize)
			local groupTeam = teams[1]

			for _, player in pairs(friendGroups[i]) do
				player.Team = groupTeam
			end
		end
	end
end

game.Players.PlayerAdded:Connect(function (player)
	groupFriends(player)
end)

game.Players.PlayerRemoving:Connect(function (player)
	removeFromGroup(player)
end)

balanceTeamsEvent.Event:Connect(balanceTeams)