How to make a very optimized Pet Follower?

Hello developers! I’ve been trying to figure out how I can make an optimized Follower like Pet Sim 99 for a couple of days now.

Each player will be able to equip a maximum of 100 pets. The limit of players on the server will be 10 == 1k pets

I have tried so many different ways.

  1. local script with one loop that handles all pets.
  2. local script, but with a separate loop for each player and his pets.
  3. a local script inside Actor, which means Parallel.
  4. Pet Class (metatables)

Almost every method makes the same fps
But I’ll take the parallel script as an example:
1 player = 100 pets, 200+ fps when not moving, and 160-170fps when moving.
2 players = 100 pets for each, and a total of 200 pets. 140± FPS when not moving and 100± FPS when moving.

But this is on PC, it’s scary to imagine what will happen on phones and weak devices.
And so I really want to know what are the ways to make an optimized Follower

Below I will provide one of the last attempts to write a good Follower

Code :

local Players = game:GetService('Players')
local RS = game:GetService('ReplicatedStorage')
local RunService = game:GetService('RunService')
local Actor : Actor = script.Parent

local player = Players:GetPlayerByUserId(Actor:GetAttribute('OwnerId'))
local PetFolder = workspace:WaitForChild(player.Name..' pets')
local Rendering = require(RS.PetRender)

local Positions = {}
local List = {}

function setPetPos(Pet, Pos)
	Positions[Pet] = Pos
end

function updatePositions()
	table.clear(List)
	List = PetFolder:GetChildren()
	local Rows = Rendering.getRows(#List)
	local SplitData = Rendering.splitPets(Rows, List)

	for rowIndex, RowData in Rows do
		local Data = SplitData[rowIndex]

		for _petIdex, petModel : Model in Data do
			local formula = _petIdex * (math.pi/1.2) / RowData.Pets

			setPetPos(petModel,CFrame.new(
				math.cos(formula) * RowData.Radius,
				0,
				2 + math.sin(formula) * RowData.Radius
				))
		end
		RunService.Heartbeat:Wait()
	end
end

function getResult(Origin : Vector3)
	local dir = Vector3.new(0,-100, 0)
	local RayParams = RaycastParams.new()
	RayParams.FilterType = Enum.RaycastFilterType.Exclude
	RayParams:AddToFilter({
		PetFolder,
		player.Character
	})

	return workspace:Raycast(Origin + Vector3.new(0,50, 0), dir, RayParams)
end

function updatePets(dt)
	local Character = player.Character
	if not Character then
		return;
	end

	local RootPart = Character.PrimaryPart
	local InitCharacterCF = RootPart:GetPivot()
	local RootCF = CFrame.new(
		InitCharacterCF.X,
		0,
		InitCharacterCF.Z
	) * InitCharacterCF.Rotation

	for petModel : Model, TargetPosition : CFrame in Positions do
		local Primary = petModel.PrimaryPart
		local FinalPosition
		if not Primary then
			continue;
		end

		FinalPosition = RootCF * TargetPosition

		local result = getResult(FinalPosition.Position)

		if not result then
			continue;
		end
		
		task.synchronize()
		Primary.CFrame = FinalPosition * CFrame.new(0, result.Position.Y + Primary.Size.Y/2, 0)
	end
end

function render()	
	updatePositions()
	RunService.RenderStepped:ConnectParallel(updatePets)

	PetFolder.ChildAdded:Connect(function(child: Instance) 
		Actor:SendMessage('petAdded')	
	end)
end

Actor:BindToMessage('petAdded', updatePositions)
Actor:BindToMessage('petRemoved', updatePositions)
Actor:BindToMessage('OnStart', render)
1 Like

I suggest you take advantage of the Roblox Constraints for this purpose.
You can instead use an AlignPosition Instance to control the movement, and just create 2 attachments, one on the Pet, and one on the Character, with an offset of where you want it to go.
Then, you’re given the following effect:
RobloxStudioBeta_yecxbRPFy8

With this, you should experience way better performance, because the physics engine handles all the changes, rather than Lua Code.

3 Likes