import
to read as follows:
import SpriteKit
var topInset = CGFloat(0) //will be used to offset labels on devices with notch
class
)
var gameScene : GameScene!
import
statement as follows:
import SpriteKit
viewDidLoad
that is provided (i.e. in the class but outside any other method):
override func viewDidLayoutSubviews() {
//create the game scene and configure it a bit
gameScene = GameScene(size: view.bounds.size)
gameScene.scaleMode = .resizeFill
gameScene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
gameScene.topInset = view.safeAreaInsets.top //accounts for notch. This is 0 in viewDidLoad(), so have to move here
//get the view from this view controller, cast it as a SKView and present the scene
let skView = self.view as! SKView
skView.presentScene(gameScene)
}
override var shouldAutorotate: Bool {
return false
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var prefersStatusBarHidden: Bool {
return true
}
The view controller is now complete - so it's time to start writing the logic for the game
On of the key elements of the game is a light that can be toggled on and off, and which will also be able to notify the game that it has been tapped. We will create a class to do this.
//designated initialzer, so we must override it
override init(){
super.init()
}
//required initializer, must implement (even though it wont be used)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private (set) var lit = false
private var index : Int?
func toggle(){
lit.toggle()
fillColor = lit ? UIColor.yellow : UIColor.purple
}
protocol LightDelegate {
func lightPressed(id: Int)
}
private var delegate : LightDelegate?
init(rect: CGRect, cornerRadius: CGFloat, index: Int, delegate: LightDelegate){
//call parent class initialisers
super.init()
self.init(rect: rect, cornerRadius: cornerRadius)
//configure the node
isUserInteractionEnabled = true //ensures that the node can handle touch events
fillColor = UIColor.purple
self.index = index
self.delegate = delegate
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let actualIndex = index {
delegate?.lightPressed(id: actualIndex)
}
}
Notice how this informs the delegate (in this project it will be the GameScene) that it has been pressed. Because lots of LightNode objects exist, the index allows it to be differentiatedThe Light node class is now completed. All that remains is to write the logic for the game!
private var buttons : Array<LightNode>!
private var moves = 0 //counts the number of moves the player makes
private var newGameLabel : SKLabelNode! //displays 'new game' on screen and will be tappable
private var movesLabel : SKLabelNode? //displays the number of moves
private var gameNumberLabel : SKLabelNode? //displays a unique number to represent the game being played, based on the light pattern at the start
private func addNewGameLabel(){
newGameLabel = SKLabelNode(text: "New Game")
newGameLabel.fontName = "Copperplate"
newGameLabel.fontColor = SKColor.white
newGameLabel.fontSize = 22.0
newGameLabel.position = CGPoint(x: 0, y: (frame.size.height/2)-(newGameLabel.frame.height+topInset + 10))
addChild(newGameLabel)
}
private func updateMovesLabel(){
if movesLabel != nil {
movesLabel!.removeFromParent()
}
movesLabel = SKLabelNode(text: "Moves: \(moves)")
movesLabel!.fontName = "Copperplate"
movesLabel!.fontColor = SKColor.white
movesLabel!.fontSize = 14.0
movesLabel!.position = CGPoint(x: frame.width/2 - (movesLabel!.frame.width/2), y: (frame.size.height/2) - (movesLabel!.frame.height+topInset))
addChild(movesLabel!)
}
private func gameNumberLabel(number: UInt32){
if gameNumberLabel != nil {
gameNumberLabel!.removeFromParent()
}
gameNumberLabel = SKLabelNode(text: "#: \(number)")
gameNumberLabel!.fontName = "Copperplate"
gameNumberLabel!.fontColor = SKColor.white
gameNumberLabel!.fontSize = 14.0
gameNumberLabel!.position = CGPoint(x:gameNumberLabel!.frame.width/2 - frame.width/2, y: (frame.size.height/2) - (gameNumberLabel!.frame.height+topInset))
addChild(gameNumberLabel!)
}
class GameScene: SKScene, LightDelegate {
private func drawLightBoard(){
buttons = Array()
//centre of canvas is 0,0
let width = min(frame.size.width, frame.size.height)
let buttonWidth = width/5
for i in 0...24 {
//grid drawn from bottom left
let row = CGFloat(i % 5)
let col = CGFloat(i / 5);
let offset = 0 - width/2
let location = CGRect(x: offset + (buttonWidth * row), y: offset + (buttonWidth * col), width: buttonWidth, height:
buttonWidth)
let lightNode = LightNode(rect: location, cornerRadius: buttonWidth/4, index:i, delegate: self)
//animate the buttons by fading them into view
lightNode.alpha = 0
let animation = SKAction.fadeIn(withDuration: 2.0)
lightNode.run(animation)
//add to screen and also add references the array
addChild(lightNode)
buttons.append(lightNode)
}
}
private func togglePattern(id: Int){
//this light
buttons[id].toggle()
//below
if id >= 5 {
buttons[id-5].toggle()
}
//left
if id % 5 > 0 {
buttons[id-1].toggle()
}
//right
if id % 5 < 4 {
buttons[id+1].toggle()
}
//above
if id < 20 {
buttons[id+5].toggle()
}
}
Again, try and understand how the maths allows us to determine the index of the buttons adjacent to the one being pressedprivate func randomLights() -> UInt32 {
var gameNumber : UInt32 = 0
for i in 0...24 {
gameNumber <<= 1 // (e.g. 00001100 becomes 00011000)
if Bool.random() { //use arc4random_uniform(2) == 1 for Swift 4.1 and earlier
togglePattern(id: i) //ensure that the game can be won, as each press toggles the same lights the player does
gameNumber += 1
//print(i) //uncomment this to see which lights to turn off to win the game
}
}
return gameNumber
}
private func dropButtons(){
var sequence = [SKAction]()
while !buttons.isEmpty {
let button = buttons.randomElement()!
button.isUserInteractionEnabled = false //prevent user tapping old button and triggering action
buttons.remove(at: buttons.firstIndex(of: button)!)
let delay = SKAction.wait(forDuration: 0.03)
let dropButton = SKAction.run {
button.physicsBody = SKPhysicsBody(rectangleOf: button.frame.size)
button.physicsBody?.collisionBitMask = 0 // do not collide with anything
}
sequence.append(dropButton)
sequence.append(delay)
}
run(SKAction.sequence(sequence))
}
Note how the code above builds the sequence inside the loop, but then executes it after the loop is complete, this enables the delay between each animation.private func completed() -> Bool{
for button in buttons {
if button.lit {
return false
}
}
return true
}
private func newGame(){
drawLightBoard()
let gameNumber = randomLights()
gameNumberLabel(number: gameNumber)
moves = 0
updateMovesLabel()
}
override func didMove(to view: SKView) {
newGame()
addNewGameLabel()
}
private func incrementMovesCount() {
moves += 1
updateMovesLabel()
}
lightPressed(id: Int)
method and add the following code:
incrementMovesCount()
togglePattern(id: id)
if completed() {
dropButtons()
}
You should now be able to play the game, but you will not be able to start a new one, even if you completed itoverride func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if newGameLabel.frame.contains(touches.first!.location(in: self)) {
newGameLabel.isUserInteractionEnabled = true //no action associated so essentially disables the button
dropButtons()
let delay = SKAction.wait(forDuration: 1.0)
let newGame = SKAction.run {
self.newGame()
}
let reEnableNewGameButton = SKAction.run {
self.newGameLabel.isUserInteractionEnabled = false
}
let sequence = SKAction.sequence([delay,newGame,reEnableNewGameButton])
run(sequence)
}
}
A completed version of the game can be downloaded from GitHub