Creating a team chat with the new VoiceChatService

So the past few weeks I’ve been playing around with the new VoiceChat API for my game and I’ve been searching the forums for ways to make a team chat like most games on other platforms have, where you can communicate with players from your team (and only players from your team) without having to be right next to them. I’d seen methods like walkie talkies but not player-to-player. Please note, this is my first time making a tutorial on this platform, so bare with me, I’ll do my best! If I get anything wrong or you need more information, feel free to leave a comment!

So first off you’re going to make sure to add VoiceChatService to your game. You can do this by going to the Model tab, and pressing the Service button on the far right. From there you can insert VoiceChatService. Then you’re going to go into it’s properties and change them to the following:

EnableDefaultVoice = false
UseAudioAPI = Enabled

It should look like this:
image

You’re also going to want to make sure the Teams service is in your game and create at least 2 teams. For testing purposes, I’ll just make a red and a blue team. Make sure each team has a different BrickColor! If not, the game will get confused and could have trouble differentiating between the teams!

Also create a RemoteEvent in ReplicatedStorage and name it whatever you want. I’m going to leave mine named “RemoteEvent” because I’m only using it for a tutorial but if you are actually using this in a game, please change the name

Now, create a Script in ServerScriptService. This script is going to be our “handler” for managing the voice chat. First we’re going to create some variables:

local Players = game:GetService("Players")
local RS = game:GetService("ReplicatedStorage")
local Event = RS:WaitForChild("RemoteEvent") --wait for the RemoteEvent to be loaded just incase it hasn't already
local Teams = {
	["Red"] = false,
	["Blue"] = false
}
--The true doesn't exactly mean much, it just makes it so we can find the team in the table by using something like Teams[Team.Name]
--You can change true to anything that has a value. Except for false or nil because then the script will not allow it

Now, we’re going to make a function to make an AudioDeviceInput instance in the player, which will run when a Player joins. Then, we’ll have a function run to check their team when they spawn, and update their AudioDeviceInput to let other clients know which team they’re on, so they know if they should listen to their mic or not.

function makeinput(plr: Player,mic: AudioDeviceInput)
	if not plr:FindFirstChildOfClass("AudioDeviceInput") then --this makes it so we can use the same function for 2 purposes. If the player doesn't have an AudioDeviceInput, it makes one. But if they already do, then it just updates the name of the existing one to the new team
		if not Teams[plr.Team.Name] then return end --If the team isn't in the team table then don't continue
		local MicInput = Instance.new("AudioDeviceInput",plr) --this creates the device that gets the player's audio from their mic
		MicInput.Player = plr --This tells the AudioDeviceInput which player's mic to get audio from
		MicInput.Name = plr.Team.Name.."Input" --set the name so the clients can easily tell which team they're on then
		return MicInput
	else
		if not Teams[plr.Team.Name] then return end
		mic.Name = plr.Team.Name.."Input" --Update the name of the existing AudioDeviceInput to the new team name
	end
end

Now, we want to run this function when the player first joins, to give them their AudioDeviceInput, then when they switch teams, so we can update the name of the AudioDeviceInput to include the name of their new team.

Players.PlayerAdded:Connect(function(plr)
	local mic = makeinput(plr) --when the player joins for the first time, we automatically make sure they get a mic right away.
	plr:GetPropertyChangedSignal("Team"):Connect(function()
		makeinput(plr,mic) --when the player's team changes, we run the function again
        Event:FireAllClients(plr) --Send the player that changed teams to all clients so they can update their wires (this will be explained in more detail later)
	end)
end)

Now, notice we didn’t do anything with the audio? If you threw this script in your game right now and hit play, it would work perfectly fine and we would get our AudioDeviceInput and you’d think wow I did it!!, buttttt there’s just one problem: Nobody would be able to hear you.

Let’s fix that. What we’re going to do is make a local script, and I prefer to place it in StarterPlayerScripts. What we need this local script to do is create an AudioDeviceOutput. Similar to an AudioDeviceInput. Then we will use Wires to connect all of the player inputs on the same team as the player to their output, so that the audio goes directly to their headphones/speakers instead of to the character.

Let’s create the variables for our ReplicatedStorage and RemoteEvent, as well as our Player instance:

local RS = game:GetService("ReplicatedStorage")
local Event = RS.RemoteEvent
local Player = game:GetService("Players").LocalPlayer

Phew! That might’ve been the hardest part of this entire tutorial! Hope you’re still with me.

Now let’s make a function to make a wire between 2 objects, so we can call that instead of repeating code.

function makewire(source: Instance,target: Instance,name: string)
	local newWire = Instance.new("Wire")
	newWire.Parent = target --this will place the wire wherever the output is
	newWire.SourceInstance = source
	newWire.TargetInstance = target
	if name ~= nil then newWire.Name = name end --if a name variable was included, set the wire's name to it. Otherwise, do nothing
end

Then, since this is a local script, we don’t need a PlayerAdded function, because it’s running on the client, aka the player. So we’ll just create the AudioDeviceOutput, and set it’s Player value to the player.

local DeviceOutput = Instance.new("AudioDeviceOutput",workspace.CurrentCamera)
DeviceOutput.Player = Player

Now we’re going to make a function to update the wires. This would run when the player first joins, then every time the RemoteEvent gets fired.

function update()
	local AudioDeviceInputs = {}
	for _,player in game:GetService("Players"):GetPlayers() do --iterate through all players in the game
		if player.Team ~= nil and player.Team.Name == Player.Team.Name then --if they aren't on the same team, stop here
			local AudioDeviceInput = player:FindFirstChild(Player.Team.Name.."Input")
			if AudioDeviceInput ~= nil then --check if they have an AudioDeviceInput with our team name
				table.insert(AudioDeviceInputs,player) --insert the player in a table to iterate through later
			end
		end
	end
	for _,plr in AudioDeviceInputs do --now iterate through the table we made and make wires
		local Input = plr:FindFirstChild(Player.Team.Name.."Input") 
		if Input then --double checking to make sure it's still there and they haven't changed teams in the split second time since we last checked
			local Wires = DeviceOutput:GetChildren() --get all the children of the deviceoutput, which should all be wires. If you're putting other stuff in there too, add a :IsA check to make sure you only do stuff to Wire instances.
			if DeviceOutput[1] then --if there are children, then we can iterate through them to find the wire of the player we're looking for
				for _,wire in Wires do --iterate through all the children
					if string.find(wire.Name,plr.Name) and not string.find(wire.Name,Player.Team.Name) then --if the wire has the name of the player we're looking for, and the wrong team name, destroy it and make a new one. If it has the right name and the right team, leave it.
						wire:Destroy()
						makewire(Input,DeviceOutput,plr.Name.."->"..Player.Name..Player.TeamColor.Name)
						return --don't continue past this because we will make it do something if no wire is found
					end
				end
			end
			makewire(Input,DeviceOutput,plr.Name.."->"..Player.Name..Player.TeamColor.Name) --this line should only run if the script doesn't find a wire for the player
		end
	end
end

Jokes aside, I think this one was the hardest and most confusing one. I hope I explained it well enough.

Now we just need to make that function run once when our LocalPlayer joins, and then again every time the RemoteEvent is fired. That’s very easy, here’s how it’s done:

update() --we don't need to wrap it in a function or anything because it's in a local script, so it'll run as soon as the script is replicated into the player and it won't run again after that

Event.OnClientEvent:Connect(function()
	update() --make it update when the event is fired. This also works if this player changes teams, so we don't need to add any extra function for that
end)

Now this final step does absolutely nothing for the functionality of your team chat system, but it is very essential to prevent a memory leak. We need to delete the wires of players when they leave the game, because we know those will never be used again.

game.Players.PlayerRemoving:Connect(function(plr)
	local Wires = DeviceOutput:GetChildren() --get all the children of the deviceoutput, which should all be wires. If you're putting other stuff in there too, add a :IsA check to make sure you only do stuff to Wire instances.
	if DeviceOutput[1] then --if there are children, then we can iterate through them to find the wire of the player we're looking for
		for _,wire in Wires do --iterate through all the children
			if string.find(wire.Name,plr.Name) then --if the wire has the name of the player that's leaving, destroy it. This helps keep memory down, because we know that wire will never be used again, which will just be a waste of memory to keep it there.
				wire:Destroy()
			end
		end
	end
end)
--i stole this directly from the update() function and slightly modified it

And that’s it! You should have a fully functional script! Thanks to anyone who stuck around, and I hope this helped out. If you have any questions, problems, or corrections, feel free to leave a comment! Thanks and good luck!

14 Likes