How to create a pet system that dynamically generates positions

Recently there has been a lot of simulator games that have been made and contain pets! Though not many people know how to do it and this is what this tutorial is for!

Please note that this tutorial:

will teach:

  • How to dynamically generate pet positions
  • How to detect floors and stay the same distance from the floor
  • Different position generations

will not teach:

  • Pet hatching system
  • Pet inventory system

First lets define our pet
the pet will just be a model with a PrimaryPart named “Base” and other details such wings can just be added to the model and it would work fine!
image


the PrimaryPart(Base):

Now we could just add a script to the pet and just tween the pet to the player right?
Nope, this is because server side tweening or lerping causing the pet to be too laggy when viewed on the client side so instead we ‘render’ the pets on the client side!

We can do this by spawning the pets in on the server side and have the clients (players) move the pets into position themselves. The only downside to this is that on the server side the pets will also be in the same positions no matter what

what the clients will see:

what the server will ‘see’:
image
(all the pets are in the same position)

but as long your does not rely on the pets attacking enemies or getting coins this should be fine

Creating pets
Using a server script inside ServerScriptService we can add a folder to workspace labeled by the player followed by “-Pets” so we can put our pets in that folder

--This code will fire every time a new player enters the game
game.Players.PlayerAdded:Connect(function(player)

	--creating player pet folder named by the player name followed by "-Pets"
	local petsFolder = Instance.new("Folder", workspace)
	petsFolder.Name = player.Name.."-Pets"
	
	--This is where you want to get what pets the player has and put them into
	--the folder. But for the sake of the tutorial I am just going to add 9
	--pets to the folder
	for i=1, 9, 1 do
		local p = game.ReplicatedStorage.Pets.Pet:Clone()
		p.Parent = petsFolder
	end
	
end)

--Make sure sure to delete the pets when the player leaves
game.Players.PlayerRemoving:Connect(function(player)
	
	if not workspace:FindFirstChild(player.Name.."-Pets") then return end
	
	workspace:FindFirstChild(player.Name.."-Pets"):Destroy()
	
end)

Now using a local script inside StarterCharacter:

--getting RunService
local RunService = game:GetService("RunService")

--Congiurable values
local maxPetsPerRow = 3 --how many pets can be in one row
local distancebetweenColumns = 5 --gap between columns
local distanceBetweenRows = 3 --gap between rows
local behindPlayerDistance = 3 --distance from player
local floatHeight = 3 --height above the ground
local smoothness = 4 --how fast the pet gets into position

--Main function to change Position formations
function GridPositionGen(numOfPets, petIndex, playerHrp) :Vector3
	
	--lua for loop starts at 1
	petIndex -= 1
	local temp = maxPetsPerRow
	if numOfPets < maxPetsPerRow then
		maxPetsPerRow = numOfPets
	end
	
	local horizontalOffset = 0

	if petIndex - maxPetsPerRow*math.floor(numOfPets/maxPetsPerRow) < 0 then
		horizontalOffset = (petIndex%maxPetsPerRow * distancebetweenColumns) - ((maxPetsPerRow-1)*distancebetweenColumns)/2
	else
		maxPetsPerRow = numOfPets - maxPetsPerRow*math.floor(numOfPets/maxPetsPerRow)
		horizontalOffset = petIndex%maxPetsPerRow * distancebetweenColumns - ((maxPetsPerRow-1)*distancebetweenColumns)/2
		maxPetsPerRow = temp
	end
	
	local backwardsOffset = playerHrp.Position - playerHrp.CFrame.LookVector*behindPlayerDistance - playerHrp.CFrame.LookVector*distanceBetweenRows*(math.floor(petIndex/maxPetsPerRow))
	
	local offset = backwardsOffset + playerHrp.CFrame.RightVector*horizontalOffset
	
	maxPetsPerRow = temp
	
	return offset
	
end

--Floor detection using raycasting
function CheckForFloor(offset:Vector3, filter:{Instance})
	
	--raycast from the offset downwards to detect the floor
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = filter
	rayParams.FilterType = Enum.RaycastFilterType.Blacklist
	local ray = workspace:Raycast(offset + Vector3.new(0, 5, 0), Vector3.new(0, -9999, 0), rayParams)

	if ray then 
		--If the ray hit a floor it will return the Y coord + floatHeight
		return ray.Position.Y + floatHeight
	else
		--If the ray didn't hit a floor is will return nil and print a warning
		warn("no floor detected")
		return
	end
	
end

--Goes through a player's pet folder and put the pets at the correct position
function RenderPets(petsFolder, player, deltaTime)
	
	--checks if the player's character spawned yet and if deltaTime was given else don't
	--render this player's pets
	if not player.Character or not player.Character.HumanoidRootPart then return end
	if not deltaTime then print("no deltaTime given") return end
	
	--getting the players's Character and HumanoidRootPart
	local char = player.Character
	local hrp = char:WaitForChild("HumanoidRootPart")
	
	--loop through each pet of the folder to lerp them
	--into eh correct position
	for i, v in pairs(petsFolder:GetChildren()) do

		--getting the offset from the player, note we have to subtract 1 from i as it starts from 1
		--but we need it to start from 0 otherwise every pet 1 space off 
		local offset = GridPositionGen(#petsFolder:GetChildren(), i, hrp)
		
		--Getting the distance we need from the floor so the pet will stay at a constant height from
		--the floor
		local verticalPos =  CheckForFloor(offset, {char, petsFolder}) or hrp.CFrame.Position.Y 
		--the or is for if the ray did not hit the floor, the pet will be at character Y position

		offset = Vector3.new(offset.X, verticalPos, offset.Z)
		
		--creating the CFrame where the pet should be
		local c = CFrame.new(offset, offset + hrp.CFrame.LookVector)
		
		--Lerping the pet to the target CFrame so its more smooth
		--We multiply smoothness by deltaTime so it's frame independent
		v:SetPrimaryPartCFrame(v.PrimaryPart.CFrame:Lerp(c, smoothness*deltaTime))
	end

end

--Fires every frame
RunService.Heartbeat:Connect(function(deltaTime)
	--loops through players to get their pets folder
	for i, player in pairs(game.Players:GetPlayers()) do
		
		--if the player does not have a pet folder then we can skip them
		if not workspace:FindFirstChild(player.Name.."-Pets") then continue end
		
		--Main function to smoothly move the pets into position
		RenderPets(workspace:FindFirstChild(player.Name.."-Pets"), player, deltaTime)

	end
end)

Note that the GridPositionGen function can be change so there can be different formations.
For example here’s a function that makes the pets surround the player and you can replace this with the GridPositionGen if its more for you:

function CircularPositionGen(numOfPets, petIndex, playerHrp:BasePart)
	
	local radius = numOfPets*raidusMultiplyer
	local angle = (petIndex/numOfPets)*360
	
	local offset = Vector3.new(math.sin(math.rad(angle)) * radius, 0, math.cos(math.rad(angle)) * radius)
	
	return offset+playerHrp.Position
	
end

And at last we are done!

As an added bonus since we generate the position every frame, we can add or remove pets and it will still work and put every pet in the expected position at runtime!

That’s all for this tutorial and I was thinking of a way to use roblox’s align position and align orientation as a way to this so if you want that I can create a tutorial for that.
Thank you for reading and cheers!

38 Likes

WOW that is acctually really cool! good job! but now its time to make a bee swarm simulator rip off >:)

4 Likes

The only downside is that it’s client :disappointed_relieved:. But you could use align position so it’s server sided. Then everyone can see it without lag.

Ye i was thinking of making a pet system using Roblox’s physics system

2 Likes