LoginSignup
11
19

More than 5 years have passed since last update.

はじめてのswiftでテトリスを作ってみよう!

Last updated at Posted at 2017-07-17

初めてswiftを触った人でも簡単にテトリスゲームを作成

まずは完成イメージを頭に入れてやる気を出します。

Screen Shot 2017-07-15 at 8.06.42.png

目次

動作環境

MacOS 10.12.5
Xcode 7.1

まずはxcode7.1をインストールしよう。

とりあえず、Appstoreからxcodeをダウンロードすると最新版になってしまいます。
ダウンロードした際のバージョン↓8.3。これだと推奨環境の7.1でないのでエラーが出る。実はこれでだいぶはまった。

Screen Shot 2017-07-15 at 8.25.37.png

Developer用のダウンロードサイトに飛んで、IDとPWを入力する。
Screen Shot 2017-07-15 at 8.18.00.png

すると、異なるバージョンがダウンロードできるので、xcode7.1を選択してダウンロードする。

無料のチュートリアルをゲットしよう

まずは参考サイトに飛んでチュートリアルをゲットします。
Swiftris

メールアドレスを入力して、Learn Moreを押すとチュートリアルが見れるようになります。
Screen Shot 2017-07-15 at 8.47.25.png

こんな画面。
Screen Shot 2017-07-15 at 8.52.54.png

これで準備は完了です。

新規プロジェクトを作成しよう!

いらないファイルを削除してアセットを追加

Create a new Xcode Projectを選択する。
Screen Shot 2017-07-15 at 9.14.18.png

次の画面でGameを選択してnextを押す。
Screen Shot 2017-07-15 at 9.15.52.png

バージョンが古い影響で画面が少しずれるけど、進めていくには問題ないので適当に記入していく。
Screen Shot 2017-07-15 at 10.10.23.png

自分の好みの場所を選んで、その場所にファイルを作成する。
Screen Shot 2017-07-15 at 10.12.17.png

Portraitを選ぶ。
Screen Shot 2017-07-15 at 10.15.12.png

不要なファイルを削除

GameScene.sksを削除、Assets.xcassets配下のSpaceshipも削除する。

Screen Shot 2017-07-15 at 10.32.09.png

コードの中のいらない部分を削除

GameScene.swiftを見てみると下記の状態になっているかと思います。

import SpriteKit

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        /* Setup your scene here */
        let myLabel = SKLabelNode(fontNamed:"Chalkduster")
        myLabel.text = "Hello, World!";
        myLabel.fontSize = 45;
        myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame));

        self.addChild(myLabel)
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
       /* Called when a touch begins */

        for touch in touches {
            let location = touch.locationInNode(self)

            let sprite = SKSpriteNode(imageNamed:"Spaceship")

            sprite.xScale = 0.5
            sprite.yScale = 0.5
            sprite.position = location

            let action = SKAction.rotateByAngle(CGFloat(M_PI), duration:1)

            sprite.runAction(SKAction.repeatActionForever(action))

            self.addChild(sprite)
        }
    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
    }
}

このコードのいらない部分をトリミングして下記の状態にします。

import SpriteKit

class GameScene: SKScene {

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
    }
}

次はGameViewController.swiftです。

import UIKit
import SpriteKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        if let scene = GameScene(fileNamed:"GameScene") {
            // Configure the view.
            let skView = self.view as! SKView
            skView.showsFPS = true
            skView.showsNodeCount = true

            /* Sprite Kit applies additional optimizations to improve rendering performance */
            skView.ignoresSiblingOrder = true

            /* Set the scale mode to scale to fit the window */
            scene.scaleMode = .AspectFill

            skView.presentScene(scene)
        }
    }

    override func shouldAutorotate() -> Bool {
        return true
    }

    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
        if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
            return .AllButUpsideDown
        } else {
            return .All
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Release any cached data, images, etc that aren't in use.
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }
}

いらない部分を削除後のコードです。

import UIKit
import SpriteKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }
}

アセットの追加

ダウンロードする場所が少しわかりづらいのですが、 赤字の Download the necessary assets をクリックするとファイルがダウンロードされます。
Screen Shot 2017-07-15 at 10.59.36.png

ファイルを解凍すると中身が6つあります。

  • Images
  • Sounds
  • Sprites.atlas
  • icon-29pt.png
  • icon-40pt.png
  • icon-60pt.png

Screen Shot 2017-07-15 at 11.03.40.png

test(*自分で作成したフォルダ名)フォルダーにSoundsフォルダーをおきます。

Soundsフォルダーを持ってくると下記の画面が出てくるので、Copy items if neededにチェックされていることを確認して、Finishボタンを押す。
Screen Shot 2017-07-15 at 11.14.57.png

下記のような画面になっていればOK。
Screen Shot 2017-07-15 at 11.15.37.png

Sprites.atlasフォルダー、Imagesフォルダーも同じように追加する。
Screen Shot 2017-07-15 at 11.23.45.png

次はAssets.xcassetsを開いてアイコンを追加していく。ドラッグ&ドロップで枠にあてはめていく。

追加前の画像
Screen Shot 2017-07-15 at 11.25.07.png

追加後の画像
Screen Shot 2017-07-15 at 11.28.29.png

GameSceneとGameViewControllerにいよいよコードを追加していく。

GameScene.swift

import SpriteKit

class GameScene: SKScene {
    required init(coder aDecoder: NSCoder) {
        fatalError("NSCoder not supported")
    }

    override init(size: CGSize) {
        super.init(size: size)

        anchorPoint = CGPoint(x: 0, y: 1.0)

        let background = SKSpriteNode(imageNamed: "background")
        background.position = CGPoint(x: 0, y: 0)
        background.anchorPoint = CGPoint(x: 0, y: 1.0)
        addChild(background)
    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
    }
}

GameViewController.swift

import UIKit
import SpriteKit

class GameViewController: UIViewController {
    var scene: GameScene!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Configure the view.
        let skView = view as! SKView
        skView.multipleTouchEnabled = false

        // Create and configure the scene.
        scene = GameScene(size: skView.bounds.size)
        scene.scaleMode = .AspectFill

        // Present the scene.
        skView.presentScene(scene)
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }
}

配列を作る

testディレクト下にArray2D.swiftファイルを作成する。

ファイルを作成をして、SwiftFileを選択する。
Screen Shot 2017-07-15 at 11.47.59.png

ファイルの中にArray2Dクラスを作成する。

class Array2D<T> {
    let columns: Int
    let rows: Int
    // #2
    var array: Array<T?>

    init(columns: Int, rows: Int) {
        self.columns = columns
        self.rows = rows
        // #3
        array = Array<T?>(count:rows * columns, repeatedValue: nil)
    }

    // #4
    subscript(column: Int, row: Int) -> T? {
        get {
            return array[(row * columns) + column]
        }
        set(newValue) {
            array[(row * columns) + column] = newValue
        }
    }
}

時間制御

GameScene.swiftにコードを追加して、テトリスが落ちていく時間の制御を行う。

import SpriteKit

let TickLengthLevelOne = NSTimeInterval(600)

class GameScene: SKScene {
    var tick:(() -> ())?
    var tickLengthMillis = TickLengthLevelOne
    var lastTick:NSDate?

    required init(coder aDecoder: NSCoder) {
        fatalError("NSCoder not supported")
    }

    override init(size: CGSize) {
        super.init(size: size)

        anchorPoint = CGPoint(x: 0, y: 1.0)

        let background = SKSpriteNode(imageNamed: "background")
        background.position = CGPoint(x: 0, y: 0)
        background.anchorPoint = CGPoint(x: 0, y: 1.0)
        addChild(background)
    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        guard let lastTick = lastTick else {
            return
        }
        let timePassed = lastTick.timeIntervalSinceNow * -1000.0
        if timePassed > tickLengthMillis {
            self.lastTick = NSDate()
            tick?()
        }
    }

    func startTicking() {
        lastTick = NSDate()
    }

    func stopTicking() {
        lastTick = nil
    }
}

テトリスのブロックに関するクラスを作成

testディレクト下にArray2D.swiftファイルを作成した時と同じようにBlockfileを作成して、Blockクラスを書いていく。最初の部分ではブロックの色に言及している。ブルー、オレンジ、パープル、レッド、ティール(青緑的な)、黄色の6種類があります。

Block.swift

import SpriteKit

let NumberOfColors: UInt32 = 6

enum BlockColor: Int, CustomStringConvertible {

    case Blue = 0, Orange, Purple, Red, Teal, Yellow

    var spriteName: String {
        switch self {
        case .Blue:
            return "blue"
        case .Orange:
            return "orange"
        case .Purple:
            return "purple"
        case .Red:
            return "red"
        case .Teal:
            return "teal"
        case .Yellow:
            return "yellow"
        }
    }

    var description: String {
        return self.spriteName
    }

    static func random() -> BlockColor {
        return BlockColor(rawValue:Int(arc4random_uniform(NumberOfColors)))!
    }
}

次にBlockクラスの中身について書いていく。(...で中身を省略。)

Block.swift

import SpriteKit
...
    static func random() -> BlockColor {
        return BlockColor(rawValue:Int(arc4random_uniform(NumberOfColors)))!
    }
}

class Block: Hashable, CustomStringConvertible {
    // Constants
    let color: BlockColor

    // Properties
    var column: Int
    var row: Int
    var sprite: SKSpriteNode?

    var spriteName: String {
        return color.spriteName
    }

    var hashValue: Int {
        return self.column ^ self.row
    }

    var description: String {
        return "\(color): [\(column), \(row)]"
    }

    init(column:Int, row:Int, color:BlockColor) {
        self.column = column
        self.row = row
        self.color = color
    }
}

func ==(lhs: Block, rhs: Block) -> Bool {
    return lhs.column == rhs.column && lhs.row == rhs.row && lhs.color.rawValue == rhs.color.rawValue
}

様々なブロックの形を用意していこう

L字型のブロックや、横長のブロックや、凸型のブロックを用意していきます。

Screen Shot 2017-07-16 at 12.02.55.png

もう慣れたと思いますが、新規ファイルShape.swiftを作り、中身のコードを書いていきます。

Shape.swift

import SpriteKit

let NumOrientations: UInt32 = 4

enum Orientation: Int, CustomStringConvertible {
    case Zero = 0, Ninety, OneEighty, TwoSeventy

    var description: String {
        switch self {
        case .Zero:
            return "0"
        case .Ninety:
            return "90"
        case .OneEighty:
            return "180"
        case .TwoSeventy:
            return "270"
        }
    }

    static func random() -> Orientation {
        return Orientation(rawValue:Int(arc4random_uniform(NumOrientations)))!
    }

    static func rotate(orientation:Orientation, clockwise: Bool) -> Orientation {
        var rotated = orientation.rawValue + (clockwise ? 1 : -1)
        if rotated > Orientation.TwoSeventy.rawValue {
            rotated = Orientation.Zero.rawValue
        } else if rotated < 0 {
            rotated = Orientation.TwoSeventy.rawValue
        }
        return Orientation(rawValue:rotated)!
    }
}

次にShapeクラスの中身を書いていきます。(まだ記述していないメソッドがあるので、エラー表示がでると思いますが、慌てないで!)

Shape.swift

// The number of total shape varieties
let NumShapeTypes: UInt32 = 7

// Shape indexes
let FirstBlockIdx: Int = 0
let SecondBlockIdx: Int = 1
let ThirdBlockIdx: Int = 2
let FourthBlockIdx: Int = 3

class Shape: Hashable, CustomStringConvertible {
    // The color of the shape
    let color:BlockColor

    // The blocks comprising the shape
    var blocks = Array<Block>()
    // The current orientation of the shape
    var orientation: Orientation
    // The column and row representing the shape's anchor point
    var column, row:Int

    // Required Overrides
    // Subclasses must override this property
    var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [:]
    }
    // Subclasses must override this property
    var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [:]
    }

    var bottomBlocks:Array<Block> {
        guard let bottomBlocks = bottomBlocksForOrientations[orientation] else {
            return []
        }
        return bottomBlocks
    }

    // Hashable
    var hashValue:Int {
        return blocks.reduce(0) { $0.hashValue ^ $1.hashValue }
    }

    // CustomStringConvertible
    var description:String {
        return "\(color) block facing \(orientation): \(blocks[FirstBlockIdx]), \(blocks[SecondBlockIdx]), \(blocks[ThirdBlockIdx]), \(blocks[FourthBlockIdx])"
    }

    init(column:Int, row:Int, color: BlockColor, orientation:Orientation) {
        self.color = color
        self.column = column
        self.row = row
        self.orientation = orientation
        initializeBlocks()
    }

    // #6
    convenience init(column:Int, row:Int) {
        self.init(column:column, row:row, color:BlockColor.random(), orientation:Orientation.random())
    }
}

func ==(lhs: Shape, rhs: Shape) -> Bool {
    return lhs.row == rhs.row && lhs.column == rhs.column
}

画面のようにそんなもんはないと怒られますが、あとで作るので気にしないでください。

Screen Shot 2017-07-16 at 12.17.43.png

まだ定義していないメソッドを呼び出しているので、エラーが出ています。それを解決します。

Shape.swift

...
convenience init(column:Int, row:Int) {
        self.init(column:column, row:row, color:BlockColor.random(), orientation:Orientation.random())
    }

    final func initializeBlocks() {
        guard let blockRowColumnTranslations = blockRowColumnPositions[orientation] else {
            return
        }

        blocks = blockRowColumnTranslations.map { (diff) -> Block in
            return Block(column: column + diff.columnDiff, row: row + diff.rowDiff, color: color)
        }
    }

}

func ==(lhs: Shape, rhs: Shape) -> Bool {
    return lhs.row == rhs.row && lhs.column == rhs.column
}

これで大枠のブロックの形は準備できました。次にそれぞれのブロックの形について書いていきましょう。

四角のブロック

SquareShape.swift

class SquareShape:Shape {
    /*
    | 0•| 1 |
    | 2 | 3 |

    • marks the row/column indicator for the shape

    */

    // The square shape will not rotate

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero: [(0, 0), (1, 0), (0, 1), (1, 1)],
            Orientation.OneEighty: [(0, 0), (1, 0), (0, 1), (1, 1)],
            Orientation.Ninety: [(0, 0), (1, 0), (0, 1), (1, 1)],
            Orientation.TwoSeventy: [(0, 0), (1, 0), (0, 1), (1, 1)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.OneEighty:  [blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.Ninety:     [blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: [blocks[ThirdBlockIdx], blocks[FourthBlockIdx]]
        ]
    }
}

T字のブロック

TShape.swift

class TShape:Shape {
    /*
    Orientation 0

      • | 0 |
    | 1 | 2 | 3 |

    Orientation 90

      • | 1 |
        | 2 | 0 |
        | 3 |

    Orientation 180

      •
    | 1 | 2 | 3 |
        | 0 |

    Orientation 270

      • | 1 |
    | 0 | 2 |
        | 3 |

    • marks the row/column indicator for the shape

    */

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero:       [(1, 0), (0, 1), (1, 1), (2, 1)],
            Orientation.Ninety:     [(2, 1), (1, 0), (1, 1), (1, 2)],
            Orientation.OneEighty:  [(1, 2), (0, 1), (1, 1), (2, 1)],
            Orientation.TwoSeventy: [(0, 1), (1, 0), (1, 1), (1, 2)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[SecondBlockIdx], blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.Ninety:     [blocks[FirstBlockIdx], blocks[FourthBlockIdx]],
            Orientation.OneEighty:  [blocks[FirstBlockIdx], blocks[SecondBlockIdx], blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: [blocks[FirstBlockIdx], blocks[FourthBlockIdx]]
        ]
    }
}

一本棒のブロック

LineShape.swift

class LineShape:Shape {
    /*
        Orientations 0 and 180:

            | 0•|
            | 1 |
            | 2 |
            | 3 |

        Orientations 90 and 270:

        | 0 | 1•| 2 | 3 |

    • marks the row/column indicator for the shape

    */

    // Hinges about the second block

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero:       [(0, 0), (0, 1), (0, 2), (0, 3)],
            Orientation.Ninety:     [(-1,0), (0, 0), (1, 0), (2, 0)],
            Orientation.OneEighty:  [(0, 0), (0, 1), (0, 2), (0, 3)],
            Orientation.TwoSeventy: [(-1,0), (0, 0), (1, 0), (2, 0)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[FourthBlockIdx]],
            Orientation.Ninety:     blocks,
            Orientation.OneEighty:  [blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: blocks
        ]
    }
}

L字のブロック

LShape.swift

class LShape:Shape {
    /*

    Orientation 0

        | 0•|
        | 1 |
        | 2 | 3 |

    Orientation 90

          •
    | 2 | 1 | 0 |
    | 3 |

    Orientation 180

    | 3 | 2•|
        | 1 |
        | 0 |

    Orientation 270

          • | 3 |
    | 0 | 1 | 2 |

    • marks the row/column indicator for the shape

    Pivots about `1`

    */

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero:       [ (0, 0), (0, 1),  (0, 2),  (1, 2)],
            Orientation.Ninety:     [ (1, 1), (0, 1),  (-1,1), (-1, 2)],
            Orientation.OneEighty:  [ (0, 2), (0, 1),  (0, 0),  (-1,0)],
            Orientation.TwoSeventy: [ (-1,1), (0, 1),  (1, 1),   (1,0)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.Ninety:     [blocks[FirstBlockIdx], blocks[SecondBlockIdx], blocks[FourthBlockIdx]],
            Orientation.OneEighty:  [blocks[FirstBlockIdx], blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: [blocks[FirstBlockIdx], blocks[SecondBlockIdx], blocks[ThirdBlockIdx]]
        ]
    }
}

J字のブロック

JShape.swift

class JShape:Shape {
    /*

    Orientation 0

      • | 0 |
        | 1 |
    | 3 | 2 |

    Orientation 90

    | 3•|
    | 2 | 1 | 0 |

    Orientation 180

    | 2•| 3 |
    | 1 |
    | 0 |

    Orientation 270

    | 0•| 1 | 2 |
            | 3 |

    • marks the row/column indicator for the shape

    Pivots about `1`

    */

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero:       [(1, 0), (1, 1),  (1, 2),  (0, 2)],
            Orientation.Ninety:     [(2, 1), (1, 1),  (0, 1),  (0, 0)],
            Orientation.OneEighty:  [(0, 2), (0, 1),  (0, 0),  (1, 0)],
            Orientation.TwoSeventy: [(0, 0), (1, 0),  (2, 0),  (2, 1)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.Ninety:     [blocks[FirstBlockIdx], blocks[SecondBlockIdx], blocks[ThirdBlockIdx]],
            Orientation.OneEighty:  [blocks[FirstBlockIdx], blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: [blocks[FirstBlockIdx], blocks[SecondBlockIdx], blocks[FourthBlockIdx]]
        ]
    }
}

S字のブロック

SShape.swift

class SShape:Shape {
    /*

    Orientation 0

    | 0•|
    | 1 | 2 |
        | 3 |

    Orientation 90

      • | 1 | 0 |
    | 3 | 2 |

    Orientation 180

    | 0•|
    | 1 | 2 |
        | 3 |

    Orientation 270

      • | 1 | 0 |
    | 3 | 2 |

    • marks the row/column indicator for the shape

    */

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero:       [(0, 0), (0, 1), (1, 1), (1, 2)],
            Orientation.Ninety:     [(2, 0), (1, 0), (1, 1), (0, 1)],
            Orientation.OneEighty:  [(0, 0), (0, 1), (1, 1), (1, 2)],
            Orientation.TwoSeventy: [(2, 0), (1, 0), (1, 1), (0, 1)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[SecondBlockIdx], blocks[FourthBlockIdx]],
            Orientation.Ninety:     [blocks[FirstBlockIdx], blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.OneEighty:  [blocks[SecondBlockIdx], blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: [blocks[FirstBlockIdx], blocks[ThirdBlockIdx], blocks[FourthBlockIdx]]
        ]
    }
}

Z字のブロック

ZShape.swift

class ZShape:Shape {
    /*

    Orientation 0

      • | 0 |
    | 2 | 1 |
    | 3 |

    Orientation 90

    | 0 | 1•|
        | 2 | 3 |

    Orientation 180

      • | 0 |
    | 2 | 1 |
    | 3 |

    Orientation 270

    | 0 | 1•|
        | 2 | 3 |


    • marks the row/column indicator for the shape

    */

    override var blockRowColumnPositions: [Orientation: Array<(columnDiff: Int, rowDiff: Int)>] {
        return [
            Orientation.Zero:       [(1, 0), (1, 1), (0, 1), (0, 2)],
            Orientation.Ninety:     [(-1,0), (0, 0), (0, 1), (1, 1)],
            Orientation.OneEighty:  [(1, 0), (1, 1), (0, 1), (0, 2)],
            Orientation.TwoSeventy: [(-1,0), (0, 0), (0, 1), (1, 1)]
        ]
    }

    override var bottomBlocksForOrientations: [Orientation: Array<Block>] {
        return [
            Orientation.Zero:       [blocks[SecondBlockIdx], blocks[FourthBlockIdx]],
            Orientation.Ninety:     [blocks[FirstBlockIdx], blocks[ThirdBlockIdx], blocks[FourthBlockIdx]],
            Orientation.OneEighty:  [blocks[SecondBlockIdx], blocks[FourthBlockIdx]],
            Orientation.TwoSeventy: [blocks[FirstBlockIdx], blocks[ThirdBlockIdx], blocks[FourthBlockIdx]]
        ]
    }
}

作成したブロックを落としてみよう!

Shape.swift

     final func initializeBlocks() {
        guard let blockRowColumnTranslations = blockRowColumnPositions[orientation] else {
            return
        }
        blocks = blockRowColumnTranslations.map { (diff) -> Block in
            return Block(column: column + diff.columnDiff, row: row + diff.rowDiff, color: color)
        }
    }

     final func rotateBlocks(orientation: Orientation) {
         guard let blockRowColumnTranslation:Array<(columnDiff: Int, rowDiff: Int)> = blockRowColumnPositions[orientation] else {
             return
         }
// #1
         for (idx, diff) in blockRowColumnTranslation.enumerate() {
             blocks[idx].column = column + diff.columnDiff
             blocks[idx].row = row + diff.rowDiff
         }
     }

     final func lowerShapeByOneRow() {
         shiftBy(0, rows:1)
     }

// #2
     final func shiftBy(columns: Int, rows: Int) {
         self.column += columns
         self.row += rows
         for block in blocks {
             block.column += columns
             block.row += rows
         }
     }

// #3
     final func moveTo(column: Int, row:Int) {
         self.column = column
         self.row = row
         rotateBlocks(orientation)
     }

     final class func random(startingColumn:Int, startingRow:Int) -> Shape {
         switch Int(arc4random_uniform(NumShapeTypes)) {
// #4
         case 0:
             return SquareShape(column:startingColumn, row:startingRow)
         case 1:
             return LineShape(column:startingColumn, row:startingRow)
         case 2:
             return TShape(column:startingColumn, row:startingRow)
         case 3:
             return LShape(column:startingColumn, row:startingRow)
         case 4:
             return JShape(column:startingColumn, row:startingRow)
         case 5:
             return SShape(column:startingColumn, row:startingRow)
         default:
             return ZShape(column:startingColumn, row:startingRow)
         }
     }

次にSwiftris.swiftを作成していきます。

Swiftris.swift

let NumColumns = 10
let NumRows = 20

let StartingColumn = 4
let StartingRow = 0

let PreviewColumn = 12
let PreviewRow = 1

class Swiftris {
    var blockArray:Array2D<Block>
    var nextShape:Shape?
    var fallingShape:Shape?

    init() {
        fallingShape = nil
        nextShape = nil
        blockArray = Array2D<Block>(columns: NumColumns, rows: NumRows)
    }

    func beginGame() {
        if (nextShape == nil) {
            nextShape = Shape.random(PreviewColumn, startingRow: PreviewRow)
        }
    }

    // #6
    func newShape() -> (fallingShape:Shape?, nextShape:Shape?) {
        fallingShape = nextShape
        nextShape = Shape.random(PreviewColumn, startingRow: PreviewRow)
        fallingShape?.moveTo(StartingColumn, row: StartingRow)
        return (fallingShape, nextShape)
    }
}

GameScene.swiftを編集していきます。

GameScene.swift

import SpriteKit

// #7
 let BlockSize:CGFloat = 20.0

 let TickLengthLevelOne = NSTimeInterval(600)

class GameScene: SKScene {
// #8
    let gameLayer = SKNode()
    let shapeLayer = SKNode()
    let LayerPosition = CGPoint(x: 6, y: -6)

    var tick:(() -> ())?
    var tickLengthMillis = TickLengthLevelOne
    var lastTick:NSDate?

    var textureCache = Dictionary<String, SKTexture>()

    required init(coder aDecoder: NSCoder) {
        fatalError("NSCoder not supported")
    }

    override init(size: CGSize) {
        super.init(size: size)

        anchorPoint = CGPoint(x: 0, y: 1.0)

        let background = SKSpriteNode(imageNamed: "background")
        background.position = CGPoint(x: 0, y: 0)
        background.anchorPoint = CGPoint(x: 0, y: 1.0)
        addChild(background)

        addChild(gameLayer)

        let gameBoardTexture = SKTexture(imageNamed: "gameboard")
        let gameBoard = SKSpriteNode(texture: gameBoardTexture, size: CGSizeMake(BlockSize * CGFloat(NumColumns), BlockSize * CGFloat(NumRows)))
        gameBoard.anchorPoint = CGPoint(x:0, y:1.0)
        gameBoard.position = LayerPosition

        shapeLayer.position = LayerPosition
        shapeLayer.addChild(gameBoard)
        gameLayer.addChild(shapeLayer)
    }

    override func update(currentTime: CFTimeInterval) {
        /* Called before each frame is rendered */
        guard let lastTick = lastTick else {
            return
        }
        let timePassed = lastTick.timeIntervalSinceNow * -1000.0
        if timePassed > tickLengthMillis {
            self.lastTick = NSDate()
            tick?()
        }
    }

    func startTicking() {
        lastTick = NSDate()
    }

    func stopTicking() {
        lastTick = nil
    }

// #9
     func pointForColumn(column: Int, row: Int) -> CGPoint {
         let x = LayerPosition.x + (CGFloat(column) * BlockSize) + (BlockSize / 2)
         let y = LayerPosition.y - ((CGFloat(row) * BlockSize) + (BlockSize / 2))
         return CGPointMake(x, y)
     }

     func addPreviewShapeToScene(shape:Shape, completion:() -> ()) {
         for block in shape.blocks {
// #10
             var texture = textureCache[block.spriteName]
             if texture == nil {
                 texture = SKTexture(imageNamed: block.spriteName)
                 textureCache[block.spriteName] = texture
             }
             let sprite = SKSpriteNode(texture: texture)
// #11
             sprite.position = pointForColumn(block.column, row:block.row - 2)
             shapeLayer.addChild(sprite)
             block.sprite = sprite

             // Animation
             sprite.alpha = 0
// #12
             let moveAction = SKAction.moveTo(pointForColumn(block.column, row: block.row), duration: NSTimeInterval(0.2))
             moveAction.timingMode = .EaseOut
             let fadeInAction = SKAction.fadeAlphaTo(0.7, duration: 0.4)
             fadeInAction.timingMode = .EaseOut
             sprite.runAction(SKAction.group([moveAction, fadeInAction]))
        }
         runAction(SKAction.waitForDuration(0.4), completion: completion)
     }

     func movePreviewShape(shape:Shape, completion:() -> ()) {
         for block in shape.blocks {
             let sprite = block.sprite!
             let moveTo = pointForColumn(block.column, row:block.row)
             let moveToAction:SKAction = SKAction.moveTo(moveTo, duration: 0.2)
             moveToAction.timingMode = .EaseOut
             sprite.runAction(
                 SKAction.group([moveToAction, SKAction.fadeAlphaTo(1.0, duration: 0.2)]), completion: {})
         }
         runAction(SKAction.waitForDuration(0.2), completion: completion)
     }

     func redrawShape(shape:Shape, completion:() -> ()) {
         for block in shape.blocks {
             let sprite = block.sprite!
             let moveTo = pointForColumn(block.column, row:block.row)
             let moveToAction:SKAction = SKAction.moveTo(moveTo, duration: 0.05)
             moveToAction.timingMode = .EaseOut
             if block == shape.blocks.last {
                 sprite.runAction(moveToAction, completion: completion)
             } else {
                 sprite.runAction(moveToAction)
             }
         }
     }
 }

GameViewControllerを修正していきます。

GameViewController.swift

import UIKit
import SpriteKit

class GameViewController: UIViewController {

    var scene: GameScene!
    var swiftris:Swiftris!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Configure the view.
        let skView = view as! SKView
        skView.multipleTouchEnabled = false

        // Create and configure the scene.
        scene = GameScene(size: skView.bounds.size)
        scene.scaleMode = .AspectFill
// #13
        scene.tick = didTick

        swiftris = Swiftris()
        swiftris.beginGame()

        // Present the scene.
        skView.presentScene(scene)

// #14
         scene.addPreviewShapeToScene(swiftris.nextShape!) {
             self.swiftris.nextShape?.moveTo(StartingColumn, row: StartingRow)
             self.scene.movePreviewShape(self.swiftris.nextShape!) {
                 let nextShapes = self.swiftris.newShape()
                 self.scene.startTicking()
                 self.scene.addPreviewShapeToScene(nextShapes.nextShape!) {}
             }
         }
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }

// #15
     func didTick() {
         swiftris.fallingShape?.lowerShapeByOneRow()
         scene.redrawShape(swiftris.fallingShape!, completion: {})
     }
}

まだ地面は突き抜けてしまいますが、ブロックが上から落ちてきているはずです。

ルールの設定をしよう

Swiftris.swiftにコードを加えていきます。

Swiftris.swift

...
let PreviewRow = 1

 protocol SwiftrisDelegate {
     // Invoked when the current round of Swiftris ends
     func gameDidEnd(swiftris: Swiftris)

     // Invoked after a new game has begun
     func gameDidBegin(swiftris: Swiftris)

     // Invoked when the falling shape has become part of the game board
     func gameShapeDidLand(swiftris: Swiftris)

     // Invoked when the falling shape has changed its location
     func gameShapeDidMove(swiftris: Swiftris)

     // Invoked when the falling shape has changed its location after being dropped
     func gameShapeDidDrop(swiftris: Swiftris)

     // Invoked when the game has reached a new level
     func gameDidLevelUp(swiftris: Swiftris)
 }

class Swiftris {
    var blockArray:Array2D<Block>
    var nextShape:Shape?
    var fallingShape:Shape?
    var delegate:SwiftrisDelegate?
...

さらに修正を加えていきます。この時点ではendGame()が定義されていないのでエラーがでますが、気にしないで進んでください。

Swiftris.swift

...
func beginGame() {
        if (nextShape == nil) {
            nextShape = Shape.random(PreviewColumn, startingRow: PreviewRow)
        }
        delegate?.gameDidBegin(self)
    }

    func newShape() -> (fallingShape:Shape?, nextShape:Shape?) {
        fallingShape = nextShape
        nextShape = Shape.random(PreviewColumn, startingRow: PreviewRow)
        fallingShape?.moveTo(StartingColumn, row: StartingRow)
// #1
         guard detectIllegalPlacement() == false else {
             nextShape = fallingShape
             nextShape!.moveTo(PreviewColumn, row: PreviewRow)
             endGame()
             return (nil, nil)
         }
         return (fallingShape, nextShape)
     }

// #2
     func detectIllegalPlacement() -> Bool {
         guard let shape = fallingShape else {
             return false
         }
         for block in shape.blocks {
             if block.column < 0 || block.column >= NumColumns
                 || block.row < 0 || block.row >= NumRows {
                 return true
             } else if blockArray[block.column, block.row] != nil {
                 return true
             }
         }
     return false
 }
...

endGame()を定義する前に、Shape.swiftにコードを加えます。

Shape.swift

...
final func rotateBlocks(orientation: Orientation) {
        guard let blockRowColumnTranslation:Array<(columnDiff: Int, rowDiff: Int)> = blockRowColumnPositions[orientation] else {
            return
        }
        for (idx, diff) in blockRowColumnTranslation.enumerate() {
            blocks[idx].column = column + diff.columnDiff
            blocks[idx].row = row + diff.rowDiff
        }
    }

// #3
     final func rotateClockwise() {
         let newOrientation = Orientation.rotate(orientation, clockwise: true)
         rotateBlocks(newOrientation)
         orientation = newOrientation
     }

     final func rotateCounterClockwise() {
         let newOrientation = Orientation.rotate(orientation, clockwise: false)
         rotateBlocks(newOrientation)
         orientation = newOrientation
     }

    final func lowerShapeByOneRow() {
        shiftBy(0, rows:1)
    }

     final func raiseShapeByOneRow() {
         shiftBy(0, rows:-1)
     }

     final func shiftRightByOneColumn() {
         shiftBy(1, rows:0)
     }

     final func shiftLeftByOneColumn() {
         shiftBy(-1, rows:0)
     }

    final func shiftBy(columns: Int, rows: Int) {
        self.column += columns
        self.row += rows
        for block in blocks {
            block.column += columns
            block.row += rows
        }
    }
...

再度Swiftris.swiftを修正していきます。ブロックが回転したり、左右に動いたりする機能を追加していきます。いくつかの関数がまだ定義していないので、エラーがでると思いますが、気にせず次に進んでください。

Swiftris.swift

...
func detectIllegalPlacement() -> Bool {
        guard let shape = fallingShape else {
            return false
        }
        for block in shape.blocks {
            if block.column < 0 || block.column >= NumColumns
                || block.row < 0 || block.row >= NumRows {
                return true
            } else if blockArray[block.column, block.row] != nil {
                return true
            }
        }
        return false
    }

// #4
     func dropShape() {
         guard let shape = fallingShape else {
             return
         }
         while detectIllegalPlacement() == false {
             shape.lowerShapeByOneRow()
         }
         shape.raiseShapeByOneRow()
         delegate?.gameShapeDidDrop(self)
     }

// #5
     func letShapeFall() {
         guard let shape = fallingShape else {
             return
         }
         shape.lowerShapeByOneRow()
         if detectIllegalPlacement() {
             shape.raiseShapeByOneRow()
             if detectIllegalPlacement() {
                 endGame()
             } else {
                 settleShape()
             }
         } else {
             delegate?.gameShapeDidMove(self)
             if detectTouch() {
                 settleShape()
             }
         }
     }

// #6
     func rotateShape() {
         guard let shape = fallingShape else {
             return
         }
         shape.rotateClockwise()
         guard detectIllegalPlacement() == false else {
             shape.rotateCounterClockwise()
             return
         }
         delegate?.gameShapeDidMove(self)
     }

// #7
     func moveShapeLeft() {
         guard let shape = fallingShape else {
             return
         }
         shape.shiftLeftByOneColumn()
         guard detectIllegalPlacement() == false else {
             shape.shiftRightByOneColumn()
             return
         }
         delegate?.gameShapeDidMove(self)
     }

     func moveShapeRight() {
         guard let shape = fallingShape else {
             return
         }
         shape.shiftRightByOneColumn()
         guard detectIllegalPlacement() == false else {
             shape.shiftLeftByOneColumn()
             return
         }
         delegate?.gameShapeDidMove(self)
     }
...

最後にエラーが出ている部分を解消していきます。

...
func settleShape() {
         guard let shape = fallingShape else {
             return
         }
         for block in shape.blocks {
             blockArray[block.column, block.row] = block
         }
         fallingShape = nil
         delegate?.gameShapeDidLand(self)
     }

// #9
     func detectTouch() -> Bool {
         guard let shape = fallingShape else {
             return false
         }
         for bottomBlock in shape.bottomBlocks {
             if bottomBlock.row == NumRows - 1
                 || blockArray[bottomBlock.column, bottomBlock.row + 1] != nil {
                     return true
             }
         }
         return false
     }

     func endGame() {
         delegate?.gameDidEnd(self)
     }

    func dropShape() {
        guard let shape = fallingShape else {
            return
        }
        while detectIllegalPlacement() == false {
            shape.lowerShapeByOneRow()
        }
        shape.raiseShapeByOneRow()
        delegate?.gameShapeDidDrop(self)
    }
...

エラーは消えているでしょうか?消えていない場合は、どこかで追加する場所を間違えたりしているかもしれません。

得点について

ただ上から落ちてくるテトリスではつまらないので、ブロックを消したときの得点についてのコードを追加していきます。

Swiftris.swift

...
 let PreviewColumn = 12
 let PreviewRow = 1

 let PointsPerLine = 10
 let LevelThreshold = 500

 protocol SwiftrisDelegate {
...

スコアとレベルの初期値を設定。

...
var blockArray:Array2D<Block>
    var nextShape:Shape?
    var fallingShape:Shape?
    var delegate:SwiftrisDelegate?

    var score = 0
    var level = 1

    init() {
        fallingShape = nil
        nextShape = nil
        blockArray = Array2D<Block>(columns: NumColumns, rows: NumRows)
    }
...
...
func endGame() {
         score = 0
         level = 1
        delegate?.gameDidEnd(self)
    }
...
...
func endGame() {
         score = 0
         level = 1
        delegate?.gameDidEnd(self)
    }
...

次にテトリスで横一列を揃えたときに関するコードを書いていきます。

...
func endGame() {
        score = 0
        level = 1
        delegate?.gameDidEnd(self)
     }

// #10
     func removeCompletedLines() -> (linesRemoved: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>) {
         var removedLines = Array<Array<Block>>()
         for row in (1..<NumRows).reverse() {
             var rowOfBlocks = Array<Block>()
// #11
             for column in 0..<NumColumns {
                 guard let block = blockArray[column, row] else {
                     continue
                 }
                 rowOfBlocks.append(block)
             }
             if rowOfBlocks.count == NumColumns {
                 removedLines.append(rowOfBlocks)
                 for block in rowOfBlocks {
                     blockArray[block.column, block.row] = nil
                 }
             }
         }

// #12
         if removedLines.count == 0 {
             return ([], [])
         }
// #13
         let pointsEarned = removedLines.count * PointsPerLine * level
         score += pointsEarned
         if score >= level * LevelThreshold {
             level += 1
             delegate?.gameDidLevelUp(self)
         }

         var fallenBlocks = Array<Array<Block>>()
         for column in 0..<NumColumns {
             var fallenBlocksArray = Array<Block>()
// #14
             for row in (1..<removedLines[0][0].row).reverse() {
                 guard let block = blockArray[column, row] else {
                     continue
                 }
                 var newRow = row
                 while (newRow < NumRows - 1 && blockArray[column, newRow + 1] == nil) {
                     newRow += 1
                 }
                 block.row = newRow
                 blockArray[column, row] = nil
                 blockArray[column, newRow] = block
                 fallenBlocksArray.append(block)
             }
             if fallenBlocksArray.count > 0 {
                 fallenBlocks.append(fallenBlocksArray)
             }
         }
         return (removedLines, fallenBlocks)
     }
...

ブロックを消す動作をする機能を書いていきます。

Swiftris.swift

func removeAllBlocks() -> Array<Array<Block>> {
         var allBlocks = Array<Array<Block>>()
         for row in 0..<NumRows {
             var rowOfBlocks = Array<Block>()
             for column in 0..<NumColumns {
                 guard let block = blockArray[column, row] else {
                     continue
                 }
                 rowOfBlocks.append(block)
                 blockArray[column, row] = nil
             }
             allBlocks.append(rowOfBlocks)
         }
         return allBlocks
     }

GameViewController.swiftに修正を加えていきます。

GameViewController.swift

class GameViewController: UIViewController, SwiftrisDelegate {
swiftris = Swiftris()
swiftris.delegate = self
swiftris.beginGame()
削除する部分!!!

scene.addPreviewShapeToScene(swiftris.nextShape!) {
             self.swiftris.nextShape?.moveTo(StartingColumn, row: StartingRow)
             self.scene.movePreviewShape(self.swiftris.nextShape!) {
                 let nextShapes = self.swiftris.newShape()
                 self.scene.startTicking()
                 self.scene.addPreviewShapeToScene(nextShapes.nextShape!) {}
             }
         }
func didTick() {
// #15
         swiftris.letShapeFall()
     }

     func nextShape() {
         let newShapes = swiftris.newShape()
         guard let fallingShape = newShapes.fallingShape else {
             return
         }
         self.scene.addPreviewShapeToScene(newShapes.nextShape!) {}
         self.scene.movePreviewShape(fallingShape) {
// #16
             self.view.userInteractionEnabled = true
             self.scene.startTicking()
         }
     }

     func gameDidBegin(swiftris: Swiftris) {
         // The following is false when restarting a new game
         if swiftris.nextShape != nil && swiftris.nextShape!.blocks[0].sprite == nil {
             scene.addPreviewShapeToScene(swiftris.nextShape!) {
                 self.nextShape()
             }
         } else {
             nextShape()
         }
     }

     func gameDidEnd(swiftris: Swiftris) {
         view.userInteractionEnabled = false
         scene.stopTicking()
     }

     func gameDidLevelUp(swiftris: Swiftris) {

     }

     func gameShapeDidDrop(swiftris: Swiftris) {

     }

     func gameShapeDidLand(swiftris: Swiftris) {
         scene.stopTicking()
         nextShape()
     }

// #17
     func gameShapeDidMove(swiftris: Swiftris) {
         scene.redrawShape(swiftris.fallingShape!) {}
     }

ここまでくるとブロックが地面へ突き抜けていかないはずです。

クリックして回転させたり、動かしてみよう

まずは、Main.storyboardを開きます。

Screen Shot 2017-07-17 at 20.51.04.png

右下でTapと検索して、Tap Gesture Recognizerを探します。それを黒い画面にドラッグアンドドロップします。

Screen Shot 2017-07-17 at 20.51.40.png

画面右上の部分をクリックして、Assistant Editorで開きます。

Screen Shot 2017-07-17 at 20.55.01.png

下記画面のようにTap Gesture RecognizerをControlを押しながらGameViewControllerにドラッグアンドドロップします。

  • Action
  • didTap
  • UITapGestureRecognizer

Screen Shot 2017-07-17 at 21.07.19.png

うまくいけば、GameViewControllerに下記のコードを加わっているはずです。

@IBAction func didTap(sender: UITapGestureRecognizer) {
}

Tap Gesture Recognizerを右クリックして、下記の画面のようにせっていします。

Screen Shot 2017-07-17 at 21.12.38.png

GameViewController.swiftに少し修正を加えます。

GameViewController.swift

class GameViewController: UIViewController, SwiftrisDelegate, UIGestureRecognizerDelegate {
@IBAction func didTap(sender: UITapGestureRecognizer) {
     swiftris.rotateShape()
 }

これでブロックが回転しているはずです。

11
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
19