読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

変種オセロのスタート画面を作ってみた。(その2)

ゲーム開発 Swift YWF

昨日はスタート画面のレイアウトだけ作成した。

今日はボタン類を作っていく。

プレイボタン

まずはゲームを開始するためのプレイボタンから。

//==============================
// YWF
//------------------------------
// PlayButtonNode.swift
//==============================

import SpriteKit

public class PlayButtonNode: SKSpriteNode {
  private static var texture: SKTexture!
  
  public class func loadAsset() {
    PlayButtonNode.texture = SKTexture(imageNamed: "PlayButton")
  }
  
  private var observers: [ObjectIdentifier:PlayButtonNodeObserver]
  
  public init() {
    self.observers = [ObjectIdentifier:PlayButtonNodeObserver]()
    
    let texture = PlayButtonNode.texture
    super.init(texture: texture, color: UIColor.whiteColor(), size: texture.size())
    
    self.userInteractionEnabled = true
  }
  
  public required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  public func addObserver(observer: PlayButtonNodeObserver) {
    self.observers[ObjectIdentifier(observer)] = observer
  }
  
  public func removeObserver(observer: PlayButtonNodeObserver) {
    self.observers.removeValueForKey(ObjectIdentifier(observer))
  }
  
  private func notifyObservers() {
    for (_, observer) in self.observers {
      observer.playButtonIsSelected(self)
    }
  }
  
  public override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
    let touch = touches.first as! UITouch
    let location = touch.locationInNode(self.parent)
    if self.containsPoint(location) {
      self.notifyObservers()
    }
  }
}

ちなみに、PlayButtonNodeObserverは、以下のとおり。

//==============================
// YWF
//------------------------------
// PlayButtonNodeObserver.swift
//==============================

public protocol PlayButtonNodeObserver: class {
  func playButtonIsSelected(button: PlayButtonNode)
}

まぁ、変種オセロのUIを作ってみた。(その11) - いものやま。変種オセロのUIを作ってみた。(その14) - いものやま。とほぼ同じ。
DRY原則に反してて、よくないんだけどね・・・

ラベルボタン

次は文字を表示するボタン。

//==============================
// YWF
//------------------------------
// LabelButtonNode.swift
//==============================

import SpriteKit

public class LabelButtonNode: SKLabelNode {
  private static let fontName = "Times New Roman Bold"
  
  private var observers: [ObjectIdentifier:LabelButtonNodeObserver]
  
  public init(name: String, fontSize: CGFloat) {
    self.observers = [ObjectIdentifier:LabelButtonNodeObserver]()
    
    super.init()
    
    self.text = name
    self.fontName = LabelButtonNode.fontName
    self.fontColor = SKColor.blackColor()
    self.fontSize = fontSize
    self.verticalAlignmentMode = .Baseline
    
    self.userInteractionEnabled = true
  }

  public required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  public func addObserver(observer: LabelButtonNodeObserver) {
    self.observers[ObjectIdentifier(observer)] = observer
  }
  
  public func removeObserver(observer: LabelButtonNodeObserver) {
    self.observers.removeValueForKey(ObjectIdentifier(observer))
  }
  
  private func notifyObservers() {
    for (_, observer) in self.observers {
      observer.labelButtonIsSelected(self)
    }
  }
  
  public override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
    let touch = touches.first as! UITouch
    let location = touch.locationInNode(self.parent)
    if self.containsPoint(location) {
      self.notifyObservers()
    }
  }
}

LabelButtonNodeObserverは、以下。

//==============================
// YWF
//------------------------------
// LabelButtonNodeObserver.swift
//==============================

public protocol LabelButtonNodeObserver: class {
  func labelButtonIsSelected(button: LabelButtonNode)
}

こっちも親クラスがSKLabelNodeという以外、ほとんど同じ。
DRY原則ェ・・・

改善するなら?

どうせ改善するなら、他のプロジェクトでも使えるようにちゃんと部品化をした方がいいから、今回は改善しようとは思っていないんだけど、もしこのDRY原則に反しているのを改善するとしたら、どうするかなぁ、という話。

例えば、ButtonNodeとPlayButtonNodeは、使っている画像が違うだけで内容はほとんど一緒。
なら、SKSpriteNodeにボタンの機能を持たせたSpriteButtonNodeというより一般的なクラスを作って、ButtonNodeやPlayButtonNodeはそのインスタンスとした方がいい。

ただ、ちょっと困るのがLabelButtonNodeの存在。
仮に上に述べたようなSpriteButtonNodeを作ったときに、どちらもボタンの機能を持っているけれど、LabelButtonNodeはSKLabelNodeを、SpriteButtonNodeはSKSpriteNodeを継承しているので、ボタンの機能についての継承関係を作ることが出来ない。

このようなケースの問題は、「意味的な継承(is-a関係のある継承)」と「実装の継承」が混在しているところにある。
そのような場合、「実装の継承」はコンポジションを使って外部のオブジェクトに委譲するのが望ましい。

具体的には、例えばObserverManagerというクラスを作って、オブザーバの追加、削除、オブザーバへの通知(あと、ObserverManagerを所有するインスタンスへの非所有参照)はそのクラスで実装し、LabelButtonNodeやSpriteButtonNodeはそのインスタンスをプロパティとして持つようにする。
そうすれば、クラスの継承関係とは別に、実装の継承を実現することが出来る。

ちなみに、Rubyだとモジュールをインクルードするだけで実装の継承が出来たりするので、かなりパワフル。
これを可能にしているのはRubyのクラスオブジェクト自身の柔軟性なんだけれど、ホントよく出来てるよなぁと感心せざるをえない。
C++も、private継承を使ってそれっぽいことは出来るけど、難しいからなぁ・・・

今日はここまで!