Hell-o one and all to this SPOOKY edition of... this... blog... Thing?! For this special SPOOKY edition, I will be your SPOOKY (G)host, Mr. Slate!
I'm here to tell you about my adventures with the SPOOKIEST feature of the Unity engine (SPOOKY pause, DRAMATIC LIGHTNING STRIKES):
CREATING TEXTURES PROGRAMMATICALLY IN UNITY!!!!!
Ok you can relax, I'm not doing that bit the whole time.
I've been working on some optimisations for the world map in my game and I thought I'd share.
For a bit of context - the game has a diegetic kingdom map which is in the character's office(it's an inn ok, the main character's office is a table in an inn). The map is presented in a D&D style 20x20 hex grid where each hex can be a different biome. Each biome image has variations so every time you create a new character you get a slightly different looking map. All the biome types are the same but the textures are different. Because the map can interact with things like light sources I use planes as opposed to UI. The map is used to pick missions from so I suspect it will be where players spend a large proportion of their time.
Back to my story! In my original prototype I was drawing each hex as it's own plane. The map was populated once, when the environment was loaded. I had a hex prefab and was populating the map with standard x/y for-loops, while loading and applying the textures to the hexes as it went. As you can imagine 400 textured planes that are also affected by lighting is not the best way to do this on a mobile device so I'd been planning to update the map building logic for a while.
I had a bit of a think and I decided to build a map texture programmatically at runtime using the individual hex textures. The way I achieved this is through the black magic of Texture2D pixel manipulation! It's kind of simple really.
A couple things to prep first. All my hex textures are read/write enabled, I have a material for the map texture and I have a reference to the map plane's MeshRenderer. The MeshRenderer is using the map material.
I set a few basic things (in reality I get these in various ways but for illustration):
var imageWidth = 64;
var imageHeight = 64;
var mapHexesWidth = 20;
var mapHexesHeight = 20;
var mapTextureWidth = imageWidth * mapHexesWidth;//1280px
var mapTextureHeight = imageHeight * mapHexesHeight;//1280px
Then I create a new texture using the Texture2D constructor:
var mapTexture = new Texture2D(mapTextureWidth, mapTextureHeight, TextureFormat.ARGB32, false);
And I build a great big heap of nested for loops like this:
for (var mapHexX = 0; mapHexX < mapHexesWidth; mapHexX++) {
for (var mapHexY = 0; mapHexY < mapHexesHeight; mapHexY++) {
//load texture from resources or get texture from your texture loading system
for (var pixelIndexX = 0; pixelIndexX < imageWidth; pixelIndexX++) {
for (var pixelIndexY = 0; pixelIndexY < imageHeight; pixelIndexY ++) {
...
}
}
}
}
At the centre of the loops I get the hex texture colour at pixelIndex, calculate the x/y position in the map texture and set the map texture pixel to the hex pixel value.
...
var hexPixelColor = hexTexture.GetPixel(pixelIndexX, pixelIndexY);
var mapXPosition = mapHexX * imageWidth + pixelIndexX;
var mapYPosition = mapHexY * imageHeight + pixelIndexY;
mapTexture.SetPixel(mapXPosition, mapYPosition, hexPixelColor);
...
Below all the for-loops I apply the new pixels to the map texture and set the texture to the map plane's material. There are a couple steps I missed intentionally because the code for them is quite specific.
The first one is an offset to the hex tiles. Hexes are different from squares in that the tiles need to be offset slightly by width and height in order to fit with each-other. Generally what you need to do is on the pointy side of the hex bring the tiles closer to each-other and on the flat side of the hex use:
x % 2 == 0
to alternate between rows or columns (whichever one the flat side is on) and offset every second one by half the hex size. A note on this. offsetting your tiles means you also need to adjust the size of the map texture when you create it, shorter on the pointy side of the hex and longer by half hex on the flat side.
The second step I missed was giving the map texture a starting colour, in my case:
var fillColor = new Color(0f, 0f, 0f, 0f);
I do this by creating an array of colours the size of the map texture (1280*1280) and set the map texture's pixels like this:
mapTexture.SetPixels(colorArray);
The third step I missed is very specific to my implementation. If you didn't know by default unity texture's 0x0 is in the lower left corner. This means that no matter if you paint by rows or columns the bottom lines of pixels are also on the bottom "layer". Now this is not an issue if you have textures where there's nothing outside of the tile. But if you had an isometric view and your textures would overlap slightly that's a major issue. So I've also got some code to start drawing from the top down. Very briefly, the way I fix this is by reversing the mapHexY for loop and making sure I still grab the hex texture for the bottom hex in the map, the rest of the logic is the same but now it draws the map from the top rather than the bottom.
So this is how you convert a grid of hexes into a single texture! This is all still very rough and it takes longer than I'd like so I'm planning a few optimisations.
Move the map texture creation to my image loader utility so I only have to create it once.
Create the map starting colour array on the game's start-up and store it for repeated use.
And then lastly I want to look into storing the map texture in the player's save game folder so it doesn't have to be created every time the map scene is loaded.
That's it for now, hope to see you next week!
Comments