How to Add Custom Player Icons to Chat


Today I’ll be showing you how to edit the default Roblox chat to add icons next to a player’s name!

Contents

Forking the Chat

First, we’ll need to fork parts of the built-in chat. In Roblox Studio, start a test via Run or Play Solo. Navigate to the “Chat” object in the Explorer and select the ChatModules and ClientChatModules folders within:

image

Copy these folders, stop the test, and paste them back into the Chat object. Upon expanding these folders, you’ll find quite a lot of scripts! Fortunately, we only need three of them. In the ChatModules folder, delete all modules except for ExtraDataInitializer, which is how we’ll process the speakers and attach the icons to their messages. In the MessageCreatorModules folder (located within ClientChatModules) delete all modules except for DefaultChatMessage, which will be responsible for processing a message’s icon data, and Util, which will build the actual GUI elements. Make sure that the InsertDefaultModules bool values are still present and set to true. Your explorer should now look like this:

image

Editing ExtraDataInitializer

Alright, let’s start programming! Go ahead and open up the ExtraDataInitializer module. We’ll start by adding the dictionary of icons to reference later. I put it directly underneath the SpecialChatColors dictionary, since it serves a very similar purpose.

local SpecialChatIcons = { --the main dictionary
	Groups = { --a list of groups
		{
			--- ChipioIndustries group
			GroupId = 2932328,
			Rank = 1,
			Icons = {"rbxasset://textures/ui/PlayerList/OwnerIcon.png"},
		}, --we'll also be setting up the system in a way that
	},			--lets internal textures be used.
	Players = { --a list of players
		{
			--- ChipioIndustries
			UserId = 18697683,
			Icons = {4081664234}, --Icons is a table because it
		},			--allows for multiple icons per user/group.
	}
}

Up next, we need to edit the ConstructIsInGroups function. This function’s job is to iterate through the SpecialChatColors dictionary and apply a useful IsInGroup function to each entry. However, we need to modify it to work with our icon dictionary by adding the following lines to the bottom of the function:

	if SpecialChatIcons.Groups then --does our table exist?
		for _,group in pairs(SpecialChatIcons.Groups) do --iterate through it
			--there's an existing function called MakeIsInGroup
			--that we can use.
			group.IsInGroup = MakeIsInGroup(group.GroupId,group.Rank)
		end
	end

Now that we have a pool of data to draw from, we need to be able to figure out which icons should be attached to the player. Fortunately, there’s already a very similar function called GetSpecialChatColor that we can very closely replicate with our own GetSpecialChatIcons.

--there are two types of image IDs: ones with just the ID itself,
--and ones with the rest of the address already attached. If it's
--just a number, we need to attach the address to it, and if it's
--a full address, we can go ahead and return it.
local function ProcessImageID(ID)
	if tonumber(ID) then
		return "rbxassetid://"..tostring(ID)
	else
		return ID
	end
end

local function GetSpecialChatIcons(speakerName)
	local chatIcons={} --an empty list for us to add to
	if SpecialChatIcons.Players then --is there a players folder?
		local playerFromSpeaker=Players:FindFirstChild(speakerName)
		if playerFromSpeaker then --does the player exist?
			--now we're going to cycle through the icon entries
			--to see if we can find matching user IDs.
			for _,player in pairs(SpecialChatIcons.Players) do
				if playerFromSpeaker.UserId == player.UserId then
					--if there's a match, we can loop through the entry's
					--icons and add them to the list, passing them through
					--ProcessImageID first.
					for __,icon in pairs(player.Icons) do
						table.insert(chatIcons,ProcessImageID(icon))
					end
				end
			end
		end
	end
	if SpecialChatIcons.Groups then --is there a groups folder?
		for _,group in pairs(SpecialChatIcons.Groups) do --cycle through groups
			--this line uses the IsInGroup function we applied earlier.
			if group.IsInGroup(Players:FindFirstChild(speakerName)) then
				--if we've found a match, add the icons to the table.
				for __,icon in pairs(group.Icons) do
					table.insert(chatIcons,ProcessImageID(icon))
				end
			end
		end
	end
	--finally, return the table.
	return chatIcons
end

Once we have a way to get the icons associated with a player, we need to do something with that information by attaching it to the speaker object as it’s created. There’s a function in the script called onNewSpeaker which we can edit to attach our own data. Anywhere in the function should work, but I’m adding it right after speaker:SetExtraData("Tags",{}).

		--has the info been set already?
		if not speaker:GetExtraData("Icons") then
			--use our function to get a player's icons
			local specialIcons=GetSpecialChatIcons(speakerName)
			if specialIcons then --if our function worked,
				--we can apply it to the speaker with SetExtraData.
				speaker:SetExtraData("Icons",specialIcons)
			end
		end

Editing DefaultChatMessage

Now we’ve successfully attached the data to the player! Now we just need to edit a couple other scripts to actually display the images. Go ahead and exit ExtraDataInitializer and open up DefaultChatMessage, located within the MessageCreatorModules folder. To keep with the design of what’s there, we’re going to create a variable for the icons right alongside the rest of the data near the top of the script.

	local icons = extraData.Icons or {}

Later on down the script, in the same are where the tags are created (for me, I started writing my code on line 55) we’ll want to create the icons.

	local iconLabels = {} --create a list for our image labels
	for i,icon in pairs(icons) do --cycle through the icons
		--this line uses a function we'll be creating in a moment.
		local label = util:AddIconLabelToBaseMessage(BaseMessage,icon)
		--guiObjectSpacing tracks where the next element should be
		--positioned. We can use this for our purposes.
		label.Position = guiObjectSpacing
		
		--Now we need to update the numNeededSpaces and guiObjectSpacing
		--variables to account for what we've added. We can just add
		--the result of another util function called GetNumberOfSpaces
		--to numNeededSpaces, passing through the icon label, as well as
		--two existing variables called useFont and useTextSize.
		numNeededSpaces = numNeededSpaces + util:GetNumberOfSpaces(label,useFont,useTextSize)
		--guiObjectSpacing is much simpler, and we only need to add the
		--horizontal offset of the label size to the total.
		guiObjectSpacing = guiObjectSpacing + UDim2.new(0, label.Size.X.Offset, 0, 0)
		
		table.insert(iconLabels,label) --finally, we can add the label to the list.
	end

So… what’s the point of adding the label objects to a list, anyway? Well, we’re actually going to iterate through the table later on in the script in order to specify how the labels fade in and out. Skip down to around line 100, right after the tag labels have been iterated through.

The FadeParmaters dictionary holds a list of items and their corresponding properties which need to be faded to specified values. The overall structure looks something like this:

FadeParmaters
-Object
–Property
—FadedIn
—FadedOut

	--loop through the icon labels
	for i, iconLabel in pairs(iconLabels) do
		--create a new entry in the existing FadeParmaters table
		--(misspelling and all) using the iconLabel itself as a key.
		--The only property we need to configure is ImageTransparency,
		--and we'll want the faded in state to be 0 transparency, and the
		--faded out state at 1.
		FadeParmaters[iconLabel] = {
			ImageTransparency={FadedIn=0,FadedOut=1}
		}
	end

Editing Util

Util holds a lot of essential functions, and we’ll want to edit some of those to work with our image labels. That, and one of the functions we used doesn’t even exist. At the top of the script, we can declare a constant for the image size:

local DEFAULT_ICON_SIZE = Vector2.new(18,18)

Up next, we need to edit a function called GetNumberOfSpaces. You’ll recall that we used this to increase numNeededSpaces by passing through the label itself. Thing is, the function is designed for text, not labels. There’s one line in particular we want to replace:

	local strSize = self:GetStringTextBounds(str, font, textSize)

More specifically, we want to replace it with the following code:

	local strSize --define the variable in a scope that can be
	--accessed by later lines of code
	if typeof(str)=="string" then --is it a string?
		--if so, treat it like a string.
		strSize = self:GetStringTextBounds(str, font, textSize)
	elseif typeof(str)=="Instance" then --or is it an icon?
		--if so, we can just use the label's AbsoluteSize property.
		strSize = str.AbsoluteSize
	end

Since GetStringTextBounds and AbsoluteSize both return Vector2 values, the rest of the function will have no problem processing the information. However, we haven’t actually written the code that will create the label–so let’s do that! I put my function around line 203, but it doesn’t matter very much.

--this is the function we used in DefaultChatMessage.
function methods:AddIconLabelToBaseMessage(BaseMessage, iconAddress)
	local iconSize=DEFAULT_ICON_SIZE --use the constant we defined earlier
	local IconLabel=self:GetFromObjectPool("ImageLabel") --see below.
	--set various properties of the IconLabel to what we want.
	IconLabel.Selectable=false
	IconLabel.Size=UDim2.new(0,iconSize.X,0,iconSize.Y)
	IconLabel.Position=UDim2.new(0,0,0,0)
	IconLabel.BackgroundTransparency=1
	IconLabel.ImageTransparency=0
	IconLabel.ScaleType=Enum.ScaleType.Fit
	IconLabel.ImageColor3=Color3.new(1,1,1)
	--Assign the IconLabel the image we want.
	IconLabel.Image=iconAddress
	IconLabel.Visible=true
	--Add it to the base message.
	IconLabel.Parent=BaseMessage
	
	--Return it to wherever the function was called from.
	return IconLabel
end

You may have noticed that we didn’t use Instance.new to get an ImageLabel, but instead a function called GetFromObjectPool. This is a function included in the chat system that recycles previously-used objects instead of deleting them. However, because we’re using recycled objects, we need to be careful to not assume any of the object’s default properties, because they may have been changed by whatever they were used for previously.

Conclusion

I was inspired by a thread questioning about whether or not it was possible, and after reading a whole lot of documentation and getting it working, I figured it would be a fun thing to share. I learned a lot about editing existing programs, and I hope you did too!

Finally, if you really don’t want to edit the scripts yourself, I’ve posted a drag-and-drop solution as a free model, which you can get here:

I hope you’ve found this guide helpful! Let me know if you have any questions/issues.

58 Likes

Great tutorial, thanks!! I’ve always tried to add in my own elements but was only restricted to text, now I can add icons!

1 Like

Wow… thats actually really cool!

This will be extremely useful when you need point out things like game moderators instead of using the standard text tags or chat colors. I have actually always wondered if this was possible but never got around to trying it out myself. Nice work! I’ll definitely be trying this out.

1 Like

i remember back in 2012 where custom icons were able to be added to the playerlist… this is JUST as cool!!!