Hexagonal Tilemaps in SpriteKit

One of the new features added into iOS 10 and Xcode 8 is the SpriteKit Tilemap Editor. This editor provides a visual editing surface for SKTileMapNode which is described as ‘A node used to render a 2D array of textured sprites‘. The SKTileMapNode is represented as layers in the Tilemap and e.g. allows a building with transparents edges to sit on top of a background land tile. SKTileMapNode supports rectangular, isometric and hexagonal tiles, with the emphasis on hexagonal tiles for this article.

Art Pipeline

I haven’t worked with hexagonal tiles before, so was a bit in the dark about what works best regarding tile size, but I’m interested in the creation of art for games, so I have used Sketch to create vector images that are then exported in PDF format for use in Xcode, since Xcode is able to generate @2x and @3x images automatically from the PDF file. For those with Sketch installed, the command to export the tiles is;

$ sketchtool export slices "art/Tiles.sketch" --output="art/output"

In Xcode the tiles can be imported as universal single scale images into the assets catologue. The next step is to create TileSets, which allow multiple images to be grouped together. Note that each layer in the Tilemap is associated with only one TileSet e.g. the background layer can only display tiles that are in the background TileSet. Once the TileSets are done then the SpriteKit Scene Editor can be used to add SKTilemapNode nodes, one for each map layer. The map is laid out in the Tilemap Editor from individual tiles contained in the TileSets.

Hexagonmap

Class Design

The example code is written in Swift 3 and uses protocols extensively to achieve modularity of the various Components. My intention is to have multiple scenes that each use Tilemaps and I want to share common functionality. An initial approach might be to move common logic into a base class, but it is a design best practice to prefer composition over inheritance because base classes can become dumping grounds for a lot of unrelated capability. A better approach is to use a combination of Inheritance and a protocol to separate SKScene functionality from SKTilemapNode functionality.

//  TileMapScene.swift
import SpriteKit

protocol TileMapScene {
    var backgroundLayer: SKTileMapNode! { get set }
    var gridLayer: SKTileMapNode! { get set }
    var selectionLayer: SKTileMapNode! { get set }

    var gameCamera: SKCameraNode! { get }
    var gameScene: SKScene! { get }
    var gameScaleMode: SKSceneScaleMode! { get set }

    var currentSelectionlocation: CGPoint? { get set }
}

The TileMapScene protocol defines what functionality should be implemented by an SKScene wanting to display a tilemap, letting other SKScene related functionality to be moved into a common base class. A protocol extension can then provide helper functions that leverage protocol members that can be reused by multiple scenes. Note that functions in the extension don’t need to be declared in the protocol, providing a modern alternative to the traditional helper component.

//  TileMapScene+Extensions.swift
import SpriteKit

extension TileMapScene {
    var gridTile: SKTileGroup {
        guard let selectionTile = self.gridLayer.tileSet.tileGroups.first(where: {$0.name == "Tiles"}) else {
            fatalError("Grid tile not found")
        }
        
        return selectionTile
    }

    func floodFillGrid() {
        self.gridLayer.fill(with: self.gridTile)
    }
}

User Gestures

I’ve used gesture recognizors to distinquish between Pan, Tap and Long Press user input. Pan is for updating the camera position i.e. Moving the tilemap, while Tap allows for selection of a tile and Long Press toggles the hexagon grid on / off.

func handleLongPressFrom(recognizer: UILongPressGestureRecognizer) {
    if recognizer.state != .began {
        return
    }
        
    // Toggle visibility of gridLayer
    self.gridLayer.isHidden = !self.gridLayer.isHidden
}

Camera

The camera represents the visible part of the Tilemap and is equivalent to the screen. When the game loads it is necessary to scale the camera (zoom) depending on the device e.g. iPhone or iPad and orientation e.g. landscape of portrait. The camera also needs to be constrained, so the user is only able to pan the tilemap a certain distance in each direction. SKConstraint can be applied to the camera;

func updateConstraintsFor(backgroundLayer: SKTileMapNode, boundaryRangeX: SKRange, boundaryRangeY: SKRange) {
    let levelEdgeConstraint = SKConstraint.positionX(boundaryRangeX, y: boundaryRangeY)
    levelEdgeConstraint.referenceNode = backgroundLayer

    self.constraints = [levelEdgeConstraint]
}

References

  1. Source code for the example project is available on GitHub
  2. Learn the basics of SpriteKit’s Tilemap Editor

Published by Mark Brownsword

I've been working as an enterprise developer for about 15 years, always using .NET platform and mostly using Oracle databases. My degree is in Business Studies, majoring in information systems, where I learnt the fundamentals of software engineering for building systems for business. In my spare time I like to play around with games programming because the emphasis is much more on pure computer science.

2 Comments

  1. This use of Protocols is very inspiring and insightful. I’ve been trying to think of how/where/when/what to use POP for, and this really helps me see. Thank you!

    Reply

    1. You can learn more about protocols in the Swift documentation and I recommend protocol extensions too, that is how I have added the SKTilemapNode functionality in the example project.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s