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

Team Balancing

Team Balancing

Dec 13 2019, 12:25 PM PST 5 min

Keeping friends together and balancing 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.

Grouping Friends

To keep track of groups of friends, start by including a Script in ServerScriptService. In its code, start by including a function that checks if any friends are currently in game, then connect it to the Players/PlayerAdded event.

local friendGroups = {}

-- Group friends to keep them on the same team
local function groupFriends(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
			-- Group mutual friends together
			if player:IsFriendsWith(user.UserId) then
				if (mutualGroups[group] == nil) then
					table.insert(mutualGroups, groupIndex)
				end
			end
		end
	end

	if #mutualGroups > 0 then
		local groupIndex = mutualGroups[1]
 
		-- If the player has multiple groups of friends playing, merge into one group
		if #mutualGroups > 1 then
			groupIndex = mergeGroups(mutualGroups)
		end
 
		table.insert(friendGroups[groupIndex], player)
		assignInitialTeam(player, friendGroups[groupIndex])
	else
		table.insert(friendGroups, {player})
		assignInitialTeam(player)
	end
end

-- Connect "PlayerAdded" events to the "groupFriends()" function
game.Players.PlayerAdded:Connect(groupFriends)

This code does the following:

  1. On line 1, the friendGroups table will store lists of players who have at least one friend in common.
  2. To find if the incoming player has any friends in the game, players in every group within friendGroups are queried with Player/IsFriendsWith|Player:IsFriendsWith(). Each group containing at least one friend of the incoming player are stored in the mutualGroups table.
  3. If multiple groups containing friends are located, those groups are first merged with the mergeGroups() helper function (defined in Combining Friend Groups) and then the player is assigned to the group. Otherwise, if no friends are found, the player is assigned to a new group.
  4. Once players have been associated with their online friends, the helper function assignInitialTeam() (defined in Assigning Player Teams) is used to place them on a team.

Combining Friend Groups

To combine multiple groups containing mutual friends into one larger group, add the following mergeGroups() helper function directly above the groupFriends() function. This function inspects groups of mutual friends, merges them into one group, removes the old separate groups, and then returns 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

Assigning Player Teams

If players are with friends, they should initially be placed on the same team. To do this:

  1. Get TeamService to interact with the game’s teams. It’s recommended to keep services at the top of your scripts.
local TeamsService = game:GetService("Teams")

local friendGroups = {}
  1. Below the mergeGroups() function, add a function named assignInitialTeam() which accepts parameters for the player and his/her potential group. If the function receives a group argument and the group isn’t unfairly large, 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)
	-- Check if the player belongs in a group and the group isn't larger than a fair team size
	if group and #group < game.Players.MaxPlayers / 2 then
		player.Team = group[1].Team
	else
		local teams = TeamsService:GetTeams()
		table.sort(teams, function(a, b) return #a:GetPlayers() < #b:GetPlayers() end)
		player.Team = teams[1]
	end
end

Handling Players Leaving

When a player leaves the game, you need to remove them from the friendGroups table. Not doing so will cause the game to believe there are larger friend groups in the game than there actually are.

In order to remove the player’s entry, iterate through friendGroups, searching for a user that matches the player, and set that player’s current index to nil. If the player’s previous group is then empty, set the group to nil as well. Finally, make sure you connect the function to the 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

-- Connect "PlayerRemoving" event to the "removeFromGroup()" function
game.Players.PlayerRemoving:Connect(removeFromGroup)

-- Connect "PlayerAdded" events to the "groupFriends()" function
game.Players.PlayerAdded:Connect(groupFriends)

Rebalancing Teams

With friend groups already determined, add a BindableEvent within ReplicatedStorage to be used when testing and to rebalance teams when needed. 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")
balanceTeamsEvent.Name = "BalanceTeamsEvent"
balanceTeamsEvent.Parent = ReplicatedStorage

local friendGroups = {}

Next, to keep teams balanced with relatively equal sizes of friend groups, add the following balanceTeams() function below removeFromGroup():

local function balanceTeams()
	local teams = TeamsService:GetTeams()

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

	-- Iterate through friend groups (already sorted from largest to smallest)
	for i = 1, #friendGroups do
		-- If the group is too big, split them into both teams
		if (#friendGroups[i] > game.Players.MaxPlayers / 2) then
			for _, player in pairs(friendGroups[i]) do
				table.sort(teams, function(a, b) return #a:GetPlayers() < #b:GetPlayers() end)
				player.Team = teams[1]
			end
		else
			-- Sort the list of teams to have the smallest team first
			table.sort(teams, function(a, b) return #a:GetPlayers() < #b:GetPlayers() end)
			local groupTeam = teams[1]

			-- Place everyone in the group on the same team
			for _, player in pairs(friendGroups[i]) do
				player.Team = groupTeam
			end
		end
	end
end

This function does the following:

  1. To keep teams balanced with relatively equal sizes of friend groups, friendGroups is sorted from largest group to smallest group (line 90).
  2. Once sorted, friendGroups is iterated through again. If any groups are larger than half the game’s Players/MaxPlayers value, those friends are split apart and assigned to smaller teams. Otherwise, the teams are simply sorted in ascending order and the group is assigned to the smallest team.

Now connect the BalanceTeamsEvent bindable BindableEvent/Event|Event to the balanceTeams() function and, to use the balancing functionality, BindableEvent/Fire|Fire the event. It’s recommended to fire the event between matches, but you might decide on other conditions when team balancing is required.

-- Connect "PlayerRemoving" event to the "removeFromGroup()" function
game.Players.PlayerRemoving:Connect(removeFromGroup)

-- Connect "PlayerAdded" events to the "groupFriends()" function
game.Players.PlayerAdded:Connect(groupFriends)

balanceTeamsEvent.Event:Connect(balanceTeams)

Complete Script

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

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

local friendGroups = {}

-- 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)
	-- Check if the player belongs in a group and the group isn't larger than a fair team size
	if group and #group < game.Players.MaxPlayers / 2 then
		player.Team = group[1].Team
	else
		local teams = TeamsService:GetTeams()
		table.sort(teams, function(a, b) return #a:GetPlayers() < #b:GetPlayers() end)
		player.Team = teams[1]
	end
end

-- Group friends to keep them on the same team
local function groupFriends(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
			-- Group mutual friends together
			if player:IsFriendsWith(user.UserId) then
				if (mutualGroups[group] == nil) then
					table.insert(mutualGroups, groupIndex)
				end
			end
		end
	end
 
	if #mutualGroups > 0 then
		local groupIndex = mutualGroups[1]
 
		-- If the player has multiple groups of friends playing, merge into one group
		if #mutualGroups > 1 then
			groupIndex = mergeGroups(mutualGroups)
		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

local function balanceTeams()
	local teams = TeamsService:GetTeams()

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

	-- Iterate through friend groups (already sorted from largest to smallest)
	for i = 1, #friendGroups do
		-- If the group is too big, split them into both teams
		if (#friendGroups[i] > game.Players.MaxPlayers / 2) then
			for _, player in pairs(friendGroups[i]) do
				table.sort(teams, function(a, b) return #a:GetPlayers() < #b:GetPlayers() end)
				player.Team = teams[1]
			end
		else
			-- Sort the list of teams to have the smallest team first
			table.sort(teams, function(a, b) return #a:GetPlayers() < #b:GetPlayers() end)
			local groupTeam = teams[1]

			-- Place everyone in the group on the same team
			for _, player in pairs(friendGroups[i]) do
				player.Team = groupTeam
			end
		end
	end
end

-- Connect "PlayerRemoving" event to the "removeFromGroup()" function
game.Players.PlayerRemoving:Connect(removeFromGroup)

-- Connect "PlayerAdded" events to the "groupFriends()" function
game.Players.PlayerAdded:Connect(groupFriends)

balanceTeamsEvent.Event:Connect(balanceTeams)
Tags:
  • team
  • match
  • balance