Sunday, May 5, 2013

Float Downstream

Fear - fear gripped my heart in its clammy fist. After making a new WebPlayer build to replace the somewhat outdated one on my webpage, I fired it up and found that it didn't work. I realized I hadn't tried playing the game in an external build for quite some time. The game worked when played in the Unity Editor's play mode, and that should just be exactly the same as a built version, right? RIGHT??

The game's main menu and auxiliary screens seemed to work fine, but when starting a new game, the player would be stuck at the character input screen, as no amount of clicking would actually select a character and launch the first level. This worked perfectly fine in the editor. The setup is, a UI script that lives on the Main Camera in this scene Instantiates all three of the player prefabs at positions based on the screen size, and hangs a scipt called UI_dummyPlayer on each of them. That script just has an OnMouseDown event that fires whenever someone clicks on the GameObject that the script lives on, loading the next level with the appropriate character as the player.

A platform inconsistency is by nature a painful problem. I started looking for diagnostic tools. The first was the fact that you can right-click on your game in the WebPlayer and bring up a crude development console, pictured above, which in my case read

MethodAccessException: Attempt to access a private/protected method failed.

and helpfully pointed me to the very function causing the problem. The two potential culprits in there are the DontDestroyOnLoad call and my XML Serialization stuff... I started to get a sinking feeling. I quickly narrowed the problem down to these two lines:

seattle = City.Load(Path.Combine(Application.dataPath, "City.xml"));
seattle.Save(Path.Combine(Application.dataPath, "seattle.xml"));

and with some searching online I started to piece things together. Unity's WebPlayer has some built in security features that stop you from reaching into code that is stored in certain ways. I did a quick test on Path.Combine alone, as I hadn't used that before either, but the WebPlayer was fine with it. It was balking at my City.Load function, which I talked about in a previous post. It's another case of using some code I don't understand well, but I understand well enough that the WebPlayer is blocking me from access to the game's underlying file structure, which would most likely disallow things like

using (var stream = new FileStream(path, FileMode.Open))

and there are plenty of threads online about this and they all die out pretty quickly when a senior user steps in to say No, you can't do this in WebPlayer, it would in fact be a glaringly dangerous security flaw if you could.

One post suggested a potential solution: if I stored my xml files in the same directory as my game on the website, I could use Unity's WWW functions to read that xml into a Unity object without touching the internal filesystem. The effect would be the same. Unfortunately, the example code for this employed XmlReader, where I was using XmlSerializer. Also, the process outlined involved junking all of my Xml code, and one of the other posts I read seemed pretty confident that I could still use the XmlSerializer class from within WebPlayer. If the FileStream was indeed the problem I might be able to keep most of my code.

The breakthrough finally came as a result of this postwhere the suggestion was to use XmlSerializer in connection with Resources.Load and something called a TextAsset to bypass use of the prohibited FileStream class. Other exotic tools in play include MemoryStream and Encoding.UTF8 ... we're somehow simulating a streaming file operation within the game's runtime memory, which I find baffling and kind of magical. We have replaced this:

var serializer = new XmlSerializer(typeof(City));
using (var stream = new FileStream(path, FileMode.Open))
  return serializer.Deserialize(stream) as City;

with this:

City loadedCity = null;
TextAsset myAsset = (TextAsset)Resources.Load(fileName, typeof(TextAsset));
byte[] bytes = Encoding.UTF8.GetBytes(myAsset.text);
using (MemoryStream stream = new MemoryStream(bytes))
  loadedCity = (City)(new XmlSerializer(typeof(City))).Deserialize(stream);
return loadedCity;

and lo and behold, the version built for the webplayer runs without error, characters are selectable, xml is deserializable, the sun is shining, and all is right with the world. I'm taking the rest of the day off.

No comments:

Post a Comment