V2 UPDATE SUMMARY
- Added animation framework for developers to control streaming aesthetics
- Added five animation presets: Rise, CartoonRise, SpaceShuttle, Transparency, and Blink
- Fully modular (previously involved some hard coding related to the default ‘Short,’ ‘Medium,’ and ‘Long’ distance folders)
- Significantly more reliable (now employs back-up checking)
Full V2 update write-up: CullingService V2 - Custom Client Sided Culling/Streaming Module - #36 by https_KingPie
Hi there and happy middle of the summer!
CullingService (CS) is a tool that I developed for my current project. It’s a configurable approach to content streaming that lets developers control, specifically, what is and isn’t streamed in their experience. I realized that there wasn’t anything like it on the DevForum and decided to open-source it for large scale use. Feel free to use it in your games or as a learning resource.
Check out the GitHub page to view the source code (in Rojo format): GitHub Repository
Download the model here: Roblox Library Model
Download the plugin here: CullingService Plugin
Example place (open sourced) with CullingService in action: Example Place
Read below for set-up information
What is Culling and Why Should I Care?
Culling, by definition, is a way of controlling rendering and typically handled by the engine. This is a necessity in games that have large maps, high detail, or a combination of both. If you look closely while playing many open-world games (Skyrim, Assassin’s Creed, etc.), you might notice that things at the edge of your screen load in or out depending on your distance). This is to reduce the performance costs of large and detailed maps. Conversely, you can flip the definition and reason that using culling also allows you to increase the detail of your map, and this is true too.
Not making sense? Think of it as a custom StreamingEnabled.
How Does it Work?
Using CS, you (the developer) assign anchor points to models that you want to be culled in and out. CS subdivides the map into cube regions (the size of these are controllable). As the player enters a region, CS checks to see if anchor parts in the regions adjacent to the one the player is currently in are within a distance to be culled. If they are, the model is culled in. Doing this increases performance by reducing the amount of distance calculations performed.
Here are some pictures to visualize what’s going
Map is divided into regions
External MediaAdjacent regions are checked (they do go away after you leave them, I just forgot to add that to the visualization)
External MediaBased on your region and distance, instances are culled in and out accordingly (modify your settings to make it look less choppy - the current settings are in place to clearly demonstrate the long, medium, and short ranges which can be culled)
External MediaCullingService now supports moving parts - read more about this in Advantages + Limitations
More Information on…
Advantages + Why Not Use StreamingEnabled?
Great question - why not use a tool built into the engine? CS offers more control than StreamingEnabled and allows you to dodge problems that have plagued StreamingEnabled. Here are a couple advantages/issues solved:
- Create specific render distances for various types of instances. For example: in a forest, cull in trees at a further distance (because they are big) and only cull in small details, like flowers, when the player comes to a much shorter distance.
- Customize everything. Customize culling distances (as mentioned above) and other settings. Because these are customizable, this allows you to provide players options, such as a render setting of “low, medium, high, ultra high, etc.” if you wish. StreamingEnabled does not allow it or related properties to be modified via code, whereas CS allows you to make immediate changes.
- If you use duplicate models (such as trees), you’re in luck! Once you set up the distance folders for one model, all of them will be set up. Only one model is stored, which enhances performance and expedites set-up.
- Only cull what you want to cull. If you have certain parts that absolutely should not be culled, CS allows you to simply, easily, and quickly exclude them from being affected. This tackles the problem of all parts being uniformly streamed in and out by StreamingEnabled, which can be a massive issue for programmers.
- Control CS via plugin to set things up. This means less work for the individual client and more work for you! Kidding, the plugin is very easy to use and makes set-up as simple as selecting the models in the Explorer menu that you want to cull and hitting a button
- Cull out parts of the map (or the entire map) as you develop, reducing the overhead costs - especially for lower-end PCs. Cull it back in as desired.
- Quickly integrate existing games with this. Anchor points can be automatically created through the plugin - just select your models and click a button.
- Simple. Call
CullingService.Initialize()
. Afterwards, you can always pause or resume CS usingCullingService:Pause()
orCullingService:Resume()
- New: CullingService allows support for moving models. Rather than move the entire model, all you need to do is move the associated anchor point (this makes CullingService minimally intrusive). CullingService features an optional built-in welding to support this (however, this only happens for moving parts)
Limitations
- Only models can be culled
- Models must have (kinda) unique names. Models with the same name will be presumed to be the same model. Therefore, duplicate models that are the same in every aspect (minus position and orientation which will automatically be handled once they are assigned an anchor point) can (and should) have the same name, whereas models that are different should be assigned a different name. Basically, don’t name all of your models “Model”
- Models which are being culled will have their PrimaryParts overwritten (to utilize movement controls)
- Only what you specify to be culled in your game design (i.e. Studio) will be culled. This can be problematic for things created mid-game. CS can be thought of as ‘semi-automatic’. At this stage, it can’t do it all.
- This has all purpose-use; however, it does not have an extreme control (used loosely, there are still lots of settings to be modified) set that allows it to be useful in every game. It may be more useful, in certain situations, to use StreamingEnabled or to fork this.
- Moving parts must have the same render distance (i.e. put them all in either Long, Medium, or Short) and only one distance folder in the model. Having multiple may break it.
How To Use It
- Install the plugin (linked above) and open Studio
- Create the following folders as children of workspace: AnchorPoints, CulledObjects, CullingRegions
- Create the following folders as children of ReplicatedStorage: ModelStorage, NonCulledObjects
- Install the CullingService model via Roblox (or import directly from GitHub)
- If installing from Roblox, put UtilityModules and CullingService in ReplicatedStorage
- Create a LocalScript on the client, require CullingService, and call
CullingService.Initialize()
- Open the plugin and hit the initialize button (this will check that all folders in step 2 are created)
- Create folders in your model with the names: Short, Medium, Long (these will decide which parts are culled in and out at which ranges). Place the parts within these relevant folders to control which culling range they fall into.
- Select (i.e. click on/highlight in your explorer menu) the model (or all models which have been set up) and generate anchor points for your selection via the plugin
- With the same selection, click add selection model storage [Note: duplicate models (with the same name) are automatically handled, so feel free to mass select many models]
- You are now free to delete those models from your workspace - they will be automatically culled in once the player enters their culling radius
- If you wish to adjust the model, simply delete the anchor point associated with that specific model (or adjust the position of the anchor point)
- Adjust settings in CullingService.Settings to create a smooth experience for your players. In it’s most ideal state, CS will run and players will not even realize it. If you’re able to tweak the settings to give the illusion of a fully loaded map while things are actually being efficiently culled in and out, you’ve hit adjusted your Settings perfectly. Don’t stress if you aren’t able to do that (since you’ll also want to factor in performance costs of culling more instances in), but I would advise this to be the goal.
I highly recommend saving a copy of the open-sourced test game (linked above) and playing around with the plugin there - it should make a lot more sense
Settings Overview
- Distances: Customize the various culling distances. These are measure in studs, so these are the distances that a player must enter to have instances in the relevant distance folder of the model culled in. If they exceed this distance, those instances will be culled out. Search radius is the distance
- Paused: boolean value that is set via
CullingService:Resume()
andCullingService:Pause()
- Region Length: the length (in studs) of the invisible regions that will divide up your map. Make sure that the length of this is at least 1/3rd of the Long Distance (although I would recommend making sure it’s around half of that distance)
- Wait Time: this is the time that CullingService checks for adjustments (updating ranges, culling whole models in, culling models out)
- Welded Anchor Points: table of strings for anchor points that should be welded to their respective models. Use this to make models moveable and make changes to the anchor point’s CFrame value
API
module:CreateSignalForModelCullIn(ModelName: string)
Returns a Signal which is fired every time a model with the name provided is culled in. Signal provides the model as a first argument
module:CreateSignalForModelCullOut(ModelName: string)
Returns a Signal which is fired every time a model with the name provided is culled out. Signal provides the model (in this case, nil) as a first argument
module:CreateSignalForModelCullInAtRange(ModelName: string, RangeName: string)
Returns a Signal which is fired every time a model with the name provided is culled in. When the Signal is fired, it provides the model (in this case nil) as the first parameter
module:CreateSignalForModelCullOutAtRange(ModelName: string, RangeName: string)
Returns a Signal which is fired every time a model with the name provided is culled out. When the Signal is fired, it provides the model as the first parameter
module:Refresh
Refreshes CullingService, essentially manually calling the Cull function around the player
module:Resume
Resumes CullingService
module:Pause
Pauses CullingService
module:ManualCull(Position: Vector3) Culls around a designated position versus just the player. Normal behavior can be resumed using
module:Resume`
Example API usage (direct from my project, which makes a boat sway with the waves when culled in)
local function ListenForShips()
for _, ShipName in pairs (AllShipNames) do
local ShipAdded = CullingService:CreateSignalForModelCullIn(ShipName)
ShipAdded:Connect(function(Ship: Model)
HandleShip(Ship)
end)
end
end
Closing Thoughts
This was a headache to work on and a very niche subject in programming (since engines typically handle this), so I wanted to open source it to give others a stepping stone in their creations. Feel free to use it as a development tool, learning resource, or whatever else you think of. If you think of improvements, feel free to submit a pull request to the GitHub repo. Unfortunately, I don’t have a ton of time right now to make a tutorial video, but it is relatively self explanatory. If anyone is able to do so, let me know and I will link it in the thread.
Special thanks to (in no particular order besides alphabetical) @Ahlvie, @boatbomber, and @DevBuckette who were immensely useful in helping me understand the concept and how to approach the topic. Huge thanks to @XAXA for her BoundingBox formula, which allowed me to create a more accurate BoundingBox of the workspace and avoid problems caused by the Terrain instance. Regions are handled using @VerdommeManDevAcc 's Object Tracker & Area Manager module (which I always find extremely useful).