SpriteKitでUIScrollView的なスクロール処理を実装する
SpriteKit でゲームやアプリを開発していると UI 系の部品がなくて困ることがあります。SpriteKit にはラベルしかなくボタンすらないです。ましてやスクロールビューなんて贅沢なものは当然ありません。現在開発しているアプリでどうしても必要になったので自作してみました(プログラムが複雑になるので横スクロールのみ実装しています)。
慣性処理のないスクロールノード
まずはじめに一番単純な実装からいきます。指で動かした分だけラベルが横にスクロールします。慣性の処理がないので touchesBegan: withEvent: と touchesMoved: withEvent: の処理だけで実現できます。
import SpriteKit class ScrollNode: SKSpriteNode { // スクロールさせるコンテンツを配置するためのノード private var contentNode = SKNode() private var startX: CGFloat = 0.0 private var lastX: CGFloat = 0.0 init(size: CGSize) { super.init(texture: nil, color: SKColor.clearColor(), size: size) self.userInteractionEnabled = true self.contentNode.position = CGPoint(x: 0, y: 0) self.addChild(self.contentNode) // スクロールさせるコンテンツ let myLabel = SKLabelNode(fontNamed: "Helvetica") myLabel.text = "scroll" myLabel.fontSize = 20 self.contentNode.addChild(myLabel) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { // store the starting position of the touch let touch = touches.first let location = touch!.locationInNode(self) startX = location.x lastX = location.x } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first let location = touch!.locationInNode(self) // set the new location of touch let currentX = location.x // スクロールのスピード。好みで変えてください let scrollSpeed: CGFloat = 1.0 let newX = self.contentNode.position.x + ((currentX - lastX) * scrollSpeed) // 左と右端の設定 let limitFactor: CGFloat = 0.3 // 好みで変えてください let leftLimitX = self.size.width * (-limitFactor) let rightLimitX = self.size.width * limitFactor // 移動処理 if newX < leftLimitX { self.contentNode.position = CGPointMake(leftLimitX, self.contentNode.position.y) } else if newX > rightLimitX { self.contentNode.position = CGPointMake(rightLimitX, self.contentNode.position.y) } else { self.contentNode.position = CGPointMake(newX, self.contentNode.position.y) } // Set new last location for next time lastX = currentX } }
使い方
使い方は簡単で適当なサイズを設定して SKScnene に配置してやるだけです。
class GameScene: SKScene { override func didMoveToView(view: SKView) { let scrollNode = ScrollNode(size: CGSize(width: 200, height: 44)) self.addChild(scrollNode) } }
参考記事
慣性処理に対応したスクロールノード
慣性処理のないスクロールノードでもとりあえずスクロールの動きはできていますがやっぱり UIScrollView のように慣性スクロールしたほうが自然だし使いごごちも良いです。というわけで慣性処理を追加します。 慣性処理は update メソッドの中で行います。 SKSpriteNode オブジェクトは SKScene オブジェクトのように自動で update メソッドが呼び出されないので自前でタイマーを仕込むか SKScene の update メソッドと連動して動かす必要があります。
import SpriteKit class ScrollNode: SKSpriteNode { private var contentNode = SKNode() private var startX: CGFloat = 0.0 private var lastX: CGFloat = 0.0 // タッチされているかどうか private var touching = false // 少しずつ移動させる private var lastScrollDistX: CGFloat = 0.0 init(size: CGSize) { super.init(texture: nil, color: SKColor.clearColor(), size: size) self.userInteractionEnabled = true self.contentNode.position = CGPoint(x: 0, y: 0) self.addChild(self.contentNode) // スクロールさせるコンテンツ let myLabel = SKLabelNode(fontNamed: "Helvetica") myLabel.text = "scroll" myLabel.fontSize = 20 self.contentNode.addChild(myLabel) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) { self.touching = true // store the starting position of the touch let touch = touches.first let location = touch!.locationInNode(self) startX = location.x lastX = location.x } override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) { let touch = touches.first let location = touch!.locationInNode(self) // set the new location of touch let currentX = location.x lastScrollDistX = lastX - currentX self.contentNode.position.x -= lastScrollDistX // Set new last location for next time lastX = currentX } override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) { self.touching = false } override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) { self.touching = false } func update(currentTime: CFTimeInterval) { // タッチされてたら guard !touching else { return } // 左と右端の設定 let limitFactor: CGFloat = 0.3 let leftLimitX: CGFloat = self.size.width * (-limitFactor) let rightLimitX: CGFloat = self.size.width * limitFactor if self.contentNode.position.x < leftLimitX { // 行き過ぎたから戻す self.contentNode.position.x = leftLimitX lastScrollDistX = 0.0 return } if self.contentNode.position.x > rightLimitX { // 行き過ぎたから戻す self.contentNode.position.x = rightLimitX lastScrollDistX = 0.0 return } // 慣性処理 var slowDown: CGFloat = 0.98 if fabs(lastScrollDistX) < 0.5 { slowDown = 0.0 } lastScrollDistX *= slowDown self.contentNode.position.x -= lastScrollDistX } }
使い方
はじめに説明しましたが SKScnene に配置してやるだけでは updata メソッドが実行されないので SKScene の update メソッドと連動させてやる必要があります。
class GameScene: SKScene { var scrollNode: ScrollNode! override func didMoveToView(view: SKView) { self.scrollNode = ScrollNode(size: CGSize(width: 200, height: 44)) self.addChild(scrollNode) } override func update(currentTime: CFTimeInterval) { // scrollNodeのupdateメソッドを呼ぶ self.scrollNode.update(currentTime) } }
参考記事
慣性処理をなめらかにしたい
慣性スクロールには対応できましたが少し動きが硬いです。そこでアニメーションを入れてなめらかに動くように修正してみます。
class ScrollNode: SKSpriteNode { : : 省略 : func update(currentTime: CFTimeInterval) { // タッチされてたら guard !touching else { return } let limitFactor: CGFloat = 0.3 let leftLimitX: CGFloat = self.size.width * (-limitFactor) let rightLimitX: CGFloat = self.size.width * limitFactor if self.contentNode.position.x < leftLimitX { // アニメーションさせる if !self.contentNode.hasActions() { let move = SKAction.moveToX(leftLimitX, duration: 0.2) move.timingMode = .EaseInEaseOut self.contentNode.runAction(move) } lastScrollDistX = 0.0 return } if self.contentNode.position.x > rightLimitX { // アニメーションさせる if !self.contentNode.hasActions() { let move = SKAction.moveToX(rightLimitX, duration: 0.2) move.timingMode = .EaseInEaseOut self.contentNode.runAction(move) } lastScrollDistX = 0.0 return } var slowDown: CGFloat = 0.98 if fabs(lastScrollDistX) < 0.5 { slowDown = 0.0 } lastScrollDistX *= slowDown self.contentNode.position.x -= lastScrollDistX } }