Optimize Large Prefab Loading To Addictive Scene Loading with PrefabSceneConverter
I’ve been developing a casual game where each level is designed and saved as a prefab. While it works fine in the Editor, it causes severe performance issues in mobile devices, especially Android. The game freezes for over 10 seconds whenever a level is loaded. To solve this problem, we decided to optimize the game by converting all the level prefabs into scenes. This allows us to utilize Unity’s addictive scene loading, which also enables us to show loading progress.
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 simplifies the process by allowing you to enter the names of all your prefabs into the textbox. It will then search and convert all prefabs found in the specified folder. Additionally, it provides an option to automatically add the scene to the Build Scenes, which is necessary for scene loading.
Every prefab is placed in a scene with the same name, becoming the only object in that scene. The original prefab remains untouched so that the level designer can still use it for level design.
That’s it! I hope you now have a good understanding of scene loading or find the PrefabSceneConverter
tool useful.
1 COMMENT
`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.`
— I Just tested on device, and i managed to find out the reason:
on device, scene loading avoided object.instantiate(copy/produce), so it’s faster than prefab, or at leaset, fps is more stable than using prefab.