补充!
放置物品和怪物的时候应该添加循环次数限制!,否则如果生成的地图因为各种偶然因素放不下物体一直循环的话就会导致死循环.
这段代码在下面会看到.,
再次补充,发现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;
}
}
}
}