Porting a PHP Based Web Game to Mobile Part 4
Alright, we've got our units in the game and they're looking rather flash (or again, you know, exactly the same as the ones in the web version). Now we need to make the rest of the map look that good. I've already taken the liberty of getting started on our next item, terrain. Terrain, thankfully, was for the most part a lot simpler than units. There are no gifs to route through Aseprite via bulk processing cli commands, no country names to apply before searching for their sprites and thankfully this time around, I was able to get in touch with a member of the web versions developer community, they were more than happy to provide the art assets for the game, which meant I had easy access to all the terrain assets. I'll just pretend I don't see those unit assets that definitely wouldn't have come in handy in part 3... In all fairness, it actually would have taken longer to get each of those setup as animations than what it would to just route the gifs through Aseprite and then Unity, so I'm not too fussed over that. Enough about units though, we're here for terrain. So let's get caught up, we started off with our map from last time, if you don't recall what it looked like, here you go: Terrain, much like its unit counterpart, has a sprite who's name can be derived from the fields in the terrain object that we parse from the game.php response HTML (let's just call it, 'The One True HTML'). So, much like before, all we need to do is download all of the terrain files, place them into our new Resources/Terrain directory and tell our code to look in that directory when it is dealing with a terrain tile, simple enough. We then run into our first hurdle, what about terrain tiles that have variants? For example, Road tiles. These tiles are for the same general type of terrain, but they are variants for things like, vertical roads, horizontal roads, turning roads, etc. When we downloaded all the art for the game, they came in a separate directory entirely and unlike the other terrain sprites, these ones don't match their server counterpart names one to one. Not a big deal as the difference isn't huge, the sprites I've received are only missing the "Road" part of VRoad, so just "V.png". This appears to be consistent across all the sprites, so I can just fill that part in via code. private List terrainSpecials = new List() { "Pipe", "Road", "River", "Shoal", "Sea" }; string terrainTypePath = "Terrain"; string specialTerrainType = terrainSpecials.Find(x => unitName.Contains(x)); if(specialTerrainType != null) { terrainTypePath = $"Terrain/{specialTerrainType}"; if(specialTerrainType != unitName) unitName = unitName.Replace($"{specialTerrainType} ", "").Replace($"{specialTerrainType}", ""); } string finalPath = $"{terrainTypePath}/{unitName}"; Sprite terrainSprite = Resources.Load(finalPath); spriteRenderer.sprite = terrainSprite; Now I know what you're going to say, "Hey Mike, that's cheating, I can clearly see you've already gone and done this for all the other special terrain types too, that's not fair. Why not take us through the issues you had for each?" And I understand, we're moving fast, but I assure you, there's a reason for it. That is because, we are about to grind to a halt, because you see, that code above is not fully working. To understand why, let me show you where we are at now, first. It looks pretty good right! But I know what you're thinking (aside from the gaping void that seeks to consume all above each mountain, we'll get to that), the water looks weird, right? Yes, reader whom I do not know, the water does look weird, why would that be? Do you remember how I told you that everything we need to know about which sprite to use could be found in each tile's json representation, meaning we could have a super easy ride through this project and have to do little to no actual development? Okay, that last part was a bit of a fabrication, but I think you see what I'm getting at, the json data in the The One True HTML, does not contain which type of Sea or Shoal sprite needs to be used. That data can not be found in the tiles name, nor in any other field in string format that could simply be parsed into its sprite name. Sure, it could maybe map to the tile_id found in each tile's data, but mapping every tile_id to its matching sprite name would be a tedious headache, something I would never do, right?... Well, let's see just how much of a tedious headache it would be compared to its alternative. That alternative being, obtaining the data of tiles around Shoal/Sea tiles, and using that to decide which sprite to spawn on our own. Before we even start coding, let's think about it for a second. What outcome are we trying to achieve, i.e. how should this look when it's done? To know that, we luckily have an already functioning map to reference in the web version. We see a pretty standard

Alright, we've got our units in the game and they're looking rather flash (or again, you know, exactly the same as the ones in the web version). Now we need to make the rest of the map look that good. I've already taken the liberty of getting started on our next item, terrain. Terrain, thankfully, was for the most part a lot simpler than units. There are no gifs to route through Aseprite via bulk processing cli commands, no country names to apply before searching for their sprites and thankfully this time around, I was able to get in touch with a member of the web versions developer community, they were more than happy to provide the art assets for the game, which meant I had easy access to all the terrain assets. I'll just pretend I don't see those unit assets that definitely wouldn't have come in handy in part 3... In all fairness, it actually would have taken longer to get each of those setup as animations than what it would to just route the gifs through Aseprite and then Unity, so I'm not too fussed over that.
Enough about units though, we're here for terrain. So let's get caught up, we started off with our map from last time, if you don't recall what it looked like, here you go:
Terrain, much like its unit counterpart, has a sprite who's name can be derived from the fields in the terrain object that we parse from the game.php response HTML (let's just call it, 'The One True HTML'). So, much like before, all we need to do is download all of the terrain files, place them into our new Resources/Terrain
directory and tell our code to look in that directory when it is dealing with a terrain tile, simple enough. We then run into our first hurdle, what about terrain tiles that have variants? For example, Road tiles. These tiles are for the same general type of terrain, but they are variants for things like, vertical roads, horizontal roads, turning roads, etc. When we downloaded all the art for the game, they came in a separate directory entirely and unlike the other terrain sprites, these ones don't match their server counterpart names one to one. Not a big deal as the difference isn't huge, the sprites I've received are only missing the "Road" part of VRoad, so just "V.png". This appears to be consistent across all the sprites, so I can just fill that part in via code.
private List terrainSpecials = new List()
{
"Pipe",
"Road",
"River",
"Shoal",
"Sea"
};
string terrainTypePath = "Terrain";
string specialTerrainType = terrainSpecials.Find(x => unitName.Contains(x));
if(specialTerrainType != null)
{
terrainTypePath = $"Terrain/{specialTerrainType}";
if(specialTerrainType != unitName)
unitName = unitName.Replace($"{specialTerrainType} ", "").Replace($"{specialTerrainType}", "");
}
string finalPath = $"{terrainTypePath}/{unitName}";
Sprite terrainSprite = Resources.Load(finalPath);
spriteRenderer.sprite = terrainSprite;
Now I know what you're going to say, "Hey Mike, that's cheating, I can clearly see you've already gone and done this for all the other special terrain types too, that's not fair. Why not take us through the issues you had for each?" And I understand, we're moving fast, but I assure you, there's a reason for it. That is because, we are about to grind to a halt, because you see, that code above is not fully working. To understand why, let me show you where we are at now, first.
It looks pretty good right! But I know what you're thinking (aside from the gaping void that seeks to consume all above each mountain, we'll get to that), the water looks weird, right? Yes, reader whom I do not know, the water does look weird, why would that be?
Do you remember how I told you that everything we need to know about which sprite to use could be found in each tile's json representation, meaning we could have a super easy ride through this project and have to do little to no actual development? Okay, that last part was a bit of a fabrication, but I think you see what I'm getting at, the json data in the The One True HTML, does not contain which type of Sea or Shoal sprite needs to be used. That data can not be found in the tiles name, nor in any other field in string format that could simply be parsed into its sprite name. Sure, it could maybe map to the tile_id found in each tile's data, but mapping every tile_id to its matching sprite name would be a tedious headache, something I would never do, right?...
Well, let's see just how much of a tedious headache it would be compared to its alternative. That alternative being, obtaining the data of tiles around Shoal/Sea tiles, and using that to decide which sprite to spawn on our own. Before we even start coding, let's think about it for a second. What outcome are we trying to achieve, i.e. how should this look when it's done? To know that, we luckily have an already functioning map to reference in the web version.
We see a pretty standard pattern, everywhere water meets land, a different sprite is used depending on how many sides surround said water, are land. It does however look like we have two exceptions, at least that we can see here, namely, mountains and bridges, those don't seem to be recognized as "land" necessarily. So how many sprites would we need to take into account? According to the sprites I received, it's a lot. 85 sprites for shoal, and 49 for sea. Sheesh... Okay, maybe we just double check one last time if there's a way to do this with the data we already have (and that doesn't involve mapping each of these 134 sprites to a unique id).
So I searched for, admittedly, not that long of a time, before arriving at the conclusion that, this would be simpler if we just chose the sprite to use ourselves. Taking a look at the sprite names we have downloaded:
We can see first see the letter A, which to be honest I have no idea the meaning behind, but looking a little further down we can start to see a pattern we do know the meaning behind.
That's right, North, South, East and West and various combinations of them, with an order that doesn't seem to change, this should be perfect to at least get us something on the board. So let's get to implementing.
private string GetTerrainFilename(TileInfo tileInfo)
{
string terrainName = tileInfo.Name;
string terrainTypePath = "Terrain";
string specialTerrainType = terrainSpecials.Find(x => terrainName.Contains(x));
if(tileInfo.Name.Contains("Shoal") || tileInfo.Name.Contains("Sea"))
{
terrainName = GetWaterTileName(tileInfo);
}
if (specialTerrainType != null)
{
terrainTypePath = $"Terrain/{specialTerrainType}";
if (specialTerrainType != terrainName)
terrainName = terrainName.Replace($"{specialTerrainType} ", "").Replace($"{specialTerrainType}", "");
}
return $"{terrainTypePath}/{terrainName}";
}
private List waterAdjacentNonInterruptors = new List()
{
"Sea",
"Shoal",
"Mountain",
"Bridge",
};
private string GetWaterTileName(TileInfo tileInfo)
{
string terrainName;
string[] adjascentTileNames = GetCardinalAdjascentTileNames((int)tileInfo.X, (int)tileInfo.Y);
terrainName = "";
for (int i = 0; i < adjascentTileNames.Length; i++)
{
if (adjascentTileNames[i] == null) continue;
bool isWaterCutoffTile = waterAdjacentNonInterruptors.Find(x => adjascentTileNames[i].Contains(x)) == null;
if (isWaterCutoffTile)
{
if (terrainName.Length != 0) terrainName += "-";
terrainName += ((RelativeDirection)i).ToString();
}
}
return terrainName;
}
private string[] GetCardinalAdjascentTileNames(int x, int y)
{
string[] tileNames = new string[4];
TileInfo[] tiles = new TileInfo[4];
tiles[0] = terrainInfo.Tiles.Find(t => t.X == x && t.Y == y - 1); //north
tiles[1] = terrainInfo.Tiles.Find(t => t.X == x + 1 && t.Y == y); //south
tiles[2] = terrainInfo.Tiles.Find(t => t.X == x && t.Y == y + 1); //east
tiles[3] = terrainInfo.Tiles.Find(t => t.X == x - 1 && t.Y == y); //west
for(int i = 0; i < tiles.Length; i++)
{
if(tiles[i].IsValid())
{
tileNames[i] = tiles[i].Name;
}
}
return tileNames;
}
Alrighty, that should do the trick for now, let's take a look at what we got.
Ah- woops, 2 problems here, firstly we forgot about buildings, so let's just tweak our code to also check for adjacent buildings if there is no adjacent terrain.
private string[] GetCardinalAdjascentTileNames(int x, int y)
{
string[] tileNames = new string[4];
TileInfo[] tiles = new TileInfo[4];
tiles[0] = terrainInfo.Tiles.Find(t => t.X == x && t.Y == y - 1); //north
tiles[1] = terrainInfo.Tiles.Find(t => t.X == x + 1 && t.Y == y); //south
tiles[2] = terrainInfo.Tiles.Find(t => t.X == x && t.Y == y + 1); //east
tiles[3] = terrainInfo.Tiles.Find(t => t.X == x - 1 && t.Y == y); //west
for(int i = 0; i < tiles.Length; i++)
{
if(tiles[i].IsValid())
{
tileNames[i] = tiles[i].Name;
}
}
if(string.IsNullOrEmpty(tileNames[0])) tileNames[0] = buildingsInfo.Tiles.Find(t => t.X == x && t.Y == y - 1).IsValid() ? "Building" : null; //north
if(string.IsNullOrEmpty(tileNames[1])) tileNames[1] = buildingsInfo.Tiles.Find(t => t.X == x + 1 && t.Y == y).IsValid() ? "Building" : null; //south
if(string.IsNullOrEmpty(tileNames[2])) tileNames[2] = buildingsInfo.Tiles.Find(t => t.X == x && t.Y == y + 1).IsValid() ? "Building" : null; //east
if(string.IsNullOrEmpty(tileNames[3])) tileNames[3] = buildingsInfo.Tiles.Find(t => t.X == x - 1 && t.Y == y).IsValid() ? "Building" : null; //west
return tileNames;
}
Not the prettiest, but pretty can wait. The second problem we had was two missing sprites on either side of the map. Upon further inspection, we learn that these are the only 2 tiles, that are considered as being entirely without a bordering land tile. Our code reveals that terrainName = "";
here in our GetWaterTileName
method, we will return an empty string if we have no neighboring land tiles. That's no good, let's return the original tile name instead so it can use the tile default.
private string GetWaterTileName(TileInfo tileInfo)
{
string terrainName;
string[] adjascentTileNames = GetCardinalAdjascentTileNames((int)tileInfo.X, (int)tileInfo.Y);
terrainName = "";
for (int i = 0; i < adjascentTileNames.Length; i++)
{
if (adjascentTileNames[i] == null) continue;
bool isWaterCutoffTile = waterAdjacentNonInterruptors.Find(x => adjascentTileNames[i].Contains(x)) == null;
if (isWaterCutoffTile)
{
if (terrainName.Length != 0) terrainName += "-";
terrainName += ((RelativeDirection)i).ToString();
}
}
return terrainName == "" ? tileInfo.Name : terrainName;
}
Now, let's take a look how things have turned out.
Much better, now all that is left are those pesky all consuming voids in each of the mountain tiles. From what I can tell, this is present in the actual sprite as well, so unsure how we'll fix this without just finding one that isn't broken. Let's see what we can do.
Looking at the sprite info, mountains are 16 x 21 pixels, whereas other sprites are all 16 x 16, we may be able to resolve the issue by simply moving the graphic up a few pixels, or tweaking the pivot on the sprite. Let's go with the latter as there are only 3 mountain sprites in the game anyway.
Awesome! It does seem like some of the mountains are now being cut off when below a building, but I suspect that this problem will go away once we add the actual buildings to the board. Buildings can wait though, we'll tackle those next time!