いものやま。

雑多な知識の寄せ集め

SpriteKitのサンプルコードを読んでみた。(その2)

昨日の続き。

ファイル構成

コードを読んでいくときに、まずやっておきたいのが、ファイル構成の把握。

Adventureのファイル構成を見ると、次のようになっている。
(ナビゲータ準拠。一部省略)

  • Adventure Shared/ 共通部
    • AdventureHUD.sks ステータス表示の雛形
    • AdventureWorld.sks マップの雛形
    • Scene/
      • AdventureScene.swift シーンの実装
      • SharedAssetManagement.swift リソース管理
      • Player.swift プレイヤー管理
    • Sprites/
      • ParallaxSprite.swift ベースとなるスプライト(視差効果)
      • Tree.swift 木
      • Character.swift キャラのベース
      • HeroCharacter.swift 自キャラのベース
      • Archer.swift アーチャー(自キャラ)
      • Warrior.swift ウォーリアー(自キャラ)
      • EnemyCharacter.swift 敵キャラのベース
      • Goblin.swift ゴブリン(敵キャラ)
      • Cave.swift ゴブリンの洞穴(敵キャラ)
      • Boss.swift ボス(敵キャラ)
    • AI/
      • ArtificialIntelligence.swift AIのベース
      • ChaseArtificialIntelligence.swift 敵キャラの追走AI
      • SpawnArtificialIntelligence.swift 敵キャラの発生AI
    • Utilities/ ユーティリティ
  • Adventure - OS X/ OS X依存部(今回は見ない)
  • Adventure iOS/ iOS依存部
    • Main.storyboard ストーリーボード
    • AppDelegate.swift デリゲート
    • ViewController.swift ビューコントローラ
    • AdventureSceneiOSEvents.swift シーンのタッチ処理
    • Supporting Files/ サポートファイル
  • Assets/ リソース
    • Sounds/ 効果音
    • UI/ UI関係の画像
    • Particles/ パーティクル
    • Environment/ マップの画像(※今は使われてないはず)
    • Texture Atlases/ テクスチャー・アトラス
      • Archer/ アーチャーの画像
        • Archer_Idle.atlas/ 待機アニメ
        • Archer_Walk.atlas/ 歩行アニメ
        • Archer_Attack.atlas/ 攻撃アニメ
        • Archer_GetHit.atlas/ 被弾アニメ
        • Archer_Death.atlas/ 死亡アニメ
      • Warrior/ ウォーリアーの画像
        • (Archerと同様)
      • Goblin/ ゴブリンの画像
        • (Archerと同様)
      • Boss/ ボスの画像
        • (Archerと同様)
      • Environment.atlas/ 木や洞穴の画像
      • Tiles.atlas/ 地面を分割した画像

何点か補足として。

まず、AdventureHUD.sksやAdventureWorld.sks。
StoryboardでグラフィカルにUIを作ることが出来るのと同様に、SpriteKitのシーンのインスタンスXcodeからグラフィカルに作ることが出来るようになっている。
これらのファイルはそうやって作られたファイルで、AdventureSceneではこれらのファイルを読み込んで、それを雛形としてシーンの構築を行っている。
(そのまま使っているわけではないので、そういう意味で「雛形」)

それと、テクスチャー・アトラス。
ゲームでたくさんの画像を使う場合に、それを1枚の画像に統合しておいて、実際に使う場合には、その統合された画像でのオフセットと元の画像のサイズの情報から元の画像の情報を取得して使うということが行われるらしい。
Xcodeでは、テクスチャー・アトラスを使うと、この画像の統合などを自動でやってくれる。
そして、プログラムからはSKTextureAtlasを使うことで、簡単に利用することが出来る。

f:id:yamaimo0625:20150706172923p:plain

テクスチャー・アトラスの各フォルダには、統合する元になる画像が入っている感じ。

なお、テクスチャー・アトラスを使う場合、Build Settingsで、SPRITEKIT_TEXTURE_ATLAS_OUTPUTの設定がYESになっている必要があるので、注意。

Main.storyboard

次に、UIがどう配置されているのか。

Main.storyboardを見てみると、次のような配置になってる。

f:id:yamaimo0625:20150706233510p:plain

ビューコントローラからviewで参照されるUIViewをSKViewが覆い、そのSKViewをさらにUIImageViewが覆う感じになっている。
このUIImageViewは、ビューコントローラからはcoverViewという名前で参照されている。
このcoverViewは、SKViewに表示する内容をロードして表示できるようになるまでの間、SKViewを隠し、代わりの背景の画像を表示するようになっている。
そして、SKViewの準備が出来たらcoverViewは外され、実際のゲーム画面が見れるようになる、というわけだ。

もう一つ補足で。
キャラクターを選択するためのボタンが用意されているけれど、これらのボタンは最初はアルファが0になっていて、見えないようになってる。
そして、SKViewの準備が出来たらアルファを1にして、見えるようにしてる。

この辺りのやり方は、実際にゲームを作るときに参考になりそう。

ViewController.swift

さて、こういったUIをコントロールするのが、ビューコントローラ。
ということで、ビューコントローラのコードを見てみる。

まずは、プロパティ。

import SpriteKit

class ViewController: UIViewController {

  // MARK: Properties

  @IBOutlet weak var coverView: UIImageView!
  @IBOutlet weak var skView: SKView!
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var gameLogo: UIImageView!
  @IBOutlet weak var loadingProgressIndicator: UIActivityIndicatorView!
  @IBOutlet weak var archerButton: UIButton!
  @IBOutlet weak var warriorButton: UIButton!
  var scene: AdventureScene!
  
  // 続く

このあたりは、Main.storyboardに配置された各UIと接続されている。

そして、ViewController#viewDidLoad()。
このメソッドは、Main.storyboardからビューがロードされると呼ばれる。

  // 続き

  override func viewDidLoad() {
    // On iPhone/iPod touch we want to see a similar amount of the scene as on iPad.
    // So, we set the scale of the image to be used to double the scale of the image,
    // This effectively scales the cover image to 50%, matching the scene scaling.
    var image = UIImage(named: "cover")!
    if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
      image = UIImage(CGImage: image.CGImage, scale: image.scale * 2.0, orientation: UIImageOrientation.Up)!
    }

    coverView.image = image
  }

  // 続き

ここで、いろんな画面サイズに対応する方法について。 - いものやま。で取り上げた方法が使われている。
すなわち、SKViewが表示されるまでの間、背景としておく画像をcoverViewにセットしているのだけど、iPhoneでもiPadでもほぼ内容が表示されるように、画像のスケールを変えている。

次に、ViewController#viewWillAppear(_: Bool)。
このメソッドは、ビューが実際に表示される直前に呼ばれる。

  // 続き

  override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)

    // Start the progress indicator animation.
    loadingProgressIndicator.startAnimating()

    AdventureScene.loadSceneAssetsWithCompletionHandler { loadedScene in
      var viewSize = self.view.bounds.size

      // On iPhone/iPod touch we want to see a similar amount of the scene as on iPad.
      // So, we set the size of the scene to be double the size of the view, which is
      // the whole screen, 3.5- or 4- inch. This effectively scales the scene to 50%.
      if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
        viewSize.height *= 2
        viewSize.width *= 2
      }

      self.scene = loadedScene
      self.scene.size = viewSize
      self.scene.scaleMode = .AspectFill

      #if DEBUG
      self.skView.showsDrawCount = true
      self.skView.showsFPS = true
      #endif

      self.scene.finishedMovingToView = {
        UIView.animateWithDuration(2.0, animations: {
          self.archerButton.alpha = 1.0
          self.warriorButton.alpha = 1.0
          self.coverView.alpha = 0.0
        }, completion: { finished in
          self.loadingProgressIndicator.stopAnimating()
          self.loadingProgressIndicator.hidden = true
          self.coverView.removeFromSuperview()
        })
      }

      self.skView.presentScene(self.scene)
    }
  }

  // 続く

まず、AdventureSceneのロードを開始する前に、loadProgressIndicatorのアニメーションを開始している。
これにより、バックグラウンドで何か処理が進んでいて、それが終わるのをユーザに待ってもらっている、ということを伝えるようにしている。

そして、AdventureSceneのロード。
ここで引数として渡しているクロージャは、AdventureSceneのロードが終わったときに呼ばれるようになっている。
(厳密には、メインディスパッチキューに処理が投げられるようになっている)

このクロージャでやってる処理は、まずはロードされたAdventureSceneのサイズを変更すること。
ここでもいろんな画面サイズに対応する方法について。 - いものやま。で取り上げた方法が使われていて、iPhoneでもiPadでもほぼ同じだけの内容が表示されるようにしている。

そのあとは、AdventureSceneがSKViewに表示された後に行う処理をクロージャで登録している。
このクロージャでは、archerButtonとwarriorButtonのアルファを1に、coverViewのアルファを0にするアニメーションを行って、最後にloadProgressIndicatorのアニメーションを終了させ、coverViewを取り除くようにしている。
coverViewが取り除かれた時点で、実際のゲーム画面(といっても、内容はcoverViewの画像と変わらないようになってるのだけど)が表示されることになる。

クロージャの登録が終わったら、いよいよAdventureSceneをSKViewに表示させている。

最後に、ボタンをタッチされたときの処理。

  // 続き
  
  // MARK: IBActions

  @IBAction func chooseArcher(_: AnyObject) {
    scene.startLevel(.Archer)
    gameLogo.hidden = true 

    warriorButton.hidden = true
    archerButton.hidden = true
  }

  @IBAction func chooseWarrior(_: AnyObject) {
    scene.startLevel(.Warrior)
    gameLogo.hidden = true 
    
    warriorButton.hidden = true
    archerButton.hidden = true
  }
}

これらのアクションは、Main.storyboardでボタンをタッチされたときのアクションとして接続されている。
これにより、ボタンを押すとアーチャー/ウォーリアーとしてゲームが開始して、ロゴやボタンが非表示になる。

今日はここまで!