2015/07/21

Content Management: Assets and Localization

Okay, so this will be the last post in this series about Content Management. I'm sick of it, you're bored of it, and we should Keep It Stupidly Simple anyway, so here's what's gonna happen:

Partial unloading of assets will happen, but we'll take an unusually optimistic approach, with just two ContentManagers; one that only gets unloaded at the end of the game holding global content, another that is used to load level-specific assets, which gets unloaded whenever there's both a fade to black and a map change. Makes seamless map changing less painful, and we can still do optimizations later. Or ask uncle Bill for help.
Parallel loading, if I decide to do it at all, we can talk about when we talk about parallelization in general. Until then, premature optimization and all that. Further research suggests that the ContentManager isn't thread safe anyway. I've seen people do it before, might just be more trouble than it's worth.

Localization

is the only thing left on my list then. I was first introduced to the troubles of localization at a summer job where I was programming in Java with another intern. We were tasked with building an installer, which would parse an XML file, build a MySQL database according to it, upload some images into BLOBs in that database and then run all SQL scripts (which we had to fix by hand, because most contained faulty syntax...) inside another directory.
After coding all that in surprisingly little time, including a half-decent, installer-like GUI (in fucking AWT, I might add), we were told, "Great, now translate it into German and give our translator a file he can translate into Polish!".

Luckily for us, we were working in Eclipse and quickly came across String Externalization. The concept is simple: Eclipse parses all your code files and replaces constant strings with calls into a static subclass of ResourceBundle, which, upon startup, is loaded from a *.properties file. Syntactically, it's roughly equal to a *.ini file, so really simple stuff.
These calls would look something like RESOURCES.getString("main_window.title"), and internally, the ResourceBundle would be implemented as a hash map.

We'll do it similarly, with these changes:
  • Our TextBank class won't be static, so you can instantiate different text banks and load only those you actually need at any one time.
  • We will use an index instead of a method. Everybody who knows what a TextBank does should be able to deduce what is meant. If they don't know, calling the method GetString won't help much - if they know the language, they can tell that a string is returned already.
  • We won't use a hash map with string keys, but a plain string[], with integer keys. This is slightly more performant, but the main motivation here is event scripting; a script can refer to a specific text without the interpreter having to learn about string literals.
The resulting class is pretty simple. The XML schema for custom content formats is as well; I used this for testing. Thing is... getting MonoGame to load the damn thing proved a bit of a challenge.

Project set up, revisited

The problem I was running into was a compile time error message: MGCB, the MonoGame Content Builder, couldn't resolve the type TextBank. Which is weird, because the Content.mgcb file was in the same project - by default - and should know types from the corresponding assembly, right?
Wrong. You need to manually add a reference to the project for it to build those types properly. However, you can't just go into the references property in MGCB and add in the assembly name (which will just result in an additional error, telling you that it can't find an assembly with that name), nor the file name (which gets you the same error), nor the full path to the built assembly (which will tell you that the assembly appears to be corrupted or incompatible - probably because it's not finished building because the content isn't built yet).
Copying the built files into the content directory won't work either, again because the assembly is supposedly corrupted/incompatible. So, google to the rescue. Except I didn't find anyone with the same issue (in MonoGame, at least. Somebody managed to fuck it up in XNA. Their solution wasn't applicable to MonoGame). So I looked through the issues on github, SOMEONE must have tried to build custom XML content before, right?
Right. Sort of. At least they were pointed into the right wrong direction. The developer in that thread claims that the error stems from problems with dependencies and the build order. The proposed work around is similar to what the XNA project templates had you doing, with an additional content assembly containing the *.mgcb file. Don't worry, you don't actually have to do this. I did, however, spend quite a while experimenting with that, fighting non-sensical error messages. In the end, the solution is changing the target platform of my assembly from x86 to Any CPU.

Fixing the TextBank class

The next build error was an easy fix: TextBank lacks a parameterless constructor, necessary to deserialize by reflection. Just add one that nulls the texts field.
Then, another build error, caused by an XmlException in MGCB. This was a bit confusing, because most google results showed the XML to be invalid. In fact, all but Shawn Hargreave's awesome blog, which reminded me that the content serializer, by default, ignores private members, meaning it didn't expect any child nodes to the <Asset> node. Adding the [ContentSerializer] attribute to the texts field actually let me build the content project.

A quick test run reveals that this actually works so far. So we're done here, right?

Introducing: The Localization Tool

Wrong again.
If you can't tell from my test XML, there isn't really any way to tell which text you're editing. There isn't even an indicator for the integer key of the text. Once there's more than five or six strings in the bank, you'll be counting to make sure you have the right one, once there's more than a couple dozen, this becomes infeasible.
I'll be doing some tools programming anyway, so I might as well start easy with this one. The bare minimum should be a table with the index and an editable text field, as well as an Add String button. There's other features you could want for this tool, but we'll see how far down the rabbit hole I'll go.

Even excluding tooling, there are some other localization details that need to be figured out: How do we initially determine what language to use (Use English and hope for the best? Check the system locale on first launch?), how do we elegantly determine the asset names of the relevant TextBank XNBs?
This is a matter of taste, among other things, but what I'll probably end up doing is using an extension method ContentManager.LoadTextBank() which appends a language identifier given in the configuration file to the asset name. Alternatively a static method GetLocalizedAssetName() on TextBank, which does the same, except you still need to pass the returned string into ContentManager.Load().

Anyway, that should wrap up this series nicely. Next, we'll get into high level flow control in the game: game state management, and what that even means.

No comments:

Post a Comment