Now I know I said I would just use double digits for the edge cases, but after some internal deliberation, I decided, why not just use the unit sprites instead? We have to do it at some point anyway, and if my theory that the name maps straight to a sprite name is correct, there's a really quick and easy way we can get all the sprites recognized and rendered.
The plan is simple; all we need to do is drop all of the game sprites into a folder that persists to the game's build. Resources or StreamingAssets will do, yes I see you addressables, but you give me headaches, so you can wait till the next project. Then we just point to the appropriate sprite directly, using the unit's unit_name value. For simplicity in loading objects directly into the appropriate type, we'll use Resources. If the picture still isn't quite forming in your head, this might help:
The file would look like:
Resources/Units/greentank.png
and the corresponding code to load it would look something like:
Resources.Load
Let's see if this is as easy as I'm hoping it will be...
He said, 4 hours ago...
As you may have guessed, it was, in fact, not that easy.
Firstly, there is no easy way to get the sprites of every unit in the game, and certainly not of every unit for every army (there are many armies in the game, and the number only increases as the community makes more). I will likely have to write a script to pull each of the sprites from the site in the future, for now however, I will just focus on getting one color on the board. This still meant I needed to download all the sprites for each unit, only when downloading the sprites, they don't come as sprites, but rather as gifs (the website pretty much always displays a unit as a gif as opposed to a static image). This presents an issue; Unity does not like gifs, it has no support for importing them aside from just discarding everything but the first frame. This would be fine, if that file then worked when trying to load it as a Sprite via Resources.Load, since I'm writing this, you can imagine it did not work that way. It could load as a Texture2D, but not as a Sprite (despite using the Sprite's name when trying to load it). Now, I could convert the Texture to a Sprite, but since I needed to eventually get these units animating like on the website anyway, I decided to instead just tackle that now.
Which brings us to our next issue, how do we get these converted relatively quickly into animated sprites, without taking each file, uploading it to a gif splitter, downloading the animation frames, re-importing back to Unity, and finally rebuilding the animation there. This would take a very very long time, and building a tool to do this for me, would likely take just as long. The answer? Aseprite. Aseprite is a pixel art tool, and the game we are porting just so happens to all be pixel art. Aseprite also supports gifs, it can automatically turn them into animations. And can you guess what Unity supports importing? That's right, Aseprite files. So, while still tedious, saving each of the pngs, importing them in bulk into Aseprite before saving each one as a .aseprite file, was the quickest way I could get these gifs into a usable format in Unity. And to be honest, I think it turned out quite nicely. With a little code to instead load an AnimatorController
from Resources:
// Because we only have the green army sprites at the moment
string unitGreenArmyName = "ge" + tileInfo.Name.ToLower();
string finalPath = $"Units/{unitGreenArmyName}";
AnimatorController unitAnimatorController = Resources.Load(finalPath);
tileGameObject.GetComponentInChildren().runtimeAnimatorController = unitAnimatorController;
The result is rather satisfying!
Although, we still have an issue of not knowing who is on who's team, for now though we can just assign a color to each country (there can be a max of 16 countries in a game) and set the units and buildings accordingly. We can deal with the custom sprites for each country later. Let's take a look at how that looks
Yikes... I don't think I can stand to look at that much longer. Now, I know I said we wouldn't do this now, but we've already got all this momentum, we may as well take it all the way. Let's get every unit for every army rendering correctly, now we're getting serious.
To do this, we need to, as I mentioned before, fetch every unit for every army (there are in actuality 19 total armies in the game). Each army has a color palette and each army's units can have completely different sprites and animations. Luckily, the unit gif names from the website all follow the same schema: {countryCode}{unitName}.gif
. And they can all be located under the same route https://awbw.amarriner.com/terrain/ani/
. We are also fortunate enough to have an absolute GIGA file containing pretty much all of the games metadata (if you still remember the response HTML from game.php). That file has a line that starts with const genericUnits =
who's value is a JSON that contains information about all units in the game. This is very useful. We can parse this, much the same as we parsed our in game unit information before, combine the countries_code
and units_name
variables, slap .gif on the end and amend our url above to it.
public const string DOMAIN = "https://awbw.amarriner.com";
public const string UNIT_GIF_ENDPOINT = "/terrain/ani/";
public void OnGUI()
{
if (GUILayout.Button("Download All Units"))
{
StartCoroutine(FetchUnitGifs());
}
}
public IEnumerator FetchUnitGifs()
{
string gameInfo = File.ReadAllText(Path.Combine(Application.streamingAssetsPath, "gamephp.html"));
GameInfoParser gameInfoParser = new GameInfoParser();
UnitsInfo unitsInfo = gameInfoParser.ParseGenericUnitInfos(gameInfo);
List countries = Resources.LoadAll("Countries").ToList();
foreach(var country in countries)
{
foreach(var tile in unitsInfo.Tiles)
{
string fileName = country.code + tile.Name.ToLower() + ".gif";
fileName = fileName.Replace(" ", "");
UnityWebRequest uwr = new UnityWebRequest(DOMAIN + UNIT_GIF_ENDPOINT + fileName);
uwr.downloadHandler = new DownloadHandlerFile("Resources/" + fileName);
yield return uwr.SendWebRequest();
}
}
Debug.Log("finished");
}
And voila, we have downloaded every unit's .gif in the game. Now, to get them all into Aseprite. To my absolute delight, Aseprite are just built different and already have a way of bulk exporting files via CLI. Because I like to torture myself, I choose to do nye all my cli work with powershell. Meaning I need to make a function in powershell that will cycle through all my .gif files and tell Aseprite to convert each to a .aseprite file.
Function AsepriteConvert([string] $fileType)
{
Get-ChildItem -Filter "*.$fileType" |
Foreach-Object {
$fullName = $_.FullName
$newName = $fullName.Replace(".$fileType", ".aseprite")
Start-Process "C:\Program Files (x86)\Steam\steamapps\common\Aseprite\Aseprite.exe" -ArgumentList "--batch $fullName --save-as $newName"
}
}
With that, we can navigate to our directory where we downloaded all our gifs, and run our command
AsepriteConvert -fileType gif
Now that we have a nice collection of over 450 .aseprite files. We add them to Unity, before filtering by t:animatorcontroller
and moving all the animator controllers into our Resources/Units folder. We're almost there, a little bit of tweaking now is all we need, once we have added country code to our TileInfo
object and connected it up to the corresponding ServerUnitInfo
during unit generation, we have our final product - visually speaking - for units. And I think it looks awesome, well, exactly the same as the web version, which is exactly what we want.
Now I know improvements can still be made, for starters, should there be another community event where yet another army is introduced to the game, I will need to add that army as well. A potential solution, might be to just swap out armies I don't have, for ones I do, after all there may be 19 armies, but there will only ever be 16 players in a game, so this is a viable workaround. That is something to think about when it becomes a problem, as for next time, we'll need to do what we did here, but for the terrain and buildings, hopefully with all the work we've done here, we can reuse what we've done to do those fairly easily.