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.
References
- Source code for the example project is available on GitHub