初めてswiftを触った人でも簡単にテトリスゲームを作成
まずは完成イメージを頭に入れてやる気を出します。
#目次
#動作環境
MacOS 10.12.5
Xcode 7.1
#まずはxcode7.1をインストールしよう。
とりあえず、Appstoreからxcodeをダウンロードすると最新版になってしまいます。
ダウンロードした際のバージョン↓8.3。これだと推奨環境の7.1でないのでエラーが出る。実はこれでだいぶはまった。
Developer用のダウンロードサイトに飛んで、IDとPWを入力する。
すると、異なるバージョンがダウンロードできるので、xcode7.1を選択してダウンロードする。
#無料のチュートリアルをゲットしよう
まずは参考サイトに飛んでチュートリアルをゲットします。
Swiftris
メールアドレスを入力して、Learn Moreを押すとチュートリアルが見れるようになります。
これで準備は完了です。
#新規プロジェクトを作成しよう!
###いらないファイルを削除してアセットを追加
Create a new Xcode Projectを選択する。
バージョンが古い影響で画面が少しずれるけど、進めていくには問題ないので適当に記入していく。
#不要なファイルを削除
GameScene.sksを削除、Assets.xcassets配下のSpaceshipも削除する。
###コードの中のいらない部分を削除
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
をクリックするとファイルがダウンロードされます。
ファイルを解凍すると中身が6つあります。
- Images
- Sounds
- Sprites.atlas
- icon-29pt.png
- icon-40pt.png
- icon-60pt.png
test(*自分で作成したフォルダ名)フォルダーにSoundsフォルダーをおきます。
Soundsフォルダーを持ってくると下記の画面が出てくるので、Copy items if neededにチェックされていることを確認して、Finishボタンを押す。
Sprites.atlasフォルダー、Imagesフォルダーも同じように追加する。
次はAssets.xcassetsを開いてアイコンを追加していく。ドラッグ&ドロップで枠にあてはめていく。
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ファイルを作成する。
ファイルの中に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字型のブロックや、横長のブロックや、凸型のブロックを用意していきます。
もう慣れたと思いますが、新規ファイル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
}
画面のようにそんなもんはないと怒られますが、あとで作るので気にしないでください。
まだ定義していないメソッドを呼び出しているので、エラーが出ています。それを解決します。
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を開きます。
右下でTapと検索して、Tap Gesture Recognizerを探します。それを黒い画面にドラッグアンドドロップします。
画面右上の部分をクリックして、Assistant Editorで開きます。
下記画面のようにTap Gesture RecognizerをControlを押しながらGameViewControllerにドラッグアンドドロップします。
- Action
- didTap
- UITapGestureRecognizer
うまくいけば、GameViewControllerに下記のコードを加わっているはずです。
@IBAction func didTap(sender: UITapGestureRecognizer) {
}
Tap Gesture Recognizerを右クリックして、下記の画面のようにせっていします。
GameViewController.swiftに少し修正を加えます。
GameViewController.swift
class GameViewController: UIViewController, SwiftrisDelegate, UIGestureRecognizerDelegate {
@IBAction func didTap(sender: UITapGestureRecognizer) {
swiftris.rotateShape()
}
これでブロックが回転しているはずです。