« Networking in Unity: Back to Basics | Main | landscape5.png »

Unity, AssetBundles, the WWW and You!

In order to populate our world, the development tools and the client machines have to agree on which objects have which IDs. Additionally, we didn't want Ethan to have to keep bugging us to register objects in some global registry somewhere every time he wanted to add a new model to the game. As such, we came up with an architecture where the development tools can send an object up to the server, and clients can download it from the server when they need it.

Well, this week, I did a lot of work on the dev side of that process. And let me tell you, it was fun. And by fun, I mean it made me want to punch Unity right in the face.

We decided to use Unity's Pro-Only AssetBundle system to represent the object files, which essentially lets you save a GameObject out to a file and then stream it in from anywhere, whenever you want. It's a great concept, and it lets us make some fairly complicated objects, tweak them, and then send them to clients without ever having to recompile or redeploy anything. Sweet!

Of course, with Unity, nothing complicated ever seems to go according to plan. The first major hurdle was saving any arbitrary object out as an AssetBundle. There isn't any built-in way to do this without coding your own editor script, so that's how I solved it. I wrote an editor script that takes the selected object, and runs BuildPipeline.BuildAssetBundle() on it. This was actually fairly easy. The problem was then saving its data out to a file; but not because of anything inherently difficult in the process.

As it turns out, every time you call BuildAssetBundle, Unity inexplicably switches your project from whatever builld target it currently has to a Web Player target. The problem with this is that there are several functions that we're using that you can't do in the Web Player version of Unity (particularly System IO functions, like writing to a file). Of course, the next line is supposed to write to a file, so it crashes and tells me that "WriteAllBytes() is not defined". It took a little while to track down the cause of this bug, but it was eventually fixed by always calling EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTarget.StandaloneWindows) after creating an AssetBundle before proceeding with the rest of the code.

Once that was figured out, I wrote some PHP scripts that would take in the file on the server and store it in a MySQL database. Easy-peasy.

The next problem came in reading the data back out, and is actually the one that ate up many hours of my time trying to fix. I used Unity's built-in WWW class to download the AssetBundle's bytes (from another PHP script which pulled them from the database) and try to save them to a file on the client's machine. I've downloaded other files like this using the WWW class before, and I've never had a problem with it. But this time, I ran into something I never expected.

Instead of setting its "isDone" variable to true when the file was finished downloading, like it was supposed to, the WWW class would just hang. It would download all the data, but it wouldn't let me retrieve it. It reported its progress as 100% done, but it stubbornly refused to let my program access the data that it had downloaded. I spent hours tearing my program apart, trying to figure out what I had done wrong. Was the PHP script reporting the headers wrong? Was something interfering with my WWW calls? Did WWW not work the way I thought it did, and it had worked before only out of some lucky happenstance?

Well, I eventually started uploading various types of files to the server and trying to download them using WWW to see if the isDone variable would get set or not. WWW downloaded text files just fine. It could download zip files and 7z files. Maybe it had something to do with the file extension? But alas, Unity would not download a .assetbundle file that had been renamed .zip, so that wasn't it.

In a last-stitch effort to try to figure out what was wrong, I tried appending a single extra byte of data to the front of the .assetbundle file. Lo and behold, it suddenly downloaded fine!

But wait, so all it needs to be able to download an .assetbundle file is a single extra byte appended to the beginning? Why would that work? Is it an off-by-one error somewhere, or...?

Oh no.

You've got to be kidding me.

So it seems that, for whatever reason, Unity specifically looks for the AssetBundle file header and treats it differently. I don't know exactly what it's trying to do, but when it sees the AssetBundle file header, it alters its behavior in such a way that the download will no longer terminate. It will just spin and spin forever, and never let you touch the data. By adding a single extra byte to the top of the file, I was able to trick Unity into not recognizing the AssetBundle header, thereby allowing it to download the file normally.

So that's what I do. I append an extra byte of data to the start of the .assetbundle file before I send it, and remove it when I retrieve it. It's too bad, because it feels like such a hack, but I couldn't find any other way to do it.