Tile Based Map Generation with SabreCSG
Now that i’m finished with my metaballs implementation I’ve started working on creating map assets and building out test levels for my game. Since I’m creating this myself I tried to find a tool or asset pack that could help me speed up the creation of the tile assets for the map.
To keep things easy and reusable I decided the level design would be a 3D tile based.
So I needed to find something that could:
- Create tile based models
- Do per face texturing
- Create reusable components
- Easy to learn
- Free - if possible
A brush based tool like Valves Hammer from the Source Engine seems like an easy way to build out a map. I had seen ProBuilder and ProBuilder free, but I didn’t really want to be tied to a free package that might be missing features I need down the line and force me to purchase the full version.
I found SabreCSG, a brush based asset that is completely free, I didn’t get around to even testing ProBuilder free because Sabre seems to do the job for me.
Tile Models
The creation of models was pretty easy with SabreCSG, my only issue I had was with some of the weird rotations the system puts on brushes when you extrude or split them, which caused issues with my procedural generation.
Once I figured out a scale that would fit the player on one tile I started building out some prototype blocks that I will be using in my maps.
Map Generation
As great as tools like probuilder and SabreCSG are for building models quickly, I wanted to have some rapid prototyping of levels. The easiest way I could implement this was to create a prefab out of each of the models I created above, then use Excel to layout my map by specifying which ID should be used in each cell, along with its Y axis Euler rotation, then iterate through the CSV file and spawn the prefabs in the correct locations.
Using the Unity resources folder, I put all my prefab models in a folder called Tiles and ensured I labeled my tiles with IDs so when the resource folder was loaded into an array, all of my tiles would be in the correct position in the array.
The excel file just contains the ID and the Y axis Rotation in a cell, separated by a space.
The magic happens in an Editor script I wrote which reads the CSV file and spawns the prefabs. It is attached to the CSGModel gameobject so the prefabs are spawned under it and become brushes. The only thing the script doesn’t do yet is clear out any existing prefabs, so if you want to make big changes, its easy enough to delete everything under the CSGModel object and generate the map again.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapGenerator : MonoBehaviour {
public GameObject CSGModel;
public GameObject[] myTiles;
public TextAsset[] myLevels;
public int tileSize = 3;
public int levelId;
private TextAsset csvFile;
private int levelHeight;
private int levelWidth;
private string[,,] level; //row/column/cell information, contains (id,y) where y is rotation.
public void GetResources()
{
//Get the list of tiles and put them into the array
myTiles = Resources.LoadAll<GameObject>("Tiles");
//Get the lsit of files and put them into the array
myLevels = Resources.LoadAll<TextAsset>("Levels");
}
public void LoadLevel()
{
//read the csv file from the levels array into an array
TextAsset csvFile = myLevels[levelId]; //loads the level
string levelData = csvFile.text.Replace("\r\n","/"); //save the level as a string, replacing the new lines with dashes
string[] levelRows = levelData.Split('/');
//Instantiate our level array with
levelHeight = levelRows.Length-1; //last line is always blank
levelWidth = levelRows[0].Split(',').Length; //split out the first row to get the width.
level = new string[levelHeight, levelWidth, 4];
for(int h = 0; h < levelHeight; h++)
{
string[] rowColumn = levelRows[h].Split(','); // split out the column
for (int w = 0; w < levelWidth; w++)
{
string[] cell = rowColumn[w].Split(' '); // split out the data
level[(levelHeight - 1) - h, w, 0] = cell[0];
level[(levelHeight - 1) - h, w, 1] = cell[1];
}
}
}
public void GenerateMap()
{
//read the csv file array and spawn prefabs
Vector3 position = Vector3.zero;
Vector3 rotation = Vector3.zero;
int tileid = -1;
position.y = 0f;
for (int h = 0; h < levelHeight; h++)
{
for (int w = 0; w < levelWidth; w++)
{
position.x = w * tileSize + (tileSize);
position.z = h * tileSize + (tileSize);
tileid = int.Parse(level[h, w, 0]);
if(tileid > myTiles.Length-1)
{
Debug.Log("missing tile= " + tileid + " reverting to tile ID 0");
tileid = 0;
}
rotation.x = 0;
rotation.y = float.Parse(level[h, w, 1]);
rotation.z = 0;
if(tileid >= 0)
Instantiate(myTiles[tileid], position, Quaternion.Euler(rotation), CSGModel.transform);
}
}
}
}
To call the above functions, I made an Editor script to run them from the Editor. If you haven’t created any Editor scripts yet, you should! They’re so helpful and speed things up.
using UnityEditor;
[CustomEditor(typeof(MapGenerator))]
public class MapGeneratorEditor : Editor
{
public override void OnInspectorGUI()
{
MapGenerator mapGen = (MapGenerator)target;
DrawDefaultInspector();
if (GUILayout.Button("Get Tiles"))
{
mapGen.GetResources();
}
if (GUILayout.Button("Load Level"))
{
mapGen.LoadLevel();
}
if (GUILayout.Button("Generate Map"))
{
mapGen.GenerateMap();
}
}
}
Now I can generate some test levels quickly!