- Home /
AssetBundles vs Resources for prefabs included in distribution build.
I have a scene with 3k objets (the objects are pretty simple themselves but there's still 3k of them). On PC / Mac, loading the 3k objects at once when loading the scene is not an issue. But on iOS, it takes about a min to load them, so it's a no go. Since the player will only see about 200 of them at a time, I want to load them in in batches. Loading separate scenes additively is not the best option in my case however. So I'm now thinking of using either resources or asset bundles. But from reading the docs it seems that the Resources class cannot load multiple objects asynchronously. That is, the one method which returns an array is not async and for some reason the async version of the method only returns a single object thru the request asset
property (I did think about making one parent object have child objects to overcome this -- it might work but haven't tried it yet.). Then I read about asset bundles. But the docs seem to imply they are meant for loading content in from external sources, like a server.
So I'm wondering:
can asset bundles be actually included in a build and only used as a means for bringing in, say, 200 objects at a time in batches, without downloading them from a network?
If not, can many resources be loaded asynchronously in a single batch?
Is there a better way to accomplish this?
Answer by saldavonschwartz · Jun 30, 2015 at 11:27 PM
Based on @fafase 's suggestions, I ended up going for a resources approach. I'll answer my question here for the sake of completeness (since my original question has not been answered so far). But note that there's still something weird going on in Unity in general when it comes to asynchronicit and blocking behavior.
So, as to my original 3 part question:
Asset bundles can be used without relying on a server. You basically would load them using the file protocol as opposed to the network protocol.
Resources being loaded asynchronously... yes and no. Based on my experience with other frameworks/ platforms (i.e native iOS), what's going on in Unity is not necessarily what one might assume from the docs. For instance,
Resources.LoadAsync
gives the impression that it is non-blocking (because of the synchronicity) and concurrent, in the sense that the docs talk of a "worker thread". What I've observed however both in iOS and in the editor is:There is no mention in Xcode of any thread other than the main one when the loading of resources is taking place. At least in native iOS, Xcode shows all threads that are currently spawned. Maybe this is because of implementation details in Unity, but in the past I've even written Unity plug-ins for iOS that in turn spawned threads in the C++ to do stuff and I would see these threads. So I'm not even sure there is a separate thread.
If you look at the log and code in this answer, you'll see that I'm trying to concurrently and asynchronously load 10 chunks of data. And I'm trying to start my game when the first chunk is ready (chunk 9 -- yes, I'm doing it in reverse order). If these operations where being carried out concurrently and asynchronously, they would not necessarily end in the same order in which they were started. Further, they would not block the UI thread. In my experience:
A. I'm not sure calling
Resources.LoadAsync
is async AND concurrent. It seems async since it return immediately, but it seems that it is the yielding in the coroutine what essentially allows for some time-slicing (note, not concurrency), which makes it where the UI thread now does have some time to run per frame, thus giving you the impression of 'concurrency'.B. In my log attached, you'll notice that the coroutines are ending in the same order they were started. This implies to me a queue structure. Further, you'll notice how each coroutine takes progressively more time to complete, proportional to their dispatch order. This to me implies a synchronous queue, as if each coroutine (or the resource loading) was waiting for the previous one to complete before completing itself. Then again, this might even be a side effect of logging to the console. A lot of time, sending data to the standard output (i.e. stdout in C, cout in C++) forces synchronization. I have to keep investigating this.
So bottom line, I think that with my approach, basically all I'm doing is queueing up coroutines, maybe in a background thread, but maybe just in a 'background queue' running still on the same main thread. And whether there is a queue running in a worker thread or a queue running on the main thread, the queue itself feels synchronous.
There's got to be a better approach. But unfortunately the Unity docs leave a lot of relevant stuff out so I'm left to try stuff out empirically without much knowledge of anything.
Anyway, here's my relevant resource loading approach:
void Awake() {
for (int chunkIdx = chunks - 1; chunkIdx >= 0; chunkIdx--) {
StartCoroutine(loadChunk(chunkIdx));
}
}
private IEnumerator loadChunk(int chunkIdx) {
float startTime = Time.realtimeSinceStartup;
Debug.Log("Starting to load chunk " + chunkIdx + " at time: " + startTime);
List<ResourceRequest> requests = new List<ResourceRequest>();
for (int i = 0; i < items.Count; i++) {
string itemPath = string.Format("{0}/{1}-{2}", items[i].root.name, items[i].root.name, chunkIdx);
StartCoroutine(loadResource(requests, itemPath));
}
yield return null;
List <ResourceRequest> pendingRequests = new List<ResourceRequest>(requests);
int resourcesLoaded = 0;
while (resourcesLoaded != items.Count) {
for (int i = 0; i < pendingRequests.Count; i++) {
ResourceRequest request = pendingRequests[i];
if (request.isDone) {
resourcesLoaded++;
int idx = requests.IndexOf(request);
pendingRequests.Remove(request);
GameObject asset = request.asset as GameObject;
GameObject newInstance = GameObject.Instantiate(request.asset) as GameObject;
newInstance.name = string.Format("{0}-{1}", items[idx].root.name, chunkIdx);
newInstance.transform.parent = items[idx].root;
newInstance.transform.localRotation = asset.transform.localRotation;
Resources.UnloadUnusedAssets(); // Not sure if this helps or not
items[idx].chunks.Add(newInstance.transform);
}
yield return null;
}
yield return null;
}
computeTrapsForChunk(chunks - chunkIdx - 1);
float endTime = Time.realtimeSinceStartup;
Debug.Log("Done loading chunk " + chunkIdx + " at time: " + endTime + " | TOTAL: " + (endTime - startTime));
if (onChunkLoaded != null) {
yield return onChunkLoaded(chunkIdx);
}
}
private IEnumerator loadResource(List<ResourceRequest> requests, string path) {
requests.Add(Resources.LoadAsync(path));
yield return requests[requests.Count - 1];
}
private void computeTrapsForChunk(int chunkIdx) {
Debug.Log("chunk " + chunkIdx + " | items: " + wayPointDescriptor.chunks[chunkIdx].childCount + " | traps: " + traps.Count);
for (int i = 0; i < wayPointDescriptor.chunks[chunkIdx].childCount; i++) {
foreach (PathItemDescriptor trapDescriptor in traps) {
// do stuff
}
}
}
And notice that I want to start the game as soon as chunk 9 is done loading / processing, so here's the code that starts the game based on the chunk processing completion callback:
void Update() {
if (startGame) {
startGame = false;
FindObjectOfType<TrapAlloc>().onChunkLoaded += startGameIfNeeded;
}
}
private object startGameIfNeeded(int chunkIdx) {
Debug.Log("startGameIfNeeded");
if (chunkIdx == 9) {
StartCoroutine(CountDownToGame(3));
FindObjectOfType<TrapAlloc>().onChunkLoaded -= startGameIfNeeded;
}
return null;
}
startGameIfNeeded
triggers an animation and starts the game btw. And the reason why I set this up in Update
is because sometimes for debugging I want to not start the game until I check a bool from the inspector.
Another interesting thing here is how I have to signal the the chunk is done processing to start the game. Notice that I'm using a Func<int, objct>
. That is, a delegate that returns a value. I had to do this instead of using an Action
because interestingly (and I have no idea why this is the case so far), if I jut attempted to execute the delegate inside the coroutine where you see the Func, the delegates would not get called until after ALL chunks where done processing. This was my workaround to call out of the coroutine.
Finally, here are the logs for the timing. Notice how even though I get away with starting the game as soon as chunk 9 is ready by using the Func
, the chunks actually seem to be processed sequentially and take each time longer, regardless of being all the same length.
2015-06-30 15:26:22.577 revolve[9430:1454129] -> registered mono modules 0x100f2aee0
-> applicationDidFinishLaunching()
2015-06-30 15:26:23.097 revolve[9430:1454129] accessConfigurationFileWithCompletion ERROR: bookmark is nil
-> applicationDidBecomeActive()
Requesting Resolution: 1334x750
Init: screen size 1334x750
Initializing Metal device caps
Initialize engine version: 5.1.0f3 (ec70b008569d)
Unloading 3 Unused Serialized files (Serialized files now loaded: 0)
UnloadTime: 7.738875 ms
Built-in distortion correction disabled. Causes: [Requires OpenGL]
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Built-in UI layer disabled. Causes: [Requires OpenGL]
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
**Starting to load chunk 9 at time: 8.034065**
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 8 at time: 8.038321
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 7 at time: 8.040235
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 6 at time: 8.04267
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 5 at time: 8.044292
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 4 at time: 8.045845
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 3 at time: 8.047756
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 2 at time: 8.049585
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 1 at time: 8.051163
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Starting to load chunk 0 at time: 8.052967
System.Collections.Generic.Transform`1:EndInvoke(IAsyncResult)
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Setting up 2 worker threads for Enlighten.
Thread -> id: 10877c000 -> priority: 1
Thread -> id: 108c84000 -> priority: 1
Unloading 18 unused Assets to reduce memory usage. Loaded Objects now: 572.
Total: 10.495916 ms (FindLiveObjects: 0.388250 ms CreateObjectMapping: 0.032375 ms MarkObjects: 1.389833 ms DeleteObjects: 8.684875 ms)
Unloading 3 Unused Serialized files (Serialized files now loaded: 0)
chunk 0 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
**Done loading chunk 9 at time: 12.86521 | TOTAL: 4.831149**
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
**startGameIfNeeded**
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 1 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 8 at time: 14.97637 | TOTAL: 6.938046
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 2 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 7 at time: 18.08471 | TOTAL: 10.04448
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 3 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 6 at time: 21.60079 | TOTAL: 13.55812
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 4 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 5 at time: 26.16571 | TOTAL: 18.12142
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 5 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 4 at time: 31.44855 | TOTAL: 23.4027
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 6 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 3 at time: 39.21934 | TOTAL: 31.17158
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 7 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 2 at time: 50.94515 | TOTAL: 42.89556
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
chunk 8 | items: 100 | traps: 2
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Done loading chunk 1 at time: 62.77767 | TOTAL: 54.72651
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Unloading 11658 unused Assets to reduce memory usage. Loaded Objects now: 16595.
Total: 49.035583 ms (FindLiveObjects: 10.985750 ms CreateObjectMapping: 10.427166 ms MarkObjects: 17.771957 ms DeleteObjects: 9.849416 ms)
**chunk 9 | items: 100 | traps: 2**
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Unloading 3 Unused Serialized files (Serialized files now loaded: 0)
Done loading chunk 0 at time: 74.76004 | TOTAL: 66.70708
(Filename: /Users/builduser/buildslave/unity/build/artifacts/generated/common/runtime/UnityEngineDebugBindings.gen.cpp Line: 65)
Unloading 402 unused Assets to reduce memory usage. Loaded Objects now: 16249.
Total: 11.923041 ms (FindLiveObjects: 3.499083 ms CreateObjectMapping: 1.867500 ms MarkObjects: 6.245000 ms DeleteObjects: 0.310583 ms)
Answer by fafase · Jun 29, 2015 at 08:35 AM
You don't really have to use Resources or Asset Bundles if you wish to ship all assets in your game. You could make a prefab with all items as children and then break the prefab if needed:
- ParentStage1
- ObjectA
- ObjectB
- ObjectC
- ... more
- ParentStage2
- ObjectA
- ObjectB
- ObjectC
- ... more
Now you load when needed and break up if needed:
public GameObject[] GetObjectStage(ref GameObject prefab)
{
GameObject parentStage = Instantiate(prefab);
List<GameObject>list = new List<GameObject>();
foreach(Transform tr in parentStage.transform)
{
tr.parent = null;
list.Add(tr.gameObject);
}
Destroy(parentStage); // you don't need that top object
return list.ToArray();
}
I'm not sure if this would help in my case. First, instantiate relies on the assumption that I have a ref to ParentStage, which even when I can drag/drop from my assets into a script without in theory instantiating, I don't know if it implies loading the prefab at runtime or not. If it does then this would be a no go since my main problem is that loading all the objects at once (even with LoadLevelAsync) takes too much time, since decompressing the scene is what takes forever. Second, the above approach is not async. And again, I need to load in about 200 objects at a time, while the game is still playing. What do you think? Thanks.
Then you can have them in Resources so that they do not occupy any space in memory until they are needed. Second, you could make your prefab of the appropriate size (this will require some testing), so that creating one is not affecting much. Then you could do that in a coroutine so it won't affect the running and is asynchronous.
IEnumerator LoadAssets(){
GameObject obj = null;
int index = 0;
do{
obj = Resources.Load<GameObject>("Name"+index);// Example of na$$anonymous$$g
index++;
if(obj != null){
// Instantiate
yield return null;
}
}
while(obj != null);
}
Also, one thing that may affect the result is the amount of work in Awake and Start of the script contained on the prefabs. Here again, using a coroutine could spread the work over time and reduce the lag.
void Awake(){
// line 1
// line 2
// line 3
}
becomes:
void Awake()
{
StartCoroutine(AwakeAsync());
}
IEnumerator AwakeAsync(){
// line 1
yield return null;
// line 2
yield return null
// line 3
}
I see what you mean. And I'll give it a try. But I'm not sure if this will be enough. So the thing is, when my level starts, essentially I have 3k objects laid out on a pretty big racing track. Of those, 1k are waypoints, 1k are one kind of "trap" and the other 1k are another kind of trap. Basically what I do at startup is, iterate 1k times and on each iteration, I decide wether a waypoint, trap1 or trap2 is enabled at that spot (via a probability distribution with Random.Range). What I noticed is that on IOS, loading in the 3k objects (out of which roughly 60% then get disabled) takes seriously about 1m. It's not the code itself, but probably just decompressing the 3k objects. So I was trying to amortize the loading while allowing the player to start playing, since, it'll take them a bit anyway to make it thru the first 200 waypoints. Ideally, if I had more control over the API, I'd dispatch a worker thread to load in 200 objects at a time or so, in the background, without noticeably blocking the UI thread. But in Unity I can't call any method which taps into the main run loop in a thread other than main (i.e. Instantiate, and haven't tried but probably Resources.Load). So using the coroutine only provides a coarse level of time slicing (per frame at most), but it's not really concurrent and each Resources.Load call will be blocking. That's why I was hoping for Resources.LoadAsync, but was surprised it returns a single object as opposed to Resources.LoadAll.