Swiftで遊ぼう! on Hatena

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

Swiftで遊ぼう! - 899 - 過去記事のマイナー調整やってます

いろいろな意味で総復習中。

UIGestureRecognizerの説明ページの内容が古いままだったと思ったら他のページに集約されていました(^_^;)

yataiblue.hatenablog.com

整理をしていってます。

Swiftで遊ぼう! - 898 - Parhfindingのチュートリアル終了

問題も解決して、再びPathfindingのチュートリアルを仕上げます。

実は、年始から非常に忙しく、Swiftの勉強がなおざりになっていました(>_<)

Pathfindingチュートリアルも最期のステップを説明していなかったので簡単に説明して締めくくります。

最期に説明したupdateVisualPathメソッドを次のupdatePathForEntitiesメソッド内でで呼びます。

func updatePathForEntities(entities: [GKEntity]) {
    for entity in entities {
       if let movementComponent = 
           entity.component(ofType: MovementComponent.self) {
           movementComponent.sprite.removeAllActions()
              
            var path = movementComponent.pathToDestination()
            path.remove(at: 0)
            
           // update visual path
            updateVisualPath(path: path)
                
            movementComponent.followPath(path: path)
        }
    }
}

これでシュミレーターをランすると次のように点線が表示されます。凄いですね。タワーを設置するとルートが自動で再計算されて変化していきます。

f:id:yataiblue:20170115095349j:plain


これでいちおうPathfindingのチュートリアルを終了にします。

次はどうしたものか...

Swiftで遊ぼう! - 897 - CGPathの初期化ステップ

昨日の記事のステップ「8」はiOS10で変更になった点線をつくる初期化ステップ...

let dashed = 
       CGPath.init(__byDashing: bezierPath.cgPath, 
                     transform: nil, 
                         phase: 0, 
                       lengths: pattern,
                         count: 2)!

ここで「pattern」と「count」の意味がよく分かりません.... 自分で調べないといけないんですが、なかなか情報に辿りつけないので、どなたかご存じの人がいらっしゃれば教えてくださいm(_ _)m

これだけ。

Swiftで遊ぼう! - 896 - チュートリアルやっと終了に向かって...

やっとGameplayKitの古い*1チュートリアルもやった終盤に入ります。

最後の課題は、Pathfindingを使って得られる道筋の描画用のSKNodeを用意します。GameScene.swiftの冒頭で宣言。

var pathLine: SKNode!

宣言をしたら初期化が必須です。カスタムクラスならinit()イニシャライザーで書くのですが、フレームワークの使用では、多くの場合、初期化メソッドが用意されています*2。SKSceneクラスの場合、didMove(to view: SKView)メソッドなので、次のコードを加えます。

pathLine = SKNode()
addChild(pathLine)

これで受け皿は用意できました。ここにpathLineに描画すれば、画面に出現するわけです。

パスラインを描画しるメソッドを用意します。

func updateVisualPath(path: [GKGridGraphNode]) {
    // 1
    pathLine.removeAllChildren()
    // 2
    var index = 0
    // 3
    for node in path {
        // 4
        let position = 
            pointFor(coordinate: node.gridPosition)
            
        if index + 1 < path.count {
            // 5
            let nextPosition = 
          pointFor(coordinate: path[index + 1].gridPosition)
            // 6
            let bezierPath = UIBezierPath()
            let startPoint = 
                CGPoint(x: position.x, y: position.y)
            let endPoint = 
                CGPoint(x: nextPosition.x, y: nextPosition.y)
            bezierPath.move(to: startPoint)
            bezierPath.addLine(to: endPoint)
            // 7
            let pattern: [CGFloat] = 
                [CGFloat(boxSize / 10), CGFloat(boxSize / 10)]
            // 8
            let dashed = 
                CGPath.init(__byDashing: bezierPath.cgPath, 
                              transform: nil, 
                                  phase: 0, 
                                lengths: pattern, 
                                  count: 2)!
            // 9
            let line = SKShapeNode(path: dashed)
            line.strokeColor = UIColor.black
            pathLine.addChild(line)

        }
        // 10
        index += 1
    }
}

このメソッドを、読んでコードがスラスラと分かるようなら、あなたのプログラミングレベルは中級です。もし、理解できなければまだまだ初級の域を脱していないってことで、コードの説明に入ります。

  1. まずpathLineに加えた全てのノードを除去します。
  2. ここのpathは[GKGridGraphNode]のことで、このクラス出持っているプロパティ「graph」のことで、nodeのパス情報を持っていることになります。そして、indexを「0」にしているのは、このアレー型のパス情報にいくつのnodeがあるかカウントするために使用します。
  3. そしてpathにある個々のnodeで作業するためにforループを使って要素を抜き出します。
  4. nodeの位置情報はint2型なので、描画のためのCGPoint型に変換。
  5. 次に移動すべきnodeの位置をCGPont型に変換してnextPositionに保持させます。
  6. UIBezierPathを使って*3今いるnodeの中点から次のnodeを中点へ線を書きます。
  7. 実践を点線に変更 <- これが理解できない
  8. この現在のnodeから次のnodeに引くBezierPathを使って、SKShapeNodeのインスタンスにして、pathLineに加えます。
  9. pathにあるすべてのnodeを繋げていく点線をpathLineに保持させます。

とこういう流れです。

ちょっと分からない「8」のステップは明日のエントリーに。

*1:iOS 9で最初に紹介されたPathfindingの説明なんで...

*2:初期化メソッドの使用が推奨されている理由は次 -> Swiftで遊ぼう! - 248 - UIViewの初期化ステップ - Swiftで遊ぼう! on Hatena

*3:UIBezierPathの基本事項 -> Swiftで遊ぼう! - 251 - draw: UIBezierPath - Swiftで遊ぼう! on Hatena

Swiftで遊ぼう! - 895 - GameplayKitは結構おもしろい

GameplayKitはiOS9で導入された新しいフレームワークですが、やっぱりゲームの世界は売れる市場なんで、変化と工夫が多岐に渡っていますね。iOS10でもその流れは変わらず、ネットに溢れるチュートリアルが、どれもこれもあっという間に時代遅れの情報になっています。

私がトライしている「GameplayKit with Swift for Beginners — Tower Defense Game — Part 4: Dynamic Pathfinding」も例外ではなく、少々古い内容です。GameplayKitの「Pathfinding」もiOS10でアップデイトされています。

残念なことに、私には新しいAPIの説明をする実力が備わっていないので、このままチュートリアルを進めます。

敵のキャラ(赤い四角)が画面左端に出現して右動いていきます。やぐらをタップして出現させても動きを遮ることができません。次はやぐらで行く手を遮るコードを加えていきます。自動的にルートを再計算して敵キャラの進行が変化するところがPathfindingの真骨頂です。

さて、SpriteKitとGameplayKitのPathfindingを扱う時に混乱しそうになるのが、それぞれの座標系が異なるため、自分で用意したメソッド、coordinateFor()とpointFor()を使って、int2にしたり、CGPointにしなければならないのが億劫です。

まず、createEnemies()メソッドにタワーを設置したときにPathfindingをアップデイトする仕組みを組み込む必要があります。

タワーの設置に関係なく突き進む次のコードを消去します。

// 一時的に以下のコードを加えて動作チェック
let path = movementComponent.pathToDestination()
MovementComponent.followPath(path: path)

そして、アップデイト様のメソッド「 updatePathForEntities(entities: [GKEntity])」を「createTower(atCoordinate: int2) 」の下に加えます。

func updatePathForEntities(entities: [GKEntity]) {
    for entity in entities {
        if let movementComponent = 
            entity.component(ofType: MovementComponent.self) {
            movementComponent.sprite.removeAllActions()
                
            var path = movementComponent.pathToDestination()
            path.remove(at: 0)
                
            // update visual path
                
            movementComponent.followPath(path: path)
        }
    }
}

entityがボード上に加わる度にパスを再計算させるメソッドです。アレー型のエンティティを引数として受け取ります。

エンティティにMovementComponentがあるかどうか判断して、あれば全てのActionを消去します。そして新たにpathTODestination()メソッドを使ってパスの再設定をしますが、スタート地点は現在エンティティが存在する場所になるので、インデックス「0」の位置は消去します。そしてfollowPath()メソッドを使って描画するって話しです。

// update visual path

これは明日説明します。

このメソッドをcreateEnemies()メソッド内で呼びます。

let action = SKAction.run { [unowned self] in
    let movementComponent = 
        enemy.component(ofType: MovementComponent.self)!
    self.addChild(movementComponent.sprite)
                
    self.updatePathForEntities(entities: [enemy])          
}

そしてもう1箇所、 createTower(atCoordinate: int2)メソッドの最期で呼びます。

updatePathForEntities(entities: enemies)

これで敵キャラはタワーを避けながら目的地に進みます。

f:id:yataiblue:20170105173701j:plain

次はパスを描画するステップです。

Swiftで遊ぼう! - 894 - 話はとびまくりGameplayKitチュートリアルに戻ります

人様が作ったチュートリアルに取り組んでいると、最近不満に思うことが増えています。「もう少しツッコんだ説明が欲しいな」とか「もう少し実践的なコーディングの説明ないかな」なんて思います。

GameplayKitのチュートリアルも少々古く、iOS10で加わった新しい機能を追加して改訂してくれたらいいのにな、なんて不満があって途中で止めてしまいそうになりましたが。何が何でも続けるという信条でPathfindingの説明を続けます。

MovementComponentを作ったので、これをEntityに加えます。新しい敵キャラ(赤いスクエアですが)をEntityとして登録します。

GameScene.swiftの冒頭に次のプロパティを登録します。

var enemies = [GKEntity]()

GKEntityタイプの空のアレーを用意します。

次に「createGrid()」メソッドの下に次のコードを加えます。

func createEnemies() {

}

ここで敵entityを作って、画面の左端から右端のゴールに向かって動いてもらいます。

let enemy = GKEntity()
let gridPosition = int2(0, Int32(height) / 2)
let destination = int2(Int32(width) - 1, Int32(height) / 2)

gridPositionはenemyの現在位置と言っていいでしょう。画面左端の真ん中からスタートします。destinationは目的地となります。どちらの値もint2型で、GKGridGraphの座標系です。

実際に画面に表示をするためにSKSpriteNodeが必要になります。

let sprite = SKSpriteNode(color: UIColor.red, 
                           size: CGSize(width: boxSize * 0.6, 
                                       height: boxSize * 0.6))
sprite.position = pointFor(coordinate: gridPosition)

spriteはただの赤い箱です。位置情報は、int2型からCGPoint型に変換するメソッド「pointFor(coordinate:)」を使います。

SKViewに表示することと、GKGridGraphに表示することが異なることを理解しておく必要があります。<-このやり方が違うような気がするんですがどうなんでしょう?

どちらにしろ次のコーディングは、GKGridGraphに表示させるためにMovementComponentインスタンスを作ります(VisualCompanentを継承してるからです)。そして移動先も登録します。

let movementComponent = MovementComponent(scene: self, 
                                         sprite: sprite, 
                                     coordinate: gridPosition, 
                                    destination: destination)

このコンポーネントを敵entityに登録します。

enemy.addComponent(movementComponent)

そして、このenemyをenemiesに登録します。

enemies.append(enemy)

これでエンティティは用意されたけど、実際SKViewでは何も生じません。次はSKSpriteKitを使って、画面上で動きを表示させるステップです。

var sequence = [SKAction]()

アクションの入れ籠を用意します。複数の敵キャラに動きを対応するためです。今のところenemyは1つだけですが、次のようにループを実行させます。

for enemy in enemies {
    let action = SKAction.run { [unowned self] in
        let movementComponent = 
            enemy.component(ofType: MovementComponent.self)!
        self.addChild(movementComponent.sprite)
                
        // updateは後で追加
                
        // 一時的に以下のコードを加えて動作チェック
        let path = movementComponent.pathToDestination()
        movementComponent.followPath(path: path)
    }
            
    let delay = SKAction.wait(forDuration: 2)
            
    sequence += [action, delay]
}

タワーが設置されることで、パスファインディングの再計算をさせてupdateさせないといけませんが、今は、ちゃんと敵が動くかどうか確認するためにMovementComponentの「followPath()メソッドを使ってActionシークエンスを実行させてみます。

このコードを「didMove(to view: SKView)」に組み込みます。

override func didMove(to view: SKView) {
        
    createGrid()
    graph = 
        GKGridGraph(fromGridStartingAt: int2(0, 0), 
                                 width: Int32(width), 
                                height: Int32(height), 
                      diagonalsAllowed: false)
    createEnemies()
}

これでランすると、赤い箱が画面の左端から右端に動いていきます。

f:id:yataiblue:20161226170543j:plain

まだupdateが実装されていないので、進行方向にタワーを設置してもすり抜けていきます。

今日はここまで。

Swiftで遊ぼう! - 893 - チュートリアルをアップデイト中

AppleチュートリアルがSwift 3に対応したので、私の翻訳バージョンもSwift 3に対応しています。

今日は2ページ目です。

yataiblue.hatenablog.com