作者:Timothy Hely

当用对象随机填充某个区域如地下城中的房间时,你可能会遇到的问题是太过随机,导致分布疏密不均或混乱。在本教程中,我将告诉大家如何使用二进制空间划分法(游戏邦注:即Binary Space Partitioning,简称为BSP,这种方法每次将一实体用任一位置和任一方向的平面分为二部分。)来解决这个问题。

我将分成几个步骤教你如何使用BSP来制作一个简单的2D地图,这个方法可以用于布局游戏中的地下城。我将教你如何制作一个基本的Leaf对象,我们将用它把区域划分成几个小分区;如何在各个Leaf中生成随机房间;如何用走廊把各个房间接通。

注:虽然这里使用的代码是AS3写的,但你应该可以把它转换成其他语言。

样本项目

我已经制作了一个能够证明BSP的强大的样本程序。这个样本是用免费的开源AS3库Flixel写的。

当你点击Generate按钮时,它就会运行相同的代码生成一些Leaf,然后把它们绘制到BitmapData对象,并显示出来(按比例以填满屏幕)。

Binary_Space_Partitioning_for_Maps_Gamedev_Screen-Demo(from tutsplus)

生成随机地图

当你点击Play按钮,它就会把生成的地图Bitmap传给FlxTilemap对象,后者再生成一个可玩的瓷砖地图,并把它显示在屏幕上:

Binary_Space_Partitioning_for_Maps_Gamedev_Screen-Demo(from tutsplus)

显示地图

使用方向键移动。

BSP是什么?

BSP是一种将区域分成更小的分区的方法。

基本做法就是,你把一个叫作Leaf的区域水平或竖直地分成两个更小的Leaf,然后在这两个Leaf上重复这个步骤,直到得到所需的房间数量。

完成上述步骤后,你就得到一个分区的Leaf,你可以在它上面布局对象。在3D图像中,你可以使用BSP分类哪些对象对玩家可见,或用于更小的空间中的碰撞检测。

为什么使用BSP生成地图?

如果你想生成随机地图,你可以使用的办法有很多种。你可以写一个简单的逻辑在随机地点生成随机大小的矩形,但这可能导致生成的地图出现大量重叠、集群或奇怪的房间。此外,增加了沟通房间的难度,且难以保证没有遗漏的房间未连上。

而使用BSP,可以保证房间布局平均,且所有房间都联系在一起。

生成Leaf

第一步是生成Leaf类。Leaf基本上是矩形的,具有一些额外的功能。各个Leaf都包含一对子Leaf或一对Room及一两个走廊。

我们的Leaf如下所示:

public class Leaf
{

private const MIN_LEAF_SIZE:uint = 6;

public var y:int, x:int, width:int, height:int; // the position and size of this Leaf

public var leftChild:Leaf; // the Leaf’s left child Leaf
public var rightChild:Leaf; // the Leaf’s right child Leaf
public var room:Rectangle; // the room that is inside this Leaf
public var halls:Vector.; // hallways to connect this Leaf to other Leafs

public function Leaf(X:int, Y:int, Width:int, Height:int)
{
// initialize our leaf
x = X;
y = Y;
width = Width;
height = Height;
}

public function split():Boolean
{
// begin splitting the leaf into two children
if (leftChild != null || rightChild != null)
return false; // we’re already split! Abort!

// determine direction of split
// if the width is >25% larger than height, we split vertically
// if the height is >25% larger than the width, we split horizontally
// otherwise we split randomly
var splitH:Boolean = FlxG.random() > 0.5;
if (width > height && height / width >= 0.05)
splitH = false;
else if (height > width && width / height >= 0.05)
splitH = true;

var max:int = (splitH ? height : width) – MIN_LEAF_SIZE; // determine the maximum height or width
if (max <= MIN_LEAF_SIZE)
return false; // the area is too small to split any more…

var split:int = Registry.randomNumber(MIN_LEAF_SIZE, max); // determine where we’re going to split

// create our left and right children based on the direction of the split
if (splitH)
{
leftChild = new Leaf(x, y, width, split);
rightChild = new Leaf(x, y + split, width, height – split);
}
else
{
leftChild = new Leaf(x, y, split, height);
rightChild = new Leaf(x + split, y, width – split, height);
}
return true; // split successful!
}
}

现在才是真正生成Leaf:

const MAX_LEAF_SIZE:uint = 20;

var _leafs:Vector<Leaf> = new Vector<Leaf>;

var l:Leaf; // helper Leaf

// first, create a Leaf to be the ‘root’ of all Leafs.
var root:Leaf = new Leaf(0, 0, _sprMap.width, _sprMap.height);
_leafs.push(root);

var did_split:Boolean = true;
// we loop through every Leaf in our Vector over and over again, until no more Leafs can be split.
while (did_split)
{
did_split = false;
for each (l in _leafs)
{
if (l.leftChild == null && l.rightChild == null) // if this Leaf is not already split…
{
// if this Leaf is too big, or 75% chance…
if (l.width > MAX_LEAF_SIZE || l.height > MAX_LEAF_SIZE || FlxG.random() > 0.25)
{
if (l.split()) // split the Leaf!
{
// if we did split, push the child leafs to the Vector so we can loop into them next
_leafs.push(l.leftChild);
_leafs.push(l.rightChild);
did_split = true;
}
}
}
}
}

这个循环结束后,你的所有Leaf中都会包含一个Vector(一种集合)。

以下是分区的Leaf的案例:

Binary_Space_Partitioning_for_Maps_Gamedev_Screen(from tutsplus)

用Leaf分区的案例

生成房间

你的Leaf做好后,我们就可以制作房间了。我们想要一种“涓流效果”,也就是从最大的“根”Leaf开始,一直划分到没有子项的最小的Leaf,然后在每个Leaf中做出房间。

把以下功能添加到Leaf类中:

public function createRooms():void
{
// this function generates all the rooms and hallways for this Leaf and all of its children.
if (leftChild != null || rightChild != null)
{
// this leaf has been split, so go into the children leafs
if (leftChild != null)
{
leftChild.createRooms();
}
if (rightChild != null)
{
rightChild.createRooms();
}
}
else
{
// this Leaf is the ready to make a room
var roomSize:Point;
var roomPos:Point;
// the room can be between 3 x 3 tiles to the size of the leaf – 2.
roomSize = new Point(Registry.randomNumber(3, width – 2), Registry.randomNumber(3, height – 2));
// place the room within the Leaf, but don’t put it right
// against the side of the Leaf (that would merge rooms together)
roomPos = new Point(Registry.randomNumber(1, width – roomSize.x – 1), Registry.randomNumber(1, height – roomSize.y – 1));
room = new Rectangle(x + roomPos.x, y + roomPos.y, roomSize.x, roomSize.y);
}
}

然后,制作好Leaf的Vector后,从你的根Leaf中调用新功能:

_leafs = new Vector<Leaf>;

var l:Leaf; // helper Leaf

// first, create a Leaf to be the ‘root’ of all Leafs.
var root:Leaf = new Leaf(0, 0, _sprMap.width, _sprMap.height);
_leafs.push(root);

var did_split:Boolean = true;
// we loop through every Leaf in our Vector over and over again, until no more Leafs can be split.
while (did_split)
{
did_split = false;
for each (l in _leafs)
{
if (l.leftChild == null && l.rightChild == null) // if this Leaf is not already split…
{
// if this Leaf is too big, or 75% chance…
if (l.width > MAX_LEAF_SIZE || l.height > MAX_LEAF_SIZE || FlxG.random() > 0.25)
{
if (l.split()) // split the Leaf!
{
// if we did split, push the child Leafs to the Vector so we can loop into them next
_leafs.push(l.leftChild);
_leafs.push(l.rightChild);
did_split = true;
}
}
}
}
}

// next, iterate through each Leaf and create a room in each one.
root.createRooms();

以下是还有房间的Leaf的案例:

Binary_Space_Partitioning_for_Maps_Gamedev_Screen(from tutsplus)

如你所见,每个Leaf都包含一个房间,大小和位置是随机的。你可以调整Leaf的大小和位置,以得到不同的布局。

如果我们移除Leaf的分隔线,你可以看到房间充满整个地图—-浪费了很多空间,并且显得太过条理。

Binary_Space_Partitioning_for_Maps_Gamedev_Screen(from tutsplus)

带房间的Leaf,移除了分隔线。

沟通Leaf

现在,我们需要做的是沟通各个房间。幸好各个Leaf之间存在内部关系,我们只需要保证各个Leaf都能够与其子leaf相互连接。

我们把各个子Leaf内的房间连接起来。我们在生成房间时可以同时做沟通的工作。

首先,我们需要一个从所有Leaf开始迭代到各个子Leaf中的房间的新功能:

public function getRoom():Rectangle
{
// iterate all the way through these leafs to find a room, if one exists.
if (room != null)
return room;
else
{
var lRoom:Rectangle;
var rRoom:Rectangle;
if (leftChild != null)
{
lRoom = leftChild.getRoom();
}
if (rightChild != null)
{
rRoom = rightChild.getRoom();
}
if (lRoom == null && rRoom == null)
return null;
else if (rRoom == null)
return lRoom;
else if (lRoom == null)
return rRoom;
else if (FlxG.random() > .5)
return lRoom;
else
return rRoom;
}
}

然后,我们需要一个功能,它将选取一对房间并在二者内选中随机点,然后生成一两个两片瓷砖大小的矩形把点连接起来。

public function createHall(l:Rectangle, r:Rectangle):void
{
// now we connect these two rooms together with hallways.
// this looks pretty complicated, but it’s just trying to figure out which point is where and then either draw a straight line, or a pair of lines to make a right-angle to connect them.
// you could do some extra logic to make your halls more bendy, or do some more advanced things if you wanted.

halls = new Vector<Rectangle>;

var point1:Point = new Point(Registry.randomNumber(l.left + 1, l.right – 2), Registry.randomNumber(l.top + 1, l.bottom – 2));
var point2:Point = new Point(Registry.randomNumber(r.left + 1, r.right – 2), Registry.randomNumber(r.top + 1, r.bottom – 2));

var w:Number = point2.x – point1.x;
var h:Number = point2.y – point1.y;

if (w < 0)
{
if (h < 0)
{
if (FlxG.random() * 0.5)
{
halls.push(new Rectangle(point2.x, point1.y, Math.abs(w), 1));
halls.push(new Rectangle(point2.x, point2.y, 1, Math.abs(h)));
}
else
{
halls.push(new Rectangle(point2.x, point2.y, Math.abs(w), 1));
halls.push(new Rectangle(point1.x, point2.y, 1, Math.abs(h)));
}
}
else if (h > 0)
{
if (FlxG.random() * 0.5)
{
halls.push(new Rectangle(point2.x, point1.y, Math.abs(w), 1));
halls.push(new Rectangle(point2.x, point1.y, 1, Math.abs(h)));
}
else
{
halls.push(new Rectangle(point2.x, point2.y, Math.abs(w), 1));
halls.push(new Rectangle(point1.x, point1.y, 1, Math.abs(h)));
}
}
else // if (h == 0)
{
halls.push(new Rectangle(point2.x, point2.y, Math.abs(w), 1));
}
}
else if (w > 0)
{
if (h < 0)
{
if (FlxG.random() * 0.5)
{
halls.push(new Rectangle(point1.x, point2.y, Math.abs(w), 1));
halls.push(new Rectangle(point1.x, point2.y, 1, Math.abs(h)));
}
else
{
halls.push(new Rectangle(point1.x, point1.y, Math.abs(w), 1));
halls.push(new Rectangle(point2.x, point2.y, 1, Math.abs(h)));
}
}
else if (h > 0)
{
if (FlxG.random() * 0.5)
{
halls.push(new Rectangle(point1.x, point1.y, Math.abs(w), 1));
halls.push(new Rectangle(point2.x, point1.y, 1, Math.abs(h)));
}
else
{
halls.push(new Rectangle(point1.x, point2.y, Math.abs(w), 1));
halls.push(new Rectangle(point1.x, point1.y, 1, Math.abs(h)));
}
}
else // if (h == 0)
{
halls.push(new Rectangle(point1.x, point1.y, Math.abs(w), 1));
}
}
else // if (w == 0)
{
if (h < 0)
{
halls.push(new Rectangle(point2.x, point2.y, 1, Math.abs(h)));
}
else if (h > 0)
{
halls.push(new Rectangle(point1.x, point1.y, 1, Math.abs(h)));
}
}
}

最后,改变createRooms()功能,以调用所有具有一对子Leaf的Leaf的createHall()功能:

public function createRooms():void
{
// this function generates all the rooms and hallways for this Leaf and all of its children.
if (leftChild != null || rightChild != null)
{
// this leaf has been split, so go into the children leafs
if (leftChild != null)
{
leftChild.createRooms();
}
if (rightChild != null)
{
rightChild.createRooms();
}

// if there are both left and right children in this Leaf, create a hallway between them
if (leftChild != null && rightChild != null)
{
createHall(leftChild.getRoom(), rightChild.getRoom());
}

}
else
{
// this Leaf is the ready to make a room
var roomSize:Point;
var roomPos:Point;
// the room can be between 3 x 3 tiles to the size of the leaf – 2.
roomSize = new Point(Registry.randomNumber(3, width – 2), Registry.randomNumber(3, height – 2));
// place the room within the Leaf, but don’t put it right against the side of the leaf (that would merge rooms together)
roomPos = new Point(Registry.randomNumber(1, width – roomSize.x – 1), Registry.randomNumber(1, height – roomSize.y – 1));
room = new Rectangle(x + roomPos.x, y + roomPos.y, roomSize.x, roomSize.y);
}
}

现在你的房间和走廊应该如下图所示:

Binary_Space_Partitioning_for_Maps_Gamedev_Screen(from tutsplus)

房间被走廊沟通的Leaf案例

正如你所见,所有Leaf都是相互沟通的,不留任何一个孤立的房间。显然,走廊逻辑可以更精确一点,避免太接近其他走廊,但现在这样已经够好了。

总结

以上!我介绍了如何生成(比较)简单的Leaf对象,你可以用它生成分区Leaf和生成各个Leaf内的随机房间,最后用走廊沟通所有房间。

目前我们制作的所有对象都是矩形的,但根据你将如何使用地下城,你可以对它们进行其他处理。

现在你可以使用BSP制作任何一种你需要的随机地图,或使用它平均分布区域内的增益道具或敌人。

在游戏邦看到一篇不错的贴子,转一下。备用。

[转] 如何用BSP树生成游戏地图的更多相关文章

  1. 空间划分的数据结构(网格/四叉树/八叉树/BSP树/k-d树/BVH/自定义划分)

    目录 网格 (Grid) 网格的应用 四叉树/八叉树 (Quadtree/Octree) 四叉树/八叉树的应用 BSP树 (Binary Space Partitioning Tree) 判断点在平面 ...

  2. BZOJ4006 JLOI2015 管道连接(斯坦纳树生成森林)

    4006: [JLOI2015]管道连接 Time Limit: 30 Sec Memory Limit: 128 MB Description 小铭铭最近进入了某情报部门,该部门正在被如何建立安全的 ...

  3. 玩转Web之easyui(二)-----easy ui 异步加载生成树节点(Tree),点击树生成tab(选项卡)

    关于easy ui 异步加载生成树及点击树生成选项卡,这里直接给出代码,重点部分代码中均有注释 前台: $('#tree').tree({ url: '../servlet/School_Tree?i ...

  4. PHP树生成迷宫及A*自己主动寻路算法

    PHP树生成迷宫及A*自己主动寻路算法 迷宫算法是採用树的深度遍历原理.这样生成的迷宫相当的细,并且死胡同数量相对较少! 随意两点之间都存在唯一的一条通路. 至于A*寻路算法是最大众化的一全自己主动寻 ...

  5. .NET技术-6.0. Expression 表达式树 生成 Lambda

    .NET技术-6.0. Expression 表达式树 生成 Lambda public static event Func<Student, bool> myevent; public ...

  6. 如何用JavaDoc命令生成帮助文档

    如何用JavaDoc命令生成帮助文档 文档注释 在代码中使用文档注释的方法 /** *@author *@version * */ 生成帮助文档 打开java文件所在位置,在路径前加入cmd (注意有 ...

  7. 不到 200 行代码,教你如何用 Keras 搭建生成对抗网络(GAN)【转】

    本文转载自:https://www.leiphone.com/news/201703/Y5vnDSV9uIJIQzQm.html 生成对抗网络(Generative Adversarial Netwo ...

  8. H5如何用Canvas画布生成并保存带图片文字的新年快乐的海报

    摘要:初略算了算大概有20天没有写博客了,原本是打算1月1号元旦那天写一个年终总结的,博客园里大佬们都在总结过去,迎接将来,看得我热血沸腾,想想自己也工作快2年了,去年都没有去总结一下,今年势必要总结 ...

  9. 自己动手写ORM(01):解析表达式树生成Sql碎片

     在EF中,我们查询数据时可能会用拉姆达表达式 Where(Func<T,ture> func)这个方法来筛选数据,例如,我们定义一个User实体类 public class User { ...

随机推荐

  1. PHP汉子转拼音

    <?php /** +------------------------------------------------------ * PHP 汉字转拼音 +------------------ ...

  2. 使用git从本地上传至git码云远程仓库

    从 http://git-scm.com/download  下载window版的客户端.下载好,一步一步安装即可. 使用前的基本设置 git  config --global user.name & ...

  3. 树莓派从 DHT11 温度湿度传感器读取数据

    时序图参考厂家说明书:DHT11数字湿温度传感器的原理和应用范例 四个阵脚连接:VCC接3.3伏电源,Dout接GPIO口,我接的是物理12针脚,NC留空,GND接地. 波折1:电阻被错接进了VCC, ...

  4. 【FAQ系列】Relay log 导致复制启动失败

    今天在使用冷备份文件重做从库时遇到一个报错,值得研究一下. 版本:MySQL5.6.27 一.报错现象 dba:(none)> start slave; ERROR (HY000): Slave ...

  5. PS小研

    1 ps输入字体不显示原因有很多,解决方法也各不相同,我总结了以下几条原因及相应的解决方法 原因一: 字体颜色和背景色相同或者过于相近,字体虽然存在,但是却看不到字体. 解决方法: 这个问题比较简单, ...

  6. Python的装饰器实例用法小结

    这篇文章主要介绍了Python装饰器用法,结合实例形式总结分析了Python常用装饰器的概念.功能.使用方法及相关注意事项 一.装饰器是什么 python的装饰器本质上是一个Python函数,它可以让 ...

  7. mysql错误总结-ERROR 1067 (42000): Invalid default value for TIMESTAMP

    1. ERROR 1067 (42000): Invalid default value for 'FAILD_TIME'   (对TIMESTAMP  类型的子段如果不设置缺省值或没有标志not n ...

  8. excel比较筛选两列不一样的数据

    在excel表中,罗列两列数据,用B列数据与A列比较,筛选出B列中哪些数据不同,并用红色标记出来.     首先选中B列.直接鼠标左键点击B列即可选中."开始"--->&qu ...

  9. linux命令:rm 命令

    昨天学习了创建文件和目录的命令mkdir ,今天学习一下linux中删除文件和目录的命令: rm命令.rm是常用的命令,该命令的功能为删除一个目录中的一个或多个文件或目录,它也可以将某个目录及其下的所 ...

  10. 平衡二叉树--java

    package com.test.tree; /** * 带有平衡条件的二叉查找树 * */ public class AVLBinarySearchTree<T extends Compara ...