著作権:raywenderlich.com
元のURLはココ

SwiftとSpriteKitでキャンディクラッシュのようなゲームを作るには:パート2

Xcode 9.3 and Swift 4.1用に更新。SwiftとSpriteKitを使って、モバイルゲームのキャンディクラッシュのようなゲームの作り方を学びましょう。アニメーションとゲームロジックの作り方を学べます。

Version

  • Swift 4, iOS 11, Xcode 9

更新履歴: このSpriteKitチュートリアルはXcode 9.3Swift 4.1用にケビン・コリガンによって更新されました。元となったチュートリアルは マスジス・ホルマンズ によって作成され、 続いてモルテン・ファークログによって更新されました。

チュートリアルに戻ってきてくれてありがとう。SpriteKitとSwiftで作る「キャンディクラッシュのようなゲームを作るには」チュートリアルのパート2になります。

  • パート1ではスタートプロジェクトから始めてゲームの基礎を構築しlevelを読み込むロジックを書きました。
  • パート2では(あなたが今読んでいる!)、スワイプの検知とクッキーのスワップにフォーカスを充てます。
  • パート3ではチェインの検知と削除、クッキーの再充填それにスコアの保存を学びます。

パート2ではパート1で実装していなかった部分を実装していきます。もし必要ならばこのページのトップかボトムにあるDownload Materialsボタンからパート2のスタータープロジェクトをダウンロードしてください。

スワイプ ジェスチャーを追加しましょう

クッキークランチアドベンチャーではプレヤーにクッキーをスワップさせる機能を持たせます。クッキーを左、右、上、下とスワイプジェスチャーをしてスワップさせます。

スワイプの検知はGameSceneの役割です。もしプレイヤーが画面上のクッキーをタップした場合、それはスワイプ動作の始まりの可能性があります。どのクッキーとスワイプするかはスワイプの方向によります。

スワイプ動作を検出する為にGameScenetouchesBegantouchesMovedそれにtouchesEndedメソッドを使用します。

GameScene.swiftへ移動し、次の2つのprivateプロパティをクラスへ追加してください:

private var swipeFromColumn: Int?
private var swipeFromRow: Int?

新しいメソッドconvertPoint(_:)を追加する必要があります。このメソッドはpointFor(column:, row:)と対照的な動作をします。先のプロパティの直後に追加してください。

private func convertPoint(_ point: CGPoint) -> (success: Bool, column: Int, row: Int) {
  if point.x >= 0 && point.x < CGFloat(numColumns) * tileWidth &&
    point.y >= 0 && point.y < CGFloat(numRows) * tileHeight {
    return (true, Int(point.x / tileWidth), Int(point.y / tileHeight))
  } else {
    return (false, 0, 0)  // invalid location
  }
}

このメソッドは引数にcookiesLayerのクッキーの座標であるCGPointを取り、列と行への値へと変換します。戻り値はタプル型であり3つの値を設定します。1)boolean型でスワイプの成功/失敗を表します、2)列の数字です、3)行の数字です。もし座標がグリッドの外側の場合、スワイプ失敗の戻り値となります。

touchesBegan(_:with:)を追加してください:

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
  // 1
  guard let touch = touches.first else { return }
  let location = touch.location(in: cookiesLayer)
  // 2
  let (success, column, row) = convertPoint(location)
  if success {
    // 3
    if let cookie = level.cookie(atColumn: column, row: row) {
      // 4
      swipeFromColumn = column
      swipeFromRow = row
    }
  }
}

プレヤーが指を画面にタップさせる時いつでもtouchesBegan(_:with:)メソッドが呼び出されます。このメソッドの動作を一つ一つ解説します:

  1. もし何かあれば、タップした場所をcookiesLayerの座標に変換します。
  2. convertPoint(_:)を呼び出し、タップがlevelの内側かどうかを判定します。もし、levelの内側の場合、このタップはスワイプ動作の始まりである可能性があります。この時点では、グリッドがクッキーを乗せているかどうかわかりせんが、すくなくとも9x9グリッドの内側をタップしたことだけは確かです。
  3. 次に、空のグリッドではなくクッキーをタップしたかどうかを判定します。この時点ではcookie変数が使われていないというワーニングがでますが、後にこのワーニングを出させないようにします。
  4. 最後にスワイプが始まった列と行の位置を記録し、後にどの方向へスワイプされたのか判定する為に使用します。

スワイプ動作を正しく完了させる為に、プレイヤーはその指をタップしたグリッドの外側へと移動させねばなりません。どの方向に指を動かしかどうかだけが問題です。どのくらい指が移動したかは関係がありません。

スワイプを検知するロジックはtouchesMoved(_:with:)の役割です。次のメソッドを追加してください:

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
  // 1
  guard swipeFromColumn != nil else { return }

  // 2
  guard let touch = touches.first else { return }
  let location = touch.location(in: cookiesLayer)

  let (success, column, row) = convertPoint(location)
  if success {

    // 3
    var horizontalDelta = 0, verticalDelta = 0
    if column < swipeFromColumn! {          // swipe left
      horizontalDelta = -1
    } else if column > swipeFromColumn! {   // swipe right
      horizontalDelta = 1
    } else if row < swipeFromRow! {         // swipe down
      verticalDelta = -1
    } else if row > swipeFromRow! {         // swipe up
      verticalDelta = 1
    }

    // 4
    if horizontalDelta != 0 || verticalDelta != 0 {
      trySwap(horizontalDelta: horizontalDelta, verticalDelta: verticalDelta)

      // 5
      swipeFromColumn = nil
    }
  }
}

このメソッドの動きはこうです:

  1. swipeFromColumnがnilならば、スワイプ動作は9x9グリッドの外側へなされたか、もしくはクッキーはすでにスワップ済みなので残りの処理は無視する必要があります。この判定にboolean型の変数を使うこともできましたが、swipeFromColumnを使う方が簡単です。これがオプショナルである理由です。
  2. これはtouchesBegan(_:with:)が行なっている列と行の計算に似ています。列と行はプレイヤーの指の下にあるクッキーの列と行です。
  3. この部分がプレイヤーのスワイプの方向を検知する部分です。スワイプ先の新しい列と行の数字をスワイプ前の数字と比較して検知します。プレイヤーは斜めにスワイプできません。else ifステートメントを使用しているので一度に変化するのはhorizontalDeltaverticalDeltaのどちら一つとなります。
  4. trySwap(horizontalDelta:verticalDelta:)メソッドはプレイヤーが元のグリッドから指を離した時のみ呼び出されます
  5. swipeFromColumnnilに戻すことによって、ゲームは残りのスワイプモーションを無視します

クッキーをスワップさせる複雑な仕事は、新しいメソッドで実装します:

private func trySwap(horizontalDelta: Int, verticalDelta: Int) {
  // 1
  let toColumn = swipeFromColumn! + horizontalDelta
  let toRow = swipeFromRow! + verticalDelta
  // 2
  guard toColumn >= 0 && toColumn < numColumns else { return }
  guard toRow >= 0 && toRow < numRows else { return }
  // 3
  if let toCookie = level.cookie(atColumn: toColumn, row: toRow),
    let fromCookie = level.cookie(atColumn: swipeFromColumn!, row: swipeFromRow!) {
    // 4
    print("*** swapping \(fromCookie) with \(toCookie)")
  }
}

これはある理由から「トライ・スワップ」(スワップの試み)と呼ばれます。この時点においてプレヤーがスワイプを上か下か左か右かに行なったことはわかりますが、スワイプの方向の先にクッキーがあるかどうかはまだわかりません。

  1. スワップ先のクッキーの列と行の位置を計算します。
  2. toColumntoRowが9x9グリッドの外側の可能性があります。これはプレイヤーがグリッドの端でスワイプ動作を行うときに起こり得ます。そのようなスワイプ動作は無視します。
  3. 最後のチェックは新しい位置に実際にクッキーがあるかどうを判定することです。もしクッキーがない場合はスワップできません。この状態はプレイヤーがタイルの無い(タイルとタイルの間の)隙間にスワイプしたときに発生します。
  4. ここにたどり着くということは、全てがOKだったということを意味し、スワップは成功です!今の所XCodeのコンソールにクッキーのログを出力するだけにしておきます。

スワップを完成させるためにtouchesEnded(_:with:)touchesCancelled(_:with:)も実装するべきです。touchesEndedはプレイヤーが指を画面から離した時に呼び出されるメソッドで、touchesCancelledはiOSが電話がかかってくるなど、ゲーム処理に割り込みをかける時に呼び出されます。

次を追加してください:

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
  swipeFromColumn = nil
  swipeFromRow = nil
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
  touchesEnded(touches, with: event)
}

スワイプジェスチャーが終わると、スワイプが成功したかどうかに関係なく、元のクッキーの列と行を保存している変数をnilにリセットします。

ビルドして実行してください。異なるクッキー同士をスワップしてみてください:

Valid swipe

ゲームの中ではまだ何も起こらないのを目の当たりにするでしょう。しかし、デバッグペーンには、正しいスワップの試みがログされます。

スワップをアニメーションする

2つのクッキーのスワッピングを表現するために、新しい型Swapを作ります。これは今までとは違うモデルオブジェクトであり、唯一の目的を持っています。それは「プレイヤーがクッキーAとクッキーBをスワップしたいと言っているよ」というのを実現することです。

Swap.swiftと名付けて新しいファイルを作成してください。内容を以下の内容で置き換えてください:

struct Swap: CustomStringConvertible {
  let cookieA: Cookie
  let cookieB: Cookie
  
  init(cookieA: Cookie, cookieB: Cookie) {
    self.cookieA = cookieA
    self.cookieB = cookieB
  }
  
  var description: String {
    return "swap \(cookieA) with \(cookieB)"
  }
}

こうしてスワップの試みを表現できるオブジェクトは手に入りましたが、疑問が湧き上がるでしょう:誰が実際のスワップ実行ロジックを扱うのだろう?スワイプの検知はGameSceneが行いますが、実際のゲームロジックの全ては今の所GameViewControllerが担当します。

これはGameSceneが正規のスワイプを行い、スワップの試みがなされる必要のあるGameViewControllerに返信する一方向の通信路を持たねばならない事を意味します。一方向の通信はデリゲートプロトコルを通じて行われますが、これはGameSceneGameViewControllerに返信しなければならない単一のメッセージなので、クロージャを使用します。

GameScene.swiftのトップに次のプロパティを追加してください:

var swipeHandler: ((Swap) -> Void)?

この変数のタイプは((Swap) -> Void)?です。->がついているので、これはクロージャかメソッドである事を意味します。このクロージャまたはメソッドはSwapオブジェクトをパラメータとして取り、戻り値はありません。

タップを扱うのはsceneの仕事です。プレイヤーのスワイプを検知すると、スワイプハンドラーに格納されたこのクロージャを呼び出します。このようにして、スワップの実行が必要な時にGameViewControllerに返信します。

まだGameScene.swiftを扱います。下記のコードをtrySwap(horizontal:vertical:)の底辺に追加し、print()文を置き換えてください。

if let handler = swipeHandler {
  let swap = Swap(cookieA: fromCookie, cookieB: toCookie)
  handler(swap)
}

これは新しいSwapオブジェクトを作り、スワップする2つのクッキーで埋めます。それからswipe handlerを呼び出し残りのスワイプ処理をします。swipeHandlernilの可能性があるため、最初にオプショナル・バインディングを使って正しいリファレンスを得ます。

GameViewControllerはスワップが成功したかどうかを決めます。もし成功なら、2つのクッキーをアニメーションさせる必要があります。アニメーションをさせる下記のメソッドをGameScene.swiftに追加してください:

func animate(_ swap: Swap, completion: @escaping () -> Void) {
  let spriteA = swap.cookieA.sprite!
  let spriteB = swap.cookieB.sprite!

  spriteA.zPosition = 100
  spriteB.zPosition = 90

  let duration: TimeInterval = 0.3

  let moveA = SKAction.move(to: spriteB.position, duration: duration)
  moveA.timingMode = .easeOut
  spriteA.run(moveA, completion: completion)

  let moveB = SKAction.move(to: spriteA.position, duration: duration)
  moveB.timingMode = .easeOut
  spriteB.run(moveB)

  run(swapSound)
}

これはSKActionによるアニメーションコードの基礎です。クッキーAをクッキーBの位置に移動させます。逆もまた然りです。

スワイプの元となったクッキーはcookieAに格納されており、これが画面のトップに表示されればアニメーションの見栄えはベストです。そのためこのメソッドは2つのクッキーのzPositionを調整してそうなるようにしています。

アニメーション完了後、cookieAはrun()の中でcompletionを呼び出し、処理の本流は何をする必要があるか関係なく動作を続けます。これはこのゲームの共通したパターンです。ゲームはアニメーションが完了するまで待機して、完了すると処理を再開します。

こうして、viewを扱うことができました。コントローラを処理する前にモデルでやるべきことが残っています!Level.swiftを開き、次のメソッドを追加してください:

func performSwap(_ swap: Swap) {
  let columnA = swap.cookieA.column
  let rowA = swap.cookieA.row
  let columnB = swap.cookieB.column
  let rowB = swap.cookieB.row

  cookies[columnA, rowA] = swap.cookieB
  swap.cookieB.column = columnA
  swap.cookieB.row = rowA

  cookies[columnB, rowB] = swap.cookieA
  swap.cookieA.column = columnB
  swap.cookieA.row = rowB
}

最初にCookieオブジェクトから行と列のコピーを得たテンポラリ変数を作ります。元の値は上書きされるからです。スワップを成すためにCookieオブジェクトの行と列を更新する他にcookies配列も更新します。これらはセットで行うべきです。以上がデータモデルで行うべきことです。

GameViewController.swiftを開き、下記のメソッドを追加してください:

func handleSwipe(_ swap: Swap) {
  view.isUserInteractionEnabled = false

  level.performSwap(swap)
  scene.animate(swap) {
    self.view.isUserInteractionEnabled = true
  }
}

最初にlevelにスワップを実行させます。データモデルを更新させてからsceneがスワップをアニメーションさせてviewを更新します。このチュートリアルを通じて、残りのゲームプレイロジックをこのメソッドに追加していくことになります。

アニメーションしている間、プレイヤーには画面の他の部分を触れさせないようにしたいはずです。ですので、一時的にviewのisUserInteractionEnabledをオフにします。後でanimate(_:completion:)に渡されたcompletionブロックの中でオンに戻します。

次のコードもまたviewDidLoad()に追加してください。sceneが現れる直前の行に追加してください:

scene.swipeHandler = handleSwipe

これはhandleSwipe(_:)メソッドをGameSceneswipeHandlerに割り当てます。GameSceneがswipeHandler(swap)を呼び出すときはいつでも、実際はGameViewControllerのメソッドを呼び出します。

アプリをビルドし、実行してください。クッキーをスワップすることができます!また、ギャップ(クッキーが無いところ)にスワップを試みてください。動かないはずです。

Swap cookies

クッキーをハイライト表示する

Candy Crush Sagaでは、プレイヤーがスワイプするキャンディはしばしの間ライトアップします。Cookie Crunch Adventureの中でも、ハイライトイメージをスプライトのトップに置くことによってこのエフェクトを実装できます。

テクスチャアトラスはハイライト・バージョンのクッキースプライトを持っています。これらはより輝いて味が染み込んだ感じに見えます。列挙型であるCookieTypeは既にハイライト・バージョンのイメージ・ファイル名を返すメソッドを持っています。

既存のクッキーの上にハイラト・クッキーを加えることによってGameSceneを改良する時がきました。新しいスプライトとして追加して、(ハイライトverではない)既存のスプライトを置き換えます。既存のスプライトはクロスフェード・バックさせるとアニメーションが簡単になります。

GameScene.swiftの中で, 新しいprivateプロパティをクラスに追加してください:

private var selectionSprite = SKSpriteNode()

以下のメソッドを追加してください:

func showSelectionIndicator(of cookie: Cookie) {
  if selectionSprite.parent != nil {
    selectionSprite.removeFromParent()
  }

  if let sprite = cookie.sprite {
    let texture = SKTexture(imageNamed: cookie.cookieType.highlightedSpriteName)
    selectionSprite.size = CGSize(width: tileWidth, height: tileHeight)
    selectionSprite.run(SKAction.setTexture(texture))

    sprite.addChild(selectionSprite)
    selectionSprite.alpha = 1.0
  }
}

これはCookieオブジェクトからハイライト版スプライトの名前を取得します。そして、選択中のスプライトの上のテクスチャとして置きます。スプライトの上のテクスチャを単に設定するだけでは、正しいイメージサイズを与えることになりませんが、SKActionを使うと自動補正してくれます。

alphaを1にしてselection spriteを可視するようにもします。selection spriteをcookie spriteの子供として追加しますので、スワップアニメーションのcookie spriteとともに移動します。

反対の動作をするメソッドhideSelectionIndicator()も追加しましょう:

func hideSelectionIndicator() {
  selectionSprite.run(SKAction.sequence([
    SKAction.fadeOut(withDuration: 0.3),
    SKAction.removeFromParent()]))
}

このメソッドは選択中のスプライトをフェードアウトさせて取り除きます。

残りのコーディングはこれらのメソッドを呼び出すことです。最初にtouchesBegan(_:with:)の中のif let cookie = ...セクションでは、Xcodeが丁寧にワーニングを指摘してくれているので、次のコードを追加してください:

showSelectionIndicator(of: cookie)

そして、touchesMoved(_:with:)の中で、trySwap(horizontalDelta:verticalDelta:)の呼び出しの後に、以下を追加してください:

hideSelectionIndicator()

最後にもう一つhideSelectionIndicator()を追加する場所があります。もしプレイヤーが画面をスワイプしないで単にタップしただけならば、ハイライトのスプライトを取り消したいはずです。以下のコードをtouchesEnded()のトップに追加してください:

if selectionSprite.parent != nil && swipeFromColumn != nil {
  hideSelectionIndicator()
}

ビルドして実行してください。クッキー達がライトアップします!:]

配列を充填するより賢い方法

現在ゲームを実行すると、画面上では既にクッキーが連鎖しているかもしれません。これは良く無いです。連鎖が起きるのはプレイヤーが二つのクッキーをスワップした時か、新しいクッキーが画面にフォールダウンしてきた時だけにしたいでしょう。

ルールはこうです。ゲームを始めて画面にクッキーが表示される時はいつでも、グリッドには連鎖がない状態にするのです。この事を保証するために、cookies配列を充填するメソッドを少し賢くします。

Level.swiftを開き、createInitialCookies()を探してください。ランダムクッキータイプを算出しているシングルラインのコードを次のコードで置き換えてください:

var cookieType: CookieType
repeat {
  cookieType = CookieType.random()
} while (column >= 2 &&
  cookies[column - 1, row]?.cookieType == cookieType &&
  cookies[column - 2, row]?.cookieType == cookieType)
  || (row >= 2 &&
    cookies[column, row - 1]?.cookieType == cookieType &&
    cookies[column, row - 2]?.cookieType == cookieType)

この数片のコードはランダムなクッキーを取得し、3つ以上の連鎖が生成されないことを確実にしています。

もし新しいランダムクッキーが3つの連鎖を成した場合、メソッドはもう一度ランダムクッキーの生成を繰り返します。このループはランダムクッキーが3つ以上の連鎖を起こさない事が確定されるまで繰り返します。確認するクッキーは左か下だけ見れば良いです。なぜなら、右か上には連鎖を起こすクッキーがもうないからです。

アプリを実行し、ゲームの初期状態には連鎖状態が一つもない事を確認してください。

トラック可能な場所を記録する

プレイヤーに2つのクッキーをスワップさせる時は、結果として3つかそれ以上の連鎖が起きる時のみにしましょう。

スワップの結果が連鎖になっているかどうかを検知するロジックを追加する必要があります。これ実現するために、レベルがシャッフルされた後に全てのスワップ可能な場所のリストを作ることになります。そうすると、試みたスワップがリストの中にあるかどうかだけをチェックするだけで済みます。リストを作るために、Swapが必要になります。SwapはすなわちHashableである必要があります。

Swap.swiftを開き、Hashableを構造体の宣言に追加してください:

struct Swap: CustomStringConvertible, Hashable {

次に、Hashableを実装するために次のメソッドを追加してください:

var hashValue: Int {
  return cookieA.hashValue ^ cookieB.hashValue
}

static func ==(lhs: Swap, rhs: Swap) -> Bool {
  return (lhs.cookieA == rhs.cookieA && lhs.cookieB == rhs.cookieB) ||
    (lhs.cookieB == rhs.cookieA && lhs.cookieA == rhs.cookieB)
}

スワップ時に同じ2つのクッキーを参照しているかどうかの判定を実装しました。順序は関係ありません。

Level.swiftの中で、新しいプロパティを追加してください:

private var possibleSwaps: Set<Swap> = []

ここでもう一度、SetArrayの代わりに使います。なぜならこのコレクションの要素の順は重要ではないからです。このSetSwapオブジェクトを含みます。プレイヤーが2つのクッキーのスワップを試みた時、このsetの中にない場合ゲームはスワップを正規の動作として許可しないようにします。

各ターンの始めに、どのクッキーをプレイヤーがスワップできるか検出します。この検出はshuffle()の中で行います。まだLevel.swiftの中です。メソッドを置き換えてください:

func shuffle() -> Set<Cookie> {
  var set: Set<Cookie>
  repeat {
    set = createInitialCookies()
    detectPossibleSwaps()
    print("possible swaps: \(possibleSwaps)")
  } while possibleSwaps.count == 0

  return set
}

見てもらった通りに、レベルをランダムクッキーオブジェクトで埋めるためにcreateInitialCookies()を呼び出します。それから(この後に実装する)新しいメソッドcreateInitialCookies()を呼び出してスワップ可能なsetを埋めます。

detectPossibleSwaps()はクッキーが連鎖の一部になっているかどうかを検知するヘルパーメソッドを使用します。次のメソッドを追加してください:

private func hasChain(atColumn column: Int, row: Int) -> Bool {
  let cookieType = cookies[column, row]!.cookieType

  // Horizontal chain check
  var horizontalLength = 1

  // Left
  var i = column - 1
  while i >= 0 && cookies[i, row]?.cookieType == cookieType {
    i -= 1
    horizontalLength += 1
  }

  // Right
  i = column + 1
  while i < numColumns && cookies[i, row]?.cookieType == cookieType {
    i += 1
    horizontalLength += 1
  }
  if horizontalLength >= 3 { return true }

  // Vertical chain check
  var verticalLength = 1

  // Down
  i = row - 1
  while i >= 0 && cookies[column, i]?.cookieType == cookieType {
    i -= 1
    verticalLength += 1
  }

  // Up
  i = row + 1
  while i < numRows && cookies[column, i]?.cookieType == cookieType {
    i += 1
    verticalLength += 1
  }
  return verticalLength >= 3
}

連鎖とは列か行に3つ以上連続して並んだ同じタイプのクッキーの事です

このメソッドは最初に左方向を見にいきます。同じタイプのクッキーが見つかり続ける限り、horizontalLengthをインクリメントして、更に左に探索を続けます。それから他の方向へ探索を続けます。

こうしてdetectPossibleSwaps()を実装する事ができます。このメソッドが上位概念上どの様に動くのか見てみましょう

  1. 2Dグリッドの列と行をループしてみていきます。それから各クッキーを1つ1つ同時にスワップしていきます。
  2. これらの2つのクッキーのスワップが連鎖を作る場合、Swapオブジェクトを作成し、possibleSwapsに格納していきます。
  3. それから、これらのスワップしたクッキーを元の状態に戻して、次のクッキーをチェックし、全てをスワップするまで続きます
  4. 上記のスワップチェックは2回行われます:1回目は水平方向のスワップ、2回目は垂直方向のスワップです。

とても大きな仕事ですので、部分的に実装していきましょう!

最初に、メッソドの外郭を作りましょう:

func detectPossibleSwaps() {
  var set: Set<Swap> = []

  for row in 0..<numRows {
    for column in 0..<numColumns {
      if let cookie = cookies[column, row] {

        // TODO: detection logic goes here
      }
    }
  }

  possibleSwaps = set
}

これは実にシンプルです:メソッドは列と行をループし各スポットを見ていきます。もし、空のグリッドではなくクッキがある場合、検出ロジックを実行します。最後に、結果のsetはpossibleSwapsプロパティへ格納されます。発生する2つのワーニングは今の所無視してください。

検出ロジックは2つの別れたロジックから成っています。同じことをするのですが、探索する方向が違います。最初に右方向に探索し、続いて上方向に探索します。行0は最底辺であることを思い出してください。なので上方向に探索するとうまくいくのです。

次のコードを“TODO: detection logic goes here”の部分に記述してください:

// Have a cookie in this spot? If there is no tile, there is no cookie.
if column < numColumns - 1,
  let other = cookies[column + 1, row] {
  // Swap them
  cookies[column, row] = other
  cookies[column + 1, row] = cookie

  // Is either cookie now part of a chain?
  if hasChain(atColumn: column + 1, row: row) ||
    hasChain(atColumn: column, row: row) {
    set.insert(Swap(cookieA: cookie, cookieB: other))
  }

  // Swap them back
  cookies[column, row] = cookie
  cookies[column + 1, row] = other
}

これは現在位置のクッキーと(もし、あれば)右隣のクッキーのスワップを試みます。3つ以上の連鎖ができた場合、Swapオブジェクトを作り、setに格納します。

上記のコードの直下に下記のコードを追加してください:

if row < numRows - 1,
            let other = cookies[column, row + 1] {
            cookies[column, row] = other
            cookies[column, row + 1] = cookie
            
            // Is either cookie now part of a chain?
            if hasChain(atColumn: column, row: row + 1) ||
              hasChain(atColumn: column, row: row) {
              set.insert(Swap(cookieA: cookie, cookieB: other))
            }
            
            // Swap them back
            cookies[column, row] = cookie
            cookies[column, row + 1] = other
          }
        }
        else if column == numColumns - 1, let cookie = cookies[column, row] {
          if row < numRows - 1,
            let other = cookies[column, row + 1] {
            cookies[column, row] = other
            cookies[column, row + 1] = cookie
            
            // Is either cookie now part of a chain?
            if hasChain(atColumn: column, row: row + 1) ||
              hasChain(atColumn: column, row: row) {
              set.insert(Swap(cookieA: cookie, cookieB: other))
            }
            
            // Swap them back
            cookies[column, row] = cookie
            cookies[column, row + 1] = other
          }

Ifステートメントは同じことをしますが右方向ではなく、上方向です。ELse Ifステートメントは最後の列の垂直スワップをカバーします(そうでなければ、検出はされなかったことになります)

アプリを実行してください。Xcodeのコンソールに次の様なログが表示されるはずです:

possible swaps: [
swap type:SugarCookie square:(6,5) with type:Cupcake square:(7,5),
swap type:Croissant square:(3,3) with type:Macaroon square:(4,3),
swap type:Danish square:(6,0) with type:Macaroon square:(6,1),
swap type:Cupcake square:(6,4) with type:SugarCookie square:(6,5),
swap type:Croissant square:(4,2) with type:Macaroon square:(4,3),
. . .

非正規なスワップを防止する

スワップ可能を表すsetを実用する時がきました。次のメソッドをLevel.swiftの次に置いてください:

func isPossibleSwap(_ swap: Swap) -> Bool {
  return possibleSwaps.contains(swap)
}

これはsetの中に特定のSwapオブジェクトが含まれているを確認します

最終的にGameViewController.swiftの中のhandleSwipe(_:)の内側で呼びされます。既存のhandleSwipe(_:)を次の内容で置き換えてください:

func handleSwipe(_ swap: Swap) {
  view.isUserInteractionEnabled = false

  if level.isPossibleSwap(swap) {
    level.performSwap(swap)
    scene.animate(swap) {
      self.view.isUserInteractionEnabled = true
    }
  } else {
    view.isUserInteractionEnabled = true
  }
}

こうしてゲームは今ところ、setの中にスワップ可能なパターンがある場合のみスワップだけする事ができます。

ビルドして試してください。結果が連鎖となる場合のみ、スワップをすることができます(スワッップするだけです)

非正規なスワップ動作をアニメーションさせるのも面白いでしょう。次のメソッドをGameScene.swiftに追加してください:

func animateInvalidSwap(_ swap: Swap, completion: @escaping () -> Void) {
  let spriteA = swap.cookieA.sprite!
  let spriteB = swap.cookieB.sprite!

  spriteA.zPosition = 100
  spriteB.zPosition = 90

  let duration: TimeInterval = 0.2

  let moveA = SKAction.move(to: spriteB.position, duration: duration)
  moveA.timingMode = .easeOut

  let moveB = SKAction.move(to: spriteA.position, duration: duration)
  moveB.timingMode = .easeOut

  spriteA.run(SKAction.sequence([moveA, moveB]), completion: completion)
  spriteB.run(SKAction.sequence([moveB, moveA]))

  run(invalidSwapSound)
}

このメソッドはanimate(_:completion:)に似ていますが、2つのクッキーを新しい位置にスライドさせてから、直ちに元の位置にフリップバックさせます。

GameViewController.swiftの中で、else句の内側のhandleSwipe(_:)を変更してください:

scene.animateInvalidSwap(swap) {
  self.view.isUserInteractionEnabled = true
}

アプリを実行し、スワップを試みてください。連鎖しない場合は次の様になります:

ここからどこへ行くべきか

ここまでに作成したプロジェクトを、このチュートリアルの上部と下部にあるDownload Materialsを使ってダウンロードできます

チュートリアルの2部が無事に終了しました。お疲れ様です。ゲームの基礎部分を構築しことはすごい仕事と言えるでしょう

パート3では、連鎖の探索と削除、スワイプ成功後のレベルを美味しいクッキーで充填する方法、それにスコアのカウントを実装します

次に進むまでに慰労の休息を取っている間、我々のフォーラムであなたの意見を聞かせてください

著作権:フリーな画像はGame Art Guppyから、音楽はKevin MacLeodから。サンプルの効果音の元はfreesound.orgから

ソースコードの一部はGabriel Nica'sのSwiftゲームポートから触発を受けた

Contributors

Comments