Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 886 - 実力無いけどなんとか取り組んでます

GameplayKitの勉強で取り組んでいるチュートリアルは、iOS9向けなんで、iOS10の環境で動かないんです。問題部分は自分なりに回避して対応させていってますが、昨日までの話はまだ準備段階です。今日のトピックから、GameplayKitの神髄、「Entity-Component」デザイン・パターンの話題が登場します。GameplayKit with Swift for Beginners — Tower Defense Game — Part 2: Entities, Components and Pathfindingに進みます。

Entittyはゲームで扱うオブジェクトクラスで、共通機能をComponentとして作ります。プロトコールのように使い回しができるクラスを用意するんです。このゲームで必要なタワー(やぐら)と敵キャラはどちらも、画面に表示する機能を持つので、この「画面に表示」という機能を持ったクラスを作ります。

メニューから「File -> New -> File...」を選び、「Cocoa Touch Class」を選択して、「Subclass of:」を「GKComponent」に変更します。名前を「VisualComponent」にしてプロジェクト内に保存します。

ファイルがオープンされたら、class宣言の前にエラーが表示されていると思います。GKComponentクラスを継承したクラスなのに、imoprtしたフレームワークはUIKitしか組み込まれていないからです。GKComponentクラスはGameplayKitフレームワークに存在するため、「import GameplayKit」と「import SpriteKit」に変更します。

そして、コードを次のようにします。

import GameplayKit
import SpriteKit

class VisualComponent: GKComponent {

    var scene: GameScene!
    var sprite: SKSpriteNode!
    var coordinate: int2!
    
    init(scene: GameScene, sprite: SKSpriteNode, coordinate: int2) {
        
        self.scene = scene
        self.sprite = sprite
        self.coordinate = coordinate
        
        super.init()
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

このVisualComponentは、画面に表示する機能だけを持ったクラスです。GKComponentクラスを継承していますが、GKComponentには何も機能が備わっていません。「描画」という機能をさせるために、sceneインスタンス(画面)とspriteインスタンス(表示させるオブジェクト)、そして、画面上の位置情報となるcordinateクラスを持つだけです。

ここで「int2」型は、GameplayKitで使われている座標系で、「vector_int2」と書いてもいいようです。「(Int32, Int32)」の組み合わせで座標情報を保持します。

プロパティ宣言をした場合、必ずイニシャライザを書かなければなりません*1。指定イニシャライザが1つ必要になります。まず、独自のプロパティを初期化してから、スーパークラスの指定イニシャライザも呼ぶ必要があります。ということで、「super.init()」が必要になります。

そして、 「required init?(coder aDecoder: NSCoder)」もストーリーボードを利用している限り必ず必要になりますが、VisualComponentはストリーボードで作成していないので、Xcodeに指定されるまま、何も書かずに放置します。

これでVisulaComponentクラスの用意ができました。

これだけです。

*1:クラスを利用するために知らなければならない基本事項の1つはイニシャライザの理解です -> Swiftで遊ぼう! - 407 - Initializer イニシャライザ - Swiftで遊ぼう! on Hatena

Swiftで遊ぼう! - 885 - GameScene.sksを回避

GameScene.sksを使うと、グリッドの配置がおかしくなります。これを自分で修正することができないので、コードでGameSceneクラスを初期化してレイアウトのズレを回避します。

GameViewControllerにコードされているviewDidLoard()メソッドは次のようになっています。

override func viewDidLoad() {
    super.viewDidLoad()
        
    if let view = self.view as! SKView? {
        // Load the SKScene from 'GameScene.sks'
        if let scene = SKScene(fileNamed: "GameScene") {
            // Set the scale mode to scale to fit the window
            scene.scaleMode = .resizeFill
                
            // Present the scene
            view.presentScene(scene)
        }
            
        view.ignoresSiblingOrder = true
            
        view.showsFPS = true
        view.showsNodeCount = true
    }
}

このコードはGameScene.sksを使ってGameSceneクラスの初期化をするんですが、これでうまくレイアウトの調整ができないので、次のようにします。

override func viewDidLoad() {
    super.viewDidLoad()
        
    if let view = self.view as! SKView? {
            
        let scene = GameScene()
            
        scene.scaleMode = .resizeFill
        scene.backgroundColor = UIColor.lightGray
            
        // Present the scene
        view.presentScene(scene)
            
        view.ignoresSiblingOrder = true
            
        view.showsFPS = true
        view.showsNodeCount = true
    }
}

あまり説明の要らないコードですね。これでランするとちゃんとグリッドが整列します。

f:id:yataiblue:20161219203034j:plain

ちょっとよく理解できてないけど次に進みます。

Swiftで遊ぼう! - 884 - GameScene.sksを使うとグリッドがズレる(泣)

何もないsceneだけ表示した状態です。この上にSKNodeインスタンスを1つ用意して、その中に画面サイズに合わせてグリッドを「addChild()」していくんです。

GameScene.swiftファイルに4つのプロパティを用意します。

var width: CGFloat!
var height: CGFloat = 9
var boxSize: CGFloat!
var gridStart: CGPoint!

画面のサイズに合わせて横のグリッド数を変更するためwidthはオプショナル型にします。縦は「9」と指定しときます。

次に初期化メソッドを用意します。update()メソッドの下に次のコードを加えます。

func createGrid() {
        
    let grid = SKNode()
        
    let usableWidth = size.width * 0.9
    let usableHeight = size.height * 0.8
        
    boxSize = usableHeight / height
        
    width = CGFloat(Int(usableWidth / boxSize))
        
    let offsetX = (size.width - boxSize * width) / 2
    let offsetY = (size.height - boxSize * height) / 2
        
    // ループを使って全てのグリッドを登録します
    for column in 0 ..< Int(width) {
        for row in 0 ..< Int(height) {
            let path = 
                UIBezierPath(rect: 
                    CGRect(x: boxSize * CGFloat(column), 
                           y: boxSize * CGFloat(row), 
                       width: boxSize, 
                      height: boxSize))
            let box = SKShapeNode(path: path.cgPath)
            box.strokeColor = UIColor.gray
            box.alpha = 0.3
            grid.addChild(box)
        }
    }
        
    gridStart = CGPoint(x: offsetX, y: offsetY)
    grid.position = CGPoint(x: offsetX, y: offsetY)
    addChild(grid) // できあがったグリッドをsceneに加えます
}

この初期化メソッドを、didMove()メソッドで呼びます*1

override func didMove(to view: SKView) {
    createGrid()
}

これでランすると...

f:id:yataiblue:20161219183239j:plain

グリッドが右上に位置してしまいます!?!

なぜ? コードを見ていてもわかりません。GameScene.sksファイルを読み込んでGameSceneクラスの初期化をさせるところで何か問題があるのは分かるのですが、どうやればいいのか分からない状態です。

いつまで悩んでいても仕方ないからGameScene.sksを使わないでコードでGameSceneクラスの初期化することにします*2

それは明日。

*1:イニシャライザーを避けるための初期化メソッドの有用性が理解できなければ、次のエントリーを読みましょう -> Swiftで遊ぼう! - 248 - UIViewの初期化ステップ - Swiftで遊ぼう! on Hatena

*2:ちょっと進歩したでしょ。

Swiftで遊ぼう! - 883 - Pathfindingの理解をすすめます

GameplayKitのチュートリアルを探していて、ちょっと興味深い記事を見つけました。

GameplayKit with Swift for Beginners — Tower Defense Game — Part 1: Gridは、タワーディフェンスタイプのゲームを作成するチュートリアルです。確かに目的物に向かって動いてくる敵キャラの動きをパス・ファインディングの機能を利用するのが打って付けです。

しかし、このチュートリアルは少々古いんです。iOS9でGameplayKitが新たに導入されたことなので、Xcodeも古いため、記述の通りに作業できません。

ちょっと工夫がひつようなので、Xcode 8で動くかどうか確認しながらコーディングしてみたのですが、解決できない問題点にいくつかぶち当たります。

分からない問題点を回避しながらプロジェクトを進めてみます。

Xcode 8で新規プロジェクトを立ち上げます。既にテンプレート選択画面から変更が加わっています。

f:id:yataiblue:20161219172225j:plain

iOSタブからApplicationの「Game」を選択します。これを選択することで、スーパービューに「SKView」クラスを持つ、GameViewControllerでコントロールできるテンプレートが用意されます*1

f:id:yataiblue:20161219172537j:plain

Xcode 8から「Integrate GameplayKit」を選択してプロジェクトに自動的に組み込むことができるんですが、まだGameplayKitが良くわかっていないので、マニュアルで導入するため、このマークは外します。

f:id:yataiblue:20161219172741j:plain

プロジェクトをセーブしたら立ち上がります。プロジェクトの設定部分で、「Portrait」のマークを外して、画面縦向きを非対応にします。ゲームは横向きにだけするんです。プロジェクト・ナビゲータをみると、色々なファイルが並んでいます。まず不要なものを消して行く作業です。

  1. Assets.xcassetsから「Spaceship」を削除します。
  2. GameScene.sksから「helloLabel」を削除して、「Scene」のアトリビュート・インスペクタにある「Color」から「Grid Color」を選択します。
  3. GameViewControllerからviewDidLoad()メソッド以外のメソッドを全て削除して、「scene.scaleMode = .aspectFill」を「scene.scaleMode = .resizeFill」に変更します。
  4. 最後に「GameScene.swift」の「didMove()」「touchesBegan()」「update()」の内容も全て消して、他のメソッドとプロパティも削除します。

ここでランします。

f:id:yataiblue:20161219175938j:plain

nodeが1つありますが、これがsceneだと思います。

ここまで。

Swiftで遊ぼう! - 882 - Pathfinding

Protocol指向プログラミングの勉強もしながら、少しGameplayKitの内容も見ています。

特にPathfindingに関心が向きました。GameplayKit Programming Guide: Pathfindingから少し勉強します。

Pathfinding

色々なゲームで共通して重要な概念がの1つが、ナビゲーションです。ボードゲームで考えれば、自分のコマを目的に応じて最適な方向に向かわせる必要があるし、アクションゲームでもアドベンチャーゲームでもキャラクターを迷路の中で動かす場合、逆に敵キャラをプレーヤーに立ち向かわせる時にも重要になります。ボードゲーム、迷宮、宇宙空間でも、何であれ共通して目的に向かって方向を決定するプロセスを、パス・ファインディング(Pathfinding)と呼び、GameplayKitは、パス・ファインディングのためのユーティリティのセットを提供してるんです。

これを使わない理由がないですね。パス・ファインディング・ユーティリティを使うために必要なものがあります。ゲームの中でナビゲーションする領域を「graph」と表現しますが、これに関連するクラスが山のようにあります。

GKGraphクラスと関連するAPIは色々あります。通れないエリアは、GKPolygonObstacleクラス、GKGraphNode2Dクラスは開かれた空間でゴールに向かう位置、そしてGKObstacleGraphクラスは、この2つのクラスを作ることができます。

SpriteKitと組み合わせて使用するなら、obstaclesFromNodePhysicsBodies:メソッドを使って、GKPolygonObstacleを作り出せます。

クラシカルなボードゲームに採用されているグリッドベースの領域は、GKGridGraphクラスとGKGridGraphNodeを使います。

ちょっと触りの説明だけです。

Swiftで遊ぼう! - 881 - 少しばかりGameplayKitをかじってみる

まだまだ新しいチャレンジを続けています。iOS9からゲーム開発にGameplayKitという新しいAPI群が追加されたことは、何となく知っていましたが、どういうものか知りませんでした。

ちょっと関連記事を読んでいると、ゲームロジックも標準で用意されていることが分かりました。色々あたらし概念が導入されているようで、EntityやComponentという存在も知りました。GameplayKit Programming Guide: Entities and Componentsに書かれている内容は興味深いものでした。OOPでのゲーム開発の問題点を指摘して、Entity-Componentデザイン・パターンの説明が入ります。

「あ!」でした。

このEntity-Componentデザイン・パターンというのが、まさにProtocol指向プログラミング・スタイルじゃありませんか。

やっぱりProtocol指向プログラミング、ちゃんと勉強しないといけませんね。

Swiftで遊ぼう! - 880 - Raywnderlichさんトコの掟

rayernderlich.comと言えば、私のお気に入りのチュートリアルサイトです。

ここで私のプログラミング・スキルは鍛えられていると言っても過言じゃないんです。

FAQ - Ray Wenderlich

ここの規約を見ると、10記事までなら日本語に翻訳してもいいようです。

10記事に翻訳制限している意味ってなんでしょう? 自分らで翻訳したいからだって。

実は、私が紹介している彼らの記事は、単なる翻訳と異なります。一度咀嚼して吸収した内容を自分の言葉に置き換えて説明しているので、彼らの言う翻訳の範疇には入りません。と言うことで、10記事以上、紹介していくつもりです。

さて、Rayさんは、どういう反応をするでしょう。

当然、彼らのコピーライトを冒さないように、コードも替え、イメージも変えて、全く異なる内容に置き換えて説明していこうと思います。

ストーリーラインと記事のポイントは丸々パクるけど、どうなることやら。

今日はこれだけ。