Demolition Page 1 - Making Levels

iOS 9 and OS X El Capitan strengthen Apple's gaming line up with GameplayKit, which augments the graphics (and physics) engines with a game engine. Over the coming weeks I'm going to walk through the process of developing a "bomber man" style game using SpriteKit and GameplayKit.

The code will be supplied in a playground that will have a page for each blog entry. In general, I'll discuss concepts in the blog, and leave explanations of specific pieces of code in comments or Playground markup. There's going to be a lot to cover, so let's get started. 

Something on the Screen

I remember I used to brag to friends "once I've got a pixel on the screen, I'll have basic game running within a day". It was rarely too far from the truth. The good news is Apple have done most of the hard work for us now. SpriteKit let's us jump straight in with graphics zipping around the screen. It wasn't really bragging, it reflected that what you often do as a game developer is get things going and tweak/refine/tweak/refine. So... we should be able to get there even quicker now, right?

The next step was to start to model the game world, or a level. I am going to do what I always used to do... have a rough idea of where I'm going and work towards that.

Squares (MapSquare)

Our level is going to be made up of a grid of squares. Each square can be one of just a few things:

  • Ground Free space where things can go
  • Solid Space that is occupied
  • Player Start Positions We are going to have four players. One starting in each corner of the screen, we need to store where that is
  • Destructable In this style of game, bonuses can be hidden behind pieces of destructable scenery. These will be randomly places around a map for each level (to add a bit of variety).
  • Clear It's not good if the player gets boxed in by the randomly placed destructable, Clear squares should be protected from destructables being placed
  • Bonuses We are goingto keep it simple and just give players an extra bomb (increasing the number of bombs they can have simulateniously) and longer flames (bigger bangs!)

We've captured these with the MapData enum, backed by an Int. This will make it easy to type in some starting data for a level. Speaking of which.

public enum MapSquare : Int {
    case Ground = 0, Solid, ClearGround, Destructable, Player1, Player2,Player3, Player4, Void, ExtraBomb,LongerFlame
    public var name : String{
        return "\(self)".componentsSeparatedByString(".")[1]
    }
}

Level Definition (LevelData)

Ultimately you'd want to save these things in files, but we are going to want to capture our early levels in code. In addition to the grid we will want some basic information such as the name and the size of the level. LevelData captures all of these details.

public struct LevelData {
    let name        : String
    var mapData     : [MapSquare]!
    let squareSize  : Int = 16
    let gridWidth   : Int
    let gridHeight  : Int
    public init(name:String, gridWidth:Int,gridHeight:Int, map:[Int]){
        self.name = name
        self.gridWidth = gridWidth
        self.gridHeight = gridHeight
        var mapData = [MapSquare]()
        for gridY in 0..<gridHeight{
            for gridX in 0..<gridWidth{
                guard let square = MapSquare(rawValue: map[mapDataOffset(gridX, gridY)]) else {
                    fatalError("Invalid map data: \(map[mapDataOffset(gridX, gridY)]) found at \(gridX),\(gridY)")
                }
                mapData.append(square)
            }
        }
        self.mapData = mapData
    }
    var size : CGSize{
        return CGSize(width: squareSize * gridWidth, height: squareSize * gridHeight)
    }
    public subscript (x:Int,y:Int)->MapSquare{
        get{
            return mapData[mapDataOffset(x,y)]
        }
        set{
            mapData[mapDataOffset(x, y)] = newValue
        }
    }
    private func mapDataOffset(x:Int,_ y:Int)->Int{
        return x + y * gridWidth
    }
}

The bulk of the code is in init, where we convert an (easy to enter) array of integers into the MapSquares.

We can now capture our level using a simple Int array passed to the constructor of LevelData... here's a little extract from the Playground

let level = LevelData(name: "Swift Destruction", gridWidth: 20, gridHeight: 16, map:[
    1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,8,
    1,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,5,1,8,
    1,2,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,2,1,8,

It's worth noting at this point we haven't tied anything to SpriteKit. We might want to move to a 3D engine like SceneKit, or even write something ourselves in Metal.

However, we are using SpriteKit and if we are going to put anything on screen we'll need to start getting some of this data into SpriteKit.

Extending MapSquare

Swift is just perfect for this kind of thing. We don't want to have to define lots of new structures and classes to be the SpriteKit version of MapSquare... We can just extend MapSquare to the right hooks SpriteKit. If we move to SceneKit we would just throw out that extenion and write a SceneKit one.

All of our graphics are going to be colored squares to start with, so we will just make each case have it's own color, another function that will create a sprite to represent the square, and one final one to add a bit of flair to how the sprite will animate onto the screen.

extension MapSquare{
    //What color should the square be
    var color : SKColor 
    //Any special animation in for the created sprite?
    func entryAction(forSprite sprite:SKSpriteNode,inLevel level:LevelData, atGridX gridX:Int, gridY:Int)->SKAction?
    //Create a sprite from the MapSquare
    func spriteForSquare(inLevel level:LevelData, atGridX gridX:Int=0, gridY:Int=0)->SKSpriteNode?
}

Something on the screen... Now?

Almost... SpriteKit organises the different modes of the game into scenes. Our level is going to be a scene. We will focus more on the mechanics and structure of SpriteKit later in the process (remember... I want to get something on the screen so you can start playing), so don't worry if you aren't very familiar with it.

class LevelScene : SKScene{
    var levelData : LevelData!
    init(levelData:LevelData){
        self.levelData = levelData
        super.init(size: levelData.size)
    }
    required init?(coder aDecoder: NSCoder) {super.init(); return nil}
    func processTilesWithBlock(block:(levelData:LevelData, tile:MapSquare, gridX:Int,gridY:Int)->Void){
        for gridY in 0..<levelData.gridHeight{
            for gridX in 0..<levelData.gridWidth{
                block(levelData: levelData, tile: levelData[gridX,gridY], gridX: gridX, gridY: gridY)
            }
        }
    }
    func randomlyAddDestructable(levelData:LevelData, tile:MapSquare, gridX:Int,gridY:Int){
        if arc4random() % 100 < 80{
            if self.levelData[gridX,gridY] == .Ground{
                //Looks like a bug. Subscripts must be explicitly unwrapped
                self.levelData![gridX,gridY] = MapSquare.Destructable
            }
        }
    }
    func levelSprite()->SKNode{
        let mapNode = SKNode()
        processTilesWithBlock { (levelData, tile, gridX, gridY) -> Void in
            //Create a sprite for the square
            if let sprite = tile.spriteForSquare(inLevel: levelData, atGridX:gridX, gridY:gridY){
                mapNode.addChild(sprite)
            }
        }
        //Nudge it by half a square
        mapNode.position = CGPoint(x: levelData.squareSize / 2, y: levelData.squareSize / 2)
        return mapNode
    }
    func begin(){
        removeAllChildren()
        //Add destructable blocks
        processTilesWithBlock(randomlyAddDestructable)
        addChild(levelSprite())
    }
}

A few things to note. We often want to iterate through the whole map, so I've written a single method to do that which takes a block. I'm not sure what the performance characteristics of this will be in a game loop, but don't prematurely optimise! Let's see how it performs. 

let spriteKitView = SKView(frame: CGRect(origin: CGPointZero, size: level.size))
spriteKitView.ignoresSiblingOrder = true
var playableLevel = LevelScene(levelData: level)
spriteKitView.presentScene(playableLevel)
playableLevel.begin()
XCPShowView("Demolition", view: spriteKitView)

With a little bit of boiler plate code to set up the view... we now have a level which animates in....

 

Next time we'll focus on building the navigation graph in GameplayKit... and maybe even get some players moving!

You can download the playground here.