Undoボタンの対応も出来た。
あとは、ユーザ情報の表示。
レイアウトマネージャの修正
ユーザ情報の表示を行うとき、そのエリアのサイズが必要となったので、レイアウトマネージャの修正を行った。
ついでに、すこしリファクタリングも実施。
//============================== // YWF //------------------------------ // LayoutManager.swift //============================== import SpriteKit public class LayoutManager { public enum Component { case Scene case Background case Header case Logo case Board case UserInfo case OpponentInfo case ExitButton case UndoButton case PassButton } public class func getInstanceFor(size: CGSize) -> LayoutManager { switch size.height { case 920..<1000: return SmallestLayout(size: size) case 1000..<1060: return SmallLayout(size: size) case 1060..<1140: return MiddleLayout(size: size) case 1140..<1500: return LargeLayout(size: size) default: return LayoutManager(size: size) } } private let size: CGSize private var userInfoHeight: CGFloat { return 80.0 } private var opponentInfoHeight: CGFloat { return 80.0 } private var buttonMargin: CGFloat { return 20.0 } private init(size: CGSize) { self.size = size } 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) } } } public 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: 190.0, height: 60.0) case .Board: return CGSize(width: 760.0, height: 760.0) case .UserInfo: return CGSize(width: self.size.width - self.buttonMargin, height: self.userInfoHeight) case .OpponentInfo: return CGSize(width: self.size.width - self.buttonMargin, height: self.opponentInfoHeight) case .ExitButton, .UndoButton, .PassButton: return CGSize(width: 120.0, height: 60.0) } } private func getUpperLeftPosition(component: Component) -> CGPoint! { return nil } // for iPhone 4S private class SmallestLayout: LayoutManager { private var userInfoMargin: CGFloat // workaround to set using method. (actually constant) private override init(size: CGSize) { self.userInfoMargin = 0.0 super.init(size: size) self.userInfoMargin = (self.size.height - self.getSize(.Board).height - self.userInfoHeight - self.opponentInfoHeight)/4.0 } private override func getSize(component: Component) -> CGSize! { switch component { case .UserInfo: let width = self.size.width - self.getSize(.UndoButton).width - self.getSize(.PassButton).width - self.buttonMargin*3.0 return CGSize(width: width, height: self.userInfoHeight) case .OpponentInfo: let width = self.size.width - self.getSize(.ExitButton).width - self.buttonMargin*2.0 return CGSize(width: width, height: self.opponentInfoHeight) default: return super.getSize(component) } } 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.opponentInfoHeight + self.userInfoMargin*2.0 return CGPoint(x: x, y: y) case .UserInfo: let y = self.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 return CGPoint(x: 0.0, y: y) case .OpponentInfo: let y = self.userInfoMargin return CGPoint(x: 0.0, y: y) case .ExitButton: let x = self.size.width - (self.buttonMargin + self.getSize(.ExitButton).width) let y = self.userInfoMargin + (self.opponentInfoHeight - 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.opponentInfoHeight + self.userInfoMargin*3.0 + self.getSize(.Board).height + (self.userInfoHeight - 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.opponentInfoHeight + self.userInfoMargin*3.0 + self.getSize(.Board).height + (self.userInfoHeight - self.getSize(.PassButton).height)/2.0 return CGPoint(x: x, y: y) } } } // for iPad private class SmallLayout: LayoutManager { private var userInfoMargin: CGFloat // workaround to set using method. (actually constant) private let headerHeight: CGFloat private let headerShadowHeight: CGFloat private override init(size: CGSize) { self.userInfoMargin = 0.0 self.headerHeight = 80.0 self.headerShadowHeight = 10.0 super.init(size: size) self.userInfoMargin = (self.size.height - self.headerHeight - self.getSize(.Board).height - self.userInfoHeight - self.opponentInfoHeight)/4.0 } private override func getSize(component: Component) -> CGSize! { switch component { case .UserInfo: let width = self.size.width - self.getSize(.UndoButton).width - self.getSize(.PassButton).width - self.buttonMargin*3.0 return CGSize(width: width, height: self.userInfoHeight) default: return super.getSize(component) } } 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.userInfoMargin*2.0 + self.opponentInfoHeight return CGPoint(x: x, y: y) case .UserInfo: let y = self.headerHeight + self.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 return CGPoint(x: 0.0, y: y) case .OpponentInfo: let y = self.headerHeight + self.userInfoMargin return CGPoint(x: 0.0, 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.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 + (self.userInfoHeight - 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.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 + (self.userInfoHeight - self.getSize(.PassButton).height)/2.0 return CGPoint(x: x, y: y) } } } // for iPhone 5 / 5s / 5c private class MiddleLayout: LayoutManager { private override var userInfoHeight: CGFloat { return 140.0 } private var userInfoMargin: CGFloat // workaround to set using method. (actually constant) private let headerHeight: CGFloat private let headerShadowHeight: CGFloat private override init(size: CGSize) { self.userInfoMargin = 0.0 self.headerHeight = 80.0 self.headerShadowHeight = 10.0 super.init(size: size) self.userInfoMargin = (self.size.height - self.headerHeight - self.getSize(.Board).height - self.userInfoHeight - self.opponentInfoHeight)/4.0 } private override func getSize(component: Component) -> CGSize! { switch component { case .UserInfo: let width = self.size.width - self.getSize(.UndoButton).width - self.buttonMargin*2.0 return CGSize(width: width, height: self.userInfoHeight) default: return super.getSize(component) } } 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.userInfoMargin*2.0 + self.opponentInfoHeight return CGPoint(x: x, y: y) case .UserInfo: let y = self.headerHeight + self.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 return CGPoint(x: 0.0, y: y) case .OpponentInfo: let y = self.headerHeight + self.userInfoMargin return CGPoint(x: 0.0, 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.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*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.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 + self.getSize(.UndoButton).height + self.buttonMargin return CGPoint(x: x, y: y) } } } // for iPhone 6 / 6 Plus private class LargeLayout: LayoutManager { private override var userInfoHeight: CGFloat { return 140.0 } private override var opponentInfoHeight: CGFloat { return 140.0 } private var userInfoMargin: CGFloat // workaround to set using method. (actually constant) private let headerHeight: CGFloat private let headerShadowHeight: CGFloat private override init(size: CGSize) { self.userInfoMargin = 0.0 self.headerHeight = 100.0 self.headerShadowHeight = 10.0 super.init(size: size) self.userInfoMargin = (self.size.height - self.headerHeight - self.opponentInfoHeight - self.getSize(.Board).height - self.userInfoHeight)/4.0 } private override func getSize(component: Component) -> CGSize! { switch component { case .UserInfo: let width = self.size.width - self.getSize(.UndoButton).width - self.buttonMargin*2.0 return CGSize(width: width, height: self.userInfoHeight) default: return super.getSize(component) } } 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.userInfoMargin*2.0 + self.opponentInfoHeight return CGPoint(x: x, y: y) case .UserInfo: let y = self.headerHeight + self.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 return CGPoint(x: 0.0, y: y) case .OpponentInfo: let y = self.headerHeight + self.userInfoMargin return CGPoint(x: 0.0, 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.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*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.opponentInfoHeight + self.getSize(.Board).height + self.userInfoMargin*3.0 + self.getSize(.UndoButton).height + self.buttonMargin return CGPoint(x: x, y: y) } } } }
ポイントとなるのは、LayoutManager#getSize(_: LayoutManager.Component)をpublicメソッドにしたこと。
一つ心配だったことは、レイアウトマネージャの実体(LayoutManagerのサブクラス)は可視性がprivateで、ファイル外から直接使うことが出来ないようにしているのだけど、その場合、メソッドの可視性もすべてprivateにしないといけなかったということ。
このせいで、各実装でオーバーライドしているgetSize()の可視性もprivateにせざるを得なかったのだけど、そのとき、オーバーライドされたメソッドが呼び出されるのかどうかが怪しかった。
けど、実際には問題なくて、ちゃんとオーバーライドされたメソッドが呼び出されていた。
ユーザ情報の表示
ユーザ情報を表示できるようにするために、UserInfoNodeを実装していく。
//============================== // YWF //------------------------------ // UserInfoNode.swift //============================== import SpriteKit public class UserInfoNode: SKNode, BoardNodeObserver { private enum SizeClass { case Large case Small } // 続く
なお、SizeClassという列挙体は、トークンやフォントのサイズを切り替えるためのもの。
フォントの定義、画像のロードなど
まずはクラス定数、クラス変数、クラスメソッドから。
// 続き private static let fontName = "Times New Roman" private static let fontSize: [SizeClass: CGFloat] = [.Large: 60.0, .Small: 40.0] private static let userNameFontColor = SKColor(red: 0.2, green: 0.1, blue: 0.0, alpha: 1.0) private static var textures = [String: SKTexture]() private static let fadeDuration = NSTimeInterval(0.2) public class func loadAssets() { let atlas = SKTextureAtlas(named: "PlayerMarker") for status in [PieceNode.Status.Bad, PieceNode.Status.Good] { for sizeClass in [UserInfoNode.SizeClass.Large, UserInfoNode.SizeClass.Small] { let name = UserInfoNode.getTextureNameFor(status, sizeClass: sizeClass) UserInfoNode.textures[name] = atlas.textureNamed(name) } } } private class func getTextureNameFor(status: PieceNode.Status, sizeClass: SizeClass) -> String { var name: String switch status { case .Bad: name = "BadPlayerMarker" case .Good: name = "GoodPlayerMarker" default: fatalError("illegal status.") } switch sizeClass { case .Large: name.extend("Large") case .Small: name.extend("Small") } return name } private class func getTextureFor(status: PieceNode.Status, sizeClass: SizeClass) -> SKTexture { let name = UserInfoNode.getTextureNameFor(status, sizeClass: sizeClass) return UserInfoNode.textures[name]! } // 続く
ここは特に書くことはないかな?
プロパティとイニシャライザ
次にプロパティとイニシャライザ。
// 続き private var playerName: String private var status: PieceNode.Status private var size: CGSize private var sizeClass: SizeClass private var playerNameNode: SKLabelNode! private var counter: SKLabelNode! private var tokenMargin: CGFloat { return 20.0 } public init(playerName: String, status: PieceNode.Status, size: CGSize, boardNode: BoardNode) { self.playerName = playerName self.status = status self.size = size if size.height <= 80.0 { self.sizeClass = .Small } else { self.sizeClass = .Large } self.playerNameNode = nil self.counter = nil super.init() let tokenNode = SKSpriteNode(texture: UserInfoNode.getTextureFor(self.status, sizeClass: self.sizeClass)) let tokenSize = tokenNode.frame.size let tokenPosition = CGPoint(x: -self.size.width/2.0 + self.tokenMargin + tokenSize.width/2.0, y: 0.0) self.addChild(tokenNode) tokenNode.position = tokenPosition let placeHolderSize = CGSize(width: self.size.width - tokenSize.width - self.tokenMargin*2.0, height: self.size.height) let placeHolderNode = SKShapeNode(rectOfSize: placeHolderSize) let placeHolderPosition = CGPoint(x: self.size.width/2.0 - placeHolderSize.width/2.0, y: 0.0) self.addChild(placeHolderNode) placeHolderNode.position = placeHolderPosition placeHolderNode.lineWidth = 0.0 self.playerNameNode = SKLabelNode(fontNamed: UserInfoNode.fontName) self.playerNameNode.fontSize = UserInfoNode.fontSize[self.sizeClass]! self.playerNameNode.fontColor = UserInfoNode.userNameFontColor self.playerNameNode.horizontalAlignmentMode = .Left self.playerNameNode.verticalAlignmentMode = .Center self.playerNameNode.position = CGPoint(x: -self.size.width/2.0 + self.tokenMargin*2.0 + tokenSize.width, y: -3.0) self.addChild(self.playerNameNode) self.setPlayerNameLabel(playerName) self.counter = SKLabelNode(fontNamed: UserInfoNode.fontName) self.counter.text = "\(boardNode.board.count((status == .Bad) ? .Bad : .Good))" self.counter.fontSize = UserInfoNode.fontSize[self.sizeClass]! * 0.8 self.counter.fontColor = (self.status == .Bad) ? SKColor.whiteColor() : SKColor.blackColor() self.counter.horizontalAlignmentMode = .Center self.counter.verticalAlignmentMode = .Center self.counter.position = CGPoint(x: 0.0, y: 0.0) tokenNode.addChild(self.counter) boardNode.addObserver(self) } public required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } // 続く
ちょっと説明が必要なのは、placeHolderNode。
これはなんなのかというと、UserInfoNodeのサイズを一定にするためのもの。
※注
ただ、実際には不要だった。
これに関してはSKNodeのpositionの仕様について理解が甘かったのが原因。
SKNodeのpositionは座標系の原点を決めるだけで、SKSpriteNodeのようにテクスチャの中心位置まで定めるわけではなかった。
それと、SKLabelNodeのhorizontalAlignmentMode。
これが.Leftになっていると、SKLabelNodeのpositionがテキストの左端に置かれるようになる。
(つまり、文字列がpositionから右に伸びることになる)
駒の数の表示の更新
BoardNodeObserverプロトコルに準拠して、駒の数の表示の更新を行う。
// 続き public func boardNodeActionIsFinished(node: BoardNode) { let fadeOutAction = SKAction.fadeOutWithDuration(UserInfoNode.fadeDuration) self.counter.runAction(fadeOutAction) { self.counter.text = "\(node.board.count((self.status == .Bad) ? .Bad : .Good))" let fadeInAction = SKAction.fadeInWithDuration(UserInfoNode.fadeDuration) self.counter.runAction(fadeInAction) } } public func boardNodeSquareIsSelected(node: BoardNode, _ square: SquareNode) { // do nothing } // 続く
ここも特に書くことはない・・・
プレイヤー名の表示
最後、プレイヤー名を表示させる部分について。
// 続き public func setPlayerNameLabel(playerName: String) { var trimmedName = playerName while (true) { self.playerNameNode.text = (trimmedName == playerName) ? trimmedName : (trimmedName + "...") let currentSize = self.calculateAccumulatedFrame().size if self.size.width < currentSize.width { let lastIndex = trimmedName.endIndex.predecessor() trimmedName.removeAtIndex(lastIndex) } else { self.playerName = trimmedName break } } } }
基本的にはそのまま表示させればいいのだけど、考慮しておいた方がいいのが、プレイヤー名が仮に長くなった場合にどうするのか、ということ。
例えば、プレイヤー名を自由に変えられるようにしたり、GameCenterに対応したりする場合、プレイヤー名が長くなるということは十分に考えられる。
そのとき、プレイヤー名がボタンとかにかかってしまうとよろしくない。
そこで、必要に応じてプレイヤー名を短縮して表示する必要がある。
それを実現するための実装がこれで、プレイヤー名をセットしたときに幅がはみ出てしまった場合には、名前を末尾から刈り取って表示するようにしている。
動作確認
さて、いつものようにコントローラを修正して、動作確認をしてみる。
//============================== // YWF //------------------------------ // GameViewController.swift //============================== import UIKit import SpriteKit class GameViewController: UIViewController { @IBOutlet weak var skView: SKView! override func viewDidLoad() { BoardNode.loadAssets() PieceNode.loadAssetsAndCreateTemplates() UserInfoNode.loadAssets() ButtonNode.loadAssets() } 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) 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) let userInfoSize = layoutManager.getSize(.UserInfo) let opponentInfoSize = layoutManager.getSize(.OpponentInfo) let userInfoNode = UserInfoNode(playerName: "You", status: .Bad, size: userInfoSize, boardNode: boardNode) let opponentInfoNode = UserInfoNode(playerName: "Computer", status: .Good, size: opponentInfoSize, boardNode: boardNode) userInfoNode.position = layoutManager.getPosition(.UserInfo, relativeTo: .Background) opponentInfoNode.position = layoutManager.getPosition(.OpponentInfo, relativeTo: .Background) background.addChild(userInfoNode) background.addChild(opponentInfoNode) self.skView.presentScene(scene) let badPlayer = Human(boardNode: boardNode, passButton: passButton) let goodPlayer = AlphaBetaCom(status: .Good, depth: 3) let turnController = TurnController(boardNode: boardNode, undoButton: undoButton, badPlayer: badPlayer, goodPlayer: goodPlayer) turnController.start() } // hide status bar. override func prefersStatusBarHidden() -> Bool { return true } }
これを実行してみると、ユーザの情報が表示され、アクションが行われるたびに駒の数の表示が更新されていくのが分かると思う。
今日はここまで!