Custom renderer | by frodev

great to see people expand on my video lol. i do wonder if its possible to make an entire game that does calculations from just viewports

2 Likes

Well do I have something for you…,
( it’s not finished)
(It’s not even working)
But I’m using this to do almost that!

1 Like

Render distance almost works, it looks a little off considering what it does, so Im going to create something to fix the problem

(I have created a module that allows me to loop through all properties in a basePart. Ik pretty cool, This is also works for MeshParts and BaseValues. When I’m done with this module I’ll probably release it)

Small problem…
Too much calculations…
Ima abandoning this but I might come back to it later

1 Like

I recently wrote a new Renderer becuase the method I mentioned earlier using the Zone module was redundant because I’m using occlusion culling calculations. In short this renderer has caused an increase of about 5-13 FPS in my use case. A good way to reduce calculations is to create a part object in place that is the result of the object not being in view, then just check the part objects if they are in view and unpack them when necessary. That’s how mine works and it saves calculating the distance of objects. Also, multi-threading your renderer to like 4-8 may increase its performance. I handle all caching on the server with 3 different actors and like 8 object categories with different rendering distance. They never exceed about .15-.25% CPU usage on the server and this section can be done very slow because rendering out is lower priority than rendering in which is handled on the client in my case. Using this code snippet which may provide some hints for your project.

-- This creates a zone for every ambient group, then listens for when the local player enters and exits
task.wait(.6)
--local Zone = require(game:GetService("ReplicatedStorage").Zone)
local localPlayer = game.Players.LocalPlayer
local playerGui = localPlayer:WaitForChild("PlayerGui")
local Remote=game.ReplicatedStorage.GlobalSpells.ZoneRemote
local zonearray={}
local id=0
local payloadarray={}
local DistanceArray = require(game:GetService("ReplicatedStorage").Zone.RenderDistance)
local camera=workspace.CurrentCamera

local objects=workspace.TouchBox

local function payloadcache(container)
	table.insert(payloadarray,container)
end


-- Calculate adjusted render distance based on FPS


-- Example usage:
local fps = playerGui.CharacterSelect.FPS.Value
--local adjustedRenderDist = calculateAdjustedRenderDist(fps)


local function IsInView(object,cameraViewportSize)
	--if math.abs(object.Position.Y-localPlayer.Character.HumanoidRootPart.Position.Y)>50 then return false end
	--print(object.Position)
	if object.Name=="FurnitureRender" or object.Name=="PlantRender" then
		return true
	end
	local objectPosition = camera:WorldToViewportPoint(object.Position)
	-- Check if the object is within the camera's viewport
	--print(objectPosition)
	if objectPosition.X <= cameraViewportSize.X and
		objectPosition.Y <= cameraViewportSize.Y and
		objectPosition.Z > 0 then -- Z > 0 means the object is in front of the camera
		return true
	else
		return false
	end
end
local minRenderDist = 40 -- Minimum render distance
local fpsThreshold = 30 -- FPS threshold below which render distance decreases

-- Calculate adjusted render distance based on FPS
local function calculateAdjustedRenderDist(maxRenderDist,fps)
	return math.max(minRenderDist,maxRenderDist - (maxRenderDist/2) * (math.max(fps, fpsThreshold) - fpsThreshold) / (60 - fpsThreshold))
end
local Tick=game.ReplicatedStorage.GlobalSpells.Tick
local writetime=Tick.Value
local arrays={}
local refresh=6
local function getobjects()
if Tick.Value>writetime+refresh then
	arrays=objects:GetChildren()	
	writetime=Tick.Value	
end
end
--lower fps shorter render distance
local RefreshRate=.6
local function checkobjects(pos)--check if object is in view and 

	local payload={}
	local cameraViewportSize = camera.ViewportSize
	local fps=playerGui.CharacterSelect.FPS.Value
	local fpstiming=playerGui.CharacterSelect.FPSTiming.Value
	local adjustedarray={}
	for i,v in DistanceArray do 
		adjustedarray[i]=calculateAdjustedRenderDist(v,fps)
	end
	getobjects()
	--local arrays=objects:GetChildren()
	local amnt=math.max(1,#arrays)
	local throttle= RefreshRate/amnt

	local increment=math.max(1,fpstiming/throttle)

	local state=function(reference) if reference.Position.Y<-16 then return adjustedarray[reference.Name] or nil else return nil end end 
	if pos.Y>-16 then
		state=function(reference) if reference.Position.Y>-16 then return adjustedarray[reference.Name] or nil else return nil end end
	end

	local current=0
	task.wait(throttle)
	for i,reference in arrays do 
		current+=1
		if current>increment then
			task.wait(throttle)
			current=0
		end
		if reference:IsA("BasePart") then
			local renderdist=state(reference)--adjustedarray[reference.Name] or nil
			if renderdist then
			local lengthvector=(reference.Position-pos).Magnitude
			if lengthvector<renderdist and IsInView(reference,cameraViewportSize) then				
				table.insert(payload,reference)
				--reference.CanQuery=true
				table.remove(arrays,i)
			elseif lengthvector>math.min(300,renderdist*2.5) then 
				table.remove(arrays,i)
				--reference.CanQuery=false
			end
			else 
				table.remove(arrays,i)
			end
		else table.remove(arrays,i)	
		end
	end
	--arrays=nil
	state=nil
	return payload,amnt
end




local function RegisterLoop()
	spawn(function() 
		--one payload per second
		local prevpos=localPlayer.Character.HumanoidRootPart.Position
		while true do 
			if camera.CFrame~=prevpos then
				prevpos=camera.CFrame
				local payload=checkobjects(localPlayer.Character.HumanoidRootPart.Position)			
				Remote:FireServer(payload)
			else
				task.wait(RefreshRate)
			end
		end		
	end)
end

RegisterLoop()

I did some optimizations with the array handler for the objects and conditions to remove objects from the array as well as increasing the Arrays refresh rate which tripled the performance of the renderer. When running this script uses .05%-.15% CPU usage on the client when using the array handler and .3-1.1% without it

Wow! This is very VERY nice!, thank you truly.

1 Like

I also want to share this easy to use Parallel luau solution which demonstrates how to invoke a bindable function and create a thread handler that evenly spreads the load to all the actors.

I apologize for certain aspects of the code such as FPSTiming not being defined.
This is the code that I use to calculate Frames per second and the time it takes to render each frame (FPSTiming)

local RunService=game:GetService("RunService")
local TimeFunction = RunService:IsRunning() and time or os.clock
local LastIteration, Start
local FrameUpdateTable = {}
local GlobalFPS=1/30
local function HeartbeatUpdate()

	LastIteration = TimeFunction()
	for Index = #FrameUpdateTable, 1, -1 do
		FrameUpdateTable[Index + 1] = FrameUpdateTable[Index] >= LastIteration - 1 and FrameUpdateTable[Index] or nil
	end
	FrameUpdateTable[1] = LastIteration
	local FrameRate=tostring(math.floor(TimeFunction() - Start >= 1 and #FrameUpdateTable or #FrameUpdateTable / (TimeFunction() - Start)))
	--Bars.Parent.CharacterSelect.FPS.Value=FrameRate
	--Bars.Radar.FrameRate.Text="FPS:"..Bars.Parent.CharacterSelect.FPS.Value
	GlobalFPS=1/FrameRate
	--Bars.Parent.CharacterSelect.FPSTiming.Value=GlobalFPS
	--FPS	
end
Start = TimeFunction()
RunService.Heartbeat:Connect(HeartbeatUpdate)

i am not going to lie this is getting to advanced for me :sweat_smile:
I am abandoning my idea (sadly) to work on my game for now…
Thank you for taking interest however!

It’s fine! although the two things I recently shared are generally very important. Calculating the time it takes to render each frame as a variable that is used for a variety of tasks. Also, I’ve been using the parallel luau solution for like everything lately to make easy to use and efficient tools for my game. But yeah a custom renderer may not be neccessary I only did it because I could compress all the objects not in view to a reference of their source object because I am doing procedural generation. Which reduced memory usage significantly and allows the worlds to be much larger.

i might start using that threader then!
If you do not mind sharing what is the game you are working on?
I have high hopes for it since you are developing it!
Currently, I am working on Prelude:Prelude - Roblox
Game might have bugs but Its just my first 2 months of scripting so bear with me lol!

This is a link to the testing server for my game Epic RPG with AI +Optimized Performance - Roblox
The main menu currently doesn’t have all the options enabled. so please select the custom character option to enter the game.
this is a link to a previous version of the game which features the story mode.
Lumina & Darkness: V 0.9 - Roblox
My old account got hacked so my game doesn’t really get traffic Yet. Gogeta - Roblox
I still consider it incomplete but I plan on a full release soon. :slight_smile:
I’ll check out your game too! Some good hints for early scripts is to try to be as organized and efficient as possible and make tools that you want to work with and build up later on.

Also your game is very cool but I’m getting very low FPS sometimes the camera glitches out. But it’s very atmospheric and cool! I couldn’t figure out how to use the sword but I was able to use an electric spell. Definitely keep up the good work and try to create some organized and efficient systems that will allow you to make unlockables or an inventory use the same code for your weapon and use variables to change their behavior

Yea the low fps is the raytracing I put in, I’m trying to figure out how to make it togglable.
Camera Is fine on my side but ill do a double check.
I really dont know what direction to take this game currently lol

Your game reminds of eve online for some reason.
Would love to play it when its done however… systems sound very intricate

features of my game are those RPG elements of power progression, equipment collecting monster fighting, resource collecting, socializing with players and AI and doing procedurally generated quests in an open world in short. Currently looking at the lua heap and doing some nice optimizations trying to get the ping stable.
I noticed the camera it glitched out when geting close to an npc possibly due to camera trying to move around a type of wall.
MY advice for your game would be to make a bunch of spells and sword skills and start out with player versus player while also designing the abilities to be used by NPCs. I had this great thing happen in my development where I was able to convert most of my player systems to be used by NPCs and it saved a lot of work and ends up being very neat in coding and gameplay.
I never played Eve Online! But secretly this game is like a mix of Kingdom Hearts, Runescape and Breath of Fire Dragon Quarter and a little bit of Yugioh inspiration.

I just enjoy making my dream game :slight_smile:

WOW! I love your game’s inspirations. I was a big fan of yugioh and kingdom hearts when I
was a kid.

Currently Its Becoming a more daunting task to work on my game as I know what the next step is, However.
(I have no idea how to get there)

This is what I wish to achieve:

This is what I have:
Nothing cant figure out how to use WCS and RefX unfortunately
Besides that I already have the VFX and SFX ready for the abilities but I’ve been stuck on figuring out how to use this framework for almost a week… I know using this one will save me tons of time from bartokens personal experience he shared with me…
Might Hire a scripter honestly :pensive:

I have a useful function I use for VFX.
This is a multiply number sequence function that can change the size of Particle Emitters in code.

local function multnumseq(sequence,Scale)
	local numberKeypoints2 = {}
	for i = 1, #sequence.Keypoints do
		local currKeypoint = sequence.Keypoints[i]
		table.insert(numberKeypoints2, NumberSequenceKeypoint.new(currKeypoint.Time,currKeypoint.Value*Scale))	
	end
	return NumberSequence.new(numberKeypoints2)
end

In my game one thing I did was create like 30 different ball style attacks.


Then wrote one function that handles all of them.

function dfg.Energyball(Subject,Target,Type,Damage,Scale)
	local charging
	local Subject=Subject
	local Target=Target
	local Type=Type
	local Damage=Damage
	local Scale=Scale
	local Human=nil
	if Scale==nil then Scale=1 end
	local Player,Player2=nil
	if Target~=nil  and Subject~=nil then
		Human=Subject.Parent:FindFirstChildOfClass("Humanoid")
		if Human==nil then
			Human=Subject.Parent.Parent:FindFirstChildOfClass("Humanoid")
		end
			local Angular
			Player=game.Players:GetPlayerFromCharacter(Human.Parent)
			local Rotate=false 	
		if Player==nil and Target then
			if Human.Parent:FindFirstChild("HumanoidRootPart"):FindFirstChildOfClass("BodyGyro")==nil then
				Angular=Instance.new("BodyGyro")
				Angular.CFrame=CFrame.new(Human.Parent:FindFirstChild("HumanoidRootPart").Position,Target.Position)
				Angular.Parent=Human.Parent:FindFirstChild("HumanoidRootPart")
				Debris:AddItem(Angular,.6)		
			if Human.AutoRotate==true then
					Rotate=true
					Human.AutoRotate=false		
					spawn(function() task.wait(.6) Human.AutoRotate=true end)
				else Human.AutoRotate=false	end	
				end
		end
	local Comp=false
	local Core=Type.Parent.Core:Clone()
	local EFX=Type:Clone()
	--local simmodel=Instance.new("Model")
	--Core.Parent=simmodel
	
--	Core.Parent
	local w
	local Speed=Type.MagicID:GetAttribute("Speed")
	local DamageScript=repspell.Damage.DmgRoll:Clone()
	DamageScript.Value=Damage
	DamageScript.Parent=Core
	local Obj=Instance.new("ObjectValue")
	Obj.Value=Subject
	Obj.Name="Owner"
	Obj.Parent=Core
	EFX.Parent=Core	
	Core.Size=Type.MagicID:GetAttribute("Size")
	Core.CFrame=Subject.CFrame:ToWorldSpace(CFrame.new(0, 0,(-1*(Core.Size.Z/2+Subject.Size.Z/2))))
	Core.Massless=true
	Core.Anchored=false			
	Core.Parent=Subject.Parent
	--simmodel:Destroy()
		if Human~=nil then
			if Human.Parent:FindFirstChild("UpperTorso") then		
				local AR=mathrandom(1,7)
				local Anim=Human:LoadAnimation(script.Animations:FindFirstChild("Energyball"..AR))
				Anim:Play()
				task.delay(Anim.Length,function() Anim:Stop() end)
			end
		end
		if Player then
			Core:SetNetworkOwner(Player)
		elseif Target then
			Player2=game.Players:GetPlayerFromCharacter(Target.Parent)
			if Player2 then
				Core:SetNetworkOwner(Player2)
			end
		end
		--task.delay(1.2,function() dfg.Deflect(Core) end)
	---local Core2=Core:Clone()	
		local DamageCharacter=Core
		--Core2.CanCollide=false
		--Core2.CanQuery=false
		--Core2.CanTouch=false
		--.Anchored=true
		
		local hittable={}
		local function checktable(hit)
			for i,v in pairs(hittable) do
				if v==hit.Parent then
					return true
				end
			end
			return false
		end
		local Hits = 0
local tcon=nil 		tcon=Core.Touched:Connect(function(hit)
				
			dfg.ExplosionConditions(hit,Core,Type.MagicID)
			if hit.Parent~=Subject and tostring(hit.Parent)~=tostring(Subject) and hit.Parent~=Human.Parent and hit.Parent~=Core.Parent then Core.CanTouch=false		
				local arrow = Core
				local Owner
				local Character=nil
				local damage
				if arrow:FindFirstChild("Owner")==nil then
					Owner=Instance.new("ObjectValue")
					Owner.Value=arrow.Parent
				end
				Owner = arrow.Owner
				local DMG =repspell.DMG:clone()
                   Debris:AddItem(DMG,7)
				local humanoid = hit.Parent:findFirstChild("Humanoid")
				local Protect=hit.Parent:findFirstChild("Protect")
				if (humanoid~=nil and Protect~=nil) and humanoid.Parent~=Subject then
					if humanoid.Parent ~= Obj.Value then			
						if checktable(hit)==false then 
							Comp=true
							arrow.CanTouch=false	
								if game.Players:FindFirstChild(Owner.Value.Name)~=nil then
									Character = game.Players:FindFirstChild(Owner.Value.Name).Character
									Core.DmgRoll.Value= game.Players:FindFirstChild(Owner.Value.Name).PlayerGui.Bonus.MAG.Value
							else 
								pcall(function() Core.DmgRoll.Value = Damage end)
								
							end
								if hit.Parent.Protect.Value ~= 2 then
									if Core.DmgRoll.Value>=80 then
										damage = (mathrandom(0,Core.DmgRoll.Value/1.1)) 
										Core.DmgRoll.Value= damage
									elseif	 Core.DmgRoll.Value<=80 then
										damage = (mathrandom(0,Core.DmgRoll.Value))
										Core.DmgRoll.Value= damage
									end
								elseif hit.Parent.Protect.Value == 2 then		
									if Core.DmgRoll.Value>=80 then
										damage = (mathrandom(0,Core.DmgRoll.Value/3)) 
									elseif Core.DmgRoll.Value<=80 then
										damage = (mathrandom(0,Core.DmgRoll.Value/2)) 
									end	
								end
								if Character~=nil then
									if Character:FindFirstChild("Staff") ~= nil  or Character:FindFirstChild("Scythe") ~= nil or Character:FindFirstChild("ScytheX") ~= nil then
										Core.DmgRoll.Value= damage+ damage*.15
										damage=Core.DmgRoll.Value
									end
							end
							damage =math.floor(damage)
								dfg.DealDamage(humanoid,damage,2,hit,Type.MagicID:GetAttribute("Element"),Subject)
								table.insert(hittable,hit.Parent)
								--humanoid.Health = humanoid.Health - damage
								--Hits = Hits + 1
								--DMG.HitSplat.Hit.Text = ""..(damage)..""
						--	if humanoid.Parent:FindFirstChild("Head")~=nil then
							--DMG.Parent = humanoid.Parent.Head
						--	else humanoid.Parent:FindFirstChild("HumanoidRootPart")	
						--	end
							
							--	DMG.HitSplat.Hit.Server.Disabled = false

							else 
							arrow.CanTouch=false	
							end	
							--		explosion = game.Lighting.Bewm:clone()
							--		explosion.Position = hit.Position
							--		explosion.Mesh.Scale = Vector3.new(0.25, 0.25, 0.25)
							--		explosion.Parent = workspace
							task.wait()
			
				
						end
					end		
				end		
				if Hits<1 then
					Core.CanTouch=true
				else tcon:Disconnect()
				tcon=nil	
				end	
				
		end)
	if (Subject) then
		w = Instance.new("Weld")
		w.Part0,w.Part1 = Subject,Core
		w.C0 = Core.CFrame:toObjectSpace(Subject.CFrame):inverse()
		w.Parent = Subject
	end
	for _,v in pairs(EFX:GetDescendants()) do		
local v=v	
	if v:IsA("Sound") then
			TweenSound(v)
			if v.Looped==true then
				spawn(function()
					v.RollOffMaxDistance*=Scale	
					repeat task.wait(.05) until Comp==true					
					local goal = {}
					goal.Volume =  0
					local tweenInfo = TweenInfo.new(.5)
					local tween = TweenService:Create(v, tweenInfo, goal)
					tween:Play() 
				end)
			end
		end	
		
		if v:IsA("PointLight") then
			spawn(function()
				local v=v
				local goal = {}
				goal.Brightness = v.Brightness
				goal.Range= v.Range*Scale
				local tweenInfo = TweenInfo.new(2)
				local tween = TweenService:Create(v, tweenInfo, goal)	
				v.Brightness=0
				v.Range=0
				tween:Play()
			end)
		end	
		if v:IsA("ParticleEmitter") then
			spawn(function()
				local Bubbleb=v				
						local PrevTrans=Bubbleb.Transparency
						local Prevsize=dfg.multnumseq(Bubbleb.Size,Scale)
				local Steps=15
				local CSteps=0
				local fram=.0333
				local deftimes=Bubbleb.TimeScale
				Bubbleb.TimeScale=Bubbleb.TimeScale/3
				repeat 
					
						v.TimeScale=deftimes*(CSteps/5)							
						CSteps=CSteps+1	
						--local Ratio=CSteps/Steps
						--local numberKeypoints2 = {
						--	NumberSequenceKeypoint.new(0, 1-Ratio), -- At t=0, size of 0
						--	NumberSequenceKeypoint.new(1, 1), -- At t=1, size of 10
						--}
									
									Bubbleb.Size = dfg.multnumseq(Prevsize,CSteps/15)
									Bubbleb.TimeScale=deftimes*(CSteps/15)
									Bubbleb.Transparency = dfg.multnumseq(PrevTrans,17/CSteps+2)
									task.wait(fram)	
					
						until CSteps+2>=Steps	
				Bubbleb.TimeScale=deftimes		
				Bubbleb.Transparency=PrevTrans
				Bubbleb.Size=Prevsize		
				local Steps=60
				local CSteps=0
				local fram=3/30
				--local c	
			repeat 
					CSteps=CSteps+1
					if Comp==true and CSteps<30 then
						CSteps=30
								
							else CSteps=CSteps+1	
							end
						if CSteps>=45 or Comp==true then
								if CSteps<45 then
									CSteps=45
								end
								task.wait(fram)	
								local Ratio=CSteps/Steps
								local numberKeypoints2 = {
									NumberSequenceKeypoint.new(0, Ratio), -- At t=0, size of 0
									NumberSequenceKeypoint.new(1, 1), -- At t=1, size of 10
								}
								Bubbleb.Transparency = dfg.multnumseq(PrevTrans,(CSteps-44)*1)
							end
					task.wait(fram)	
					until CSteps>=Steps	
					Bubbleb.Enabled=false 
			end)
		end
	end
	if Target~=nil then					
			local goal = {}
			
			local tweenInfo
			local Tick
			local tween
			task.wait(.75)
			local Vectir=nil
			if Target then
				Vectir=(Target.Position-Subject.Position).Magnitude
			else 
				Vectir=80
			end	
			local B=Instance.new("BodyPosition")
			B.Position=Core.Position	
			w:Destroy()
			Core.Anchored=true
			if Vectir<40 and Vectir>12.5 then
		tweenInfo = TweenInfo.new(Vectir/10,Enum.EasingStyle.Linear)
		Tick=Vectir/10
		elseif Vectir>=40 then
			tweenInfo = TweenInfo.new(4,Enum.EasingStyle.Linear)
			Tick=4
		elseif Vectir<=12.5 then
			Tick=1.25
			tweenInfo = TweenInfo.new(1,Enum.EasingStyle.Linear)
		end	
			if Vectir>8 and Target then -- follow the target
			goal.Position = Target.Position
			tween = TweenService:Create(B, tweenInfo, goal)
			tween:Play()
			spawn(function()
			local Cero=0
			repeat		
					Cero=Cero+.25
						
					
					if (Target.Position-Core.Position).Magnitude<2 then
						Comp=true	
					end
					local tweenInfo
					if Tick-Cero>0 then
						tweenInfo=TweenInfo.new(Tick-Cero,Enum.EasingStyle.Linear)	
					else	
						tweenInfo=TweenInfo.new(.4,Enum.EasingStyle.Linear)	
					end
				goal.Position = Target.Position
				local tween2 = TweenService:Create(Core, tweenInfo, goal)
				tween:Pause()
				tween=tween2
							tween:Play()	
							task.wait(.25/Speed)
							
				until Cero>=Tick	or Comp==true
						Comp=true
						
						task.wait(.6)
						Core.CanTouch=false
						task.wait(1.5)
					if Player and Core.Parent~=nil then
						Core.Anchored=false
						Core:SetNetworkOwner(nil)
					end
					Core:Destroy()
					 tcon:Disconnect() 
						
			end)	
		else
			tweenInfo = TweenInfo.new(1/Speed,Enum.EasingStyle.Linear)
			goal.Position = Target.Position	
			local tween = TweenService:Create(B, tweenInfo, goal)
			tween:Play()
			tween.Completed:Wait()			
			Comp=true
					task.wait(.6)
					Core.CanTouch=false
					task.wait(1.5)
				if Player then
					Core:SetNetworkOwner(nil)
				end
				Core:Destroy()
				 tcon:Disconnect() 
					--if Core2~=nil then
					--	Core2:Destroy()
					--end	
		end	
				
			end	
	end		
end	

This is definitely the way to go about creating simple and effective systems. So don’t worry! Less is more!
They also utilize this neat object that determines their behavior.

Thank you for providing this but I’m probably gonna use refx because it’s documented and doesn’t need presintalled assets

Hey! I just started UI Design!
Wondering if this is a good Fps Reader or if it can be optimized lol

local RunService = game:GetService("RunService")
local FpsLabel = script.Parent

local TimeFunction = RunService:IsRunning() and time or os.clock

local LastIteration, Start
local FrameUpdateTable = {}

local function HeartbeatUpdate()
	LastIteration = TimeFunction()
	
	for Index = #FrameUpdateTable, 1, -1 do
		FrameUpdateTable[Index + 1] = FrameUpdateTable[Index] >= LastIteration - 1 and FrameUpdateTable[Index] or nil
	end
	
	FrameUpdateTable[1] = LastIteration
	FpsLabel.Text = "FPS: " .. tostring(math.floor(TimeFunction() - Start >= 1 and #FrameUpdateTable or #FrameUpdateTable / (TimeFunction() - Start)))
end

Start = TimeFunction()
RunService.Heartbeat:Connect(HeartbeatUpdate)

Yes it’s optimized if you run it only once and use the reading of the FPS for all your FPS needs. I use the FPS value FPS timing value for things that require being done once per frame and it is completely seamless, no issues with the calculations on my end.
At some point i had to go through and make sure all my scripts were not calculating 1/FPS in exchange for the FPS timing object which is much more important.

FPSTiming.Value=1/FPS.Value--equivalent to .0333 at 30 FPS and .01666 at 60fps

The code was more of a demonstration on how to go about it of course you can use whatever you’d like. It’s really not that hard and can be a fun project to write them yourself. Generally VFX requires that you have a library of configured assets in my example it goes through the children of an attachment and does tweening of sound, particle emitter size, light and tweening of the CFrame. That’s pretty much all it takes for VFX. Another popular library for VFX is [UPDATE 0.1.0] Lumina - A custom particle system - Resources / Community Resources - Developer Forum | Roblox

Yea was thinking about using lumina but wcs isn’t that compatible with it unfortunately