2D Level Generation For Unity

Brian Li
CodeX
Published in
8 min readMay 18, 2021
Photo by Lorenzo Herrera on Unsplash

Introduction

It hasn’t been that long since I first got into Unity, and even beginning to think about randomized level generation got my head spinning. I thought it would be some ridiculous algorithm or some insane amount of work.

Captured in Unity Editor - Assets credited to Unity Technologies

Now, this is/could be still very true for more advanced techniques and special requirements for terrain generation, but for the basics, it was actually extremely simple, it just took a lot of time. The idea is simple, to use something I had been using my entire undergrad: Nested Loops.

Some of you may already be nodding along, others may be scared by the use of nested loops. But regardless of your stance, let’s run through it once and we’ll see how it goes.

Side Notes

I created this tutorial based on the assumption that you’ve at least used Unity a little bit and are familiar with using Unity Editor, Prefabs, Sprites, and Scripts. And that you are also already working on an existing project (maybe your first game?).

If you’re just starting a project, don’t start here. There are lots of things you should be setting up and configuring/learning before doing randomized level generation.

If you haven’t already done a few Unity tutorials, there’s a lot of great tutorials at Unity that can help you kickstart things up. I highly suggest doing a few, even if they seem repetitive, so you can get familiar with Unity and with the different ways you can achieve the same things depending on your project.

I learned this while learning with Unity, so credit goes to them. If you’re interested in the assets and full code, check the link. Otherwise, I will run through the ideas so you can apply them to your own project.

Coding Portion

using Random = UnityEngine.Random;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public int columns = 8; // Number of Columns on our 2D board
public int rows = 8; // Number of Rows on our 2D board
public GameObject[] gameTiles; // Array of base Tile prefabs
public GameObject[] wallTiles; // Array of outer wall Tile prefabs
public GameObject[] enemyTiles; // Array of enemy prefabs
public GameObject[] pickupTiles;// Array of pick-up prefabs
public GameObject exit; // Object for our exit
// Use to child all our GameObjects to keep the hierarchy clean
private Transform boardHolder;
// List of valid locations to place tiles upon
private List<Vector3> gridPositions = new List<Vector3>();

First, we are going to declare all our variables. I added comments to explain everything briefly.

For boardHolder, it’s to keep all the tile game objects under it so we can collapse them and hide them all without filling up the hierarchy.

For gridPositions, it is used to track which positions on the board are valid (ex. we don’t want to fill edges) and to keep track of if the board position has been filled or not.

For all the different kinds of tiles (like wallTiles, pickupTiles, etc.), create as many as needed for the different kinds of objects you plan on end up putting into the scene. This can be mini walls, barricades, buffs, and so forth.

Optional public Min/Max variables

I do suggest adding some public number(s) (or a custom class) for the minimum and/or the maximum number of objects you want for a particular object. Especially if you want to change it on the fly in the inspector. For example, for pick-ups, you may want a minimum of 1 and a maximum of 3 per level, so add in something to keep track of publically.

Creating a 2D array for valid object locations

void InitialiseList() { 
// clear tiles from last generation
gridPositions.Clear();
// create our 2D array of valid tile locations
for (int x = 1; x < columns - 1; x++) {
for (int y = 1; y < rows - 1; y++) {
gridPositions.Add(new Vector3(x, y, 0f));
}
}
}

We use a nested for-loop to traverse the x-axis and y-axis. We clear and initialize the list to all the squares other than the outer two layers as we want to make sure there’s at least one path that the player can always traverse to the other corner, albeit the longest way around. And so we also have a layer for the outer blocker walls.

At the end of this function, we now have a 2D array of the list of all possible (valid) positions to place our game objects like enemies, coins, resources, etc.

We create an empty 2D array for everything inside the red box, which is all “valid” locations to place blockers, enemies, pick-ups, and so forth.

Next, we set up our board with base floor tiles and outer walls.

Creating the Base Floor

void BoardSetup() {
// Initialize our board
boardHolder = new GameObject("Board").transform;
for (int x = -1; x < columns + 1; x++) {
for (int y = -1; y < rows + 1; y++) {
// choose a random floor tile and prepare to instantiate it
GameObject o = gameTiles[Random.Range(0, gameTiles.Length)];
// If at edges, choose from outer wall tiles instead
if (x == -1 || x == columns || y == -1 || y == rows) {
o = wallTiles[Random.Range(0, wallTiles.Length)];
}
// Instantiate the chosen tile, at current grid position
GameObject instance = Instantiate(
o,
new Vector3(x, y, 0f),
Quaternion.identity) as GameObject
// Set parent of our new instance object to boardHolder
instance.transform.SetParent(boardHolder);
}
}
}

To summarize, at each loop as we traverse through the entire 2D array (including both outer layers this time!) we do the following:

We select a randomized floor tile (done by selecting a random number from 0 to the total number of tile prefabs we have in the array).

If we happen to be on the far outer edge (when either x or y is equal to -1 or the rows/columns variables which is 8 here), then we select a random tile from our outer wall array instead.

After we select the tile we want, we simply use Unity’s Instantiate function to create a gameObject for that tile at the current loop’s x / y coordinates into Unity.

This time, we traverse everything within the blue border, including inside the red, in order to place our “base” tileset with the ground and outer walls.

RandomPosition Helper Function

Vector3 RandomPosition(){ 
// random index between 0 and total count of items in gridPositions
int randomIndex = Random.Range(0, gridPositions.Count);
// random position selected from our gridPosition using randomIndex
Vector3 randomPosition = gridPositions[randomIndex]
// remove the entry from gridPosition so it can't be re-used
gridPositions.RemoveAt(randomIndex);
return randomPosition;
}

This is just a function to help us easily get a random position from our list of valid locations to use to place our game objects on our board.

Filling the Game Board with Objects

We won’t use this function until the last step, so don’t worry about where we will be getting our tile array, min, or max.

void LayoutObjectAtRandom(GameObject[] tileArray, int min, int max){
// random amount to instantiate from given range
int objectCount = Random.Range(min, max+1);
// Place objects at random locations until object count limit
for (int i = 0; i < objectCount; i++ {
// use our helper function to get random position
Vector3 randomPosition = RandomPosition();
// Choose a random tile (pick-up, enemy, etc.)
GameObject tileChoice =
tileArray[Random.Range(0, tileArray.Length)];
// Instantiate the chosen tile at the chosen location
Instantiate(tileChoice, randomPosition, Quaternion.identity);
}
}

First, we select a random number in the range between min/max given. We will loop through that exact amount of times. This means that the number selected here will be the total amount of objects created for the given tile array.

As we loop, we select random locations by using our helper function from before. Then we select random tiles using Unity’s random as we did with our base tiles.

At the end, we once again use Instantiate with our selected random location and random tile.

Setting Up the Scene

Now, let’s create our setup function, and use our function we just created above. This will be the only function called externally which will set up the entire map from scratch.

public void SetupScene(int level){
// Call our function to create our outer walls and floor.
BoardSetup();
// Re-create our list of valid gridPositions
InitialiseList();
// Fill a random number of pick-up tiles
LayoutObjectAtRandom(pickupTiles, 1, 3)
// Determine number of enemies based on current "level"
// Fill a random number of creatures
int enemies = (int)Mathf.Log(level, 2f);
LayoutObjectAtRandom(enemyTiles, enemies, enemies);
// Put in our exit in top right
Instantiate(
exit,
new Vector3 (columns -1, rows -1, 0f),
Quaternion.identity
);
}

If you created a public number (or custom class), in our first step, for the minimum/maximum of your object, use it here!

Feel free to tweak how many enemies you want depending on the “level” or “difficulty.” I simply added it here to show that you can do something special with a level.

The same thing can be done for our exit object, depending on where your character starts, feel free to place the exit at some other location.

Wrapping Up

To finish up, you should select the C# script for level generation in Unity Editor explorer and drag it into the hierarchy. You’ll need to drag in the prefabs for your objects from the explorer into the inspector under the correct public variables of your script.

Tip: After selecting the script, click the lock in the upper right so you have an easier time. Make sure to save your script prefab afterward!

The only thing left is to call the SetupScene function in some “GameManager” or some manager script to control when you want to create a new random map. This can be in many different forms depending on your project. For my project, I used a Singleton pattern for my game manager. Some tips I can give for now is making sure to use DontDestroyOnLoad and the standard GetComponent<BoardManager>(); It's relatively straightforward.

Final Thoughts

Now it’s been a long tutorial, but I wanted to ensure I had everything covered so you can take this as a template and apply it to your own project or as a general walkthrough for ideas on the level generation for your game. Hopefully, by going into more detail, I was able to help you understand the process better so you can more easily integrate this into your project.

I know this tutorial probably felt quite long-winded and draining, but I wanted to be detailed so that it benefits both beginners and intermediates at the same time.

I find adding extra details helps with answering questions people may have and helps them better understand what is going on even if it requires extra reading.

If you have any questions, feel free to ask! I hope I was able to help you better understand level generation in 2D Unity.

--

--

Brian Li
CodeX
Writer for

I graduated from the Sauder School of Business at the University of British Columbia with a Combined Major in Business and Computer Science (Honours).