読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

「BirdHead」の仕上げをしてみた。(その4)

ゲーム開発 Swift BirdHead

昨日はスタート画面とルール画面を作った。

今日はゲーム結果の表示を実装していく。

MinusPointInfoNodeの修正

ということで、早速実装を。

まずは、ゲームの順位を表示できるようにするために、マイナス点一覧の修正を行う。
具体的には、指定された順位に応じて、名前の横に順位を示す画像を表示するようにした。

//==============================
// BirdHead
//------------------------------
// MinusPointInfoNode.swift
//==============================

import SpriteKit

class MinusPointInfoNode: SKNode {
  // 省略
  
  private static var crownTextures = [SKTexture]()
  
  class func loadAssets() {
    let textureAtlas = SKTextureAtlas(named: "Crown")
    MinusPointInfoNode.crownTextures.append(textureAtlas.textureNamed("CrownGold"))
    MinusPointInfoNode.crownTextures.append(textureAtlas.textureNamed("CrownSilver"))
    MinusPointInfoNode.crownTextures.append(textureAtlas.textureNamed("CrownBronze"))
  }
  
  // 省略

  func showCrowns(topIndices: [Int], secondIndices: [Int], thirdIndices: [Int],
                  completion: (() -> Void)! = nil)
  {
    self.actionQueue.addActionBlock { executor in
      let indicesArray = [topIndices, secondIndices, thirdIndices]
      
      for ith in 0..<indicesArray.count {
        for index in indicesArray[ith] {
          let crownNode = SKSpriteNode(texture: MinusPointInfoNode.crownTextures[ith])
          crownNode.position.x = self.nameNodes[index].position.x + crownNode.size.width/2.0
          crownNode.position.y = (self.nameNodes[index].position.y - self.nameNodes[index].frame.height
                                   + crownNode.size.height/2.0)
          crownNode.alpha = 0.0
          self.addChild(crownNode)
          
          let fadeInAction = SKAction.fadeInWithDuration(MinusPointInfoNode.actionDuration)
          executor.executeAction(fadeInAction, forNode: crownNode)
        }
      }
      
      for nameNode in self.nameNodes {
        let moveAction = SKAction.moveByX(MinusPointInfoNode.crownTextures[0].size().width,
                                          y: 0.0,
                                          duration: MinusPointInfoNode.actionDuration)
        executor.executeAction(moveAction, forNode: nameNode)
      }
    }
    
    if completion != nil {
      self.actionQueue.addBlock(completion)
    }
  }
}

1位が複数いたり、2位や3位がいないといった場合もあるので、各順位のプレイヤー番号の配列をそれぞれ受け取るようなインタフェースにしてある。

GameSceneの修正

表示が出来るようにしたので、次は実際に表示させる処理。

コントローラであるGameSceneに、ゲームが終わったときの処理を追加する。

//==============================
// BirdHead
//------------------------------
// GameScene.swift
//==============================

import SpriteKit

class GameScene: SKScene, GameInfoObserver, ButtonNodeDelegate {
  // 省略

  func gameInfoEnded() {
    self.actionQueue.addActionBlock { executor in
      var minusPoints = [Int]()
      for playerIndex in 0..<4 {
        var minusPoint = 0
        for minusPointCard in self.info.minusPointCards[playerIndex] {
          minusPoint += minusPointCard
        }
        minusPoints.append(minusPoint)
      }
      
      var humanRank = 0
      var rankIndices: [[Int]] = [[], [], [], []]
      for playerIndex in 0..<4 {
        let playerMinusPoint = minusPoints[playerIndex]
        var rank = 4
        for minusPoint in minusPoints {
          if playerMinusPoint <= minusPoint {
            rank -= 1
          }
        }
        rankIndices[rank].append(playerIndex)
        if playerIndex == 0 {
          humanRank = rank
        }
      }
      
      // if loser is empty,
      // the lowest rank becomes loser.
      if rankIndices[3].isEmpty {
        for i in [2, 1, 0] {
          if !rankIndices[i].isEmpty {
            rankIndices[3] = rankIndices[i]
            rankIndices[i] = []
            if humanRank == i {
              humanRank = 3
            }
            break
          }
        }
      }
      
      if self.minusPointInfoNode.parent == nil {
        self.responseLayer.addChild(self.minusPointInfoNode)
      }
      self.minusPointInfoNode.okButtonNode.userInteractionEnabled = false
      
      executor.startAction()
      self.minusPointInfoNode.showCrowns(rankIndices[0],
                                         secondIndices: rankIndices[1],
                                         thirdIndices: rankIndices[2]) {
        let messageList = ["TopMessage", "SecondMessage", "ThirdMessage", "LoseMessage"]
        let dialog = UIAlertController(title: NSLocalizedString("EndTitle", comment: "end title"),
                                       message: NSLocalizedString(messageList[humanRank], comment: "game result"),
                                       preferredStyle: .Alert)
        dialog.addAction(UIAlertAction(title: "OK", style: .Default,
                                       handler: { action in
                                        self.minusPointInfoNode.okButtonNode.userInteractionEnabled = true
                                        executor.endAction()
                                       }))
        self.view?.window?.rootViewController?.presentViewController(dialog, animated: true, completion: nil)
      }
    }
  }
  
  // 省略
}

前半でやってるのは、マイナス点の合計を出して、順位を決める処理。
そして、後半ではその結果を使ってマイナス点一覧に順位を表示させている。
(おまけとして、ダイアログも表示させている)

ポイントとなるのは、順位を決めるためのアルゴリズム

同じ点数なら同じ順位にして、その分だけ次の順位を繰り下げる、としたい(例えば、A: 5pt, B: 7pt, C: 5pt, D: 22ptなら、AとCは1位、Bは2位ではなく3位、Dは4位(負け)といった具合)のだけど、そのアルゴリズムとなると、パッとは浮かばない・・・
そこで、グーグル先生の力を借りたら、次のようなアイディアを使えばいいことが分かった:

「Aの順位を出したい場合、Aの点数以下のプレイヤーが何人いるかを調べればいい(それが下からの順位に等しい)」

実際、先ほどの例(A: 5pt, B: 7pt, C: 5pt, D: 22pt)で確認してみると、

  • A以下のプレイヤーは4人いる→Aは下から4番目の順位→1位
  • B以下のプレイヤーは2人いる→Bは下から2番目の順位→3位
  • C以下のプレイヤーは4人いる→Cは下から4番目の順位→1位
  • D以下のプレイヤーは1人いる→Dは下から1番目の順位→4位

といった具合。(「以下」なので、自分も入れることに注意)

(よく考えると「より点数の高いプレイヤーが何人いるか」を調べるでもいいのか・・・その方が自然かも)

ただ、このアルゴリズムを使った場合、一番下の点数が同じ場合、負けのプレイヤーがいなくなる。
例えば、A: 5pt, B: 22pt, C: 5p, D: 22ptの場合、AとCが1位、BとDが3位となる。
なので、4位のプレイヤーが存在しなかった場合、3位(場合によっては2位や1位)の順位を繰り下げる処理も入れてある。
(あらかじめ、22点以上とったプレイヤーを全員負けにしてしまう、という手もある。ただ、22点以上取るのは敗北条件ではなく、ゲームの終了条件なので、とりあえず上記のようにしている)

動作確認

さて、これでゲームを行って、ゲームが終了すると、次のような表示がされる:

f:id:yamaimo0625:20151203154700p:plain:h600

ちゃんと順位が表示されてるのが分かると思う。

余談だけど、このゲームは激戦だったw
見ての通り、5と6がマイナス点として全部使われていて、カードのランクがさらに狭まっているw

今日はここまで!