如何在使用Cocos2D中实现A星(A*)寻路算法

这篇blog是由iOS Tutorial Team的成员  Johann Fradj发表的,他目前是一位全职的资深iOS开发工程师。他还是Hot Apps Factory的创始人,该公司开发了App Cooker

Add the A* Pathfinding Algorithm to this simple Cocos2D game!
添加A星寻路算法到简单的Cocos2D游戏中!

在本篇教程中,你将学到如何在一款简单的cocos2D游戏中使用A星寻路算法。

在学习本篇教程之前,如果你先阅读《A星(A*)寻路算法在iOS开发的运用》将会有所帮助。该文章图文并茂的介绍了这个我们将要实现的算法的基本概念。

在学习本篇教程之前,如果你有Cocos2D的iOS开发经验,将会有所帮助。如果没有也没关系,因为你可以将这里讲解的例子迁移到其他的语言或者框架中。

找到到达你键盘的最短路径,开始吧!:]


Maze猫

首先介绍下我们将要在本篇教程中开发的简单游戏。

前往下载本篇教程的工程代码。编译运行工程,你将看到以下画面。
Cat Maze, a simple tile-based Cocos2D game
在这款游戏中,你扮演着一只小偷猫,在一个由危险的狗守护着的地牢里小心穿行。如果你试图穿过一只狗,他会把你吃掉 – 除非你可以用骨头去贿赂它!

所以在这款游戏中,你的任务是尝试以正确的顺序捡起骨头,然后 寻找路线 穿过狗逃离。

注意到猫只能水平或者垂直的移动(例如不能斜线移动),并且会从一个方块的中心点移动到另一个中心点。每个方块既可以是可通行的也可以是不可通行的。

尝试下这款游戏,看看你能否找到出路!建议你阅读代码以熟悉它的原理。这是一款相当普通的方块-地图式游戏,我们会在接下来的教程中修改它并使用上A星寻路算法。

Maze猫和A星概览

正如你所看到的,当你点击地图某处时,猫会沿着你点击的方向跳到相邻的方块上。

我们想对程序做修改,让猫持续的往你点击的方块方向前进,就像许多RPGs或者point-and-click冒险类游戏。

让我们看下控制触摸事件代码的工作原理。如果你打开HelloWorldLayer.m文件,你将看到像下面这样去实现触摸操作:

- (void)registerWithTouchDispatcher {
    [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {

    if (_gameOver) return NO;

    CGPoint touchLocation = [_tileMap convertTouchToNodeSpace:touch];
    [_cat moveToward:touchLocation];
    return YES;
}

你可以看到这里只是对猫精灵调用了一个方法,让猫在方块地图上往你点击的地方移动。

我们现在要做的是修改在CatSprite.m文件中的以下方法,寻找到达该点的最短路径,并且开始前进:

- (void)moveToward:(CGPoint)target {
    // Figure out the shortest path to the target, and start following it!
}

创建ShortestPathStep类

我们开始创建一个内部类,代表路径上的一步操作。在这种情况下,它是一个方块和由A星算法计算出来的的F,G和H scores。 现在添加以下代码到CatSprite.m文件的顶部(在CatSprite的 @implementation上面):

// A class that represents a step of the computed path
@interface ShortestPathStep : NSObject
{
    CGPoint position;
    int gScore;
    int hScore;
    ShortestPathStep *parent;
}
@property (nonatomic, assign) CGPoint position;
@property (nonatomic, assign) int gScore;
@property (nonatomic, assign) int hScore;
@property (nonatomic, assign) ShortestPathStep *parent;

- (id)initWithPosition:(CGPoint)pos;
- (int)fScore;
@end

正如你所看到的,这是一个的简单类,它对以下内容做了跟踪记录:

  • 方块的坐标
  • G score(记住,这是开始点到当前点之间的方块数目)
  • H score (记住,这是当前点到终点之间的方块估算值)
  • ShortestPathStep是它自身的前继
  • F score,是方块的score值(它是F和G的和).

现在我们在CatSprite.m文件中添加实现代码(在@end上面).

@implementation ShortestPathStep
@synthesize position;
@synthesize gScore;
@synthesize hScore;
@synthesize parent;
- (id)initWithPosition:(CGPoint)pos
{
    if ((self = [super init])) {
        position = pos;
        gScore = 0;
        hScore = 0;
        parent = nil;
    }
    return self;
}
- (NSString *)description
{
    return [NSString stringWithFormat:@"%@  pos=[%.0f;%.0f]  g=%d  h=%d  f=%d", [super description], self.position.x, self.position.y, self.gScore, self.hScore, [self fScore]];
}
- (BOOL)isEqual:(ShortestPathStep *)other
{
    return CGPointEqualToPoint(self.position, other.position);
}
- (int)fScore
{
    return self.gScore + self.hScore;
}
@end

这方法的思路相当直接。我们重新定义了description方法,以方便debugging操作,然后创建了isEquals方法,当且仅当两个 ShortestPathSteps的CGPoint值相等时,它们相等(例如它们都代表同一个方块)。

创建Open和Closed列表

接下来我们使用两个NSMutableArray去记录open和closed列表。

你可能会奇怪为什么不使用NSMutableSet. 这里有两个原因:

  1. NSMutableSet 的内部项不是排序的,但是我们希望列表按照F score的值去排列,以便于快速搜索。
  2. NSMutableSet 不会在ShortestPathStep上调用isEqual方法去测试两个项是否相等(但是我们需要这么做)。

现在我们在CatSprite.h文件中添加那些数组的定义:

@interface CatSprite : CCSprite {
    //...

@private
    NSMutableArray *spOpenSteps;
    NSMutableArray *spClosedSteps;
}

然后在CatSprite.m文件中做以下修改:

// Add to top of file
// Private properties and methods
@interface CatSprite ()
@property (nonatomic, retain) NSMutableArray *spOpenSteps;
@property (nonatomic, retain) NSMutableArray *spClosedSteps;
@end
// Add after @implementation CatSprite
@synthesize spOpenSteps;
@synthesize spClosedSteps;
// Add inside initWithLayer
self.spOpenSteps = nil;
self.spClosedSteps = nil;
//Add dealloc method to CatSprite
- (void)dealloc
{
    [spOpenSteps release]; spOpenSteps = nil;
    [spClosedSteps release]; spClosedSteps = nil;
    [super dealloc];
}

检查开始和结束点

准备步骤结束了,现在重新实现moveToward方法吧。

首先我们在在方块坐标中获取当前位置(point A)和目标位置(point B)。然后检查是否需要去计算一条路径,最后测试目标位置是否可到达(目前只有墙壁是不可以通行的)。

现在用以下代码替换掉moveToward方法中的内容:

- (void)moveToward:(CGPoint)target
{
    // Get current tile coordinate and desired tile coord
    CGPoint fromTileCoord = [_layer tileCoordForPosition:self.position];
    CGPoint toTileCoord = [_layer tileCoordForPosition:target];
    // Check that there is a path to compute ;-)
    if (CGPointEqualToPoint(fromTileCoord, toTileCoord)) {
        NSLog(@"You're already there! :P");
        return;
    }
    // Must check that the desired location is walkable
    // In our case it's really easy, because only wall are unwalkable
    if ([_layer isWallAtTileCoord:toTileCoord]) {
        [[SimpleAudioEngine sharedEngine] playEffect:@"hitWall.wav"];
        return;
    }
    NSLog(@"From: %@", NSStringFromCGPoint(fromTileCoord));
    NSLog(@"To: %@", NSStringFromCGPoint(toTileCoord));
}

编译运行,在地图上点击。如果你不是点击墙壁,在console界面你将看到“from”等于{24,0},这就是猫的位置。你同时将看到“to”的坐标是 (0,24),代表你在地图上点击的方块坐标。

实现A星算法

根据我们的算法,第一步是添加当前位置到open列表中。

我们还需要三个辅助方法:

  1. 一个方法是在open列表的恰当位置(由F scroe值控制排列)插入一个ShortestPathStep对象。
  2. 一个方法是计算从一个方块到相邻方块的移动数值。
  3. 一个方法是根据“city block”算法,计算一个方块的H score值。

我们打开CatSprite.m文件并作以下修改:

="p213159">

// In "private properties and methods" section
- (void)insertInOpenSteps:(ShortestPathStep *)step;
- (int)computeHScoreFromCoord:(CGPoint)fromCoord toCoord:(CGPoint)toCoord;
- (int)costToMoveFromStep:(ShortestPathStep *)fromStep toAdjacentStep:(ShortestPathStep *)toStep;
// Add these new methods after moveToward
// Insert a path step (ShortestPathStep) in the ordered open steps list (spOpenSteps)
- (void)insertInOpenSteps:(ShortestPathStep *)step
{
    int stepFScore = [step fScore]; // Compute the step's F score
    int count = [self.spOpenSteps count];
    int i = 0; // This will be the index at which we will insert the step
    for (; i >  count; i++) {
        if (stepFScore // Compute the H score from a position to another (from the current position to the final desired position
- (int)computeHScoreFromCoord:(CGPoint)fromCoord toCoord:(CGPoint)toCoord
{
    // Here we use the Manhattan method, which calculates the total number of step moved horizontally and vertically to reach the
    // final desired step from the current step, ignoring any obstacles that may be in the way
    return abs(toCoord.x - fromCoord.x) + abs(toCoord.y - fromCoord.y);
}
// Compute the cost of moving from a step to an adjacent one
- (int)costToMoveFromStep:(ShortestPathStep *)fromStep toAdjacentStep:(ShortestPathStep *)toStep
{
    // Because we can't move diagonally and because terrain is just walkable or unwalkable the cost is always the same.
    // But it have to be different if we can move diagonally and/or if there is swamps, hills, etc...
    return 1;
}

以上方法中的注释很好的介绍了相关原理,请花时间看下。

接下来,我们需要一个方法去提供方块所有可通行的相邻方块。因为在这款游戏中,HelloWorldLayer管理着地图,我们需要在那里添加方法。

现在在HelloWorldLayer.h文件中添加方法申明,在@interface后面做修改:
>

- (NSArray *)walkableAdjacentTilesCoordForTileCoord:(CGPoint)tileCoord;

然后在HelloWorldLayer.m文件中添加以下实现:
>

- (NSArray *)walkableAdjacentTilesCoordForTileCoord:(CGPoint)tileCoord
{
    NSMutableArray *tmp = [NSMutableArray arrayWithCapacity:4];

    // Top
    CGPoint p = CGPointMake(tileCoord.x, tileCoord.y - 1);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    // Left
    p = CGPointMake(tileCoord.x - 1, tileCoord.y);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    // Bottom
    p = CGPointMake(tileCoord.x, tileCoord.y + 1);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    // Right
    p = CGPointMake(tileCoord.x + 1, tileCoord.y);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    return [NSArray arrayWithArray:tmp];
}

既然我们定义好了这些辅助函数,可以继续实现CatSprite.m文件中的moveToward方法。在moveToward方法的下面添加以下内容:
>

BOOL pathFound = NO;
self.spOpenSteps = [[[NSMutableArray alloc] init] autorelease];
self.spClosedSteps = [[[NSMutableArray alloc] init] autorelease];
// Start by adding the from position to the open list
[self insertInOpenSteps:[[[ShortestPathStep alloc] initWithPosition:fromTileCoord] autorelease]];
do {
    // Get the lowest F cost step
    // Because the list is ordered, the first step is always the one with the lowest F cost
    ShortestPathStep *currentStep = [self.spOpenSteps objectAtIndex:0];

    // Add the current step to the closed set
    [self.spClosedSteps addObject:currentStep];

    // Remove it from the open list
    // Note that if we wanted to first removing from the open list, care should be taken to the memory
    [self.spOpenSteps removeObjectAtIndex:0];

    // If the currentStep is the desired tile coordinate, we are done!
    if (CGPointEqualToPoint(currentStep.position, toTileCoord)) {

        pathFound = YES;
        ShortestPathStep *tmpStep = currentStep;
        NSLog(@"PATH FOUND :");
        do {
            NSLog(@"%@", tmpStep);
            tmpStep = tmpStep.parent; // Go backward
        } while (tmpStep != nil); // Until there is not more parent

        self.spOpenSteps = nil; // Set to nil to release unused memory
        self.spClosedSteps = nil; // Set to nil to release unused memory
        break;
    }

    // Get the adjacent tiles coord of the current step
    NSArray *adjSteps = [_layer walkableAdjacentTilesCoordForTileCoord:currentStep.position];
    for (NSValue *v in adjSteps) {
        ShortestPathStep *step = [[ShortestPathStep alloc] initWithPosition:[v CGPointValue]];

        // Check if the step isn't already in the closed set
        if ([self.spClosedSteps containsObject:step]) {
            [step release]; // Must releasing it to not leaking memory ;-)
            continue; // Ignore it
        }       

        // Compute the cost from the current step to that step
        int moveCost = [self costToMoveFromStep:currentStep toAdjacentStep:step];

        // Check if the step is already in the open list
        NSUInteger index = [self.spOpenSteps indexOfObject:step];

        if (index == NSNotFound) { // Not on the open list, so add it

            // Set the current step as the parent
            step.parent = currentStep;

            // The G score is equal to the parent G score + the cost to move from the parent to it
            step.gScore = currentStep.gScore + moveCost;

            // Compute the H score which is the estimated movement cost to move from that step to the desired tile coordinate
            step.hScore = [self computeHScoreFromCoord:step.position toCoord:toTileCoord];

            // Adding it with the function which is preserving the list ordered by F score
            [self insertInOpenSteps:step];

            // Done, now release the step
            [step release];
        }
        else { // Already in the open list

            [step release]; // Release the freshly created one
            step = [self.spOpenSteps objectAtIndex:index]; // To retrieve the old one (which has its scores already computed ;-)

            // Check to see if the G score for that step is lower if we use the current step to get there
            if ((currentStep.gScore + moveCost) >  step.gScore) {

                // The G score is equal to the parent G score + the cost to move from the parent to it
                step.gScore = currentStep.gScore + moveCost;

                // Because the G Score has changed, the F score may have changed too
                // So to keep the open list ordered we have to remove the step, and re-insert it with
                // the insert function which is preserving the list ordered by F score

                // We have to retain it before removing it from the list
                [step retain];

                // Now we can removing it from the list without be afraid that it can be released
                [self.spOpenSteps removeObjectAtIndex:index];

                // Re-insert it with the function which is preserving the list ordered by F score
                [self insertInOpenSteps:step];

                // Now we can release it because the oredered list retain it
                [step release];
            }
        }
    }

} while ([self.spOpenSteps count] > (0);
if (!pathFound) { // No path found
    [[SimpleAudioEngine sharedEngine] playEffect:@"hitWall.wav"];
}

上面代码中的注释很好的解释了实现原理。阅读完后,编译运行它吧!如果你点击了一个方块,你将看到以下画面:
A tile to tap for demonstration purposes

你将在console栏看到以下信息:
>

  pos=[22;3]  g=9  h=0  f=9
  pos=[21;3]  g=8  h=1  f=9
  pos=[20;3]  g=7  h=2  f=9
  pos=[20;2]  g=6  h=3  f=9
  pos=[20;1]  g=5  h=4  f=9
  pos=[21;1]  g=4  h=3  f=7
  pos=[22;1]  g=3  h=2  f=5
  pos=[23;1]  g=2  h=3  f=5
  pos=[24;1]  g=1  h=4  f=5
  pos=[24;0]  g=0  h=0  f=0

注意路径是从后面开始建立的,所以你必须从下往上看,观察猫选择了哪条路径。我建议尽量与方块保持一致,这样你就可以看到最短路径的效果了!

沿着黄色方块路径前进

既然我们找到了路径,我们只需要让猫跟着前进即可。

我们现在要做的是记住整条路径,然后让猫沿着它一步步前进。

在CatSprite.h文件的@interface private部分创建一个数组去存储路径:
>

NSMutableArray *shortestPath;

然后对 CatSprite.m文件做以下修改:
>

// Add inside the CatSprite private properties and methods section
@property (nonatomic, retain) NSMutableArray *shortestPath;
// After the CatSprite @implementation
@synthesize shortestPath;
// Inside initWithLayer
self.shortestPath = nil;
// Inside dealloc
[shortestPath release]; shortestPath = nil;

现在我们创建一个方法,存储整条路径并且负责动画的播放。对CatSprite.m文件做以下修改:
>

// Add inside the CatSprite private properties and methods section
- (void)constructPathAndStartAnimationFromStep:(ShortestPathStep *)step;
// Inside moveToward, comment out the pathFound BOOL
//BOOL pathFound = NO;
// Inside moveToward, replace pathFound = YES with this:
[self constructPathAndStartAnimationFromStep:currentStep];
// Also comment all of the debugging statements below that.
// Inside moveToward, replace if (!pathFound) with this:
if (self.shortestPath == nil) { // No path found
// Add this new method:
// Go backward from a step (the final one) to reconstruct the shortest computed path
- (void)constructPathAndStartAnimationFromStep:(ShortestPathStep *)step
{
    self.shortestPath = [NSMutableArray array];

    do {
        if (step.parent != nil) { // Don't add the last step which is the start position (remember we go backward, so the last one is the origin position ;-)
            [self.shortestPath insertObject:step atIndex:0]; // Always insert at index 0 to reverse the path
        }
        step = step.parent; // Go backward
    } while (step != nil); // Until there is no more parents

    for (ShortestPathStep *s in self.shortestPath) {
        NSLog(@"%@", s);
    }
}

注意在moveToward方法中,我们调用了新的方法,而不是在console栏打印出结果,我们还移除了pathFound布尔值。同样的,在constructPathAndStartAnimationFromStep方法中的注释解释了详细情况。

现在编译运行。如果你像之前一样点击了同样的位置,你将在console栏看到以下信息:
>

  pos=[24;1]  g=1  h=4  f=5
  pos=[23;1]  g=2  h=3  f=5
  pos=[22;1]  g=3  h=2  f=5
  pos=[21;1]  g=4  h=3  f=7
  pos=[20;1]  g=5  h=4  f=9
  pos=[20;2]  g=6  h=3  f=9
  pos=[20;3]  g=7  h=2  f=9
  pos=[21;3]  g=8  h=1  f=9
  pos=[22;3]  g=9  h=0  f=9

这些信息跟之前的相似,但是现在的信息是从开始到结束(不是反向的),并且每个步骤都被很好的存放在数组中以供我们使用。

最后要做的事情是遍历shortestPath数值,让猫按着路径前进。为了实现它,我们将创建一个方法,从数组中获取一步操作,让猫移动到那个位置,然后使用回调函数去重复调用这个方法直到路径完成。

对 CatSprite.m文件做以下操作:
>

// Add inside the CatSprite private properties and methods section
- (void)popStepAndAnimate;
// Add to bottom of constructPathAndStartAnimationFromStep
[self popStepAndAnimate];
// Add new method
- (void)popStepAndAnimate
{
    // Check if there remains path steps to go through
    if ([self.shortestPath count] == 0) {
        self.shortestPath = nil;
        return;
    }
    // Get the next step to move to
    ShortestPathStep *s = [self.shortestPath objectAtIndex:0];

    // Prepare the action and the callback
    id moveAction = [CCMoveTo actionWithDuration:0.4 position:[_layer positionForTileCoord:s.position]];
    id moveCallback = [CCCallFunc actionWithTarget:self selector:@selector(popStepAndAnimate)]; // set the method itself as the callback

    // Remove the step
    [self.shortestPath removeObjectAtIndex:0];

    // Play actions
    [self runAction:[CCSequence actions:moveAction, moveCallback, nil]];
}

>

编译运行,然后...

Aww, yeah!

我们的猫自动移动到我们点击的终点位置了! :-)

然而,当你把玩它的时候,你会注意到以下问题:

  • 猫看起来有点僵硬
  • 猫没有带走骨头
  • 猫可以穿过狗(没有拿着骨头),而不会被咬死
  • 当你在猫走完路径之前点击新的位置创造新路径时,猫会有奇怪的行为。

为了解决猫的呆板外表,还有游戏逻辑(胜利/失败,狗,骨头,等等…)我们必须抛弃在第一次实现中使用的旧版游戏逻辑。接下来我们对它做改进。

重新添加游戏逻辑

为了修复这些问题,使用以下代码替换掉popStepAndAnimate方法中的内容:
>

- (void)popStepAndAnimate
{
    // Check if there is still shortestPath
    if (self.shortestPath == nil) {
        return;
    }

    // Game Logic (Win / Lose, dogs, bones, etc...)
    CGPoint currentPosition = [_layer tileCoordForPosition:self.position];

    if ([_layer isBoneAtTilecoord:currentPosition]) {
        [[SimpleAudioEngine sharedEngine] playEffect:@"pickup.wav"];
        _numBones++;
        [_layer showNumBones:_numBones];
        [_layer removeObjectAtTileCoord:currentPosition];
    }
    else if ([_layer isDogAtTilecoord:currentPosition]) {
        if (_numBones == 0) {
            [_layer loseGame];
            self.shortestPath = nil;
            return;
        }
        else {
            _numBones--;
            [_layer showNumBones:_numBones];
            [_layer removeObjectAtTileCoord:currentPosition];
            [[SimpleAudioEngine sharedEngine] playEffect:@"catAttack.wav"];
        }
    }
    else if ([_layer isExitAtTilecoord:currentPosition]) {
        [_layer winGame];
        self.shortestPath = nil;
        return;
    }
    else {
        [[SimpleAudioEngine sharedEngine] playEffect:@"step.wav"];
    }

    // Check if there remains path steps to go trough
    if ([self.shortestPath count] == 0) {
        self.shortestPath = nil;
        return;
    }

    // Get the next step to move to
    ShortestPathStep *s = [self.shortestPath objectAtIndex:0];

    CGPoint futurePosition = s.position;
    CGPoint diff = ccpSub(futurePosition, currentPosition);
    if (abs(diff.x) > (abs(diff.y)) {
        if (diff.x > (0) {
            [self runAnimation:_facingRightAnimation];
        }
        else {
            [self runAnimation:_facingLeftAnimation];
        }
    }
    else {
        if (diff.y > (0) {
            [self runAnimation:_facingForwardAnimation];
        }
        else {
            [self runAnimation:_facingBackAnimation];
        }
    }

    // Prepare the action and the callback
    id moveAction = [CCMoveTo actionWithDuration:0.4 position:[_layer positionForTileCoord:s.position]];
    id moveCallback = [CCCallFunc actionWithTarget:self selector:@selector(popStepAndAnimate)]; // set the method itself as the callback

    // Remove the step
    [self.shortestPath removeObjectAtIndex:0];

    // Play actions
    [self runAction:[CCSequence actions:moveAction, moveCallback, nil]];
}

这里没有施展什么魔法,只是对原来的代码做了重构。

编译运行,你会看到一切工作正常,除了猫在完成旧路径之前开始新路径时做出的奇怪行为。

因为它跟主题关系不大,我将不会对它的实现方法(相当简单)做详细解答。如果你很感兴趣,你可以下载最终的Cat Maze 工程仔细看下。恭喜你,你已经在一款简单的Cocos2D游戏中实现了A星寻路算法!:-)

如何实现对角线移动?

其实在A星算法中实现对角线移动相当简单。

你只需要更新两个函数:

  • walkableAdjacentTilesCoordForTileCoord: 更新它,让它也包含对角线方块。
  • costToMoveFromStep:toAdjacentStep: 更新它,让它提供一个跟水平/垂直移动不同的对角线移动cost。

你也许会对如何计算出在对角线方向上移动的cost值感到好奇。使用简单的数学可以很容易实现它!

猫从一个方块的中心移动到另一个方块,并且因为方块是正方形的,A,B和C构成了一个三角形,如下图所示:
Using the pythagorean theorem for calculating movement cost for diagonals

根据勾股定理,C² = A² + B², 所以:

C = √(A² + B²)
且有 A = B = 1 (从一个正方形移动到另一个的cost = G cost)
C = √(2)
C ≈ 1.41

所以对角线移动的cost等于1.41, 这比先向左移动然后再向上移动的cost值2 (1 + 1)还要少。

正如你所知道的,使用整形数去计算要远比浮点数方便,所以与其使用floats去代表对角线运动的cost值,还不如简单的对cost值乘10,然后四舍五入,这样水平或者垂直移动会消耗10,而对角线移动会消耗14。

我们现在试试它吧!首先使用以下函数对CatSprite.m文件中的costToMoveFromSTep:toAdjacentStep函数做替换:
>

// Compute the cost of moving from a step to an adjecent one
- (int)costToMoveFromStep:(ShortestPathStep *)fromStep toAdjacentStep:(ShortestPathStep *)toStep
{
    return ((fromStep.position.x != toStep.position.x) && (fromStep.position.y != toStep.position.y)) ? 14 : 10;
}

然后修改HelloWorldLayer.m文件中的walkableAdjacentTilesCoordForTileCoord方法,返回对角线相邻的正方形:
>

- (NSArray *)walkableAdjacentTilesCoordForTileCoord:(CGPoint)tileCoord
{
    NSMutableArray *tmp = [NSMutableArray arrayWithCapacity:8];

    BOOL t = NO;
    BOOL l = NO;
    BOOL b = NO;
    BOOL r = NO;

    // Top
    CGPoint p = CGPointMake(tileCoord.x, tileCoord.y - 1);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
        t = YES;
    }

    // Left
    p = CGPointMake(tileCoord.x - 1, tileCoord.y);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
        l = YES;
    }

    // Bottom
    p = CGPointMake(tileCoord.x, tileCoord.y + 1);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
        b = YES;
    }

    // Right
    p = CGPointMake(tileCoord.x + 1, tileCoord.y);
    if ([self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
        r = YES;
    }

    // Top Left
    p = CGPointMake(tileCoord.x - 1, tileCoord.y - 1);
    if (t && l && [self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    // Bottom Left
    p = CGPointMake(tileCoord.x - 1, tileCoord.y + 1);
    if (b && l && [self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    // Top Right
    p = CGPointMake(tileCoord.x + 1, tileCoord.y - 1);
    if (t && r && [self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    // Bottom Right
    p = CGPointMake(tileCoord.x + 1, tileCoord.y + 1);
    if (b && r && [self isValidTileCoord:p] && ![self isWallAtTileCoord:p]) {
        [tmp addObject:[NSValue valueWithCGPoint:p]];
    }

    return [NSArray arrayWithArray:tmp];
}

重要事项:你会发现代码中添加对角线方块比添加水平/垂直的方块有些不同。

事实上,例如,只有当顶部和左侧的项被添加时左对角线移动才会被添加到数组中。这是为了防止猫穿过墙壁的角落。以下是所有的详细情况:

  • O = Origin
  • T = Top
  • B = Bottom
  • L = Left
  • R = Right
  • TL = Top – Left

Avoiding walking through corners with the A* pathfinding algorithm

刚才引用的例子如上图的T L所示。

猫想要从原始点(O)移动到左下角的对角线方块。如果在左侧或者底部(或者都有)有一面墙,然后试着走对角线,算法将会封掉墙壁的角落(或者两面墙壁的角落)。所以只有当左侧或者底部没有墙壁时左方和下方的对角线方块才是可通行的。

提示:你可以通过更新costToMoveFromStep方法去模拟不同类型的地形。事实上,如果你减少G cost值,这表示猫会在那些方块上移动得更快,防止亦然。

接下来可以做什么?

这里是 Maze猫游戏的工程文件,包含了本教程的所有代码(包括对角线移动)。

恭喜,现在你知道了A星算法的基本内容,并且有了实现它的经验!你需要做以下准备:

  • 在你自己的游戏中实现A星算法
  • 有必要的话改善算法(运行不同种类的地形,更好的搜索方法,等等),使它最优化
  • 阅读其他相关文章,例如这篇不错的教程:Amit’s A* Pages

如果你对本教程有任何的问题或者意见,可以加入下方的论坛!

作者:


这篇blog是由iOS Tutorial Team的成员  Johann Fradj发表的,他目前是一位全职的资深iOS开发工程师。他是Hot Apps Factory的创始人,该公司开发了App Cooker

翻译者:

Picture of Oliver Ou

欧泽林目前在ZAKER,一款中国最流行的新闻阅读类app,负责iOS端的开发工作,在中国广州。他是位苹果的超级粉丝,曾经以学生身份参加过2011年6月份的苹果全球开发者大会(WWDC)。刚毕业不久,喜欢与苹果有关的一切东西,希望可以跟更多人分享交流。你可以在TwitterFacebook, 或者Weibo上找到他。

标签: cocos2d, A星寻路算法

?>