How to get a NPC sit down on a seat on client / How to attach a seat to an Humanoid (Performance issue, very complex)

  1. What do you want to achieve? Getting my NPC.Character.Humanoid SeatPart Property to their seat AND Seat.Occupant to the NPC. Because i want the NPC to sit down and stay at this seat.

  2. What is the issue? The problem is that SeatPart and Occupant are read-only. I can’t figure how to achieve that, i have tried several solution.

  3. What solutions have you tried so far? I tried using Seat:Sit, but it just set the sit property of the Humanoid to True, and the Occupant property of the currentSeat is still nil.
    I also debbugged the code to see if currentSeat exist, it exist, and after that the code pass at currentSeat:Sit(humanoid) nothing happen, only the humanoid Sit property is set to True. No error is being raised. And somehow, some NPC have still have they Sit property to False. I also tried Humanoid.Sit = true, but it just force the property to be true and not truly sitting where i want. It says that “if the Humanoid isn’t attached to a seat while in its sitting state, it will trip over with no collision in its legs”, but i can’t find out how to attach the Humanoid to a sit. That’s why i tried Seat:Sit(Humanoid: Instance) but it just won’t work. So in summary i’m here to get how could i attach a sit to an humanoid.

I also wanna inform you that the script is running on client. And also i have some Beta Feature in Roblox Studio Enabled

Here is the code that i’m using with Seat:Sit (not working as explained above), if you need more for help me, ask me.

elseif boardingState.Value == "MovingToSeat" then
			-- Check if we have a current position to compare
			if lastPosition then
				local currentPosition = passenger:GetPivot().Position
				local movement = (currentPosition - lastPosition).Magnitude

				-- If passenger hasn't moved
				if movement < 0.1 then
					stuckTime = stuckTime + 0.1
					if stuckTime > 0.5 and currentSeat then
						-- Use the Seat's Sit function directly
						currentSeat:Sit(humanoid)
						boardingState.Value = "Seated"
						return false
					end
				else
					stuckTime = 0
				end

				lastPosition = currentPosition
			end

The issue of NPCs not sitting properly on seats in Roblox often stems from physics timing and synchronization limitations, especially when handling this client-side. When calling Seat:Sit(humanoid) on the client, Roblox sets the NPC’s Humanoid.Sit to true, but the attachment between the Seat and the Humanoid may fail to register properly… This occurs because seat assignments rely on server-side physics updates, which may not synchronize instantly with the client. :+1:

So in sumarry you mean that my script is unoptimized or that i need to use this on server-side ?

1 Like

Well, NPC handling on the client side is only useful if only 1 or a selected group of players needs to see the NPC. In a case with a global presence, it’s better to do a server-side call, in my opinion. Or you could use asynchronous to ensure the thing takes its time and adapts to its own timing.

EDIT: grammar… :sob:

The problem is that i’m doing AI NPC for a train game. But the train movement logic is on all client, because on server-side it’s lagging the whole server. So i want my NPC to move and seat on all client, rather than server and defaulty replicate on client. I don’t know anything about asynchronous thing, can we do it on client and if yes how can i implement it?

here is my whole function

-- Function to make passenger board the train
local function boardTrain(passenger, train, door)
	local humanoid = passenger:FindFirstChild("Humanoid")
	if not humanoid then return end

	-- Set up collision immediately
	setPassengerCollision(passenger)

	-- Create a new state value if it doesn't exist
	local boardingState = passenger:FindFirstChild("BoardingState") or Instance.new("StringValue")
	boardingState.Name = "BoardingState"
	boardingState.Value = "MovingToDoor"
	boardingState.Parent = passenger

	-- Variables to track movement
	local lastPosition = nil
	local stuckTime = 0
	local currentSeat = nil

	-- Movement update function
	local function updateMovement()
		if not passenger.Parent or not humanoid then
			return false
		end

		-- Check distance from player for optimization
		local playerCharacter = LocalPlayer.Character
		if playerCharacter and playerCharacter:FindFirstChild("HumanoidRootPart") then
			local distanceFromPlayer = (passenger:GetPivot().Position - playerCharacter.HumanoidRootPart.Position).Magnitude
			if distanceFromPlayer > PASSENGER_LOAD_DISTANCE then
				return true  -- Keep the connection but don't process movement
			end
		end

		if boardingState.Value == "MovingToDoor" then
			-- Move to door center
			humanoid:MoveTo(door.centerPoint)

			-- Check if at door
			local distance = (passenger:GetPivot().Position - door.centerPoint).Magnitude
			if distance < DISTANCE_TO_DOOR then
				boardingState.Value = "FindingSeat"
			end

		elseif boardingState.Value == "FindingSeat" then
			local seat = findNearestSeat(passenger, train)
			if seat then
				currentSeat = seat
				boardingState.Value = "MovingToSeat"
				humanoid:MoveTo(seat.Position)
				lastPosition = passenger:GetPivot().Position
			end

		elseif boardingState.Value == "MovingToSeat" then
			-- Check if we have a current position to compare
			if lastPosition then
				local currentPosition = passenger:GetPivot().Position
				local movement = (currentPosition - lastPosition).Magnitude

				-- If passenger hasn't moved
				if movement < 0.1 then
					stuckTime = stuckTime + 0.1
					if stuckTime > 0.5 and currentSeat then
						-- Use the Seat's Sit function directly
						currentSeat:Sit(humanoid)
						boardingState.Value = "Seated"
						return false
					end
				else
					stuckTime = 0
				end

				lastPosition = currentPosition
			end

		elseif boardingState.Value == "Seated" then
			return false
		end

		return true
	end

	-- Connect to MoveToFinished event for precise movement tracking
	--[[humanoid.MoveToFinished:Connect(function(reached)
		if boardingState.Value == "MovingToSeat" and reached and currentSeat then
			humanoid.Sit = true
			boardingState.Value = "Seated"
		end
	end)]]

	-- Set up the movement loop
	local connection
	connection = RunService.Heartbeat:Connect(function()
		if not updateMovement() then
			connection:Disconnect()
		end
	end)
end

-- Main function to check for and manage boarding
local function startBoardingSystem()
	-- Wait for character to load
	if not LocalPlayer.Character then
		LocalPlayer.CharacterAdded:Wait()
	end

	-- Main loop
	while true do
		local passengerFolder = workspace:FindFirstChild("Passengers")
		if passengerFolder then
			-- Set collision for new passengers immediately
			for _, stationFolder in ipairs(passengerFolder:GetChildren()) do
				for _, passenger in ipairs(stationFolder:GetChildren()) do
					if not passenger:FindFirstChild("CollisionSet") then
						setPassengerCollision(passenger)
						-- Mark as processed
						local marker = Instance.new("BoolValue")
						marker.Name = "CollisionSet"
						marker.Value = true
						marker.Parent = passenger
					end
				end
			end
			local playerCharacter = LocalPlayer.Character
			if playerCharacter and playerCharacter:FindFirstChild("HumanoidRootPart") then
				local playerPosition = playerCharacter.HumanoidRootPart.Position

				-- Check each passenger folder
				for _, stationFolder in ipairs(passengerFolder:GetChildren()) do
					-- Get average position of passengers in this folder
					local folderPosition = getFolderAveragePosition(stationFolder)
					if folderPosition then
						local distanceToStation = (playerPosition - folderPosition).Magnitude

						if distanceToStation <= PASSENGER_LOAD_DISTANCE then
							-- Extract station info from folder name
							local currentStation, headingStation = stationFolder.Name:match("(.+)To(.+)Passengers")
							if currentStation and headingStation then
								-- Get SIEL models for this route
								local sielModels = getSIELModel(currentStation, headingStation)

								-- Check each SIEL model
								for _, sielModel in ipairs(sielModels) do
									local trainValue = sielModel:FindFirstChild("TrainAtStation")
									if trainValue and trainValue.Value then
										local train = trainValue.Value

										-- Check if train is ready for boarding
										local boardingReady = train:FindFirstChild("BoardingReady")
										if boardingReady and boardingReady.Value then
											-- Find valid doors
											local validDoors = findValidDoors(train)
											if #validDoors > 0 then
												-- Start boarding process for each passenger
												for _, passenger in ipairs(stationFolder:GetChildren()) do
													-- Skip passengers who are already boarding
													if not passenger:FindFirstChild("BoardingState") then
														-- Find nearest door
														local nearestDoor = validDoors[1]
														local shortestDistance = math.huge

														for _, doorInfo in ipairs(validDoors) do
															local distance = (passenger:GetPivot().Position - doorInfo.centerPoint).Magnitude
															if distance < shortestDistance then
																shortestDistance = distance
																nearestDoor = doorInfo
															end
														end

														-- Start boarding process
														local humanoid = passenger:FindFirstChild("Humanoid")
														if humanoid then
															humanoid.WalkSpeed = WALKING_SPEED
															boardTrain(passenger, train, nearestDoor)
														end
													end
												end
											end
										end
									end
								end
							end
						end
					end
				end
			end
		end
		task.wait(BOARDING_UPDATE_RATE)
	end
end

The startBoardingSystem() function is called when the player character spawn.

I also read the Multithreading thing, if i place an actor as the parent of the client script it will be okay or do i need to do something else?

I’ll try to look at it today super character limit

1 Like

I a bit modified my script to use async or idk, and i also added an actor, and i can confirm that it’s a “performance issue”.

-- Async function to handle seating process
local function attemptToSeat(humanoid, seat)
	local success = false
	local attempts = 0
	local maxAttempts = 5

	-- Return a promise that resolves when seating is successful or max attempts reached
	return {
		await = function()
			while not success do
				attempts = attempts + 1

				-- Attempt to sit
				seat:Sit(humanoid)

				-- Wait for physics to update
				task.wait(0.2)
				print("ya")
				-- Check if actually seated
				if humanoid.Sit and humanoid.SeatPart == seat then
					success = true
					break
				end

				-- If not successful, wait a bit before next attempt
				task.wait(0.3)
			end
			return success
		end
	}
end

-- Function to make passenger board the train
local function boardTrain(passenger, train, door)
	local humanoid = passenger:FindFirstChild("Humanoid")
	if not humanoid then return end

	-- Set up collision immediately
	setPassengerCollision(passenger)

	-- Create a new state value if it doesn't exist
	local boardingState = passenger:FindFirstChild("BoardingState") or Instance.new("StringValue")
	boardingState.Name = "BoardingState"
	boardingState.Value = "MovingToDoor"
	boardingState.Parent = passenger

	-- Variables to track movement
	local lastPosition = nil
	local stuckTime = 0
	local currentSeat = nil

	-- Movement update function
	local function updateMovement()
		if not passenger.Parent or not humanoid then
			return false
		end

		-- Check distance from player for optimization
		local playerCharacter = LocalPlayer.Character
		if playerCharacter and playerCharacter:FindFirstChild("HumanoidRootPart") then
			local distanceFromPlayer = (passenger:GetPivot().Position - playerCharacter.HumanoidRootPart.Position).Magnitude
			if distanceFromPlayer > PASSENGER_LOAD_DISTANCE then
				return true  -- Keep the connection but don't process movement
			end
		end

		if boardingState.Value == "MovingToDoor" then
			-- Move to door center
			humanoid:MoveTo(door.centerPoint)

			-- Check if at door
			local distance = (passenger:GetPivot().Position - door.centerPoint).Magnitude
			if distance < DISTANCE_TO_DOOR then
				boardingState.Value = "FindingSeat"
			end

		elseif boardingState.Value == "FindingSeat" then
			local seat = findNearestSeat(passenger, train)
			if seat then
				currentSeat = seat
				boardingState.Value = "MovingToSeat"
				humanoid:MoveTo(seat.Position)
				lastPosition = passenger:GetPivot().Position
			end

		elseif boardingState.Value == "MovingToSeat" then
			-- Check if we have a current position to compare
			if lastPosition then
				local currentPosition = passenger:GetPivot().Position
				local movement = (currentPosition - lastPosition).Magnitude

				-- If passenger hasn't moved
				if movement < 0.1 then
					stuckTime = stuckTime + 0.1
					if stuckTime > 0.5 and currentSeat then
						-- Start async seating process
						boardingState.Value = "AttemptingToSeat"

						task.spawn(function()
							local seatingPromise = attemptToSeat(humanoid, currentSeat)
							local success = seatingPromise:await()
							print("bruh")
							if success then
								boardingState.Value = "Seated"
							else
								-- If seating failed, try finding another seat
								boardingState.Value = "FindingSeat"
							end
						end)

						return false
					end
				else
					stuckTime = 0
				end

				lastPosition = currentPosition
			end

		elseif boardingState.Value == "Seated" then
			return false
		end

		return true
	end

	-- Connect to MoveToFinished event for precise movement tracking
	--[[humanoid.MoveToFinished:Connect(function(reached)
		if boardingState.Value == "MovingToSeat" and reached and currentSeat then
			humanoid.Sit = true
			boardingState.Value = "Seated"
		end
	end)]]

	-- Set up the movement loop
	local connection
	connection = RunService.Heartbeat:Connect(function()
		if not updateMovement() then
			connection:Disconnect()
		end
	end)
end

The problem is that we need to wait that it print around 2000 “ya”, and with our task.wait it will be long. If i remove the task.wait(), roblox freeze for 30sec or more i think, and the passenger seat correctly on the seat (succes = true). And the print are looking like this when the freezing end:

And sometimes, it’s my roblox that crash…

SO i don’t know what to do, BUT i want to make it worrk. i need help to make it work WITHOUT performance issue. Please help :pray:

Here is also the script performance at a moment when it freezing when there aren’t task.wait():
image
I know that i’s hitting 100%, but if i don’t want to use task.wait, is there a way to reduce the activity??

1 Like