How would I get this procedural generation to work?

So first I want to create an array of all our rooms for that floor. The array would have prebuilt rooms. I would want to make sure there is always one shop and one boss room and one start room. Then I would want to spawn them and make it so the boss room is on the opposite side of the start room. Then I would connect all the rooms with a path.

How would I do all of this?

preview

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 };

Getting a good looking procedural dungeon is pretty complicated but to get you started;

  1. Think about how a pathfinding algorithm works. Usually, pathfinding algorithms go by some distance metric. There are also usually many different paths that are equivalent. Why not add some randomness to which direction is picked? So if you are at a location (0, 0) and you have candidates (1, 0), (0, 1), or (1, 1) you can compute the distance and add some random value. Ie; Distance((a, b) + Random.value. You can look into random walks for more info.
  2. The above will generate a single winding path. Getting branches is as simple as rerunning the same algorithm multiple times.
  3. As long as removing a room does not prevent any paths from start to boss room you are safe to remove it. You can control how dense the dungeon is by defining rules like “rooms connected to 4 other rooms have a X% chance of being deleted”. The lower the value of x the more dense your dungeon will be. You can define odds for each level of connectivity. Making sure to backtrack if you remove a room that prevents a path from start to the boss room. Actually with this step it isn’t technically neccisary to do the second step.
    4.You want the start room to be the first room and the boss room to be the furthest. You can reuse the two points you picked in point 1 or you could recompute by picking the two rooms which are the furthest away.

There are plenty of other ways to do this. You can go beyond the above by looking into L-Systems (rules for placing room types) and Topological sorting (defining and ordering in which rooms can be encountered).