検索
連載

デザインパターン「ファーストクラスコレクション」でSwiftコードの保守性・可読性を上げる方法をゲームのコードから学ぶiOS SDKとSwiftで始めるゲーム作成入門(4)(1/2 ページ)

iPhoneゲームをSwift言語で作成してみたいという初心者向けにiOSのゲームフレームワークを使った作り方を一から解説する入門連載。今回は、今後発生し得る要望を意識したクラス設計のために、デザインパターン「ファーストクラスコレクション」を適用する方法を解説。

PC用表示 関連情報
Share
Tweet
LINE
Hatena

 本連載「iOS SDKとSwiftで始めるゲーム作成入門」は、iPhone向けのゲーム開発の入門連載です。タワーディフェンスを題材に、「SpriteKit」というゲーム開発フレームワークの解説やゲームの開発手法について書いています。

 実装に入る前に本連載で作るアプリの完成形を確認しておきます。本連載では、下記6つのルールを満たすタワーディフェンスを作っていきます。

  1. プレーヤーは開始前に与えられた所持金を元に、敵を攻撃するユニットを設置する
  2. ゲームは「Wave(ウエーブ)」という単位で行われる。基本的には1つのWaveで登場する敵の種類は1種類のみとなっている。Waveが始まると敵は入口から登場し、目的地に向かって行進する。プレーヤーが設置したユニットは攻撃可能範囲に入ると自動的に攻撃を行う
  3. 1Waveの敵を全て全滅させるとWaveクリアとなる。プレーヤーは次のWaveが始まるまでにユニットの増強(新設・アップグレード・売却など)を行う
  4. 以上の2と3を繰り返して行う
  5. 敵が目的地に到達すると、自分が所持しているライフが減少する。全て失うとゲームオーバーとなる
  6. ライフを全て失う前に、最終Waveの敵を全て全滅させることができればクリアとなる

 今回は前回の「iOS 9の最新機能で自動ルート検索を簡単にゲームに組み込む」に引き続き、ルール2に関する部分として敵の動きの実装を行っていきます。前回では敵がルートに沿って移動する処理を実装しました。今回は敵が1体だけでなく複数体出てくる処理を実装をしたいと思います。

 さらに今回からはコードの可読性や拡張性を意識した実装をしていきます。ゲームだけではなく、さまざまな開発現場で利用できる知見を共有できるように進めていきます。

クラスを分割して可読性を上げる

 GameSceneクラスのコード量が増えてきたので機能を実装する前に現状のコードを少し整理します。一部の処理を別にクラスに移動してみます。

フィールドに関する処理を分ける

 GameSceneクラスからフィールド生成処理を別クラスに移動します。

 Xcodeのメニューの「File」→「New」→「File...」からフィールド情報を持つFieldクラスとフィールドを生成する「FieldFactory」クラスを作成してください。

import SpriteKit
 
class FieldFactory {
    private let fieldData = [
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 1],
        [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
    ]
 
    func createField(viewSize: CGSize, fieldImageLength: CGFloat) -> Field {
        let field = Field()
        field.nodes = createFieldNodes(viewSize, fieldImageLength: fieldImageLength)
        field.start = CGPoint(x: fieldImageLength *  2, y: viewSize.height)
        field.end   = CGPoint(x: fieldImageLength * 13, y: viewSize.height - fieldImageLength * 9)
 
        return field
    }
 
    private func createFieldNodes(viewSize: CGSize, fieldImageLength: CGFloat) -> [SKSpriteNode] {
        var fieldNodes = [SKSpriteNode]()
 
        for (i, data) in fieldData.enumerate() {
            for (j, value) in data.enumerate() {
                let fieldNode = SKSpriteNode(imageNamed: "Field\(value)")
                fieldNode.name = "Field\(value)"
                fieldNode.size = CGSize(width: fieldImageLength, height: fieldImageLength)
                fieldNode.physicsBody = SKPhysicsBody(rectangleOfSize: fieldNode.size)
                fieldNode.physicsBody?.categoryBitMask = 0x0
                fieldNode.physicsBody?.collisionBitMask = 0x0
                fieldNode.position = CGPoint(
                    x: CGFloat(j) * fieldImageLength,
                    y: viewSize.height - CGFloat(i - 1) * fieldImageLength)
                fieldNode.zPosition = -1
                fieldNodes.append(fieldNode)
            }
        }
        return fieldNodes
    }
}
 
class Field {
    var nodes = [SKSpriteNode]()
    var start = CGPoint()
    var end   = CGPoint()
}

フィールド情報を基にルートを生成する処理をメソッドとして分割

 GameSceneクラスも少し修正します。それに同時にフィールド情報を基にルートを生成する処理をメソッドとして分割していきます。

class GameScene: SKScene, SKPhysicsContactDelegate {
    enum State {
        case Playing
        case GameClear
        case GameOver
    }
    var state = State.Playing
    let enemy = SKSpriteNode(imageNamed: "Enemy")
    let char = SKSpriteNode(imageNamed: "Char")
 
    override func didMoveToView(view: SKView) {
        physicsWorld.gravity = CGVectorMake(0, 0)
        physicsWorld.contactDelegate = self
 
        // ↓ ここにあったField生成処理をFieldFactoryに移動
        let fieldImageLength = view.frame.width / 10
        let field = FieldFactory().createField(view.frame.size, fieldImageLength: fieldImageLength)
        field.nodes.forEach {
            addChild($0)
        }
        // ↑ ここにあったField生成処理をFieldFactoryに移動
 
        char.position = CGPoint(x:250, y:300)
        char.physicsBody = SKPhysicsBody(rectangleOfSize: char.size)
        char.physicsBody?.contactTestBitMask = 0x1
        addChild(char)
 
        // routesを計算する処理をメソッド化
        routes = routesWithField(field)
 
        // 省略
    }
 
    private func routesWithField(field: Field) -> [vector_float2] {
        let fields = children.filter { $0.name == "Field0" }
        let obstacles = SKNode.obstaclesFromNodePhysicsBodies(fields)
        let graph = GKObstacleGraph(obstacles: obstacles, bufferRadius: 10)
        let start = GKGraphNode2D(point: vector_float2(Float(field.start.x), Float(field.start.y)))
        let end = GKGraphNode2D(point: vector_float2(Float(field.end.x), Float(field.end.y)))
        graph.connectNodeUsingObstacles(start)
        graph.connectNodeUsingObstacles(end)
        let nodes = graph.findPathFromNode(start, toNode: end)
        return nodes.flatMap { $0 as? GKGraphNode2D }.map { $0.position }
    }
    // 省略
}

 フィールドの情報とフィールドの生成処理を外に出したことでGameSceneがすっきりしました。

コラム「今後発生し得る要望を意識したクラス設計」

 前項ではFieldクラスを分割しました。これはGameSceneの処理を別クラスに移動することによる可読性向上もありますが、拡張性向上という意図もあります。

 今回のようなゲームを開発する場合、フィールドについて今後いろいろな要望が出てくる可能性があります。「フィールドの道順を変えたい」「フィールドを草原でなく砂漠にしたい」など要望が来るかもしれません。

 そういったときにGameSceneにフィールド生成処理が書かれていると一気にコードが肥大化して可読性が著しく落ちます。しかし、Fieldを生成するクラスが分かれていれば、新規フィールドに対応するField生成クラスを実装するだけで済むので出てきた要望にもスムーズに対応できます。

 このようなクラスの分割方法はデザインパターンのFactoryMethodパターンやAbstract Factoryパターンを学ぶと、さらに理解できると思うので、興味がありましたらその辺りも併せて勉強すると楽しいかと思います。


Copyright © ITmedia, Inc. All Rights Reserved.

       | 次のページへ
ページトップに戻る