補充!
放置物品和怪物的時候應該添加循環次數限制!,否則如果生成的地圖因爲各種偶然因素放不下物體一直循環的話就會導致死循環.
這段代碼在下面會看到.,
再次補充,發現300有時會導致直接放棄的情況,所以推薦改爲3000
正文
最終效果,寫了了兩天兩夜,之前寫了一個400多行的,但是思路不對白寫了..所以加起來總共寫了800行,我還是很少寫這麼大的程序的..比較菜.然後,思路參考了一個帖子,不過到一半以後就不一樣了.感興趣可以看看.
近看
unity面板
首先,在寫腳本之前,應該想好思路.如果我要建造這樣一個縱橫交錯的地圖應該怎麼做呢?
說說我的思路吧,可能還有其他的思路
1.先挖出一個房間來,房間大小隨機.
2.從房間的四個方向中選擇一到4個方向,往外挖一條路出來.然後每條路的末端再生成一個隨機大小的房間,然後又從新生成的這個房間再重複第一步,第二步,這樣循環下去,就挖出一個地圖來了,不過要記得添加限制,否則就是死循環,會導致unity死機.然而,如果沒有足夠大小的空間來生成房間的話,就收回挖出去的那條路.這一步中並不會在數組中產生牆,只有地板,牆會在後面直接生成.
3.按照以上步驟就能挖出來基本地形了,然後就再更具邊緣檢測來添加牆或者裝飾.
4.放置物品或敵人.
完成了
再從代碼方面來說說思路
1.建立一個map二維數組來存放邏輯位置.並全部置空,我們可以用字符來表示存儲的地圖元素的種類,比如
private char[,] map;
enum Mark {
air='0',
floor='1',
door='2',
hall='3',
chest='4',
mark='5',
wall='6'
}
void Start () {
//數組初始化
map = new char[DungeonSize, DungeonSize];
for(int i=0;i<DungeonSize;i++)
{
for(int j=0;j<DungeonSize;j++)
{
map[i, j] = '0';
}
}
//生成地圖和放置物品
GenerateMap();
}
2.在代碼中通過方法來對數組進行邏輯上的修改,比如先創建一個房間.這個方法是整個代碼的核心部分.先來看看他的參數
public void CreateRoom(int x,int y,int length,int width, List<int> usedDirection)
x,y表示座標(數組中),length,width代表長寬,最後一個列表表示這個房間用過的牆面,也就是挖出去過的牆面.在接下來的算法中就通過這個列表在決定從哪個牆面挖出去時來決定會跳過哪些牆面.這個列表的意義在於選擇牆面時不能選擇同一個牆面,而且在通道挖出去後並且生成了新房間後,新房間的和通道接觸的這個牆面也應該被設置爲使用過了,避免挖回來了.
然後是整個生成房間的代碼,他完成了核心邏輯,生成房間後尋找方向挖出幾條通道,在通過通道生成新房間,又通過新房間生成新通道,循壞下去就生成了整個地形..難點在於通道挖到末端時決定新房間的生成位置.
public void CreateRoom(int x,int y,int length,int width, List<int> usedDirection)
{
for(int i=x;i<x+length;i++)
{
for(int j=y;j<y+width;j++)
{
map[i, j] = '1';
}
}
//選擇牆面
int times = Random.Range(1, 4);
int direction = 0;
for (int i=0;i<times;i++ )
{
int hallDistance = Random.Range(3,15);//通道長度隨機
int ox = Random.Range(1, length-1);
int oy = Random.Range(1, width-1);
while (true)
{
direction = Random.Range(0, 4);
if (!usedDirection.Contains(direction))
{
usedDirection.Add(direction);
break;
}
}
Debug.Log("ox=" + ox + ",oy=" + oy);
bool hasHall = false;//是否生成了通道
switch (direction)
{
case 0://左
Debug.Log("左");
//生成通道,先判斷是否可生成
if (IsNullArea(x-hallDistance-1,y+oy-1,hallDistance,2))
{
Debug.Log("生成左方通道");
for (int zi = x; zi > x - hallDistance; zi--)
{
map[zi, y + oy] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength = Random.Range(5, 15);
int nWidth = Random.Range(5, 15);
Vector2 newPosition = new Vector2(x - hallDistance-nLength+1, y + oy - Random.Range(1, nWidth-2));
//生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection1 = new List<int> { 2 };
if (IsNullArea((int)newPosition.x, (int)newPosition.y, nLength, nWidth))//判斷是否可生成
{
Debug.Log("可在左側生成");
CreateRoom((int)newPosition.x, (int)newPosition.y, nLength, nWidth, usedDirection1);
}
else
{
Debug.Log("左側不可生成");
//收回伸出的通道
if (hasHall)
{
for (int zi = x - 1; zi > x - hallDistance; zi--)
{
Debug.Log("回收左方通道" + zi + "," + oy);
map[zi, y + oy] = (char)Mark.air;
}
}
return;
}
break;
case 1:
Debug.Log("上");
//生成通道,先判斷是否可生成
if (IsNullArea(x+ox-1,y+width,2,hallDistance))
{
Debug.Log("生成上方通道");
for (int si = y + width - 1; si < y + width - 1 + hallDistance; si++)
{
map[x + ox, si] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength1 = Random.Range(5, 15);
int nWidth1 = Random.Range(5, 15);
Vector2 newPosition1 = new Vector2(x +ox - Random.Range(1, nLength1 - 2), y +width+hallDistance-1 );
// 生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection2 = new List<int> { 3 };
if (IsNullArea((int)newPosition1.x, (int)newPosition1.y, nLength1, nWidth1))//判斷是否可生成
{
Debug.Log("可在上方生成");
CreateRoom((int)newPosition1.x, (int)newPosition1.y, nLength1, nWidth1, usedDirection2);
}
else
{
Debug.Log("不可在上方生成");
//收回伸出的通道
if (hasHall)
{
for (int si = y + width; si < y + width - 1 + hallDistance; si++)
{
Debug.Log("回收上方通道" + x + ox + "," + si);
map[x + ox, si] = (char)Mark.air;
}
}
return;
}
break;
case 2:
Debug.Log("右");
//生成通道,先判斷是否可生成
if (IsNullArea(x +length, y+oy-1, hallDistance, 2))
{
Debug.Log("生成右方通道");
for (int yi = x + length - 1; yi < x + length - 1 + hallDistance; yi++)
{
map[yi, y + oy] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength2 = Random.Range(5, 15);
int nWidth2 = Random.Range(5, 15);
Vector2 newPosition2 = new Vector2(x +length+hallDistance-1, y + oy - Random.Range(1, nWidth2 - 2));
//生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection3 = new List<int> { 0 };
if (IsNullArea((int)newPosition2.x, (int)newPosition2.y, nLength2, nWidth2))//判斷是否可生成
{
Debug.Log("可在右方生成");
CreateRoom((int)newPosition2.x, (int)newPosition2.y, nLength2, nWidth2, usedDirection3);
}
else
{
Debug.Log("不可在右方生成");
//收回伸出的通道
if (hasHall) {
for (int yi = x + length ; yi < x + length - 1 + hallDistance; yi++)
{
Debug.Log("回收右方通道" + yi + "," + y + oy);
map[yi, y + oy] = (char)Mark.air;
}
}
return;
}
break;
case 3:
Debug.Log("下");
//生成通道,先判斷是否可生成
if (IsNullArea(x+ox-1,y-1-hallDistance,2,hallDistance))
{
for (int xi = y; xi > y - hallDistance + 1; xi--)
{
Debug.Log("生成下方通道" );
map[x + ox, xi] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength3 = Random.Range(5, 15);
int nWidth3 = Random.Range(5, 15);
Vector2 newPosition3 = new Vector2(x + ox - Random.Range(1, nLength3 - 2), y -hallDistance-nWidth3+2);
//生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection4 = new List<int> { 1 };
if (IsNullArea((int)newPosition3.x, (int)newPosition3.y, nLength3, nWidth3))//判斷是否可生成
{
Debug.Log("可在下方生成");
CreateRoom((int)newPosition3.x, (int)newPosition3.y, nLength3, nWidth3, usedDirection4);
}
else
{
Debug.Log("不可在下方生成");
//收回伸出的通道
if (hasHall) {
for (int xi = y-1; xi > y - hallDistance + 1; xi--)
{
map[x + ox, xi] = (char)Mark.air;
}
}
return;
}
break;
}
}
}
3.生成新房間時先判斷是否有足夠的空間來生成這個新房間.這裏有很多坑,尤其是數組越界.多加註意
public bool IsNullArea(int x, int y, int length, int width)//判斷某塊區域是否爲空
{
if (x-length < 0 || y-width < 0)
return false;
if (x+length > DungeonSize || y+width > DungeonSize )
return false;
for (int i = x; i < x + length; i++)
{
for (int j = y; j < y + width; j++)
{
//Debug.Log(i + "," + j);
if (map[i,j]=='0')
{
continue;
}
else
{
return false;
}
}
}
return true;
}
4.生成基本地形後,添加邊緣的牆或者裝飾物吧
/// <summary>
/// 添加圍牆
/// </summary>
public void AddWall()
{
for(int i=1;i<DungeonSize-1;i++)
{
for(int j=1;j<DungeonSize-1;j++)
{
if (map[i,j] == (char)Mark.floor || map[i, j]==(char)Mark.hall)
{
for(int k = -1; k < 2; k++)
{
if(map[i+k,j]==(char)Mark.air)
{
map[i + k, j] = (char)Mark.wall;
}
if(map[i,j+k] == (char)Mark.air)
{
map[i, j + k] = (char)Mark.wall;
}
}
}
}
}
}
注意觀察i,j的注意範圍,若果起始不加一的話,末尾不減一的話就會數組越界!
5.然後是把數組轉換成實際的物體生成在世界中
public void DrawMap()//生成實際的地圖
{
for (int i = 0; i < DungeonSize; i++)
{
for (int j = 0; j < DungeonSize; j++)
{
if(map[i,j]==(char)Mark.floor|| map[i, j] == (char)Mark.hall)
{
Instantiate(FloorGo(), new Vector3(i, 0, j), Quaternion.identity);
}
if(map[i,j]== (char)Mark.mark)
{
MarkController._instance.ShowMarkOnPoint(new Vector3(i, 0, j));
}
if(map[i,j]==(char)(Mark.wall))
{
Instantiate(WallGo(), new Vector3(i, 0, j), Quaternion.identity);
}
}
}
}
可以看到生成的時候使用了FloorGO()這個方法,這個方法是幹什麼的呢?他可以更具概率來從數組中去除物體,就比如地板數組
public GameObject FloorGo()//隨機取出地板數組中的一個物體
{
while (true)
{
for (int i = 0; i < floorGo.Length; i++)
{
float p = Random.Range(0f, 1f);
if (p < probability[i])
{
return floorGo[i];
}
}
}
}
其中probability是他對應的概率數組.
6.然後終於可以開始生成寶箱之類的東西了
public GameObject PlaceGOByRadius(GameObject go, int radius)
{
int count = 0;
while(true)//循環判斷半徑範圍內是否全是地板,是的話放置物體然後return;否則一直循環
{
count++;
if(count>3000)
{
return null;
}
int x = Random.Range(radius+1, DungeonSize - radius-1);
int y = Random.Range(radius + 1, DungeonSize - radius - 1);
bool canPlace = true;
for(int i = x-radius; i < x + radius + 1; i++)
{
for(int j = y-radius; j < y + radius + 1; j++)
{
if (map[i, j] == (char)Mark.floor)
{
continue;
}
canPlace = false;
break;
}
}//除了判斷地圖數組裏的地板之外,還應判斷實際世界中是否會和已經放好的物體重疊
if (Physics.OverlapBox(new Vector3(x, 2, y), new Vector3(radius, 1f, radius), Quaternion.identity).Length!=0)
{
canPlace = false;
}
if (canPlace)
{
GameObject iGo =Instantiate(go, new Vector3(x, 1, y), Quaternion.identity, sceneParent.transform);
return iGo;
}
}
}
這裏寫了一個通過半徑來決定生成位置的方法,他會判斷周圍一定半徑是否都是地板來決定要不要生成,要注意的是,除了判斷地圖數組裏的地板之外,還應判斷實際世界中是否會和已經放好的物體重疊,效果挺好的,可以觀察效果圖中的寶箱位置
7.最後是整個的方法的執行過程
public void GenerateMap()//生成地圖的邏輯,包括地圖,箱子怪物之類
{
List<int> usedWall = new List<int>();//第一個房間四個面都未被使用過
CreateRoom(40,40,15,15,usedWall);
AddWall();
int chestCount = Random.Range(3, 8);
for(int i=0;i<chestCount;i++)
{
PlaceGOByRadius(Chest,2);
}
DrawMap();
}
總代碼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DungeonMapGenerator : MonoBehaviour {
public int DungeonSize = 100;
private char[,] map;
[Header("地板數組中生成各個物體的概率")]
public float[] probability;
public GameObject[] floorGo;
[Header("其餘物體")]
public GameObject[] wallGo;
public GameObject[] hall;
public GameObject Chest;
enum Mark {
air='0',
floor='1',
door='2',
hall='3',
chest='4',
mark='5',
wall='6'
}
// Use this for initialization
void Start () {
//數組初始化
map = new char[DungeonSize, DungeonSize];
for(int i=0;i<DungeonSize;i++)
{
for(int j=0;j<DungeonSize;j++)
{
map[i, j] = '0';
}
}
//生成地圖和放置物品
GenerateMap();
}
public GameObject FloorGo()//隨機取出地板數組中的一個物體
{
while (true)
{
for (int i = 0; i < floorGo.Length; i++)
{
float p = Random.Range(0f, 1f);
if (p < probability[i])
{
return floorGo[i];
}
}
}
}
public GameObject WallGo()//隨機去除牆數組中的一個物體
{
int g = Random.Range(0, wallGo.Length);
return wallGo[g];
}
public void GenerateMap()//生成地圖的邏輯,包括地圖,箱子怪物之類
{
List<int> usedWall = new List<int>();//第一個房間四個面都未被使用過
CreateRoom(40,40,15,15,usedWall);
AddWall();
int chestCount = Random.Range(3, 8);
for(int i=0;i<chestCount;i++)
{
PlaceGOByRadius(Chest,2);
}
DrawMap();
}
public void CreateRoom(int x,int y,int length,int width, List<int> usedDirection)
{
for(int i=x;i<x+length;i++)
{
for(int j=y;j<y+width;j++)
{
map[i, j] = '1';
}
}
//選擇牆面
int times = Random.Range(1, 4);
int direction = 0;
for (int i=0;i<times;i++ )
{
int hallDistance = Random.Range(3,15);//通道長度隨機
int ox = Random.Range(1, length-1);
int oy = Random.Range(1, width-1);
while (true)
{
direction = Random.Range(0, 4);
if (!usedDirection.Contains(direction))
{
usedDirection.Add(direction);
break;
}
}
Debug.Log("ox=" + ox + ",oy=" + oy);
bool hasHall = false;//是否生成了通道
switch (direction)
{
case 0://左
Debug.Log("左");
//生成通道,先判斷是否可生成
if (IsNullArea(x-hallDistance-1,y+oy-1,hallDistance,2))
{
Debug.Log("生成左方通道");
for (int zi = x; zi > x - hallDistance; zi--)
{
map[zi, y + oy] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength = Random.Range(5, 15);
int nWidth = Random.Range(5, 15);
Vector2 newPosition = new Vector2(x - hallDistance-nLength+1, y + oy - Random.Range(1, nWidth-2));
//生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection1 = new List<int> { 2 };
if (IsNullArea((int)newPosition.x, (int)newPosition.y, nLength, nWidth))//判斷是否可生成
{
Debug.Log("可在左側生成");
CreateRoom((int)newPosition.x, (int)newPosition.y, nLength, nWidth, usedDirection1);
}
else
{
Debug.Log("左側不可生成");
//收回伸出的通道
if (hasHall)
{
for (int zi = x - 1; zi > x - hallDistance; zi--)
{
Debug.Log("回收左方通道" + zi + "," + oy);
map[zi, y + oy] = (char)Mark.air;
}
}
return;
}
break;
case 1:
Debug.Log("上");
//生成通道,先判斷是否可生成
if (IsNullArea(x+ox-1,y+width,2,hallDistance))
{
Debug.Log("生成上方通道");
for (int si = y + width - 1; si < y + width - 1 + hallDistance; si++)
{
map[x + ox, si] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength1 = Random.Range(5, 15);
int nWidth1 = Random.Range(5, 15);
Vector2 newPosition1 = new Vector2(x +ox - Random.Range(1, nLength1 - 2), y +width+hallDistance-1 );
// 生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection2 = new List<int> { 3 };
if (IsNullArea((int)newPosition1.x, (int)newPosition1.y, nLength1, nWidth1))//判斷是否可生成
{
Debug.Log("可在上方生成");
CreateRoom((int)newPosition1.x, (int)newPosition1.y, nLength1, nWidth1, usedDirection2);
}
else
{
Debug.Log("不可在上方生成");
//收回伸出的通道
if (hasHall)
{
for (int si = y + width; si < y + width - 1 + hallDistance; si++)
{
Debug.Log("回收上方通道" + x + ox + "," + si);
map[x + ox, si] = (char)Mark.air;
}
}
return;
}
break;
case 2:
Debug.Log("右");
//生成通道,先判斷是否可生成
if (IsNullArea(x +length, y+oy-1, hallDistance, 2))
{
Debug.Log("生成右方通道");
for (int yi = x + length - 1; yi < x + length - 1 + hallDistance; yi++)
{
map[yi, y + oy] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength2 = Random.Range(5, 15);
int nWidth2 = Random.Range(5, 15);
Vector2 newPosition2 = new Vector2(x +length+hallDistance-1, y + oy - Random.Range(1, nWidth2 - 2));
//生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection3 = new List<int> { 0 };
if (IsNullArea((int)newPosition2.x, (int)newPosition2.y, nLength2, nWidth2))//判斷是否可生成
{
Debug.Log("可在右方生成");
CreateRoom((int)newPosition2.x, (int)newPosition2.y, nLength2, nWidth2, usedDirection3);
}
else
{
Debug.Log("不可在右方生成");
//收回伸出的通道
if (hasHall) {
for (int yi = x + length ; yi < x + length - 1 + hallDistance; yi++)
{
Debug.Log("回收右方通道" + yi + "," + y + oy);
map[yi, y + oy] = (char)Mark.air;
}
}
return;
}
break;
case 3:
Debug.Log("下");
//生成通道,先判斷是否可生成
if (IsNullArea(x+ox-1,y-1-hallDistance,2,hallDistance))
{
for (int xi = y; xi > y - hallDistance + 1; xi--)
{
Debug.Log("生成下方通道" );
map[x + ox, xi] = (char)Mark.hall;
}
hasHall = true;
}
else
{
continue;
}
//生成下一個房間
int nLength3 = Random.Range(5, 15);
int nWidth3 = Random.Range(5, 15);
Vector2 newPosition3 = new Vector2(x + ox - Random.Range(1, nLength3 - 2), y -hallDistance-nWidth3+2);
//生成房間後將新房間的和通道的連接面設置爲已使用,
List<int> usedDirection4 = new List<int> { 1 };
if (IsNullArea((int)newPosition3.x, (int)newPosition3.y, nLength3, nWidth3))//判斷是否可生成
{
Debug.Log("可在下方生成");
CreateRoom((int)newPosition3.x, (int)newPosition3.y, nLength3, nWidth3, usedDirection4);
}
else
{
Debug.Log("不可在下方生成");
//收回伸出的通道
if (hasHall) {
for (int xi = y-1; xi > y - hallDistance + 1; xi--)
{
map[x + ox, xi] = (char)Mark.air;
}
}
return;
}
break;
}
}
}
public void DrawMap()//生成實際的地圖
{
for (int i = 0; i < DungeonSize; i++)
{
for (int j = 0; j < DungeonSize; j++)
{
if(map[i,j]==(char)Mark.floor|| map[i, j] == (char)Mark.hall)
{
Instantiate(FloorGo(), new Vector3(i, 0, j), Quaternion.identity);
}
if(map[i,j]== (char)Mark.mark)
{
MarkController._instance.ShowMarkOnPoint(new Vector3(i, 0, j));
}
if(map[i,j]==(char)(Mark.wall))
{
Instantiate(WallGo(), new Vector3(i, 0, j), Quaternion.identity);
}
}
}
}
/// <summary>
/// 添加圍牆
/// </summary>
public void AddWall()
{
for(int i=1;i<DungeonSize-1;i++)
{
for(int j=1;j<DungeonSize-1;j++)
{
if (map[i,j] == (char)Mark.floor || map[i, j]==(char)Mark.hall)
{
for(int k = -1; k < 2; k++)
{
if(map[i+k,j]==(char)Mark.air)
{
map[i + k, j] = (char)Mark.wall;
}
if(map[i,j+k] == (char)Mark.air)
{
map[i, j + k] = (char)Mark.wall;
}
}
}
}
}
}
public bool IsNullArea(int x, int y, int length, int width)//判斷某塊區域是否爲空
{
if (x-length < 0 || y-width < 0)
return false;
if (x+length > DungeonSize || y+width > DungeonSize )
return false;
for (int i = x; i < x + length; i++)
{
for (int j = y; j < y + width; j++)
{
//Debug.Log(i + "," + j);
if (map[i,j]=='0')
{
continue;
}
else
{
return false;
}
}
}
return true;
}
//通過半徑放置物品
public GameObject PlaceGOByRadius(GameObject go, int radius)
{
int count = 0;
while(true)//循環判斷半徑範圍內是否全是地板,是的話放置物體然後return;否則一直循環
{
count++;
if(count>3000)
{
return null;
}
int x = Random.Range(radius+1, DungeonSize - radius-1);
int y = Random.Range(radius + 1, DungeonSize - radius - 1);
bool canPlace = true;
for(int i = x-radius; i < x + radius + 1; i++)
{
for(int j = y-radius; j < y + radius + 1; j++)
{
if (map[i, j] == (char)Mark.floor)
{
continue;
}
canPlace = false;
break;
}
}//除了判斷地圖數組裏的地板之外,還應判斷實際世界中是否會和已經放好的物體重疊
if (Physics.OverlapBox(new Vector3(x, 2, y), new Vector3(radius, 1f, radius), Quaternion.identity).Length!=0)
{
canPlace = false;
}
if (canPlace)
{
GameObject iGo =Instantiate(go, new Vector3(x, 1, y), Quaternion.identity, sceneParent.transform);
return iGo;
}
}
}
}