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

いものやま。

雑多な知識の寄せ集め

変種オセロの仕上げをしてみた。(その4)

昨日はLabelButtonNodeの反応が悪い原因を調査した。

今日は実際にLabelButtonNodeの修正を行っていく。

原因と解決策

まず、おさらいということで、反応性が悪かった原因と、その解決策について。

原因は、文字列をタップしているつもりで、そのちょっと下をタップしてしまっていることが多かったということ。
なので、反応する部分を上下左右に少し広げてやる必要があった。

そして、その解決策として、LabelButtonNodeを矩形で覆って、そのノードの方でイベントの処理を行うという方法を考えた。

LabelButtonNodeの修正

ということで、以下が修正後のコード。

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

import SpriteKit

public class LabelButtonNode: SKLabelNode {
  private class CoverNode: SKShapeNode {
    private weak var labelButton: LabelButtonNode!
    private var observers: [ObjectIdentifier:LabelButtonNodeObserver]

    private init(size: CGSize, labelButton: LabelButtonNode) {
      self.labelButton = labelButton
      self.observers = [ObjectIdentifier:LabelButtonNodeObserver]()
      
      super.init()
      
      self.path = CGPathCreateWithRect(CGRect(origin: CGPoint.zeroPoint, size: size), nil)
      self.fillColor = SKColor.clearColor()
      self.lineWidth = 0.0
      
      self.userInteractionEnabled = true
    }

    required init?(coder aDecoder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
    }
    
    private func notifyObservers() {
      for (_, observer) in self.observers {
        observer.labelButtonIsSelected(self.labelButton)
      }
    }
    
    private 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()
      }
    }
  }
  
  private static let fontName = "Times New Roman Bold"
  
  private static let coverMargin: CGFloat = 20.0
  
  private var cover: CoverNode!
  
  public override var verticalAlignmentMode: SKLabelVerticalAlignmentMode {
    didSet {
      if self.cover != nil {
        self.adjustCoverPosition()
      }
    }
  }
  
  public override var horizontalAlignmentMode: SKLabelHorizontalAlignmentMode {
    didSet {
      if self.cover != nil {
        self.adjustCoverPosition()
      }
    }
  }
  
  public init(name: String, fontSize: CGFloat) {
    self.cover = nil
    
    super.init()
    
    self.text = name
    self.fontName = LabelButtonNode.fontName
    self.fontColor = SKColor.blackColor()
    self.fontSize = fontSize
    self.verticalAlignmentMode = .Baseline
    
    var coverSize = self.frame.size
    coverSize.width += LabelButtonNode.coverMargin * 2.0
    coverSize.height += LabelButtonNode.coverMargin * 2.0
    self.cover = CoverNode(size: coverSize, labelButton: self)
    
    self.addChild(self.cover)
    self.adjustCoverPosition()
  }

  public required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
  
  public func addObserver(observer: LabelButtonNodeObserver) {
    self.cover.observers[ObjectIdentifier(observer)] = observer
  }
  
  public func removeObserver(observer: LabelButtonNodeObserver) {
    self.cover.observers.removeValueForKey(ObjectIdentifier(observer))
  }
  
  public func removeAllObservers() {
    self.cover.observers.removeAll()
  }
  
  private func adjustCoverPosition() {
    let labelOrigin = self.frame.origin
    let labelPosition = self.position
    let labelSize = self.frame.size
    self.cover.position.x = (labelOrigin.x - labelPosition.x) / self.xScale - LabelButtonNode.coverMargin
    self.cover.position.y = (labelOrigin.y - labelPosition.y) / self.yScale - LabelButtonNode.coverMargin
  }
}

まず、SKShapeNodeを継承したCoverNodeを内部クラスとして定義。
タッチの処理やオブザーバへの通知は、CoverNodeが行っている。
(ただ、外部からはLabelButtonNodeが行っているように見えるけど)

そして、LabelButtonNodeはメンバとしてCoverNodeのインスタンスを持つようにしている。
このインスタンスを子ノードにして、LabelButtonNodeに覆いかぶせるようにすることで、LabelButtonNodeを押そうとするとCoverNodeが押され、通知が行われるようになる。
ここで、CoverNodeの大きさをLabelButtonNodeより少し大きくすることで、上下左右に少しズレてもちゃんと通知がされるようにしている。

ポイントは、verticalAlignmentModeとhorizontalAlignmentModeにプロパティオブザーバを追加していること。
SKLabelNodeはこれらのプロパティが変わると座標系の原点が変わるので、これらのプロパティが変わった時にCoverNodeのポジションも変えてやらないと、CoverNodeの位置がズレてしまう。
なので、プロパティオブザーバを使って、これらのプロパティが変わったタイミングでCoverNodeのポジションも変えるようにしている。

その肝心のCoverNodeのポジション調整だけれど、その実装をしているのがLabelButtonNode#adjustCoverPosition()
CoverNodeの座標系の原点は矩形の左下にあるようだったので(なぜか矩形の中心じゃなかった・・・)、この位置がLabelButtonNodeの座標系でLabelButtonNodeの左下(+マージン)に重なるようにしている。
なお、スケールで割り算しているのは、LabelButtonNodeのフレームの大きさやポジションは親ノードの座標系で得られるので、それをLabelButtonNodeの座標系でのサイズに変える必要があるから。 (それにしても、座標系がノードごとに異なるのは、難しいね・・・)

あと、見ての通り、LabelButtonNodeのインタフェースは何も変わってない。
なので、他に変更は不要となっている。

動作確認

まず、CoverNodeがちゃんとLabelButtonNodeを覆えているかをチェック。
CoverNodeのlineWidthプロパティを1.0にして動かすと、次のような感じ。

f:id:yamaimo0625:20150918152309p:plain

ちゃんと覆えているのが分かると思う。

そして、実際に動かした様子が、以下。

文字列から少しズレた部分をタップしても、ちゃんと反応しているのが分かると思う。

今日はここまで!