Custom Exception Handling for First-Person Particle Hiding

Introduction

After Roblox had announced the new feature of hiding particles of non-Tool objects in first-person mode, many people were dismayed to see that there was no special tag/property for exception handling when it came to hiding particles on certain objects. This problem was especially apparent for games whose weapons were not based on the Tool object such as a one I was helping develop.

For a while I had no solution to the problem until recently.

Part 1: Understanding How Particles are Hidden

All of the stock movement and camera controls you see when you open up a Baseplate are dictated by a ModuleScript named PlayerModule that is loaded by the LocalScript PlayerScriptsLoader.
image

The ModuleScript within PlayerModule named TransparencyController, as its name suggests, handles the transparency of objects on the client. First-Person particle exception handling is managed by function HasToolAncestor.

function TransparencyController:HasToolAncestor(object: Instance)
	if object.Parent == nil then return false end
	assert(object.Parent, "")
	return object.Parent:IsA('Tool') or self:HasToolAncestor(object.Parent)
end

Therefore, we must change TransparencyController to manage particle hiding in first-person differently. Additionally, we must change PlayerScriptsLoader to override the default TransparencyController before initializing the PlayerModule.
Copy these scripts during runtime and paste them after stopping.

Part 2: What to Change

TransparencyController requires minimal change, only needing to add an additional exception for the HasToolAncestor function. For me, I added a check to see if a part’s ancestor’s name is Handle, as all custom weapons have an invisible Handle Part as the Parent for all of the weapon.

function TransparencyController:HasToolAncestor(object: Instance)
	if object.Parent == nil then return false end
	assert(object.Parent, "")
	return object.Parent:IsA('Tool') or object.Parent.Name == "Handle" or self:HasToolAncestor(object.Parent)
end

In Roblox, if you place a script in StarterPlayerScripts/StarterCharacterScripts that has the name of a default script, your custom script will override the default one. Your modified PlayerScriptsLoader MUST be in StarterPlayerScripts before any players join for this to work.

To prevent having to manually replace all of the PlayerModule each time Roblox changes it, we will be adjusting PlayerScriptsLoader to override default PlayerModule ModuleScritps before requiring the PlayerModule only if custom versions exist.

Place your modified TransparencyController as a child of the PlayerScriptsLoader you will modify. PlayerScriptsLoader should be modified to replace all scripts in PlayerModule of the same name of its children before requiring PlayerModule. I will provide my code for example.

local plrs = game:GetService("Players")

--

local plr = plrs.LocalPlayer
local pMod = script.Parent:WaitForChild("PlayerModule")

--

for _, p in script:GetChildren() do
	local m = pMod:FindFirstChild(p.Name, true)
	if not m then
		repeat m = pMod:FindFirstChild(p.Name, true); task.wait() until m
	end
	
	local par = m.Parent
	m:Destroy()
	
	p:Clone().Parent = par
end

require(pMod)

Part 3: Verify the Adjustments Work

If all is done correct, particles should now work in first-person if objects meet the given exception for hiding. Enjoy!

3 Likes
local character = script.Parent

local function newParticle(v:ParticleEmitter)
	if not v:IsA("ParticleEmitter") then return end
	
	v:GetPropertyChangedSignal("LocalTransparencyModifier"):Connect(function()
		v.LocalTransparencyModifier = 0
	end)
	
	v.LocalTransparencyModifier = 0
end

-- possibly modify these 2 to only work for the tool model
character.DescendantAdded:Connect(newParticle)
for _,v in character:GetDescendants() do newParticle(v) end

And you dont have to edit core scripts!

While this allows you not to modify core scripts, there are downsides to this method.

  • You are setting up connections for each particle compared to TransparencyController’s utilization of an instance cache which stores all instances subject to transparency change in a table that is iterated through every time function Update is called per frame. The large number of listeners for instances’ properties is not optimal compared to TransparencyController’s cache.

  • Additionally, the connection functions will run every frame when the camera is within the range to modify the LocalTransparencyModifier, causing a large burst of connection functions for potentially dozens of instances that are affected by first-person mode.

  • This method creates unnecessary property overriding between the connection function and TransparencyController. While the performance impact is likely negligible, it is still something that could be avoided by integrating the exception handling in TransparencyController itself.