Improving script that tracks the velocity of unanchored parts

VelocityTracker.rbxm (6.9 KB)

I’m creating a script that checks the movement of unanchored parts and tracks the velocity. It’s a simple code and it works fine for me.

In-depth explanation of the code

Basically the whole code runs on heartbeat, it checks for every part inside a folder and checks if it’s unanchored and a BasePart, later it checks if the BasePart was inserted inside a table containing all the parts being tracked, if it wasn’t, insert it, otherwise, update it. After all of that later it will check for
unnecessary values inside the table, if the BasePart inside the table does not exist, the parent does
not exist, or it isn’t moving, if any of those conditions is met, the value will be removed.

The code
local RunService = game:GetService("RunService")
local Heartbeat = RunService.Heartbeat

local UserGeneratedObjects = game.Workspace:FindFirstChild("UserGeneratedBlocks")

local VelocityTracker = {}

Heartbeat:Connect(function(Step)
	--// Tracking objects 
	for _, Object in pairs(UserGeneratedObjects:GetDescendants()) do
		if Object:IsA("BasePart") then
			print("It's a base part")
			if not Object.Anchored then
				print("Not anchored")
				if not VelocityTracker[Object] then
					print("Value not found")
					VelocityTracker[Object] = Object.AssemblyLinearVelocity
				else
					if Object.AssemblyLinearVelocity == Vector3.new(0,0,0) then
						print("Object is not moving, removing value")
						VelocityTracker[Object] = nil
						continue
					end
					print("Value found")
					local OldVelocity = VelocityTracker[Object]
					VelocityTracker[Object] = Object.AssemblyLinearVelocity
					print((OldVelocity - VelocityTracker[Object]).Magnitude)
					if (OldVelocity - VelocityTracker[Object]).Magnitude >= 190 then
						print("Velocity magnitude over required amount")
					end
				end
			end
		end
	end
	--// Checking for unnecessary values
	for TrackedObject, TrackedVelocity in pairs(VelocityTracker) do
		if not TrackedObject then
			VelocityTracker[TrackedObject] = nil
		elseif TrackedObject then
			if not TrackedObject.Parent or TrackedObject.Anchored then
				VelocityTracker[TrackedObject] = nil
			end
		end
	end
	
end)

The issue I’m having currently is that the Script Activity goes around 6-20% when the code is tracking around 1000 parts:
image

An idea I had would be tracking objects every 0.2 or more seconds depending on the server’s performance, basically skipping some frames on Heartbeat, but that didn’t work. Again, the code is running fine, but the only issue I currently have is the Script Activity of it.

I would like to know if there’s any better way to optimize this script. Any kind of help is appreciated.

1 Like

you could make this

for TrackedObject, TrackedVelocity in pairs(VelocityTracker) do
    if not TrackedObject or (not TrackedObject.Parent or TrackedObject.Anchored) then
        VelocityTracker[TrackedObject] = nil
    end
end

no need for all those if statements

You’re right about that, thanks for telling me, however, the issue is still occurring. Thanks for the help anyway.

Wouldn’t it be more effective if, instead of checking every step, you only checked when the Velocity property changed? Like with GetPropertyChangedSignal()?

it has to do with two major things

  1. Heartbeat runs every frame, it isn’t waiting for those for loops to finish
  2. the first for loop has no yielding

How are you using this VelocityTracker? If we were to know how it’s being used/what it’s being used for, I can recommend some optimization tips.

Also how are objects being added to the UserGeneratedObjects folder and more importantly what are they?

I’m not sure about that, I remember reading a few topics around a year ago pointing out that using too many connections might cause issues related to performance, in this case I’m not sure, correct me if I’m wrong.

Mind explaining me what do you mean by no yielding?

Basically the player can build stuff with blocks, like a Build to Survive game or Creeper Chaos, after the building time passes, this script gets enabled, all parts are unanchored and welded, this script is used to detect when a block is for example falling off a building or anything like that, for example: a block made of glass that breaks when there’s a sudden change of speed from X to 0, or a reactive block which does a special function after X velocity.

They are created by the server with a RemoteEvent during the building time, and the parts inside are blocks created by the player.

Before:

image

After:
image

While 2.1% is high, I found in my tests that 2.1% was its peak number. Sometimes while testing it would only go up to 0.5%.

Also don’t worry about Script Rate. Script Rate increasing doesn’t matter unless it’s an extremely high number (it won’t be don’t worry).

VelocityTracker.rbxm (7.1 KB)

What I did was instead of looping through all parts every heartbeat, I’d manually add them to a registry of sorts and loop through that. So every frame it would only check unanchored BaseParts instead of having to check everything inside the folder, like the models.

I also added optimizations like if there were no parts to check, it would stop running and then it would restart running if new parts were added. Should save you a very small amount of script activity.

New Code
local WorkspaceService = game:GetService("Workspace")
local RunService = game:GetService("RunService")

local Connections = {}
local Registry = {}

local function Unregister(Object)
	if Registry[Object] ~= nil then
		--# Sets the registry of the
		--| object to nil as it's no
		--| longer needed.
		Registry[Object] = nil
		
		if #Registry == 0 then
			--# Disconnects most connections because
			--| they're no longer needed.
			for Index, Connection in pairs(Connections) do
				if Index ~= "ObjectAdded" and Connection.Connected == true then
					Connection:Disconnect()
				end
			end
		end
	end
end

local function Register(Object)
	if Object:IsA("BasePart") and Object.Anchored == false and Registry[Object] == nil then
		--# After an object passes the requirements,
		--| it will get registered.
		Registry[Object] = Vector3.new(0,0,0)
		
		if Connections["ObjectRemoved"] == nil then
			Connections["ObjectRemoved"] = WorkspaceService:WaitForChild("UserGeneratedBlocks").DescendantRemoving:Connect(Unregister)
		end
		
		if Connections["Heartbeat"] == nil then
			--# Creates the track function to track
			--| all registered parts.
			Connections["Heartbeat"] = RunService.Heartbeat:Connect(function()
				for Object, Velocity in pairs(Registry) do
					if Object.AssemblyLinearVelocity == Vector3.new(0,0,0) then
						Unregister(Object)
					else
						Registry[Object] = Object.AssemblyLinearVelocity
					end
				end
			end)
		end
	end
end

--# Creates the initial connection needed
--| to register new parts.
Connections["ObjectAdded"] = WorkspaceService:WaitForChild("UserGeneratedBlocks").DescendantAdded:Connect(Register)

--# Loops through all objects that may
--| have been missed or already added
--| before the script started.
for _, Object in pairs(WorkspaceService:WaitForChild("UserGeneratedBlocks"):GetDescendants()) do
	Register(Object)
end

Let me know if something in here doesn’t work and I’ll try to modify it to fit your game.


Extra Note:

Also when no parts are moving anymore, the script will stop until more parts are added. This means that script activity will drop to zero with the new code I gave you when all parts have stopped moving.

1 Like

As long as you destroy the connections when the parts are gone, it should not lag. Checking the velocity only when it changes allows your computer to save memory when parts are stationary.

I would make something like this:

local Connections = {}
local VelocityTracker = {}

Folder.ChildAdded:Connect(function(child)
    if child:IsA("BasePart") then
        Connections[child] = child:GetPropertyChangedSignal("Velocity"):Connect(function()
            VelocityTracker[child] = child.Velocity
        )
    end
)

Folder.ChildRemoved:Connect(function(child)
    if Connections[child] then
        Connections[child]:Disconnect()
        Connections[child] = nil
        VelocityTracker[child] = nil
    end
)
1 Like

It works fine, thanks for helping, but I have an issue, as the code is intended to work, when a part is not moving, it gets removed from the registry table, but when it starts to move again, it does not get added to the table again, I can fix it for myself by just keeping track of the parts and not removing them from the table, but I would like to know if you have a better idea.

if Object.AssemblyLinearVelocity == Vector3.new(0,0,0) then
						Unregister(Object)
					else
						Registry[Object] = Object.AssemblyLinearVelocity
					end

In this part, just delete the Unregister(Object) line.

There really isn’t a better idea unfortunately.

1 Like

When an object is destroyed, all subsequent event connections on the object are automatically disconnected.
The only case where this is necessary is if you’re connecting up another connection, unrelated to the object itself.

1 Like