Create Your Own Isometric Tile-Based Game: Part 3

tutorial_iso_part3_1200x630

A* Pathfinding

A* (pronounced A Star) is an algorithm that considers traversable and non-traversable nodes while finding the shortest distance between 2 points. It’s widely used in tile-based games.

There are loads of resources for this on the web already, though in my efforts, I was unable to find a pure Swift solution, so I translated one from various sources, primarily referring to this Flash implementation by Joseph Hocking.

Note: This is part 3 of an ongoing series on isometric game dev. The series begins with part 1, which you can find here. If you are new to the series, you may want to start from the beginning, however, if you are looking specifically for an A* Pathfinding tutorial, then you’re in the right place. You can download the sample project here and pick up the code from this stage in the series.

Alternatively, if you’re following the series and you’ve completed parts 1 and 2, you can continue on with your own code, or if you’d prefer, you can download the source material above and go from there.

Ok, let’s get started. Open your IsoGame xCode project and run your app. You should see something like this:

Screen Shot 2015-02-26 at 3.32.54 pm

Our droid is moving to our touch location in our isometric view, but he currently moves through walls without any resistance. Implementing pathfinding will ensure that he respects the boundaries of the level, while moving to the touch location via the most direct traversable path.

We’re going to keep all our pathfinding code in 1 file, so select File > New > File, then select iOS > Source > Swift File and click Next. Name the file PathFinder and click Create. In PathFinder.swift, replace the contents with this code:

import UIKit
import SpriteKit

class PathFinder {

    let moveCostHorizontalOrVertical = 10
    let moveCostDiagonal = 14

    var iniX:Int
    var iniY:Int
    var finX:Int
    var finY:Int
    var level:[[Int]]
    var openList:[String: PathNode]
    var closedList:[String: PathNode]
    var path = [CGPoint]()

    init(xIni:Int, yIni:Int, xFin:Int, yFin:Int, lvlData:[[Int]]) {

        iniX = xIni
        iniY = yIni
        finX = xFin
        finY = yFin
        level = lvlData
        openList = [String: PathNode]()
        closedList = [String: PathNode]()
        path = [CGPoint]()

        //invert y coordinates - pre conversion (spriteKit inverted coordinate system). This PathFnding code ONLY works with positive (absolute) values

        iniY = -iniY
        finY = -finY

        //first node is the starting point

        let node:PathNode = PathNode(xPos: iniX, yPos: iniY, gVal: 0, hVal: 0, link: nil)

        //use the x and y values as a string for the dictionary key

        openList[String(iniX)+" "+String(iniY)] = node;

    }
    func findPath() -> [CGPoint] {

        searchLevel()

        //invert y cordinates - post conversion

        let pathWithYInversionRestored = path.map({i in i * CGPoint(x:1, y:-1)})
        return pathWithYInversionRestored.reverse()
    }
    func searchLevel() {

        var curNode:PathNode?
        var endNode:PathNode?
        var lowF = 100000
        var finished:Bool = false

        for obj in openList {

            let curF = obj.1.g + obj.1.h

            //currently this is just a brute force loop through every item in the list
            //can be sped up using a sorted list or binary heap, described http://www.policyalmanac.org/games/binaryHeaps.htm
            //example http://www.gotoandplay.it/_articles/2005/04/mazeChaser.php

            if (lowF > curF) {
                lowF = curF
                curNode = obj.1
            }

        }


        if (curNode == nil) {

            //no path exists!
            return

        } else {

            //move selected node from open to closed list

            let listKey = String(curNode!.x)+" "+String(curNode!.y)

            openList[listKey] = nil
            closedList[listKey] = curNode

            //check target

            if ((curNode!.x == finX) && (curNode!.y == finY)) {
                endNode = curNode!
                finished = true
            }

            //check each of the 8 adjacent squares

            for i in -1..<2 {
                for j in -1..<2 {

                    let col = curNode!.x + i;
                    let row = curNode!.y + j;

                    //make sure on the grid and not current node

                    if ((col >= 0 && col < level[0].count)
                        && (row >= 0 && row < level.count)
                        && (i != 0 || j != 0))
                    {

                        //if traversable, not on closed list, and not already on open list - add to open list

                        let listKey = String(col)+" "+String(row)

                        if ((level[row][col] == Global.tilePath.traversable)
                            && (closedList[listKey] == nil)
                            && (openList[listKey] == nil))
                        {

                            //prevent cutting corners on diagonal movement

                            var moveIsAllowed = true

                            if ((i != 0) && (j != 0)) {
                                //is diagonal move

                                if ((i == -1) && (j == -1)) {
                                    //is top-left, check left and top nodes
                                    if (level[row][col+1] != Global.tilePath.traversable //top
                                        || level[row+1][col] != Global.tilePath.traversable //left
                                        ) {
                                            moveIsAllowed = false
                                    }

                                } else if ((i == 1) && (j == -1)) {
                                    //is top-right, check top and right nodes
                                    if (level[row][col-1] != Global.tilePath.traversable //top
                                        || level[row+1][col] != Global.tilePath.traversable //right
                                        ) {
                                            moveIsAllowed = false
                                    }
                                } else if ((i == -1) && (j == 1)) {
                                    //is bottom-left,check bottom and left nodes
                                    if (level[row][col+1] != Global.tilePath.traversable //bottom
                                        || level[row-1][col] != Global.tilePath.traversable //left
                                        ) {
                                            moveIsAllowed = false
                                    }
                                } else if ((i == 1) && (j == 1)) {
                                    //is bottom-right, check bottom and right nodes
                                    if (level[row][col-1] != Global.tilePath.traversable //bottom
                                        || level[row-1][col] != Global.tilePath.traversable //right
                                        ) {
                                            moveIsAllowed = false
                                    }
                                }

                            }

                            if (moveIsAllowed) {

                                //determine g
                                var g:Int
                                if ((i != 0) && (j != 0)) {
                                    //is diagonal move
                                    g = moveCostDiagonal

                                } else {
                                    //is horizontal or vertical move
                                    g = moveCostHorizontalOrVertical
                                }

                                //calculate h (heuristic)
                                let h = heuristic(row: row, col: col)

                                //create node and add to openList
                                openList[listKey] = PathNode(xPos: col, yPos: row, gVal: g, hVal: h, link: curNode)

                            }
                        }

                    }
                }
            }

            if (finished == false) {
                searchLevel();
            } else {
                retracePath(endNode!)
            }

        }
    }

    // Calculate heuristic

    //Diagonal Shortcut method (slightly more expensive but more accurate than Manhattan method)
    //Read more on heuristics here: http://www.policyalmanac.org/games/heuristics.htm

    func heuristic(#row:Int, col:Int) -> Int {
        let xDistance = abs(col - finX)
        let yDistance = abs(row - finY)
        if (xDistance > yDistance) {
            return moveCostDiagonal*yDistance + moveCostHorizontalOrVertical*(xDistance-yDistance)
        } else {
            return moveCostDiagonal*xDistance + moveCostHorizontalOrVertical*(yDistance-xDistance)
        }
    }

    func retracePath(node:PathNode) {

        let step = CGPoint(x: node.x, y: node.y)
        path.append(step)

        if (node.g > 0) {
            retracePath(node.parentNode!);
        }
    }

}

class PathNode {

    let x:Int
    let y:Int
    let g:Int
    let h:Int
    let parentNode:PathNode?

    init(xPos:Int, yPos:Int, gVal:Int, hVal:Int, link:PathNode?) {

        self.x = xPos
        self.y = yPos
        self.g = gVal
        self.h = hVal

        if (link != nil) {
            self.parentNode = link!;
        } else {
            self.parentNode = nil
        }
    }
}

As complex as it appears, the code above is more or less, a straight forward Swift implementation of A* Pathfinding. It won’t really make sense without first understanding how the A* algorithm works and there are plenty of good resources already available on the web that cover this. If you want to customise your A* code, or have a curiosity for its workings, A* Pathfinding for Beginners by Patrick Lester is a great place to start. Otherwise, the code here will be enough to get you up and running.

You’ll notice some errors popping up in xCode, referring to unresolved identifier ‘Global’. So let’s tend to that now.

Select File > New > File, then select iOS > Source > Swift File and click Next. Name the file Global and click Create. In Global.swift, replace the contents with this code:

import Foundation

struct Global {
    struct tilePath {
        static let traversable = 0
        static let nonTraversable = 1
    }
}

All we’ve done here is create some global static constants for traversable and nonTraversable tiles. We’ll be using these values in our GameScene as well as our PathFinder class, hence, we’ve made them globally accessible. You should now see the errors in your PathFinder class have disappeared.

Ok, now we have our powerful PathFinder class but how do we put it to use? Well, you’ll notice that the Pathfinder init(...) method takes 4 Ints (xIni, yIni, xFin, yFin) and a lvlData array of type [[Int]]. The 4 integers are the x and y coordinates for the starting point and destination point of our path, easy enough. The lvlData array is a 2 dimensional array that maps the traversable and nonTraversable tiles. Let’s start by building a method that converts our games tiles array:

[
[(1,7), (1,0), (1,0), (1,0), (1,0), (1,1)],
[(1,6), (0,0), (0,0), (0,0), (0,0), (1,2)],
[(1,6), (0,0), (2,2), (0,0), (0,0), (1,2)],
[(1,6), (0,0), (0,0), (0,0), (0,0), (1,2)],
[(1,6), (0,0), (0,0), (0,0), (0,0), (1,2)],
[(1,5), (1,4), (1,4), (1,4), (1,4), (1,3)]
]

into our pathfinders traversable lvlData format:

[
[1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1],
]

where 0 = traversable and 1 = nonTraversable

In GameScene.swift just under the sortDepth() method, add this function:

func traversableTiles() -> [[Int]] {

        //1
        var tTiles = [[Int]]()

        //2
        func binarize(num:Int) ->Int {
            if (num == 1) {
                return Global.tilePath.nonTraversable
            } else {
                return Global.tilePath.traversable
            }
        }

        //3
        for i in 0..<tiles.count {
            let tt = tiles[i].map{i in binarize(i.0)}
            tTiles.append(tt)
        }

        return tTiles
}

In the above code, we are doing the following:

  1. Initiating our temporary tTiles array, to store our traversable values.
  2. Here we built a nested function that will binarize our values. It returns 1 as 1, then any other number becomes a value of 0. Why are we doing this? Because our droid tile in the games tiles array has a value of 2, but we know that where ever the droid is placed, is a traversable area (ground), so we substitute a 0 to mark that position as traversable. Also, note that we’re using our Global static constants that we setup earlier.
  3. We then iterate through the tiles array and use the map method and our nested binarize function, to populate our tTiles array.

Next, add this function directly below the traversableTiles() function you just added:

func findPathFrom(from:CGPoint, to:CGPoint) -> [CGPoint]? {

        let traversable = traversableTiles()

        //1
        if (Int(to.x) > 0)
            && (Int(to.x) < traversable.count)
            && (Int(-to.y) > 0)
            && (Int(-to.y) < traversable.count)
        {

            //2
            if (traversable[Int(-to.y)][Int(to.x)] == Global.tilePath.traversable ) {

                //3
                let pathFinder = PathFinder(xIni: Int(from.x), yIni: Int(from.y), xFin: Int(to.x), yFin: Int(to.y), lvlData: traversable)
                let myPath = pathFinder.findPath()
                return myPath

            } else {

                return nil
            }

        } else {

            return nil
        }

}

This method takes a 2 CGPoints (e.g. from: the droids current location, to: user touch location). It then formats these points into separate x and y values, so it can feed them (and the traversable tiles array) to our PathFinder class. Broken down:

  1. Check the to CGPoint (e.g. user touch) is within the boundaries of our map/level.
  2. Check the to CGPoint (e.g. user touch) is on a traversable tile. (If the user touches a nonTraversableTile e.g. a wall, then we obviously can’t move the droid to that destination).
  3. We then instantiate our PathFinder class with our formatted values and run the findPath method to retrieve the path.

Next, add this function directly below the findPathFrom(...) function you just added:

func highlightPath2D(path:[CGPoint]) {

        //clear previous path
        layer2DHighlight.removeAllChildren()

        for i in 0..<path.count {
            let highlightTile = SKSpriteNode(imageNamed: textureImage(Tile.Ground, Direction.N, Action.Idle))
            highlightTile.position = pointTileIndexToPoint2D(path[i])
            highlightTile.anchorPoint = CGPoint(x: 0, y: 0)

            highlightTile.color = SKColor(red: 1.0, green: 0, blue: 0, alpha: 0.25+((CGFloat(i)/CGFloat(path.count))*0.25))
            highlightTile.colorBlendFactor = 1.0

            layer2DHighlight.addChild(highlightTile)
        }

}

Don’t worry too much about what we’re doing in this function. Its purpose is to highlight our path in our view2D, it will aid our understanding in this tutorial but it’s not necessarily something you’d include in your final game.

Once you’ve added the highlightPath2D(...) code, you should see a few errors pop up in xCode, that’s because we haven’t created the layer2DHighlight instance yet. Let’s quickly do that now.

At the top of your GameScene class, under the line:

let view2D:SKSpriteNode

add this line:

let layer2DHighlight:SKNode

then in the init(..) method, under this line:

view2D = SKSpriteNode()

add this line:

layer2DHighlight = SKNode()

then in the didMoveToView(..) method, under this line:

addChild(view2D)

add this code:

layer2DHighlight.zPosition = 999
view2D.addChild(layer2DHighlight)

lastly, navigate down your class, and add this code directly after your PointIsoTo2D(...) method:

func point2DToPointTileIndex(point:CGPoint) -> CGPoint {

    return floor(point / CGPoint(x: tileSize.width, y: tileSize.height))

}
func pointTileIndexToPoint2D(point:CGPoint) -> CGPoint {

    return point * CGPoint(x: tileSize.width, y: tileSize.height)

}

These functions convert coordinates from pixels to tile position (index) e.g. given our map and tileSize, these coordinates (x,y) are in pixels:

(0,  0), (32,  0), (64,  0), (96,  0), (128,  0), (160,  0)
(0, 32), (32, 32), (64, 32), (96, 32), (128, 32), (160, 32)
(0, 64), (32, 64), (64, 64), (96, 64), (128, 64), (160, 64)
(0, 96), (32, 96), (64, 96), (96, 96), (128, 96), (160, 96)
(0,128), (32,128), (64,128), (96,128), (128,128), (160,128)
(0,160), (32,160), (64,160), (96,160), (128,160), (160,160)

where these coordinates (x,y) are in tile index:

(0,0), (1,0), (2,0), (3,0), (4,0), (5,0)
(0,1), (1,1), (2,1), (3,1), (4,1), (5,1)
(0,2), (1,2), (2,2), (3,2), (4,2), (5,2)
(0,3), (1,3), (2,3), (3,3), (4,3), (5,3)
(0,4), (1,4), (2,4), (3,4), (4,4), (5,4)
(0,5), (1,5), (2,5), (3,5), (4,5), (5,5)

Right, with all our conversion methods setup, we can now use our PathFinder. In our touchesEnded(...) method, we’re going to remove our basic positioning code and replace it with our new positioning code that utilises our PathFinder.

Update your touchesEnded(...) method to look like this:

override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {

        //////////////////////////////////////////////////////////
        // Original code that we still need
        //////////////////////////////////////////////////////////

        let touch = touches.first as! UITouch
        let touchLocation = touch.locationInNode(viewIso)

        var touchPos2D = pointIsoTo2D(touchLocation)

        touchPos2D = touchPos2D + CGPoint(x:tileSize.width/2, y:-tileSize.height/2)

        //////////////////////////////////////////////////////////
        // PathFinding code that replaces our old positioning code
        //////////////////////////////////////////////////////////

        //1
        let path = findPathFrom(point2DToPointTileIndex(hero.tileSprite2D.position), to: point2DToPointTileIndex(touchPos2D))

        if (path != nil) {

            //2
            var newHeroPos2D = CGPoint()
            var prevHeroPos2D = hero.tileSprite2D.position
            var actions = [SKAction]()

            //3
            for i in 1..<path!.count {

                //4
                newHeroPos2D = pointTileIndexToPoint2D(path![i])
                let deltaY = newHeroPos2D.y - prevHeroPos2D.y
                let deltaX = newHeroPos2D.x - prevHeroPos2D.x
                let degrees = atan2(deltaX, deltaY) * (180.0 / CGFloat(M_PI))
                actions.append(SKAction.runBlock({
                    self.hero.facing = self.degreesToDirection(degrees)
                    self.hero.update()
                }))

                //5
                let velocity:Double = Double(tileSize.width)*2
                var time = 0.0

                if i == 1 {

                    //6
                    time = NSTimeInterval(distance(newHeroPos2D, hero.tileSprite2D.position)/CGFloat(velocity))

                } else {

                    //7
                    let baseDuration =  Double(tileSize.width)/velocity
                    var multiplier = 1.0

                    let direction = degreesToDirection(degrees)

                    if direction == Direction.NE
                        || direction == Direction.NW
                        || direction == Direction.SW
                        || direction == Direction.SE
                    {
                        //8
                        multiplier = 1.4
                    }

                    //9
                    time = multiplier*baseDuration
                }

                //10
                actions.append(SKAction.moveTo(newHeroPos2D, duration: time))

                //11
                prevHeroPos2D = newHeroPos2D

            }

            //12
            hero.tileSprite2D.removeAllActions()
            hero.tileSprite2D.runAction(SKAction.sequence(actions))

            //13
            highlightPath2D(path!)

        }

}

OK, so there’s quite a bit going on here, let’s break it down:

  1. Get our path using our PathFinder class via our findPathFrom(…) method.
  2. Declare our variables that will be used through our iteration of our path array. newHeroPos2D will store our destination for each iteration, prevHeroPos2D will store our current position for each iteration. The actions array, will be a a list of actions we build, that we will ultimately run as a sequence on our hero.tileSprite2D.
  3. Execute our iteration, we begin with the 1 index (not 0), as the 0 index of the path array coordinates will be close to, if not, exactly match our current position.
  4. Get the angle the same way we did in our original positioning code. We then append a runBlock action to our actions array that will update our hero to face the given direction.
  5. Establish our desired velocity and initiate the time var.
  6. If it’s the first iteration (i == 1), then we are moving our hero from an unknown random position, to a set tile position from the path array. This means the distance could be of any random quantity, so we calculate it accurately to get our value for time.
  7. Once we’ve calculated the first iteration, all following distances will be from tile to tile. So it’s either a straight move (vertical or horizontal), in which case our baseDuration is unaffected (multiplier = 1.0)
  8. …or it’s a diagonal move, so we multiply the baseDuration by 1.4 to keep velocity consistent over the slightly greater distance. Note: the 10:14 ratio (or 1.0:1.4) is used as a low cost approximation of a squares hypotenuse. It avoids having to process Pythagoras theorem (a*a + b*b = c*c) which is more accurate but more CPU intensive.
  9. We then adjust the baseDuration by our multiplier, to get our time var for the iteration.
  10. Append the moveTo action to our actions array.
  11. We’re now done with our postions for this iteration, so our newHeroPos2D becomes our prevHeroPos2D, ready for use in the next iteration.
  12. Once we’ve iterated through all the nodes in our path array, we run our collective actions as a sequence on our hero sprite.
  13. Finally, we highlight the path in our 2D view, so we can clearly see the path that our PathFinder chose.

Run your App.

Screen Shot 2015-03-04 at 5.22.41 pm

Now, when we direct our heroic droid through the interior wall, he refuses to do it. Instead, he finds his way around it. You can see the pathFinder‘s returned path highlighted in the 2D view.

Let’s alter the level design to further test our new functionality. In GameScene.swift change your tiles array so it reads like this:

tiles =     [[(1,7), (1,0), (1,0), (1,0), (1,0), (1,0), (1,0), (1,0), (1,1)]]
tiles.append([(1,6), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (0,0), (1,2)])
tiles.append([(1,6), (0,0), (2,2), (0,0), (0,0), (0,0), (0,0), (0,0), (1,2)])
tiles.append([(1,6), (0,0), (0,0), (0,0), (0,0), (1,5), (1,4), (1,4), (1,5)])
tiles.append([(1,6), (0,0), (0,0), (1,7), (0,0), (0,0), (0,0), (0,0), (0,0)])
tiles.append([(1,6), (0,0), (0,0), (1,6), (0,0), (0,0), (0,0), (0,0), (0,0)])
tiles.append([(1,6), (0,0), (0,0), (1,5), (1,4), (1,4), (1,1), (0,0), (0,0)])
tiles.append([(1,6), (0,0), (0,0), (0,0), (0,0), (0,0), (1,2), (0,0), (0,0)])
tiles.append([(1,6), (0,0), (0,0), (0,0), (0,0), (0,0), (1,3), (0,0), (0,0)])
tiles.append([(1,5), (1,4), (1,4), (1,3), (0,0), (0,0), (0,0), (0,0), (0,0)])

While we’re at it, lets move the views around a bit to prevent overlapping. In the didMoveToView(...), remove these 2 lines:

view2D.xScale = deviceScale
view2D.yScale = deviceScale

and in its place, put this code:

let view2DScale = CGFloat(0.4)
view2D.xScale = deviceScale * view2DScale
view2D.yScale = deviceScale * view2DScale

Then change the position of the 2D view to read like this:

view2D.position = CGPoint(x:-self.size.width*0.48, y:self.size.height*0.43)

and the isometric view positioning to read like this:

viewIso.position = CGPoint(x:self.size.width*0, y:self.size.height*0.25)

Run your app.

Screen Shot 2015-03-04 at 6.07.06 pm

Great. We can now see our droid negotiate complex paths all by himself. So proud. Feel free to play around with the level design more, to test the pathfinding to your own satisfaction.

Conclusion

Congratulations! You’ve just completed part 3 of the ongoing isometric tutorial series. Here is a sample project with all of the code to this point:

I’ve got a few things in the works at the moment, so I’m uncertain as to when, or if, I’ll have time to produce part 4. I’ll post any progress or expected delivery dates on Twitter and Facebook, as details come to light. Please follow us there and/or sign up to our newsletter to stay informed. Cheers!

Dave Longbottom

Dave is the indie dev behind Big Sprite Games. Read more on the About page.

20 thoughts on “Create Your Own Isometric Tile-Based Game: Part 3

  1. Wow! Thank you so much for this tutorial. It is a great intermediate level tutorial for anyone interested in creating an isometric type game. Thanks to the clear instructions, I was able to handle the bugs/changes to 6.3. I highly recommend your tutorial and hope you do more.

    1. Could you please expand on how you adapted this to Xcode 6.3, I’ve just updated and now having issues compiling with the protocol changes

  2. Hi Dave, I am creator of iTunes App store game called Little Avenger. I’m still learning many things about game development and working on new ideas and projects. Just wanted to say this was a great tutorial and hopefully you find the time to keep adding, VERY helpful!

    Cheers

  3. Hey Dave,

    first of all, you made a great tutorial series so far! Keep up the good work!

    i am wondering what software you were using to create these isometric tiles. Are you simply using Adobe Illustrator or maybe something else, that suits this particular job better?

    1. Hi Christian, Illustrator would work fine for this approach, as the in-game assets are all 2D bitmaps. Though, you can make your life easier by using 3D software to build the models, then render out to 2D frames. Have a look at 3D Studio Max and Maya. Also Blender is an open source alternative. There’s a bunch of others but that’s somewhere to start if you’re interested. Cheers.

  4. Hey Dave,

    great tutorial, easy to follow and understand! Please keep up this fantastic series. It helped me a lot with my own projects. Thank you a lot!!!

    Michael

  5. This is a very satisfying tutorial! The concepts are presented clearly enough that everything makes sense, even though I don’t know Swift, yet. And you get a nice visual experience as you learn. Thanks for putting this together!!!

    1. @Akshea – yep, that shouldn’t be difficult. Just use the same code as in the touchesEnded function but when setting your path, use your specific point as the parameter instead of the converted touch coordinate e.g.

      //1
      let path = findPathFrom(point2DToPointTileIndex(hero.tileSprite2D.position), to: point2DToPointTileIndex(yourCGPoint))
      
  6. Hello, I’d like to know if there is any way for building swift apps like this for other operating system and platforms such as Android.

    1. Hi Alex, Swift is Apple’s programming language. You could build the equivalent app in Android, you’d just need to translate the code into their programming language/syntax.

  7. Hey Dave, what a beautiful example. This is getting me excited as I had thought most of ketchapp’s 3d games were just 2d rendered sprites of the 3d elements.

    Nice.

    Could you perhaps tell us when part 4 is coming out? Thank you.

    Pavan

  8. Hi Dave,
    I noticed an issue with your traversable check, the if statement:

    if (Int(to.x) > 0)
                && (Int(to.x) < traversable.count)
                && (Int(-to.y) > 0)
                && (Int(-to.y) < traversable.count)
            {
    

    Should actually be:

    if (Int(to.x) > 0)
                && (Int(to.x) < traversable.count)
                && (Int(-to.y) > 0)
                && (Int(-to.y) < traversable[Int(to.x)].count)
            {
    

    Otherwise you will get an index out of bounds error, because you are not checking the max number of Y co-ordinates.

Leave a Reply

Your email address will not be published. Required fields are marked *

Use Markdown backticks to wrap your code blocks e.g.

```
// This is my example code block
func myFunction( ) -> Bool {
    return true
}
```

You may also use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <pre> <cite> <code> <em> <strong>