Friday, November 29, 2013

Unity, Localization and oh so much anger...



So powerful, pretty easy to use and so annoying when something breaks.

For the game we're making in our Integration Projects 3 class, we are making our game in Unity.  The class states that the game has to be localized for English, Dutch, French and German, as well as Win 8  Store ready.

We thought everything was A-OK till this last week when we went to do a Win 8 build (cause the school finally got the tablets for us to test on...) and that's when we found out something that hinges our whole localization code and throws it in the the garbage...

Since our game is going to be using quite a bit of text we decided to spend some time making sure our localization would work flawlessly from the start instead of going back and doing it after (since that seems like a dumb idea anyway...)

So we came up with a system that is basically as follows:

One class - GlobalData -> basically keeps track of what language the game should be in.

Holds an Enum,

 LanguageEnum {English, Nederlands, French, German}  

and an int that is CurrentLanguage which of course keeps track of our language by casting the LanguageEnum to an int and storing it.

Then for each class that will have text in it, we have an xml file with a pretty basic structure that is like this:

 <?xml version="1.0" encoding="utf-8"?>  
   <Eng Text="Main Menu"/>  
   <Nl Text="Hoofdmenu"/>  
   <Fr Text="Menu Principal"/>  
   <Ger Text="Hauptmen├╝"/>  
   <Eng Text="New Game"/>  
   <Nl Text="Nieuw Spel"/>  
   <Fr Text="Nouveaux Jeu"/>  
   <Ger Text="Neues Spiel"/>  

Then in our class with text we have two things, another Enum that corresponds with the Elements in our xml such as:
 public enum MenuStringKey  
 { Title, NewGame, Controls, Options, Credits, Exit }  

And a public void  LoadStrings(); method which is as follows:

 // Open Xml Document with tranlations.  
     XmlDocument xmlDoc = new XmlDocument();  
     // Loop over all needed option strings. (By looping over OptionStringKey enums.)  
     foreach (MenuStringKey neededStr in Enum.GetValues(typeof(MenuStringKey)))  
       // Get all possible translations for the needed string.  
       XmlNodeList languageList = xmlDoc.GetElementsByTagName(neededStr.ToString());  
       // Get needed translation.  
       foreach (XmlNode translations in languageList)  
         //Get the attribute that goes with the localization.  
         MenuStrings.Add(neededStr, translations.ChildNodes.Item((int)GlobalData.CurrentLanguage).Attributes["Text"].Value);  

We store our localized strings in a Dictionary of
 Dictionary<MenuStringKey, string>   

 Or whichever Enum it is.

And whenever we wanted to get the localized string it is just
 MenuStrings[MenuStringKey.Title]  //Gets the corresponding string in whichever language

We had to do our loading with relative paths as for some reason, Resources didn't want to work us before (it does in our new code but it didn't back then...)

This also made another problem we didn't at first notice, because we're referring to the assets/localization folder, we also have to copy that folder structure over to the build else, no text, no working game.

But hey, not a big deal right?

Well, this was working fine for us up till this week, as I said before.  When we went to build for Win 8 app store, we found that XmlDocument and friends are not supported...Great...
 Just GREAT...

So now we have to figure out how to get our xml reading to work again with XDocument which is actually what I wanted to use in the first place but at the time (only a few weeks ago) Unity didn't support which is why we had to use the xmlDocument instead of XDocument and LINQ (which is sooo much nicer)

So after messing around and trying to get used to XDocument and LINQ again, I have finally come up with an actually, even more elegant solution which solves 2 problems for us.

It solves the having to copy over the folder structure as well as our xml loading problems.

So we still have the same setup with the enums and such, the only change I have made is in the GlobalData one by changing it from the original (see above) to
 LanguageEnum {Eng, Nl, Fre, Ger}  
 This makes it so when we use it for LINQ it auto finds the same tag in our XML.

So I changed our LoadStrings to

 var textAsset = (TextAsset)Resources.Load("Localization/MenuText");  
     var doc = new XDocument(XDocument.Parse(textAsset.text));  
     var root = doc.Root;  
     foreach (MenuStringKey neededStr in Enum.GetValues(typeof(MenuStringKey)))  
Now there is a few things I can say about this chunk of code besides it being one foreach instead of 2 and actually one line :P

 var textAsset = (TextAsset)Resources.Load("Localization/MenuText");  

This is using the Resources of Unity.  What is Resources?  It's a folder called Resources inside your Assets folder. It apparently is quite handy for a lot of things not only limited to loading text files and such.

So I moved our localization folder from the Assets into our Resources folder. I kept it in our Localization folder just cause you never know what else we're gonna throw in there (order and stuff...)

So for resource loading of xmls, you don't put the .xml, just the file name and Unity will actually look where you tell it and find the file that matches what you want. I tested because some documentation says you can just say the file name and it will search ALL folders in your resources folder but this seems to not be completely true, so be warned that if you have a sub folder, make sure you are including that folder name in your path as well like above.

The second thing is
 var doc = new XDocument(XDocument.Parse(textAsset.text));</code></pre>  
 Which was actually a pretty annoying thing to get figured out. &nbsp;A lot of online sources say use XDocument.load which doesn't seem to work. I did finally stumble across someone saying doing this current line which made it work correctly.  

Now for our XML structure, we only have the one attribute in our Element which is the text for that language.
Since this is inside our ForEach, we check for each Enum that is in MenuStringKey and try to find the element that corresponds with it and then finding the element in that one that corresponds to our CurrentLanguage and then finally the attribute which is the Text="" part
This works quite nicely and actually makes it so we can keep all the other code (so far, I do have to check to make sure it will Build for Win 8 after I get this all re done in the other classes...)for the actual using of our text and such, which is great.

I'll do an edit when I've found out if this will completely fix our build problem.
Thanks for reading and hopefully this has helped someone :D
This code has made it so I was able to make a Win 8 App store build with no problems. Cheers, hope it helps.