当前位置:首页 >教程首页 > 游戏设计 > 3D模型大师班 >程序关卡生成教程系列(下)

程序关卡生成教程系列(下)

发布时间:2018-11-17 19:57:24
  注:这是作为iOS 7 Feast组成部分的全新Sprite Kit教程。本篇文章是该系列教程的第二部分也是最后一部分,即教授你如何使用Drunkard Walk算法去执行程序生成关卡。

  在上篇教程中,你已经创造了基本的关卡生成,并学会如何使用Sprite Kit的嵌入式物理引擎去设置碰撞检测,从而让玩家不能穿越墙壁。(查看上篇教程

  而在今天的下篇教程中,你将延伸算法在更开放的空间中生成更多地牢式关卡,顾及多条路径的同时创造并包含属性让自己更好地控制关卡生成过程。

  FloorMaker类

  你也许注意到了第一部分的关卡生成就像一条漫长且蜿蜒的走廊。很明显这不是一个有趣的关卡设计,让寻找出口变得不再具有挑战性。

  会得出这样的结果并不让人惊讶。毕竟你所执行的算法是基于任意方向移动一个砖块,并不断重复,从而连接到之前放置好的砖块上。尽管这有可能生成宽广的空间领域,但是我们却不能频繁地使用这一方法去创造地牢式的地图。

  现在你将修改算法从而让它能够同时随机游走。基本上,它将会将所有喝醉的人丢出酒吧并命令他们回家。

  地图生成需要追踪同一时间被创造出来的不同路径。你将使用一个名为FloorMaker的类面向每个路径执行这一点。

  来到File\New\New File…,选择iOS\Cocoa Touch\Objective-C class模版并点击Next。将类命名为FloorMaker,将其设置为NSObject的子类并点击Next。确保选中ProceduralLevelGeneration,然后点击Create。

  打开FloorMaker.h并在@interface和@end间添加如下代码:

@property (nonatomic) CGPoint currentPosition;
@property (nonatomic) NSUInteger direction;

- (instancetype) initWithCurrentPosition:(CGPoint)currentPosition andDirection:(NSUInteger)direction;

现在打开FloorMaker.m并执行初始化器方法:

- (instancetype) initWithCurrentPosition:(CGPoint)currentPosition andDirection:(NSUInteger)direction
{
if (( self = [super init] ))
{
self.currentPosition = currentPosition;
self.direction = direction;
}
return self;
}

  FloorMarker非常简单。它带有2个属性去追踪当前的位置和方向,初始化器允许你在创造类的实体时设置这些属性。

  当FloorMarker类得到有效设置时,你可以继续在地图生成中使用它。

  运行FloorMaker

  第一步便是将FloorMaker输入Map.m中。在现有的#import预处理机指令后添加如下代码:

#import “FloorMaker.h”

  你将重构generateTileGrid去同时使用多个FloorMaker对象,但你将在不同阶段中执行它。首先做出如下修改,从而让它可以使用一个单一FloorMaker。

  在generateTileGrid中将:CGPoint currentPosition = startPoint;

  换成:FloorMaker* floorMaker = [[FloorMaker alloc] initWithCurrentPosition:startPoint andDirection:0];

  你不再需要在局部变量中储存当前位置,因为每个FloorMaker将储存它自己的当前位置。所以你可以删除currentPosition,并添加名为floorMaker的变量,在startPoint进行初始化。

  既然你已经删除了currentPosition,你便可以用floorMaker.currentPosition取代每个currentPosition的使用。不要担心,Xcode将提供误差帮助你找到它们。

  接下来将下一行:NSInteger direction = [self randomNumberBetweenMin:1 andMax:4];

  换成:floorMaker.direction = [self randomNumberBetweenMin:1 andMax:4];

  正如你将局部变量currentPosition换成floorMaker.currentPosition一样,在此你也是基于同样的原因将局部变量direction换成了floorMaker.direction。

  创建并运行,应用应该会像之前那样运行。

same_tiles1

  现在你将改变Map去支持使用多个FloorMaker。在Map.m添加如下属性到Map类扩展中:

@property (nonatomic) NSMutableArray *floorMakers;

floorMakers数组持有所有积极FloorMaker的参照内容。

  然后回到generateTileGrid中做出如下改变去使用floorMaker数组而不是FloorMaker对象。

  将如下行:

NSUInteger currentFloorCount = 1;
FloorMaker* floorMaker = [[FloorMaker alloc] initWithCurrentPosition:startPoint andDirection:0];

  换成:

__block NSUInteger currentFloorCount = 1;
self.floorMakers = [NSMutableArray array];
[self.floorMakers addObject:[[FloorMaker alloc] initWithCurrentPosition:startPoint andDirection:0]];

  你添加__block类型说明到currentFloorCount声明,如此你便可以在Objective-C组块中修改它的值,你将快速完成这点。你需要移除局部floorMaker变量并基于可变数组(包含一个FloorMaker对象)去初始化Map的floorMakers。之后你将添加更多FloorMaker到这个数组中。

  在generateTileGrid中修改while循环的内容:

while ( currentFloorCount < self.maxFloorCount ) {
[self.floorMakers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
FloorMaker *floorMaker = (FloorMaker *)obj;

{
//...
// original contents of the while loop HERE
//...
}
}];
}

  这改变了方法所以它能在floorMaker数组中的对象上进行迭代,并面向每个对象执行Drunkard Walk。最终你将拥有一个以上的FloorMaker运行,但是你只能拥有一个出口点。为了确保游戏创造的最后一个楼层成为出口,将设置_exitPoint的行从当前位置移动到currentFloorCount++; 之后。这是在游戏创造了所有楼层后对_exitPoint的分配,最后的砖块创造变成了出口点。

  再次创建并运行,我们可以注意到形式似乎并未发生改变。

same_tiles

  顾及多条路径

  尽管地图生成可行,它却仍只能运行一个FloorMaker实体,所以关卡仍与你在第一部分教程中所看到的内容非常相似。因为FloorMaker理念是拥有许多实体,所以你现在需要改变generateTileGrid方法而允许更多FloorMaker的生成。

  回到generateTileGrid,在组块最后,即闭括号和括号前添加如下代码:

if ( [self randomNumberBetweenMin:0 andMax:100] <= 50 )
{
FloorMaker *newFloorMaker = [[FloorMaker alloc] initWithCurrentPosition:floorMaker.currentPosition andDirection:[self randomNumberBetweenMin:1 andMax:4]];

[self.floorMakers addObject:newFloorMaker];
}

  这一代码为能在FloorMaker的每一步创造一个新的FloorMaker提供了50%的机会。我们可以注意到代码创造了一个newFloorMaker,带有等同于当前FloorMaker的当前位置的位置,但却是基于随机方向。

  再次创建并运行。是否注意到一些奇怪的地方?

rooms_too_big

  这里存在两个问题。首先,现在的算法生成了更宽广的空间,而不只是一条漫长的走廊。你将在之后做出一些改变去影响它创造的地图类型,所以你可以暂且忽视这一问题。

  第二个问题很容易被忽视,但是如果你生成了一些地图并计算楼层,你便会发现自己的应用不再涉及maxFloorCount值。它将在maxFloorCout和maxFloorCount+floorMakers数量-1间创造一些楼层数。这是因为如果你在generateTileGrid中的while循环于floorMaker迭代前创造了足够多的墙,它便会进行检查。

  举个例子来说吧,如果你拥有砖块的当前值为62,最大值为64以及10个FloorMaker,你将通过检查进入while循环,但之后当你进行floorMaker迭代时便会创造10个以上的额外楼层。

  为了解决这点,在generateTileGrid(游戏邦注:即验证newPosition先添加一个楼层类型)中找到如下if检查:

if([self.tiles isValidTileCoordinateAt:newPosition] &&
![self.tiles isEdgeTileAt:newPosition] &&
[self.tiles tileTypeAt:newPosition] == MapTileTypeNone)

  添加一个额外的检查去验证currentFloorCount,如下:

if([self.tiles isValidTileCoordinateAt:newPosition] &&
![self.tiles isEdgeTileAt:newPosition] &&
[self.tiles tileTypeAt:newPosition] == MapTileTypeNone &&
currentFloorCount < self.maxFloorCount)

  现在当你运行时,你将获得准确的maxFloorCount.楼层。去计算看看吧!

  当你靠近一个真正的程序生成关卡时,你会发现仍存在一些缺陷。你唯一能控制的只是关卡的大小。这真的足够吗?如果你能决定关卡带有更开放的空间或者带有更长且狭窄的走廊不是更好?亲爱的,这都是关于属性啊!

  调整地图生成

  为了让地图类更多面,你将添加一些属性去影响关卡生成。

  打开Map.h并添加如下属性:

@property (nonatomic) NSUInteger turnResistance;
@property (nonatomic) NSUInteger floorMakerSpawnProbability;
@property (nonatomic) NSUInteger maxFloorMakerCount;

  这三个属性将通过控制FloorMaker的表现而直接影响地图生成:

  turnResistance决定FloorMaker转弯的难度。将其设置为100会生成一条长而直的路径,而0则会生成迂回曲折的路径。

  floorMakerSpawnProbability既能控制创造另一个FloorMaker的概率,也能生成砖块网格。设为100的值将确保游戏在每次迭代创造一个FloorMaker,而0则会导致没有额外的FloorMaker会超越最初的那个。

  maxFloorMaker是FloorMaker的最大数值,会在某一时刻被激活。

  为了使用这些属性,你需要在代码中执行它们。首先确保将属性初始化为一个默认值。打开Map.m并添加如下代码到initWithGridSize:(在self.gridSize=gridSize之后):

self.maxFloorCount = 110;
self.turnResistance = 20;
self.floorMakerSpawnProbability = 25;
self.maxFloorMakerCount = 5;

  到Map.m的generateTileGrid中找到如下行:

floorMaker.direction = [self randomNumberBetweenMin:1 andMax:4];

  将其变成如下if声明:

if ( floorMaker.direction == 0 || [self randomNumberBetweenMin:0 andMax:100] <= self.turnResistance ){
floorMaker.direction = [self randomNumberBetweenMin:1 andMax:4];
}

  这一小小的改变能够保证游戏只会改变FloorMaker的方向,而前提是FloorMaker没有固定的方向或当turnResistance概率被超越时。就像之前所解释的,turnResistence的值更高,FloorMaker改变方向的概率也就更高。

  接下来将下面一行内容:

if ( [self randomNumberBetweenMin:0 andMax:100] <= 50 )

  改为:

if ( [self randomNumberBetweenMin:0 andMax:100] <= self.floorMakerSpawnProbability &&
self.floorMakers.count < self.maxFloorMakerCount )

  现在,比起只有50%创造一个全新FloorMaker的机会,你可以调整概率去创造一个全新FloorMaker(如果现有FloorMaker的数值少于最大值)—-就像maxFloorMakerCount所定义的那样。

thinner_rooms

  打开MyScene.m并在self.map.maxFloorCount = 64后添加如下内容:

self.map.maxFloorMakerCount = 3;
self.map.floorMakerSpawnProbability = 20;
self.map.turnResistance = 30;

  创建并运行

ProceduralLevels

  在继续之前,尝试着为这些属性和maxFloorCount设置不同值,以熟悉它们是如何影响关卡生成。

  当你完成这一步,改变MyScene.m中的值,如下:

self.map.maxFloorCount = 110;
self.map.turnResistance = 20;
self.map.floorMakerSpawnProbability = 25;
self.map.maxFloorMakerCount = 5;

  调整空间大小

  到目前为止,FloorMaker每次只能放置一个楼层,但是这里所使用的方法能像第一步那样轻松地放置一些楼层,从而在生成地图上留出更多开放领域。

  为了保持关卡生成的灵活性,你需要在Map.h文件上添加一些属性:

@property (nonatomic) NSUInteger roomProbability;
@property (nonatomic) CGSize roomMinSize;
@property (nonatomic) CGSize roomMaxSize;

  然后在Map.m中的initWithGridSize:将这些属性初始化为默认值,即在设置maxFloorMakerCount行之后:

self.roomProbability = 20;
self.roomMinSize = CGSizeMake(2, 2);
self.roomMaxSize = CGSizeMake(6, 6);

  基于默认值,20%的游戏时间将生成(2,2)砖块和(6,6)砖块大小的空间。

  仍然在Map.m中插入如下方法:

- (NSUInteger) generateRoomAt:(CGPoint)position withSize:(CGSize)size
{
NSUInteger numberOfFloorsGenerated = 0;
for ( NSUInteger y = 0; y < size.height; y++)
{
for ( NSUInteger x = 0; x < size.width; x++ )
{
CGPoint tilePosition = CGPointMake(position.x + x, position.y + y);

if ( [self.tiles tileTypeAt:tilePosition] == MapTileTypeInvalid )
{
continue;
}

if ( ![self.tiles isEdgeTileAt:tilePosition] )
{
if ( [self.tiles tileTypeAt:tilePosition] == MapTileTypeNone )
{
[self.tiles setTileType:MapTileTypeFloor at:tilePosition];

numberOfFloorsGenerated++;
}
}
}
}
return numberOfFloorsGenerated;
}

  在带有通过大小的通过位置上,该方法在左上角添加了一个空间,并传回一个代表被创造出来的砖块的数值。如果空间与任何现有的楼层重叠,那么重叠数便不会包含于方法所传回的数值中。

  为了开始生成空间,来到Map.m中的generateTileGrid,在currentFloorCount++后插入如下内容:

if ( [self randomNumberBetweenMin:0 andMax:100] <= self.roomProbability )
{
NSUInteger roomSizeX = [self randomNumberBetweenMin:self.roomMinSize.width
andMax:self.roomMaxSize.width];
NSUInteger roomSizeY = [self randomNumberBetweenMin:self.roomMinSize.height
andMax:self.roomMaxSize.height];

currentFloorCount += [self generateRoomAt:floorMaker.currentPosition
withSize:CGSizeMake(roomSizeX, roomSizeY)];
}

  这在FloorMaker的当前位置上生成了一个新的空间,并且空间大小是介于最小和最大数值间,即只要满足创造一个空间(而非砖块)的概率便可。

  为了测试这些属性,前往MyScene.m并通过在initWithSize的[self generate]前面插入如下代码而设置Map类的属性:

self.map.roomProbability = 20;
self.map.roomMinSize = CGSizeMake(2, 2);
self.map.roomMaxSize = CGSizeMake(6, 6);

  创建并运行各种不同的值去明确它们是如何影响地图生成。

map_with_rooms

  这时候,我们可能创造更多maxFloorCount楼层,因为空间创造逻辑并不能通过内部检查去确保所添加的砖块不会超越极限。

  你已经创造了一个多面地图生成类,即每次当你发送生成信息到Map类的实体时便会出现一个新的关卡。通过改变这一属性,如果你想要一个大的开放空间或狭窄走廊,你便可以对此进行直接控制。

  接下来该做什么?

  本文是这系列教程中所有代码的最后部分。

  Drunkard Walk算法真的是一种强大又简单的程序生成方法,即你可以通过轻松的扩展去生成自己想要创造的各种地牢变量。但这只是众多程序生成算法中的一种,而其它的所有算法也都有自己的优势和劣势。
学员作品赏析
  • 2101期学员李思庭作品

    2101期学员李思庭作品

  • 2104期学员林雪茹作品

    2104期学员林雪茹作品

  • 2107期学员赵凌作品

    2107期学员赵凌作品

  • 2107期学员赵燃作品

    2107期学员赵燃作品

  • 2106期学员徐正浩作品

    2106期学员徐正浩作品

  • 2106期学员弓莉作品

    2106期学员弓莉作品

  • 2105期学员白羽新作品

    2105期学员白羽新作品

  • 2107期学员王佳蕊作品

    2107期学员王佳蕊作品

专业问题咨询

你担心的问题,火星帮你解答
×

确定