iPhoneゲームをSwift言語で作成してみたいという初心者向けにiOSのゲームフレームワークを使った作り方を一から解説する入門連載。今回は、今後発生し得る要望を意識したクラス設計のために、デザインパターン「ファーストクラスコレクション」を適用する方法を解説。
本連載「iOS SDKとSwiftで始めるゲーム作成入門」は、iPhone向けのゲーム開発の入門連載です。タワーディフェンスを題材に、「SpriteKit」というゲーム開発フレームワークの解説やゲームの開発手法について書いています。
実装に入る前に本連載で作るアプリの完成形を確認しておきます。本連載では、下記6つのルールを満たすタワーディフェンスを作っていきます。
今回は前回の「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.