StreamingEnabled behavior: What instances are guaranteed to be streamed?

Solved

  • The (non-BasePart) descendants of a BasePart are guaranteed to be streamed in with the BasePart though not immediately (i.e. after added events related to the BasePart)
  • Because the descendants aren’t all added at the same time, CPU spikes generally won’t be a problem, unless there are a large amount
  • Read solution for more details

Hello DevForum!

I am creating a game with streaming enabled, where collections of Instances form “machines” that players can interact with.

There is a lot here, if you’re very knowledgeable about streaming enabled please feel free to scroll to the bottom section that says questions (maybe glance at the third picture).

Here is an example of a machine:
image

I have two goals with using streaming enabled:

  • The data can be streamed in and out
    • By “data” I mean the value Instances and remotes of a machine. The BaseParts too would be an awesome added bonus.
    • This is to save client memory, most of the game’s memory is anticipated to be taken up by many machines’ data
    • Will use binders/tags to manage machines going in and out
  • Data is guaranteed to stream in fully, all at once
    • It would also be great if there was a way I could assume that the entire machine model will be streamed in and out all at once.
    • It is fairly important that the machine can be guaranteed to have all of its data on the client. I have a few work arounds I can implement, but they aren’t nearly as clean.

I have read the content streaming article and found some helpful information:

“When a player joins, all instances in the Workspace are sent to the client, excluding BaseParts and descendants of those instances. Then, during gameplay, the server streams in BaseParts and their descendants to the client.”

That would mean with my current structure, the Instances highlighted below would never be streamed in and out:

image

I can’t have all that data on all clients, so I am thinking about this structure:

image

(Where the parent Part just a non-collide hitbox around the machine.) This seems like a perfect solution, besides that the same article says to avoid exactly this:

“Avoid parenting a part to another part, especially when the parent is far away from the child. Instead, group them as a Model.”

I am also worried if all the Instances are streamed in all at once that there might be problems:

Avoid creating moving assemblies [which stream in all at once] with unnecessarily large numbers of instances, as all of the instances streaming in unison may cause network/CPU spikes.

It would be nice if there was an optimization or something that prebuilds the assemblies in nil, then time slices up the operation, then moves it to Workspace all at once. That might exist already though. I can also probably solve this problem with meshes or something though.

Questions

This comes to my technical questions about streaming enabled:

  • Are all of the descendants of a BasePart guaranteed to be streamed in with the BasePart (example, Folders, value Instances). Maybe just the non-BasePart descendants and their descendants? Maybe none are guaranteed? Edit: BasePart descendants aren’t guaranteed unless unanchored.
    • (I know I can use attributes to guarantee some of this, but I also need remotes and object references.)
  • Does parenting a part to another part disable some important optimizations? (I assume the article says “especially when the parent is far away from the child” because this streams the child in at the wrong times.) A significant amount of my game will be machines, so if streaming enabled isn’t acting as intended it will have a large impact.
  • I am also curious if all the Instances are guaranteed to steam in at once that this will cause CPU or network spikes. The machines will probably have 10-30 parts and 15-45 instances (total).

I will be working on some testing and performance comparisons. If you know the answer to any of these questions or have any great sources (RGC talks, articles, forum posts, etc.) relating to StreamingEnabled those would be greatly appreciated.

Thanks a bunch :smiley: :wave:


Research notes/edits:

Really hope that feature is coming :thinking:

1 Like

I guess after doing a bunch of research and reading dozens of posts requesting atomic streaming, it doesn’t appear this feature exists yet. I’m probably going back to programming my own replication solution.

Some good news for people in the future:

You can support a related feature request here:

I am still curious if the non-BasePart descendants of a BasePart are guaranteed to be streamed in with the BasePart.


I wrote a quick test, it looks like non-BasePart descendants of a BasePart are basically guaranteed to never be streamed in with the BasePart (edit: but they are pretty much always added within a few moments of the added event firing, which is what “average find time” is showing. Previously by “with” I meant all the instances are accessible when the parent is added).

image

Note that in this first test I wasn’t able to get any parts to stream out. Probably because my computer was doing fine on memeory.

The numbers to the left are children of that class in a basepart that were present when the child was added of the total number of baseparts.

Those are the numbers with default stream out behavior. These are the numbers for opportunistic:

image

Those numbers are looking pretty good as far as consistency. I guess if there is a weird condition where something just doesn’t load I can do a half-baked job of trying to alleviate that rare problem.


I really hope they get customizable atomic replication of instances soon. I wrote the code for an example machine and it’s pretty ugly:

Server:

local CollectionService = game:GetService("CollectionService")

local function setUp(testMachine)
	local dataFolder = testMachine.Data
	local remotesFolder = testMachine.Remotes
	local structureFolder = testMachine.Structure
	
	local isOnValue = dataFolder.IsOn
	local pushButtonRemote = remotesFolder.PushButton
	local buttonPart = structureFolder.Button
	
	
	local pushButtonConnection = pushButtonRemote.OnServerEvent:Connect(function(player)
		print("Server push!")
		isOnValue.Value = not isOnValue.Value
		if isOnValue.Value then
			buttonPart.Color = Color3.new(0, 0.847059, 0)
		else
			buttonPart.Color = Color3.new(0.866667, 0, 0)
		end
	end)
	
	local endConnection
	endConnection = testMachine.AncestryChanged:Connect(function()
		if not testMachine:IsDescendantOf(workspace) then
			pushButtonConnection:Disconnect()
			endConnection:Disconnect()
		end
	end)
end

CollectionService:GetInstanceAddedSignal("TestMachine"):Connect(setUp)

for _, testMachine in ipairs(CollectionService:GetTagged("TestMachine")) do
	setUp(testMachine)
end

Client:

local CollectionService = game:GetService("CollectionService")

local function setUp(testMachine)
	local dataFolder = testMachine:WaitForChild("Data")
	local remotesFolder = testMachine:WaitForChild("Remotes")
	local structureFolder = testMachine:WaitForChild("Structure")
	
	-- imagine that I added and handled time outs
	
	local isOnValue = dataFolder:WaitForChild("IsOn")
	local pushButtonRemote = remotesFolder:WaitForChild("PushButton")
	local buttonPart = structureFolder:WaitForChild("Button")
	
	-- imagine that I added and handled time outs
	
	local connections = {}

	local prompt = Instance.new("ProximityPrompt")
	prompt.Parent = buttonPart
	local promptTriggeredConnection = prompt.Triggered:Connect(function()
		print("Client push")
		pushButtonRemote:FireServer()
	end)
	table.insert(connections, promptTriggeredConnection)
	
	local endConnection
	endConnection = testMachine.AncestryChanged:Connect(function()
		if not testMachine:IsDescendantOf(workspace) then
			testMachine:Destroy()
			for _, connection in ipairs(connections) do
				connection:Disconnect()
			end
		end
	end)
	table.insert(connections, endConnection)
	
	for _, child in ipairs(structureFolder:GetChildren()) do
		local connection = child.AncestryChanged:Connect(function()
			if not testMachine:IsDescendantOf(workspace) then
				testMachine:Destroy()
				for _, connection in ipairs(connections) do
					connection:Disconnect()
				end
			end
		end)
		table.insert(connections, connection)
	end
end

CollectionService:GetInstanceAddedSignal("TestMachine"):Connect(setUp)

for _, testMachine in ipairs(CollectionService:GetTagged("TestMachine")) do
	setUp(testMachine)
end

TestMachine

Note that I wrote it purely as a test so it’s not using OOP/binders or anything like that. Going to do some more testing to see if I can find any problems.


Enabled the destroyed replication change. Seems that caused an occasional child to be found at the same time as the parent is added (happens for all of the children for rare instances, about 1/100). Not really a problem though because WaitForChild is already needed. It does mean that when using ChildAdded people need to loop through the existing children too. (They should probably already be doing that though).

One thing that’s super important for anyone doing the binder pattern on both the client and the server with filtering enabled is to add code that handles half the BaseParts being streamed out. I think right now the best practice for that is to assert (not the function) that all the required BaseParts exist before running code that uses/requires them.

When a BasePart is streamed all its non-part descendants will be streamed (instances that are not descendants of other BasePart descendants). At a high level the engine makes decisions on streaming based on parts because parts have a location in the world. Instances like folders don’t have spatial information, so they are only subject to streaming if they are a descendant of something with spatial information.

It just means parts may be streamed in even when they are far away from the player. In addition the engine may not be able to stream out parts that are far away because they have descendants that are near by. This can result in instances being send earlier than normally necessary, and using more memory than desired since some instances can’t be streamed out.

I wouldn’t expect to see noticeable spikes or stuttering with machines with those numbers of instances, but for much larger machines it could be noticeable.

1 Like