This is a project which I began work on in June 2023.
As I was working on a 3D Pokémon game and I wanted to create a game with the feel of Let's Go Pikachu and Let's Go Eevee visually, I needed to find a way to create world maps using 3D tiles. This created an interesting challenge as I was aware Unity had built-in support for tilemaps in 2D and I found videos of people using 3D tiles with Unity's tile system, however, I would need a new tilemap for each layer of elevation and creating colliders for these 3D tilemaps seemed like a nightmare. So I decided that some sort of mesh-based approach would make the most sense as a mesh is not limited in verticality and its dimensions can be used to form a collider hull.
This is a reference image from Pokémon: Let's Go Pikachu
In the first iteration of the project, I made lots of tiles in Blender and then wrote a problem where the user could left-click on the face of an existing tile to add another or right-click on the face of a tile to remove that tile. Then all neighbouring tiles would re-evaluate their shape, replacing themselves with a different tile if necessary. This was quite an elegant system which married together the ideas of three different YouTube videos I had watched on the topic. My plan was to then use Unity's CombineInstance class to combine the meshes together into a single mesh, to help improve the performance of the game I intended to create as I imagined there would be hundreds of these tiles in the scene and it would be a lot of wasted resources having all of those individual colliders and game objects. However, when I attempted to use the CombineInstance class, the mesh that was created was missing faces and put all of the faces into a single sub mesh meaning that any tiles used could only have one material. I decided for these reasons I needed to change my approach.
When thinking of ways in which I could change my approach, I realised that all of my tiles would fit within a 1x1x1 cube and therefore they were essentially voxels. I had experience attempting to create a Minecraft clone in Unity in 2021 so I took some of the code from that project as a base, optimised it and then built my new world builder using 3D tiles on top. With the new approach rather than creating a tileset where I modelled each tile in Blender, I instead created a scriptable object class which I would use to define the vertex positions, triangles, normals and UVs of each face. This way, when the tile is placed in the world, I can cycle through the faces, if a neighbour with an appropriately shaped face is present, I don't render the face (preventing rendering of unseen triangles). Also when updating neighbours after placing a new tile, only the face that borders the new tile has to be updated not the entire voxel. As this approach is purely programmatic, the mesh manipulation and combining elements of the world creation is also a lot easier than the method where each voxel is its own game object. The other important change I made with the new implementation was to allow rotating of tiles instead of creating lots of duplicates of the same tile at different rotations in order to capture all of the rotations. This sped up asset creation and provided some interesting challenges with correcting normals, UVs and neighbour face-checking.
The new version also offers better user feedback as there is a ghost tile where the user is pointing so they can more consistently determine where a tile will be placed or removed. In placement mode, the ghost tile matches the shape and rotation of the tile being placed to further improve feedback.
With regard to meshes and submeshes, this version uses one mesh per tile family. So all of the grass tiles, though different shapes, would be considered the same family as they use the same materials, same with the sand tiles being a family, stair tiles being a family and water tiles being a family. In the video, the created world would have 4 meshes in total. The grass, sand and stair meshes use two submeshes as each family uses up to two materials per tile. While the water mesh would only use one submesh.
As I wanted to use this project as a base for moderately-sized worlds, I figured I would need to create a method for inputting larger amounts of data more quickly as creating worlds with the current tools would become very tedious and time-consuming with the need to place each individual tile by hand. I decided to implement a system where an image could be used to represent the placement of all of the tiles of the same family in a given area. Using an image placed a new restriction on world size that was not previously present, so I decided to use the size of an image as the size of one world chunk and then each world chunk would have a submesh for each tile family. For now, I delete unused submeshes when I export the tiles, though, in the future, I intend to delete these programmatically. I decided to use 64x64 pixel images for this, along with 17 unique greyscale values to represent 16 heights as well as void values. I chose this image size and this number of greyscale values as after some testing this worked out nicely for the number of triangles present in one Unity mesh. Images provided a really nice solution for adding lots of data quickly to the world builder as I could use colour to determine the height to place blocks. In the future, I would also like to separate out the red, green and blue channels to better distinguish between orientation (for tiles like stairs) or between fill modes when adding blocks to a chunk. Currently, when the height is read from the image, blocks are added from the bottom of the chunk up to that height, however, by taking advantage of the other colour channels I could instead fill from the top of the chunk or just add blocks at the specified height, allowing for the creation of overhangs and other interesting terrain features. This would reduce the amount of work required to add in other small manual changes.
Alongside this latest change I decided to revisit some of the design ideas I had in my first attempt at this project and combine those with other ideas I had in the second attempt. I kept the Minecraft style voxels for each tile inside of the chunks, however, this time I used models I had created in Blender and had the program read the mesh data from these models in order to generate each chunk. This means that tiles can look like anything and can be more easily swapped in and out than when they were scriptable objects. I also use a similar algorithm to the first approach to automatically determine which shaped piece to use for each block given the number of neighbours it has. These changes make manual editing as easy as in approach 1 but not quite as free as in approach 2.