Adding Command Buttons to the Hexagonal Map Game

Introduction

This article demonstrates how to overlay Button Elements onto an Hexagonal TileMap in SpriteKit. The buttons will stay fixed in place while the map moves and the buttons will respond to touch events by executing specific commands. The Hexagonal TileMap is an SKTileMapNode and the buttons are a composite of SKSpriteNode and SKLabelNode.

Prerequisites

Positioning buttons above the TileMap is a simple task, as long as the TileMap has its scaleMode property set to resizeFill. This ensures that the scene will be rendered to the screen at the exact size as in the design editor and width and height values of the scenes frame will be correct relative to the devices viewport.

// GameViewController.swift
override func viewDidLoad() {
    ...
        
    guard let sceneNode = self.loadScene(gameFolder: "MarsGameScene") else {
        fatalError("Scene not loaded")
    }
        
    sceneNode.scaleMode = .resizeFill
    sceneNode.gameDelegate = self
        
    ...
}

Using resizeFill as scaleMode will result in individual tiles rendering at there native resolution, so if the tiles are quite large the map may look magnified. This can be corrected by using an SKCameraNode with scaling.

// GameSceneBase.swift
override func didMove(to view: SKView) {
    guard let backgroundLayer = childNode(withName: "background") as? SKTileMapNode else {
        fatalError("Background node not loaded")
    }
    
    ...
    
    guard let camera = self.childNode(withName: "gameCamera") as? SKCameraNode else {
        fatalError("Camera node not loaded")
    }

    guard let view = self.view else {
        fatalError("View not available")
    }

    // Initialise Camera
    camera.updateScale()
    camera.updateConstraints(backgroundLayer: backgroundLayer, viewBounds: view.bounds)

    ...
}

Designing the Button

The buttons appearance is achieved by layering an SKSpriteNode on top of another SKSpriteNode, then placing an SKLabelNode wth text on the top. The second SKSpriteNode is smaller than the first, to give the appearance of a bordered button. A command to be executed is passed as a parameter when the button is initialised.

// CommandButton.swift
class CommandButton: SKNode {
    ...
    init(size: CGSize, text: String, command: Command) {
        super.init()

        let width = size.width - buttonBorder
        let height = size.height - buttonBorder
        self.main = SKSpriteNode(color: .black, size: CGSize(width: width, height: height))
        self.main.name = self.mainName
        self.main.alpha = normalAlpha
    
        self.border = SKSpriteNode(color: .white, size: size)
        self.border.name = self.borderName
        self.border.alpha = normalAlpha

        self.label = SKLabelNode(fontNamed: "Verdana")
        self.label.name = self.labelName
        self.label.text = text
        self.label.fontSize = 22
        self.label.fontColor = .white
        self.label.verticalAlignmentMode = SKLabelVerticalAlignmentMode.center;
        self.label.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center;

        self.command = command

        self.isUserInteractionEnabled = true
        self.addChild(self.border)
        self.addChild(self.main)
        self.addChild(self.label)
    }
}

Handle Touch Event

When the button is tapped there should be some feedback to the user to indicate the touch has been received. A change of background alpha is used with a small delay. The touched ended function also determines if the users finger is still on the button before executing the buttons command.

// CommandButton.swift
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
    self.border.alpha = touchedAlpha
}

override func touchesEnded(_ touches: Set, with event: UIEvent?) {
    self.run(SKAction.wait(forDuration: touchDelay), completion: {
        guard let touch = touches.first else { return }
        let location = touch.location(in: self)

        if self.main.contains(location) {
            self.command.execute()
        }

        self.border.alpha = self.normalAlpha
    })
}

Adding the Buttons to the TileMap

For the buttons to stay in place above the map, they need to added to the scene as children of the camera, or within a component that is a child of the camera. The dashboard component serves this purpose.

// MarsGameScene.swift
self.dash = Dashboard(displayRect: scene.frame)
self.dash.name = dashboardName
camera.addChild(self.dash)

The buttons are initialised and positioned within the Dashboard component.

// Dashboard.swift
func addCommandButtons() {   
    let buttons = self.systemCommands()
    for button in buttons {
        self.buttonContainer.addChild(button)
    }
}

The Buttons on the map.
Hexagonmap

References

  1. Source code for the example project is available on GitHub

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 )

Facebook photo

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

Connecting to %s