いものやま。

雑多な知識の寄せ集め

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

昨日は勝敗の表示を実装した。

今日はLabelButtonNodeの反応性の向上を行っていく。

原因の調査

まずは原因の調査から。
変種オセロのルール画面を作ってみた。(まとめ) - いものやま。では、文字でなく背景をタッチしたときに反応が返ってきてないんじゃないかと予想したけど、その仮説が間違っていたら、元も子もないからね。

ということで、調査用のプロジェクトを作って、確認していく。

さて、どのように確認しようかなと考えたのだけど、一番簡単なのは、LabelButtonNodeのフォントのサイズを大きくして、背景の部分を簡単にタッチできるようにするという方法。

import SpriteKit

class GameScene: SKScene, LabelButtonNodeObserver {
  private static var touchCount: Int = 0
  
  override func didMoveToView(view: SKView) {
    let background = SKShapeNode(rect: view.frame)
    background.fillColor = SKColor.whiteColor()
    addChild(background)
    
    let labelButton = LabelButtonNode(name: "の", fontSize: 360)
    labelButton.position = CGPoint(x: view.frame.width/2.0, y: view.frame.height/2.0)
    labelButton.addObserver(self)
    
    background.addChild(labelButton)
  }
  
  func labelButtonIsSelected(button: LabelButtonNode) {
    GameScene.touchCount += 1
    println("[\(GameScene.touchCount)] OK")
  }
  
  override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
    let touch = touches.first as! UITouch
    let location = touch.locationInNode(self)
    println("location: \(location)")
  }
}

これで試してみると、文字の線のスキマをタッチした場合も、ちゃんとタッチが拾われていることが分かる。
なので、背景をタッチしたときに反応が返ってきていないんじゃないかという仮説は大ハズレ。

でも、文字のちょっとでも下をタッチすると、タッチがLabelButtonNodeには拾われてなくて、シーンの方で拾われていることが分かる。
なので、文字をタップしているつもりでも、実際には文字をタップできていなくて、上下にズレてるのかもしれない。

ということで、次のようにコードを書き換えて、調査。

import SpriteKit

class GameScene: SKScene, LabelButtonNodeObserver {
  private static var touchCount: Int = 0
  
  override func didMoveToView(view: SKView) {
    let background = SKShapeNode(rect: view.frame)
    background.fillColor = SKColor.whiteColor()
    addChild(background)
    
    let labelButton = LabelButtonNode(name: "のんのんびより", fontSize: 24)
    labelButton.position = CGPoint(x: view.frame.width/2.0, y: view.frame.height/2.0)
    labelButton.addObserver(self)
    background.addChild(labelButton)
    
    let shapeSize = labelButton.calculateAccumulatedFrame().size
    let shapeNode = SKShapeNode(rectOfSize: shapeSize)
    shapeNode.fillColor = SKColor.clearColor()
    shapeNode.strokeColor = SKColor.blackColor()
    shapeNode.lineWidth = 5.0
    labelButton.addChild(shapeNode)
    
    // adjust shape node position
    let baseY = labelButton.position.y - labelButton.frame.origin.y
    let centerY = labelButton.frame.height/2.0
    let shapeY = centerY - baseY
    shapeNode.position = CGPoint(x: 0.0, y: shapeY)
    
    let shapeNewOrigin = self.convertPoint(shapeNode.frame.origin, fromNode: labelButton)
    let shapeRect = CGRect(origin: shapeNewOrigin, size: shapeNode.frame.size)
    println("shape rect: \(shapeRect)")
  }
  
  func labelButtonIsSelected(button: LabelButtonNode) {
    GameScene.touchCount += 1
    println("[\(GameScene.touchCount)] OK")
  }
  
  override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent) {
    let touch = touches.first as! UITouch
    let location = touch.locationInNode(self)
    println("location: \(location)")
  }
}

途中、SKShapeNodeの位置を調整しているのはLabelButtonNodeにちょうど重なるようにするため。
SKLabelNodeの座標系の原点はverticalAlignmentModeプロパティ、およびhorizontalAlignmentModeプロパティによって変わってくるので、その座標系でSKShapeNodeがちょうどLabelButtonNodeに重なるようにしないといけない。

このコードを使って実機で実際にタップをしてみると、次のような出力になった。

shape rect: (75.5, 279.5, 169.0, 30.0)
location: (156.0, 276.0)
[1] OK
[2] OK
[3] OK
location: (166.5, 278.0)
[4] OK
[5] OK
location: (152.0, 281.5)
[6] OK
location: (154.5, 278.0)
location: (161.0, 280.0)
[7] OK
[8] OK
location: (153.5, 281.0)

文字をタップしているようでいて、何回かタップに失敗しているのが分かる。
そして、そのときの座標をSKShapeNodeの矩形の座標と比べてみると、少し下にズレているっぽいことが分かる。
(なお、いくつかの座標は279.5より大きい値になっているけれど、これは、LabelButtonNodeから下に外れた部分がまずタッチされ、そのまま指が押されるように上にいき、LabelButtonNode内で指が離れた、という動きになっていると思われる。最初にtouchesBegan:withEvent:をシーンで拾ってしまっているので、LabelButtonNodeの方ではなくシーンの方でtouchesEnded:withEvent:が呼ばれてしまう)

解決策

文字をタップしているようで、下側にズレてしまっている場合があるということが分かったので、この解決方法を考える。

まず、一つ考えられるのは、クラスの継承関係を変更して、LabelButtonNodeはSKShapeNodeを継承したクラスとして、内部に子ノードとしてSKLabelNodeを持たせ、そのSKLabelNodeに文字列を表示させるというのも考えられる。
つまり、今までは文字列自体がタッチに反応するようにしていたけど、タッチに反応するボタンとなる矩形をまず用意してやって、その内側に文字列を表示させるようにする。
このやり方だと、内部で子ノードとして持たせるオブジェクトをSKSpriteNodeに変えれば、画像のボタンとして使うことも出来る。

ただし、その場合、SKLabelNodeに用意されたメソッドやプロパティを利用することが出来なくなるという問題もある。
例えば、いくつかのLabelButtonNodeを左揃えにしたい場合に、SKLabelNodeを継承していれば、horizontalAlignmentModeプロパティを.Leftにしてやれば、簡単に左揃えにすることが出来る。
けれど、これが使えないとなると、ちょっと面倒・・・
(もちろん、実装できないことはないんだけど)

そこで、別の方法として、(ちょっとトリッキーだけど)上の調査用のコードでLabelButtonNodeに覆いかぶせるようにしていたSKShapeNodeの上下左右にマージンをつけて、そのSKShapeNodeの方でイベントの処理を行うという方法が考えられる。
この方法のいいところは、LabelButtonNode自体の継承関係やインタフェースを変更する必要がないというところ。
これについては、実際に明日コードを見ていこうと思う。

今日はここまで!