昨日はレイアウトのリデザインを行った。
今日はこのレイアウトを実現するための仕組みを実装する。
レイアウトマネージャ
SpriteKitでレイアウトを決めるときに必要になるのが、プロパティのpositionの値。
なので、各要素のpositionの適切な値を返すクラスを用意するといい。
これをレイアウトマネージャと呼ぶことにする。
レイアウトマネージャに必要な機能は、各要素の適切なpositionの値を返すというもの。
ただし、
- positionの値は、その要素が追加されるノードの座標系での値である必要がある。
- positionの値は、その要素の中心の位置である必要がある。
なので、それを考慮しないといけない。
ところで、各要素の中心の位置というのは、結構分かりにくい。
画面の左上を原点として、各要素の左上の位置を返す方が分かりやすい。
それと、各デバイスによってその位置も変わってくることに注意しないといけない。
そこで、以下のようにする。
- レウアウトマネージャは、各要素の適切なpositionの値を返すインタフェースを持つ。
- レイアウトマネージャの実体は、インタフェースとなる抽象クラスのサブクラスとし、デバイスごとに異なるものとする。
- レイアウトマネージャの実体は、各要素の左上の位置、および、各要素のサイズを返す実装を持つ。
- レイアウトマネージャのpositionの値を返すインタフェースは、各要素の左上の位置、各要素のサイズを返す実装から、適切なpositionの値を計算して返す。
うーん、言葉で書くのだと分かりにくいなぁ(^^;
実際にコードを見た方が分かりやすいかも。
LayoutManagerの実装
ということで、早速コードを。
//============================== // YWF //------------------------------ // LayoutManager.swift //============================== import SpriteKit public class LayoutManager { public enum Component { case Scene case Background case Header case Logo case Board case ExitButton case UndoButton case PassButton } // 続く
とりあえず、要素の列挙型を定義。
インスタンスの取得
次に、レイアウトマネージャのインスタンスを取得するためのインタフェース。
// 続き public class func getInstanceFor(size: CGSize) -> LayoutManager { switch size.height { case 920..<1020: return SmallestLayout(size: size) case 1020..<1080: return SmallLayout(size: size) case 1080..<1180: return MiddleLayout(size: size) case 1180..<1500: return LargeLayout(size: size) default: return LayoutManager(size: size) } } // 続く
引数で与えられたSceneのサイズに応じて、返す実体を変えている。
なお、各実体の境目となる値は、余白を除いて必要なサイズとなっている。
具体的には
実体 | ヘッダ | 相手情報 | ボード | 自分情報 | 合計 |
---|---|---|---|---|---|
Smallest | なし | 80 | 760 | 80 | 920 |
Small | 100 | 80 | 760 | 80 | 1020 |
Middle | 100 | 80 | 760 | 140 | 1080 |
Large | 140 | 140 | 760 | 140 | 1180 |
という感じ。
プロパティとイニシャライザ
次はプロパティとイニシャライザ。
// 続き private let size: CGSize private init(size: CGSize) { self.size = size } // 続く
なお、インタフェースとなる抽象クラスが作られては困るので、イニシャライザの可視性はprivateとなっている。
positionを返すインタフェース
さて、レイアウトマネージャの肝となる、positionを返すインタフェース。
// 続き public func getPosition(component: Component, relativeTo base: Component) -> CGPoint! { assert( component != .Scene, ".Scene is specified for component.") let point = self.getUpperLeftPosition(component) let size = self.getSize(component) if point == nil || size == nil { return nil } if base == .Scene { let x = point.x + size.width/2.0 let y = self.size.height - point.y - size.height/2.0 return CGPoint(x: x, y: y) } else { let basePoint = self.getUpperLeftPosition(base) let baseSize = self.getSize(base) if basePoint == nil || baseSize == nil { return nil } else { let x = (point.x + size.width/2.0) - (basePoint.x + baseSize.width/2.0) let y = -((point.y + size.height/2.0) - (basePoint.y + baseSize.height/2.0)) return CGPoint(x: x, y: y) } } } // 続く
positionの値はその要素が追加されるノードの座標系での値である必要があるので、positionの値が欲しい要素だけじゃなくて、基準となる要素も引数で指定するようにしてある。
さて、肝心の計算の仕方。
まず、基準となる要素の中心の位置は、画面の左上を原点として、次のようになる。
(基準要素の中心のx座標) = (基準要素の左上のx座標) + (基準要素の幅)/2 (基準要素の中心のy座標) = (基準要素の左上のy座標) + (基準要素の高さ)/2
同様に、指定された要素の中心の位置は、画面の左上を原点として、次のようになる。
(指定要素の中心のx座標) = (指定要素の左上のx座標) + (指定要素の幅)/2 (指定要素の中心のy座標) = (指定要素の左上のy座標) + (指定要素の高さ)/2
ところで、基準となる要素の座標で、画面の左上の座標は次のようになる。
(基準となる要素の座標は、右上に行くほど値が大きくなることに注意)
(画面の左上のx座標) = - ( (基準要素の左上のx座標) + (基準座標の幅)/2 ) (画面の左上のy座標) = (基準要素の左上のy座標) + (基準座標の高さ)/2
したがって、基準となる要素の座標で、指定された要素の座標は、次のようになる。
(やはり、基準となる要素の座標は、右上に行くほど値が大きくなることに注意)
(指定要素の中心のx座標) = ( (指定要素の左上のx座標) + (指定要素の幅)/2 ) - ( (基準要素の左上のx座標) + (基準要素の幅)/2 ) (指定要素の中心のy座標) = - ( (指定要素の左上のy座標) + (指定要素の高さ)/2 ) + ( (基準要素の左上のy座標) + (基準座標の高さ)/2 )
ただし、基準となる要素がSceneの場合、ちょっと変わってくる。
というのも、Sceneの座標はノードの中心が原点ではなく、ノードの左下が原点となっているから。
ということで、画面の左上の座標が、Sceneの座標では次のようになる。
(画面の左上のx座標) = 0 (画面の左上のy座標) = (Sceneの高さ)
よって、基準となる要素がSceneの場合、指定された要素の座標は、次のようになる。
(指定要素の中心のx座標) = (指定要素の左上のx座標) + (指定要素の幅)/2 (指定要素の中心のy座標) = - ( (指定要素の左上のy座標) + (指定要素の高さ)/2 ) + (Sceneの高さ)
レイアウトマネージャの実体のためのインタフェース
positionの計算を可能にするために、各レイアウトマネージャの実体が実装しなければならないメソッドのインタフェースを用意する。
// 続き private func getUpperLeftPosition(component: Component) -> CGPoint! { return nil } private func getSize(component: Component) -> CGSize! { switch component { case .Scene: return self.size case .Background: return CGSize(width: 828.0, height: 1472.0) case .Header: return CGSize(width: 828.0, height: 160.0) case .Logo: return CGSize(width: 260.0, height: 90.0) case .Board: return CGSize(width: 760.0, height: 760.0) case .ExitButton, .UndoButton, .PassButton: return CGSize(width: 120.0, height: 60.0) } } // 続く
といっても、各要素のサイズは(今のところ)どれも同じなので、ここで定義してしまっている。
必要ならサブクラスでオーバーライドする。
なお、これらのメソッドの可視性はprivateになっている。
ここらへんがSwiftの面白いところで、可視性はファイルベースなので、こんな指定が出来たりもする。
各レイアウトマネージャの実体の定義
レイアウトマネージャのサブクラスで実体を定義していく。
// 続き // for iPhone 4S private class SmallestLayout: LayoutManager { private var userInfoSpaceHeight: CGFloat // workaround to set using method. (actually constant) private let buttonMargin: CGFloat private override init(size: CGSize) { self.userInfoSpaceHeight = 0.0 self.buttonMargin = 20.0 super.init(size: size) self.userInfoSpaceHeight = (self.size.height - self.getSize(.Board).height)/2.0 } private override func getUpperLeftPosition(component: Component) -> CGPoint! { switch component { case .Scene, .Background: return CGPoint(x: 0.0, y: 0.0) case .Header, .Logo: return nil case .Board: let x = (self.size.width - self.getSize(.Board).width)/2.0 let y = self.userInfoSpaceHeight return CGPoint(x: x, y: y) case .ExitButton: let x = self.size.width - (self.buttonMargin + self.getSize(.ExitButton).width) let y = (self.userInfoSpaceHeight - self.getSize(.ExitButton).height)/2.0 return CGPoint(x: x, y: y) case .UndoButton: let x = self.size.width - (self.buttonMargin*2.0 + self.getSize(.UndoButton).width + self.getSize(.PassButton).width) let y = self.userInfoSpaceHeight + self.getSize(.Board).height + (self.userInfoSpaceHeight - self.getSize(.UndoButton).height)/2.0 return CGPoint(x: x, y: y) case .PassButton: let x = self.size.width - (self.buttonMargin + self.getSize(.PassButton).width) let y = self.userInfoSpaceHeight + self.getSize(.Board).height + (self.userInfoSpaceHeight - self.getSize(.PassButton).height)/2.0 return CGPoint(x: x, y: y) } } } // for iPad private class SmallLayout: LayoutManager { private let userInfoSpaceHeight: CGFloat private var userInfoSpaceMargin: CGFloat // workaround to set using method. (actually constant) private let headerHeight: CGFloat private let headerShadowHeight: CGFloat private let buttonMargin: CGFloat private override init(size: CGSize) { self.userInfoSpaceHeight = 80.0 self.userInfoSpaceMargin = 0.0 self.headerHeight = 100.0 self.headerShadowHeight = 10.0 self.buttonMargin = 20.0 super.init(size: size) self.userInfoSpaceMargin = (self.size.height - self.headerHeight - self.getSize(.Board).height - self.userInfoSpaceHeight*2.0)/4.0 } private override func getUpperLeftPosition(component: Component) -> CGPoint! { switch component { case .Scene, .Background: return CGPoint(x: 0.0, y: 0.0) case .Header: let offset = self.getSize(.Header).height - self.headerHeight - self.headerShadowHeight return CGPoint(x: 0.0, y: -offset) case .Logo: let margin = (self.headerHeight - self.getSize(.Logo).height)/2.0 return CGPoint(x: margin, y: margin) case .Board: let x = (self.size.width - self.getSize(.Board).width)/2.0 let y = self.headerHeight + self.userInfoSpaceMargin*2.0 + self.userInfoSpaceHeight return CGPoint(x: x, y: y) case .ExitButton: let x = self.size.width - (self.buttonMargin + self.getSize(.ExitButton).width) let y = (self.headerHeight - self.getSize(.ExitButton).height)/2.0 return CGPoint(x: x, y: y) case .UndoButton: let x = self.size.width - (self.buttonMargin*2.0 + self.getSize(.UndoButton).width + self.getSize(.PassButton).width) let y = self.headerHeight + self.userInfoSpaceHeight + self.getSize(.Board).height + self.userInfoSpaceMargin*3.0 + (self.userInfoSpaceHeight - self.getSize(.UndoButton).height)/2.0 return CGPoint(x: x, y: y) case .PassButton: let x = self.size.width - (self.buttonMargin + self.getSize(.PassButton).width) let y = self.headerHeight + self.userInfoSpaceHeight + self.getSize(.Board).height + self.userInfoSpaceMargin*3.0 + (self.userInfoSpaceHeight - self.getSize(.PassButton).height)/2.0 return CGPoint(x: x, y: y) } } } // for iPhone 5 / 5s / 5c private class MiddleLayout: LayoutManager { private let userInfoSpaceHeight: CGFloat private let opponentUserInfoSpaceHeight: CGFloat private var userInfoSpaceMargin: CGFloat // workaround to set using method. (actually constant) private let headerHeight: CGFloat private let headerShadowHeight: CGFloat private let buttonMargin: CGFloat private override init(size: CGSize) { self.userInfoSpaceHeight = 140.0 self.opponentUserInfoSpaceHeight = 80.0 self.userInfoSpaceMargin = 0.0 self.headerHeight = 100.0 self.headerShadowHeight = 10.0 self.buttonMargin = 20.0 super.init(size: size) self.userInfoSpaceMargin = (self.size.height - self.headerHeight - self.getSize(.Board).height - self.userInfoSpaceHeight - self.opponentUserInfoSpaceHeight)/4.0 } private override func getUpperLeftPosition(component: Component) -> CGPoint! { switch component { case .Scene, .Background: return CGPoint(x: 0.0, y: 0.0) case .Header: let offset = self.getSize(.Header).height - self.headerHeight - self.headerShadowHeight return CGPoint(x: 0.0, y: -offset) case .Logo: let margin = (self.headerHeight - self.getSize(.Logo).height)/2.0 return CGPoint(x: margin, y: margin) case .Board: let x = (self.size.width - self.getSize(.Board).width)/2.0 let y = self.headerHeight + self.userInfoSpaceMargin*2.0 + self.opponentUserInfoSpaceHeight return CGPoint(x: x, y: y) case .ExitButton: let x = self.size.width - (self.buttonMargin + self.getSize(.ExitButton).width) let y = (self.headerHeight - self.getSize(.ExitButton).height)/2.0 return CGPoint(x: x, y: y) case .UndoButton: let x = self.size.width - (self.buttonMargin + self.getSize(.UndoButton).width) let y = self.headerHeight + self.opponentUserInfoSpaceHeight + self.getSize(.Board).height + self.userInfoSpaceMargin*3.0 return CGPoint(x: x, y: y) case .PassButton: let x = self.size.width - (self.buttonMargin + self.getSize(.PassButton).width) let y = self.headerHeight + self.opponentUserInfoSpaceHeight + self.getSize(.Board).height + self.userInfoSpaceMargin*3.0 + self.getSize(.UndoButton).height + self.buttonMargin return CGPoint(x: x, y: y) } } } // for iPhone 6 / 6 Plus private class LargeLayout: LayoutManager { private let userInfoSpaceHeight: CGFloat private var userInfoSpaceMargin: CGFloat // workaround to set using method. (actually constant) private let headerHeight: CGFloat private let headerShadowHeight: CGFloat private let buttonMargin: CGFloat private override init(size: CGSize) { self.userInfoSpaceHeight = 140.0 self.userInfoSpaceMargin = 0.0 self.headerHeight = 140.0 self.headerShadowHeight = 10.0 self.buttonMargin = 20.0 super.init(size: size) self.userInfoSpaceMargin = (self.size.height - self.headerHeight - self.getSize(.Board).height - self.userInfoSpaceHeight*2.0)/4.0 } private override func getUpperLeftPosition(component: Component) -> CGPoint! { switch component { case .Scene, .Background: return CGPoint(x: 0.0, y: 0.0) case .Header: let offset = self.getSize(.Header).height - self.headerHeight - self.headerShadowHeight return CGPoint(x: 0.0, y: -offset) case .Logo: let margin = (self.headerHeight - self.getSize(.Logo).height)/2.0 return CGPoint(x: margin, y: margin) case .Board: let x = (self.size.width - self.getSize(.Board).width)/2.0 let y = self.headerHeight + self.userInfoSpaceMargin*2.0 + self.userInfoSpaceHeight return CGPoint(x: x, y: y) case .ExitButton: let x = self.size.width - (self.buttonMargin + self.getSize(.ExitButton).width) let y = (self.headerHeight - self.getSize(.ExitButton).height)/2.0 return CGPoint(x: x, y: y) case .UndoButton: let x = self.size.width - (self.buttonMargin + self.getSize(.UndoButton).width) let y = self.headerHeight + self.userInfoSpaceHeight + self.getSize(.Board).height + self.userInfoSpaceMargin*3.0 return CGPoint(x: x, y: y) case .PassButton: let x = self.size.width - (self.buttonMargin + self.getSize(.PassButton).width) let y = self.headerHeight + self.userInfoSpaceHeight + self.getSize(.Board).height + self.userInfoSpaceMargin*3.0 + self.getSize(.UndoButton).height + self.buttonMargin return CGPoint(x: x, y: y) } } } }
長々とやってるけど、やってること自体は簡単。
各要素の左上の座標を返すメソッドを実装しているだけ。
一応、固定値で返してしまってもいいのだけど、読めば意味が分かるように、各座標をどう出しているのかを式で表すようにしてある。
レイアウトマネージャの動作確認
コントローラを書き換えて、動作を確認する。
//============================== // YWF //------------------------------ // GameViewController.swift //============================== import UIKit import SpriteKit class GameViewController: UIViewController { @IBOutlet weak var skView: SKView! override func viewDidLoad() { BoardNode.loadAssets() ButtonNode.loadAssets() PieceNode.loadAssetsAndCreateTemplates() } override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) var size = self.view.bounds.size if UIDevice.currentDevice().userInterfaceIdiom == .Phone { size.width *= 2.0 size.height *= 2.0 } let scene = GameScene(size: size) scene.scaleMode = .AspectFill let layoutManager = LayoutManager.getInstanceFor(size) let backgroundTexture = SKTexture(imageNamed: "Background") let background = SKSpriteNode(texture: backgroundTexture) background.position = layoutManager.getPosition(.Background, relativeTo: .Scene) scene.addChild(background) let headerPosition = layoutManager.getPosition(.Header, relativeTo: .Background) if headerPosition != nil { let headerTexture = SKTexture(imageNamed: "Header") let header = SKSpriteNode(texture: headerTexture) header.position = headerPosition background.addChild(header) let logoTexture = SKTexture(imageNamed: "Logo") let logo = SKSpriteNode(texture: logoTexture) logo.position = layoutManager.getPosition(.Logo, relativeTo: .Header) header.addChild(logo) } var board = Board() let boardNode = BoardNode(board: board) boardNode.position = layoutManager.getPosition(.Board, relativeTo: .Background) background.addChild(boardNode) boardNode.lightUpEnableSquares() let undoButton = ButtonNode(type: .Undo) let passButton = ButtonNode(type: .Pass, enabled: false) let exitButton = ButtonNode(type: .Exit) undoButton.position = layoutManager.getPosition(.UndoButton, relativeTo: .Background) passButton.position = layoutManager.getPosition(.PassButton, relativeTo: .Background) exitButton.position = layoutManager.getPosition(.ExitButton, relativeTo: .Background) background.addChild(undoButton) background.addChild(passButton) background.addChild(exitButton) self.skView.presentScene(scene) } // hide status bar. override func prefersStatusBarHidden() -> Bool { return true } }
今まで割とベタに書いてた位置計算が、その内容をレイアウトマネージャに移したことで、割とシンプルなコードに。
そして、これを実行すると、次のようになる。
まだプレイヤーの情報を表示していないので、スキマがあるようにも感じるけど、いい感じなんじゃないかな。
今日はここまで!