Optimize Large Prefab Loading To Addictive Scene Loading with PrefabSceneConverter

Richard Fu
Sweaty Chair
Published in
4 min readSep 26, 2021

--

I’ve been working on a casual game that every level was designed and saved as a prefab. It works great in Editor but not in mobile, especially in Android devices. Each level took a very long time to load and the game became unresponsive for 10+ seconds. We decided to optimize it by turning all level prefabs to scenes so we can use Unity’s addictive scene loading, which also enables progress showing.

A list of large level prefabs, each contains 100+ 3D objects.

Prefab vs scene loading

When you have a lot of objects on a level, it is important to save the level as scenes instead of prefabs. The reason for this is that prefab loading always happens in one single frame. Loading a large prefab will cause the device to be unresponsive, and greatly impact the user experience.

While using scenes to store the levels, you can use SceneManager.LoadScene("YourScene", LoadSceneMode.Additive); to load a level additively and shows a progress bar if needed. It is also a must-do for games like endless platformers which developers can preload a level in advance without causing a CPU and memory spike in runtime.

In the profiler, loading scene addictive would not lower the resource required or increase the framerate, somehow it makes a small overhead compared to prefabs, but giving a smoother loading or transition.

However, on Android devices, I’ve found that scene loading is much faster than prefab loading, while there’s not much difference in iOS. I couldn’t explain this behaviour, it may be related to how Unity handles assets in Android.

So if your project is still in the early stage, and its levels will gonna be large, make sure to use scenes to store levels.

Then we can use the following standard code to load the level and show the progress:

private IEnumerator LoadSceneCoroutine()
{
string currentSceneName = SceneManager.GetActiveScene().name;
AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(_prefab.name, LoadSceneMode.Additive); while (!asyncOperation.isDone) {
// Update progress bar
_progressSlider.normalizedValue = asyncOperation.progress;
_percentageText.text = Mathf.Round(asyncOperation.progress * 100) + "%";
if (asyncOperation.progress >= 0.9f)
_percentageText.text = "100%";
yield return null;
}
Scene scene = SceneManager.GetSceneByName(_prefab.name);
SceneManager.SetActiveScene(scene);
asyncOperation = SceneManager.UnloadSceneAsync(SceneManager.GetSceneByName(currentSceneName).buildIndex);
while (!asyncOperation.isDone)
yield return null;
}

Prefab to scene converter

If you’re unfortunately in the late stage of the project, like us, here’s the PrefabSceneConverter that you can use:

using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using System.IO;
using UnityEditor.SceneManagement;
public class PrefabSceneConverter : EditorWindow
{
private const string PREFS_PREFAB_PATH = "PrefabSceneConverterPrefabPath";
private const string PREFS_SCENE_PATH = "PrefabSceneConverterScenePath";
private const string PREFS_OVERRIDE_EXIST = "PrefabSceneConverterOverrideExist";
private const string PREFS_ADD_TO_BUILD_SCENES = "PrefabSceneConverterAddToBuildScenes";
private string _prefabPath = "Prefabs/Levels/";
private string _scenePath = "Scenes/Levels/";
private string _prefabNames;
private bool _overrideExist = true;
private bool _addToBuildScenes = true;
private Vector2 _scroll; [MenuItem("RF Dev/Prefab Scene Converter")]
private static void Init()
{
PrefabSceneConverter window = (PrefabSceneConverter)GetWindow(typeof(PrefabSceneConverter));
window.Show();
}
private void Awake()
{
_prefabPath = EditorPrefs.GetString(PREFS_PREFAB_PATH, _prefabPath);
_scenePath = EditorPrefs.GetString(PREFS_SCENE_PATH, _scenePath);
_overrideExist = EditorPrefs.GetBool(PREFS_OVERRIDE_EXIST, _overrideExist);
_addToBuildScenes = EditorPrefs.GetBool(PREFS_ADD_TO_BUILD_SCENES, _addToBuildScenes);
}
private void OnGUI()
{
string prefabPath = EditorGUILayout.TextField("Prefab Path:", _prefabPath);
if (prefabPath != _prefabPath) {
EditorPrefs.SetString(PREFS_PREFAB_PATH, _prefabPath = prefabPath);
}
string scenePath = EditorGUILayout.TextField("Scene Path:", _scenePath);
if (scenePath != _scenePath) {
EditorPrefs.SetString(PREFS_SCENE_PATH, _scenePath = scenePath);
}
EditorGUILayout.LabelField("Prefab Names: (separate by new line)"); _scroll = EditorGUILayout.BeginScrollView(_scroll);
_prefabNames = EditorGUILayout.TextArea(_prefabNames, GUILayout.Height(position.height - 30));
EditorGUILayout.EndScrollView();
bool overrideExist = EditorGUILayout.Toggle("Override Exist:", _overrideExist);
if (overrideExist != _overrideExist) {
EditorPrefs.SetBool(PREFS_OVERRIDE_EXIST, _overrideExist = overrideExist);
}
bool addToBuildScenes = EditorGUILayout.Toggle("Add Scenes to Build Scenes", _addToBuildScenes);
if (addToBuildScenes != _addToBuildScenes) {
EditorPrefs.SetBool(PREFS_ADD_TO_BUILD_SCENES, _addToBuildScenes = addToBuildScenes);
}
if (GUILayout.Button("Convert")) {
string[] prefabNames = _prefabNames.Split('\n');
foreach (string prefabName in prefabNames) {
string newSceneAssetPath = Path.Combine("Assets/", _scenePath, prefabName + ".unity");
string newSceneFullPath = Path.Combine(Application.dataPath, _scenePath, prefabName + ".unity");
string prefabAssetPath = Path.Combine("Assets/", _prefabPath, prefabName + ".prefab"); bool newSceneExist = File.Exists(newSceneFullPath); if (_overrideExist || !newSceneExist) { Scene scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
GameObject prefab = (GameObject)AssetDatabase.LoadAssetAtPath(prefabAssetPath, typeof(GameObject));
if (prefab == null) {
Debug.LogError($"Prefab doesn't exist at {prefabAssetPath}, skipping...");
continue;
}
GameObject go = (GameObject)PrefabUtility.InstantiatePrefab(prefab);
Debug.Log(go.name);
bool succeed = EditorSceneManager.SaveScene(scene, newSceneAssetPath);
Debug.Log($"Save scene {(succeed ? "succeed" : "failed")} at {newSceneAssetPath}");
if (succeed && _addToBuildScenes && !newSceneExist) {
EditorBuildSettingsScene[] buildScenes = EditorBuildSettings.scenes;
EditorBuildSettingsScene[] newBuildScenes = new EditorBuildSettingsScene[buildScenes.Length + 1];
System.Array.Copy(buildScenes, newBuildScenes, buildScenes.Length);
EditorBuildSettingsScene newbuildScene = new EditorBuildSettingsScene(newSceneAssetPath, true);
newBuildScenes[newBuildScenes.Length - 1] = newbuildScene;
EditorBuildSettings.scenes = newBuildScenes;
}
} else { Debug.Log($"Scene already exists at {newSceneAssetPath}, skipping..."); }
}
}
}
}

This tool allows you to simply put the names of all your prefabs into the textbox, and it searches and converts all prefabs found in the given folder. It also provides an option to add the scene into the Build Scenes automatically (which you have to do for scene loading).

Each prefab is simply put inside a scene with the same name, and becomes the only object in the corresponding scene. The original prefab is untouched so the level designer can still use the prefab for level design.

That’s it, wish you have some understanding of scene loading or find the PrefabSceneConverter useful.

Please follow me and Sweaty Chair for more latest game dev updates.

--

--

Richard Fu
Sweaty Chair

Frontend Developer at Twist, Easygo. Ex-cofounder at Sweaty Chair and Block42. Hit me up for any game idea and cooperative opportunities. 🎮