How do I get this client-sided NPC animation script to work with StreamingEnabled?

I am currently working on enabling StreamingEnabled in this game, to hopefully fix this issue(s).

I’ve experimented with StreamingEnabled on this place before, but I had some issues with it that made me postpone the update. However, I want to push this update out ASAP.

Once thing StreamingEnabled messed with was the way vehicles spawn in the game. The vehicle spawning system works by having an NPC near the garage/airfield, that waves at you when you are near, and you interact with its ProximityPrompt to open up the vehicle spawning GUI, as seen below:

All of this runs on a LocalScript, as seen here:

local animation = script:WaitForChild("Wave",2)
local rs = game:GetService("RunService")
local chars = {}
local player = game.Players.LocalPlayer

local function checkForNPC(child)
	if child.Name == "vehicleSpawnNPC" then
		local data = {plrNear = false,dummy = child}
		chars[child] = data
		local head = child:WaitForChild("Head",10)
		local prox = head:WaitForChild("ProximityPrompt",3)
		if prox ~= nil then
			local playIt = child.Humanoid:LoadAnimation(animation)
			prox.PromptShown:Connect(function()
				chars[child].plrNear = true
				playIt:Play()
				playIt.Looped = true
				prox.PromptHidden:Wait()
				chars[child].plrNear = false
				playIt:Stop()
			end)
			prox.Triggered:Connect(function(player)
				print("success on the NPC")
				if player.PlayerGui.vehicleSelector.Enabled == false then
					player.PlayerGui.vehicleSelector.SpawningNPC.Value = child;
					player.PlayerGui.vehicleSelector.Enabled = true
				end
			end)
		end
	end
end

for _,child in pairs(workspace.tycoons:GetDescendants()) do
	print(tostring(child).." added")
	checkForNPC(child)
end

workspace.tycoons.DescendantAdded:Connect(function(child)
	print(tostring(child).." removed")
	checkForNPC(child)
end)

workspace.tycoons.DescendantRemoving:Connect(function(child)
	if child.Name == "vehicleSpawnNPC" then
		chars[child] = nil
	end
end)

rs.Stepped:Connect(function(Time, deltaTime)
	for _,data in next,chars do
		if data.plrNear == true then
			local char = player.Character or player.CharacterAdded:Wait()
			local npcPosition = data.dummy.HumanoidRootPart.Position
			local playerPosition = char.HumanoidRootPart.Position
			local _,Y = CFrame.lookAt(npcPosition,playerPosition):ToOrientation();
			data.dummy.HumanoidRootPart.CFrame = CFrame.Angles(0,Y,0)+npcPosition;
		end
	end
end)

There is an issue, however, where this script will not run correctly when StreamingEnabled is on. The NPC won’t wave at the player, and the player will be unable to open the ProximityPrompt to spawn a vehicle.

How can I fix this?

Ok, so I got some output on this error:

I’m not exactly sure how this breaks sometimes and works other times… Can someone help?

The line number doesn’t match up with any of the :WaitForChild() calls in the original post, but i assume it is one of these:

Interestingly, it almost seems like that error shouldn’t even be possible, given that to reach that point in the function in the first place, it already has to check if child.Name == "vehicleSpawnNPC", meaning that the NPC should theoretically have already been streamed in.

By chance, does the game happen to teleport players away from the map upon joining (e.g. for the purpose of a menu screen)?


Consider setting the ModelStreamingMode property for the NPC to Atomic, as that would guarantee that once you’re able to access the Model, all of its descendants have already been streamed in.

1 Like

Yep. Essentially, when they join the game, they spawn on a “neutral” team that has its spawns very far from the map, but they have their camera views set to rotate around a village until they pick a team.

1 Like

I had initially thought that something like that could be causing the situation where the NPC is immediately streamed in to the client but then because they are teleported far way soon after, it streams out the NPC, leading to the outlined issues.

However, upon reading through the “Technical Behavior” section of the Instance Streaming Roblox Creator Documentation guide I realized that doing that on its own probably is not the issue.

When StreamingEnabled is turned on, the default behavior is that each Model Instance on its own is replicated on player join, but its contents are not streamed in until it’s met the particular requirements to do so.

As a result, the loop that looks through the descendants of the tycoons as well as the function that’s listening for new descendants to be added to the tycoons will be able to reference the vehicleSpawnNPC model before it’s actually within range to have all of its descendants be fully streamed in to the client. This means that if a player does not get within range of each vehicleSpawnNPC within 10 seconds from the moment that the checkForNPC finds the model, the first :WaitForChild() will timeout, causing the code to proceed and error.


This leads me to believe that the suggestion in my previous post of setting the ModelStreamingMode of each vehicleSpawnNPC to Atomic will be the solution to this, as that would ensure that the checkForNPC function will not be called for the NPC until both it and its descendants have been replicated to the client.

1 Like

Yeah, the game was made not using StreamingEnabled originally, so going back and trying to fix things retrospectively for StreamingEnabled has been a MAJOR pain in the ass.

I was going to test your suggested fix, and another bug occurred:

As you can see, once I picked a team, the spinning effect of the camera stopped, my character died/was force respawned and… it gets stuck.

This is not a consistent bug, sometimes it works fine, as seen below:

Would you know how to fix this too?

1 Like

Hmmm, that’s really strange. Are the camera effects / respawning somehow tied together / linked to the LocalScript from the original post?

Oh and did that bug happen before making the change to the NPCs, or after? And are there any warnings / errors that appear in the Developer Console or Output when that occurs?

Without having a better idea of how the spawning functionality generally works, I don’t immediately know what could be causing that to happen. *However, one thing to try could be to force “reset” the player’s camera back to a view of their Character model upon respawning, since maybe it remained in the Scriptable CameraType from the menu cutscene.

Of course you don’t need to (and probably shouldn’t) share large sections of code from such a fundamental part of the game just in case someone finds a way to take advantage of that, so if you would be able to at least offer a brief description of how the team selection and / or respawning process is structured, that would make it easier to think of possible solutions.

1 Like

Here’s the script for it:

local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local cameraEffect = workspace:WaitForChild("cameraEffect",100)

local target = cameraEffect:WaitForChild("Part",10)
local camera = workspace.CurrentCamera
camera.CameraType = Enum.CameraType.Scriptable
local rotationAngle = Instance.new("NumberValue")
local tweenComplete = false

local cameraOffset = Vector3.new(0, 10, 12)
local rotationTime = 40  -- Time in seconds
local rotationDegrees = 360
local rotationRepeatCount = -1  -- Use -1 for infinite repeats
local lookAtTarget = true  -- Whether the camera tilts to point directly at the target

local function updateCamera()
	if not target then return end
	camera.Focus = target.CFrame
	local rotatedCFrame = CFrame.Angles(0, math.rad(rotationAngle.Value), 0)
	rotatedCFrame = CFrame.new(target.Position) * rotatedCFrame
	camera.CFrame = rotatedCFrame:ToWorldSpace(CFrame.new(cameraOffset))
	if lookAtTarget == true then
		camera.CFrame = CFrame.new(camera.CFrame.Position, target.Position)
	end
end

-- Set up and start rotation tween
local tweenInfo = TweenInfo.new(rotationTime, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, rotationRepeatCount)
local tween = TweenService:Create(rotationAngle, tweenInfo, {Value=rotationDegrees})
tween.Completed:Connect(function()
	tweenComplete = true
end)
tween:Play()

-- Update camera position while tween runs
RunService.RenderStepped:Connect(function()
	if tweenComplete == false then
		updateCamera()
	end
end)

the gui with this script gets deleted at one point (hence why it stops spinning) and gets stuck.

This glitch happened before the change to the NPCs, and there was no available output to help diagnose this.

1 Like

Is it deleted the moment when the player selects a Team to join from the main menu?

Does there happen to be a separate script somewhere else in the game that is responsible for:

  1. Setting the Camera.CameraType back to Custom (the default value)
  2. Updating the Camera.CameraSubject to the Character’s new Humanoid (after respawning)

If so, when is it instructed to run that code?


Using the code you provided, I tested the following situation:

  1. Start a playtest
  2. Delete the ScreenGui that contains the LocalScript
  3. Reset my Character
  4. Observe that the camera is stuck in the place that it was when the LocalScript was deleted

However, I then created another LocalScript that would update the CameraType to Custom and the CameraSubject to my Character’s Humanoid whenever something touched a part in the workspace. Then, I tested the following situation:

  1. Start a playtest

  2. Delete the ScreenGui that contains the LocalScript

  3. Reset my Character

  4. Observe that the camera is stuck in the place that it was when the LocalScript was deleted

  5. Touch the part in the workspace

  6. Observe that the view of the camera returns back to the player’s Character, even upon respawning again afterwards, without needing to touch the part.


Something I noticed (which might not contribute to those inconsistencies, but I figured I’d mention it anyway, just in case) is the following:

As far as I’m aware, when the repeat count for a Tween is set to -1, it will never fire the Tween.Completed event, so tweenComplete would permanently be false.

Additionally, based on some quick tests, it appears that infinitely looping Tweens are not automatically cancelled when the script that initially played it is destroyed. The RenderStepped function would be stopped from the deletion of the LocalScript, but for some reason, the Tween isn’t.

Although the behavior showcased in the video you posted doesn’t seem to be related to the Tween (since the camera is stationary rather than rotating around an object), that might be something to look into.

1 Like

Correct, the tween goes on indefinitely because I want the camera spinning to occur as long as the player is in the lobby area.

I assume that, I should have a case when the parent script/GUI gets deleted, the tween is cancelled then?

It’s likely that a simple script that re-adjusts the camera to be the player’s character on respawn every time should suffice. I was under the impression ROBLOX does this anyway, since most of the time, when you respawn from the lobby, your camera is corrected.

1 Like

ye, the function connected to the tween.Completed event would not be necessary in that case, then.

Ideally, yes. It could be stored in a separate module, perhaps, so that Tween:Cancel() can be called on the Tween after script.Destroying fires.

Yup! The only two properties of the Camera that I needed to update in my testing for the view to return back to the Character model were:

Upon doing that just once (after the LocalScript that had set the CameraType to Scriptable for the TweenService cutscene was deleted), the code didn’t need to be called again for subsequent respawns of the Character to readjust the camera back to the Character model.

However, it would be wise to call it once upon *every respawn to avoid any edge cases where the camera gets stuck in place beyond the situation of choosing a team to join.

It does do this by default, unless the CameraType is set to Scriptable, as that one is specifically meant to be used by developers. I assume that the game internally check if the CameraType is set to Scriptable before respawning the Character, and if it is, it doesn’t change anything as to avoid interfering with the changes that the developer has made.

If the error persists and you end up in that static camera view again, try printing the value of CurrentCamera.CameraType in the Developer Console to see if it was still set to Scriptable. If so, then there was probably an issue at some point in the process where it wasn’t set back to Custom after respawning from the lobby.

1 Like