Upgrades to Model Streaming

Hello Creators,

As we have discussed previously, we’re dedicated to keep improving Streaming because we believe it’s the best way to improve join time and reduce client crashes. Today, we are pleased to announce updates to Streaming Enabled that further optimize how models are sent to clients as well as how models can be streamed out when no longer needed.

At a high level enabling this “improved” model streaming behavior causes models to be sent when the client needs them, not on join, helping to reduce join times and memory requirements. Previously streaming operated mainly at the part level, resulting in models without their part descendants.

How to Use

These updates are initially off by default but you can turn them on in your Workplace settings by toggling ModelStreamingBehavior to “Improved.”

Please note that the Default value is currently equivalent to Legacy, which results in the current model streaming behavior where models are sent during join unless they are Atomic/Persistent. In the future we expect to change the meaning of Default to behave the same as Improved.

Read on for more details on why we think this will help. Or, just give it a try today!

Note that knowledge of existing model streaming modes may help understand this new change. For information on the existing modes and persistent loaded signal see: New Improvements to Streaming Enabled

Reduce Instance Sent during Join

Currently in streaming enabled experiences all instances in Workspace that are not parts, or descendants of parts are sent during join. This results in instances that must be replicated to all clients before join is complete, and these instances cannot be garbage collected in low memory situations.

Consider an experience with a bunch of humanoid NPCs that are located far away from the player spawn point and where the parts aren’t initially sent to players. After join the client workspace will contain models for all of these NPCs, as well as humanoids, animations, and other non-part instances:

Figure 1: Traditional Streaming

When ModelStreamingBehavior is set to Improved models are no longer sent during join. When the model is sent to clients depends on if the model has any part descendants. A model that has no part descendants is considered a global model, meaning it has no spatial information. A global model is streamed to clients when its ancestor is sent to clients, unless that ancestor is sent during join. If the ancestor is sent during join then the global model is sent after join, but before the PersistentLoaded signal is fired for the player.

Figure 2: Improved Model Streaming. Only model for streamed NPC is present

Models that are spatial, meaning they have part descendants, are not sent to a client until one of their part descendants needs to be streamed to the client. When a spatial model is streamed we send the model and descendants, until we encounter a part descendant or another spatial model. Similarly when a part is streamed all non-part and non-spatial model descendants (and their descendants) are streamed. This means that models are not always sent atomically, unless they have no part descendants or all their parts belong to a single network ownership unit (NOU). This means that important use cases, like avatars will behave atomically, but if the developer creates a model for an entire city the engine will not attempt to stream the city in/out atomically.

Here is a simple example Workspace, without atomic or persistent models, and how it is replicated to clients with existing streaming behavior:

Note that everything that is not a part or a descendant of a part is sent at join. In particular the model with two part descendants is sent at join, even though its parts may never be sent to a particular client.

When Improved Model Streaming is enabled the behavior is different:

There is no change to the folder, but the two models behave differently. The model with part descendants is not sent until one of the descendant parts needs to be sent. At that time model and descendant script are sent, but not the other model, unless it also needs to be streamed at the same time.The model is streamed out when its last descendant model is streamed out.

The model without any descendant parts is not sent during join, but soon after, and before the persistent loaded signal is fired. And of course, fewer models sent during join mean faster joins!

Note: There is no change to models that have their model streaming modes set to Atomic/Persistent/PersistentPerPlayer. Those are already not sent during join and are streamed in/out atomically.

Note: Workspace itself is still sent during join even though it is technically a model instance.

Stream Out (Garbage Collection)

Previously in streaming enabled experiences models and their non-part descendants were never streamed out, unless the model was manually set to be atomic. With improved model streaming the engine can now stream out models when they are no longer needed, potentially freeing up memory on the client and reducing what instances need property updates.

A spatial model is streamed out along with its last remaining part descendant. Just because one part descendant of a model is streamed out doesn’t necessarily mean that the model will be streamed out at that time. A model could have multiple part descendants, some near to the player/replication focus and some far away. We may be streaming away distant parts while nearby parts are still required.

A global model (no part descendants) is streamed out when its ancestor is streamed out. This is no different from behavior without improved model streaming.

Deferred Parenting

With traditional replication and streaming when an instance and its descendants are replicated to a client when parented into the datamodel on the client first the ancestor is parented into the data model, and then children, and so on. This means that if a localscript wants to access some instance deep in the parented subgraph it must first WaitForChild on the parent, then child, and so on.

To address this issue when we stream models and/or part subtrees we don’t parent the root of the subtree until all descendants being sent at that time have been parented to the subtree root. This means that once a subtree arrives there is less need for localscripts to use WaitForChild to ensure that instances have arrived. As shown in Figure 1, with traditional streaming the models exist for all NPCs even when they are streamed out. If a local script wants to access the parts in the NPC then it must wait for all the parts it needs to access, possibly using WaitForChild on each part. With improved model streaming the local script would just need to wait for the model to arrive since it would not be sent to the client until needed, and when it is sent the appropriate parts will be sent along with it.

For additional information on streaming enabled please see: Instance Streaming

If you encounter a bug related to this API and are able to file bug reports please create a separate bug report for better visibility.

Thank you!

185 Likes

This topic was automatically opened after 10 minutes.

Wow this looks great thanks for all the hard work put into this! Really smooths out gameplay

18 Likes

Besides streaming, will we ever see Visible property on Models? It’s one of the most basic features of any game engine, not to mention that It would benefit developers who want to implement custom LoD. From I heard, popular experiences like Mad City have custom LoD for building interiors, and I’m sure they would benefit from this feature a lot.

98 Likes

As the author and maintainer of the networking library Red, I’ve recently started receiving a lot of help requests for sending instances over remotes due to streaming being enabled by default. It appears that when the server moves an instance into a streamable area around a player, it will not always be streamed in the same frame, resulting in that instance being networked as nil instead of the instance.

I would love if this could be fixed in streaming or if there were a way for me to fix this in my library, keep up the good work guys <3.

24 Likes

Will give this a try with my streaming project later this week. I don’t think this impacts our code, but if I find any problems I’ll let you folks know :slight_smile:

11 Likes

This update seems pretty helpful! I did origanally turn streaming back off my game due to it breaking many of the models and scripts. I will give it a try with this feature :slight_smile:

12 Likes

Whichever script breaks, you can track the object referenced in that script that si being streamed out and set it to be Persistent. Thats at least how i dealt with these kind of issues in my games

14 Likes

I don’t know if this problem is also fixed now, but as far as I know, this problem still occurred in one of my experiences !

It is mainly the first part of this post

14 Likes

setting a model to persistent kinda defeats the purpose of streaming, it’ll just stay in memory and it’s better to just have it be tolerant of streaming in and out instead of a band-aid fix like that

11 Likes

Will the legacy mode ever be deprecated/eventually removed? I rely on the fact Non-Atomic models initially replicate the empty parent model to the client for referencing reasons.

Incase anyone is curious I take advantage of this fact to have a unified module script table for both the server/client that includes a direct reference to the Model along with other information. This is a really easy solution to solve ether by using a separate module script or just dynamically adding the reference on the server only but i’m lazy :smiley:

I do imagine some people have situations where its non-trivial to replace this functionality which is why I’m curious if it will ever be removed.

11 Likes

Thank you so much for this update! This hasn’t solved all of our problems with streaming enabled and memory usage from it but it’s definitely a welcome improvement.

9 Likes

I appreciate the constant improvements to streaming and the benefits it provides. I would love to utilize streaming but find it impossible to until locally created parts are simulated outside the streaming radius, which they are currently not even while in a persistent model.

Since streaming correlates to loading and unloading parts on the client, locally created parts, especially while in a persistent model, should always run physics.

10 Likes

Why can’t we just have a ReplicationService where we can manually toggle replication on individual objects, including terrain? I know exactly what I want to stream in/out and exactly what I don’t want to bother wasting bandwidth on. Streaming isn’t a toggleable ‘upgrade’, there are networking costs that come with it due to having to send large amounts of parts and voxels to potentially hundreds of players. This network impact is rarely discussed and instead streaming seems to be treated as this perfect ideal package.

21 Likes

Sounds great. FWIW, the principles of less models being sent improving join times is also why I created a custom replication system for my own experience. ReplicatedStorage has the absolute bare minimum and we manually replicate instances the client wants soon after they join. I am able to achieve 1-second join times with a codebase of 5.4K+ ModuleScripts, StreamingEnabled, Deferred SignalBehavior and a bare minimum amount of replication during the initial snapshot.

What is the behaviour of streaming out, a destruction or a nil parent? I’d like to know what the proper way to check for parts streamed out is so I can know when to cleanup lifetime objects that need to observe part descendants of the workspace. Probably the best way to check is a nil parent after AncestryChanged, though other methods like Destroy also fire it, no?

As more and more optimisation features visit developers’ gift boxes that have to do with contextual parenting by the engine, it would also be helpful to start thinking about methods to differentiate between destruction and nil parenting. This is already a pain point with our own systems and it will grow as features like this become more widely used.

Also deferred parenting sounds especially great for characters. To avoid daisy chaining waits I take advantage of replication also being reliably ordered to essentially “snapshot” an instance tree at a moment of replication. It’s unnecessary boilerplate though, and character loading events are a wick of evil old engine code that was intended to be fixed but encountered technical blockers. Deferred literally solves 99% of most deep tree access issues (hence the fancy display name). Hopefully this works even without StreamingEnabled or with Deferred SignalBehavior at the very least.

9 Likes

We recently added a clarification to streaming out in the documentation:

“When an instance streams out, it is parented to nil so that any existing Luau state will reconnect if the instance streams back in. As a result, removal signals such as ChildRemoved or DescendantRemoving fire on its parent or ancestor, but the instance itself is not destroyed in the same sense as an Instance:Destroy() call.”

Signals related to destruction do not fire on stream out.

13 Likes

I doubt Roblox will remove it, but just take that with a grain of salt since it’s likely they will unfortunatly.

5 Likes

Hoping soon we see an update that makes parts be always streamed in by default, except for the parts that we mark as safe to stream out. So essentially the opposite of what we have now. Unfortunately this is a very limiting factor for us, so we have to rely on a custom implementation for streaming.

5 Likes

Does this decrease load times for games with lets say 50 zombies in a current game? I want to increase the amount of zombies in my game while also decreasing load times/lag for lower end devices by streaming them out.

I currently use Streaming in my game that relies heavily on mesh but I can’t really test that parts/models are streamed out. Is there a way for me to test that without buying a potato? Even with min radius of 128 it still seems to load everything when I join?

Game in question if anyone can help: Valor FPS 🎃 ZOMBIES EVENT - Roblox

5 Likes

The reason why they are not streaming in and persisting is because players are having their devices crash and they are unable to play Roblox if Roblox is unable to stream out.

4 Likes