
using System.Collections.Generic;
using UnityEngine;
using Random = System.Random;
public class ProceduralLevelGeneration101 : MonoBehaviour
{
[SerializeField][Min(4)] int _width = 12;
[SerializeField][Min(4)] int _height = 12;
[SerializeField][Min(0.01f)] float _cellSize = 1f;
[SerializeField] int _seed = 0;
Cell[,] GenerateLevel ( int seed )
{
var rnd = new Random( _seed );
var layout = new Cell[ _width , _height ];
HashSet<Vector2Int> occupied = new HashSet<Vector2Int>();
// plot start location:
Vector2Int start;
do start = RandomCoord( rnd , xmin:1 , xmax:_width-1 , ymin:1 , ymax:_height-1 );
while(
// roll the dice again if:
occupied.Contains(start)// index is occupied
);
layout[start.x,start.y] = Cell.Start;
occupied.Add( start);
// plot boss location:
Vector2Int boss;
do boss = RandomCoord( rnd , xmin:1 , xmax:_width-1 , ymin:1 , ymax:_height-1 );
while(
// roll the dice again if:
occupied.Contains(boss)// index is occupied
|| ( boss - start ).magnitude<Mathf.Max(_width,_height)/2-1// start and boss rooms are too close
);
layout[boss.x,boss.y] = Cell.Boss;
occupied.Add( boss );
// plot shop location:
Vector2Int shop;
do shop = RandomCoord( rnd , xmin:1 , xmax:_width-1 , ymin:1 , ymax:_height-1 );
while(
// roll the dice again if:
occupied.Contains(shop)// index is toccupied
);
layout[shop.x,shop.y] = Cell.Shop;
occupied.Add( shop );
// plot corridors:
FillSegment( a:start , b:boss , value:Cell.Corridor , grid:layout , occupied:occupied );
Vector2Int midway = start+(boss-start)/2;
FillSegment( a:midway , b:shop , value:Cell.Corridor , grid:layout , occupied:occupied );
// plot walls:
for( int y=0 ; y<_height ; y++ )
for( int x=0 ; x<_width ; x++ )
{
var coord = new Vector2Int{ x=x , y=y };
if( !occupied.Contains(coord) )
{
bool fill = false;
for( int dy=-1 ; dy<2 ; dy++ )
for( int dx=-1 ; dx<2 ; dx++ )
fill |= occupied.Contains( coord + new Vector2Int{ x=dx , y=dy } );
if( fill )
layout[coord.x,coord.y] = Cell.Wall;
}
}
return layout;
}
static Vector2Int RandomCoord ( Random rnd , int xmin , int xmax , int ymin , int ymax ) => new Vector2Int{ x=rnd.Next(xmin,xmax) , y=rnd.Next(ymin,ymax) };
// src: https://www.redblobgames.com/grids/line-drawing.html#orthogonal-steps
static void FillSegment <T> (
Vector2Int a , Vector2Int b ,
T value , T[,] grid ,
HashSet<Vector2Int> occupied
)
{
float dx = b.x-a.x, dy = b.y-a.y;
int nx = (int) Mathf.Abs(dx), ny = (int) Mathf.Abs(dy);
int sign_x = dx > 0? 1 : -1, sign_y = dy > 0? 1 : -1;
Vector2Int coord = a;
if( !occupied.Contains(coord) )
{
grid[coord.x,coord.y] = value;
occupied.Add(coord);
}
for( int ix=0, iy=0 ; ix<nx || iy<ny; )
{
if( (0.5+ix)/nx < (0.5+iy)/ny ) { coord.x += sign_x; ix++; }
else { coord.y += sign_y; iy++; }
if( !occupied.Contains(coord) )
{
grid[coord.x,coord.y] = value;
occupied.Add(coord);
}
}
}
#if UNITY_EDITOR
void OnDrawGizmos ()
{
var layout = GenerateLevel( _seed );
Vector3 origin = transform.position;
Vector3 cellSize = new Vector3{ x=_cellSize , y=_cellSize };
for( int y=0 ; y<_height ; y++ )
for( int x=0 ; x<_width ; x++ )
{
Vector2Int coord = new Vector2Int{ x=x , y=y };
Cell value = layout[ coord.x , coord.y ];
Vector3 localPos = new Vector3{ x=x*_cellSize , y=y*_cellSize , z=0 };
Vector3 worldCornerPos = origin + localPos;
Vector3 worldCenter = worldCornerPos + cellSize*0.5f;
string text = value!=Cell.Void ? value.ToString() : string.Empty;
var color = TextToColor( text );
Gizmos.color = new Color{ r=color.r , g=color.g , b=color.b , a=0.2f };
Gizmos.DrawCube( worldCenter , cellSize );
Gizmos.DrawWireCube( worldCenter , cellSize );
UnityEditor.Handles.Label( worldCenter , text );
}
}
Color TextToColor ( string text )
{
var md5 = System.Security.Cryptography.MD5.Create();
var bytes = md5.ComputeHash( System.Text.Encoding.UTF8.GetBytes(text) );
md5.Dispose();
var color = new Color32( bytes[0] , bytes[1] , bytes[2] , 255 );
return color;
}
#endif
public enum Cell
{
Void = 0 ,
Start = 1 ,
Boss = 2 ,
Shop = 3 ,
Corridor = 4 ,
Wall = 5
}
}
To instantiate level layout as GameObjects, add these few lines:
[SerializeField] GameObject[] _prefabs = new GameObject[6];
void Awake ()
{
var layout = GenerateLevel( _seed );
Vector3 origin = transform.position;
Vector3 cellSize = new Vector3{ x=_cellSize , y=_cellSize };
for( int y=0 ; y<_height ; y++ )
for( int x=0 ; x<_width ; x++ )
{
Vector2Int coord = new Vector2Int{ x=x , y=y };
Cell value = layout[ coord.x , coord.y ];
Vector3 localPos = new Vector3{ x=x*_cellSize , y=y*_cellSize , z=0 };
Vector3 worldCorner = origin + localPos;
Vector3 worldCenter = worldCorner + cellSize*0.5f;
int prefabIndex = (int) value;
if( prefabIndex>=_prefabs.Length ){ Debug.LogWarning($"List of prefabs has no {prefabIndex} index, instantiation at [{coord.x},{coord.y}] failed",gameObject); continue; }
GameObject prefab = _prefabs[ prefabIndex ];
if( prefab!=null )
{
var instance = GameObject.Instantiate( original:prefab , position:worldCenter , rotation:Quaternion.identity );
instance.transform.SetParent( this.transform );
}
}
}
Then fill this new list of Prefabs to match your Cell indices (as defined in that enum), and voilà!:
Sidenotes:
To make this more suited for 3d games, replace every occurrence of:
Vector3 cellSize = new Vector3{ x=_cellSize , y=_cellSize };
with:
Vector3 cellSize = new Vector3{ x=_cellSize , z=_cellSize };
and:
Vector3 localPos = new Vector3{ x=x*_cellSize , y=y*_cellSize , z=0 };
with:
Vector3 localPos = new Vector3{ x=x*_cellSize , y=0 , z=y*_cellSize };