WWDC 2017 の SceneKit サンプル Fox 2 を調べる その37
iOS 用のバーチャルパッドについて見てゆく。
今回は Swift ファイルの構成と振る舞いについて。
ファイルの構成
ファイル名 | 内容 |
---|---|
ButtonOverlay.swift | ControlOverlay.swift で使用するボタン用の ButtonOverlay クラス |
PadOverlay.swift | ControlOverlay.swift で使用するヴァーチャルパッド用の PadOverlay クラス |
ControlOverlay.swift | ボタンとヴァーチャルパッドで設定し Overlay.swift ボタンでこちらを設定する |
ButtonOverlay.swift
ButtonOverlay クラスは SpriteKit の SKNode で設定され A、B ボタンで使い回せる様につくられており、
ButtonOverlay.swift は最終的に GameController で操作できる様にするデリゲート ButtonOverlayDelegate と ButtonOverlay クラスで構成されている。
ButtonOverlayDelegate
ボタンが押されたかをデリゲートで渡す関数設定されており、最終的に GameController クラスでこちらが設定されている。
ここでは ButtonOverlay クラスでタッチイベントからアクセスしている。
func willPress(_ button: ButtonOverlay) func didPress(_ button: ButtonOverlay)
ButtonOverlay クラス
クラスの中身を見てゆく。
初期設定
- ボタンのサイズを決める
- デリゲートの設定
- UITouch からタッチの取得
- inner 変数で SKShapeNode を使いボタン自体を設定。タップ時の色変え用
- background 変数で SKShapeNode を使いボタンの背景を設定
- label 変数で SKShapeNode を使いボタン内のテキストを設定
var size = CGSize.zero { didSet { if size != oldValue { updateForSizeChange() } } } weak var delegate: ButtonOverlayDelegate? private var trackingTouch: UITouch? private var inner: SKShapeNode! private var background: SKShapeNode! private var label: SKLabelNode?
イニシャライズ
- サイズを 40 で設定
- 透明度を 0.7
- ユーザー操作を許可
- buildPad 関数でボタンを作成、引数の文字列を渡しボタンに文字を表示する
init(_ text: NSString) { super.init() size = CGSize(width: CGFloat(40), height: CGFloat(40)) alpha = 0.7 isUserInteractionEnabled = true buildPad(text) }
ボタンの作成
背景の background ノード > タップ時に色を変える inner ノート > 文字を表示する label ノードの重なり順で設定されている。
background と inner は SKShapeNode に CGPath の円が渡され描画しており、 label SKShapeNode で文字を表示している。
func buildPad(_ text: NSString) { let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height)) background = SKShapeNode() background.path = CGPath( ellipseIn: backgroundRect, transform: nil) background.strokeColor = SKColor.black background.lineWidth = 3.0 addChild(background) inner = SKShapeNode() inner.path = CGPath( ellipseIn: CGRect( x: 0, y: 0, width: innerSize.width, height: innerSize.height ), transform: nil) inner.lineWidth = 1.0 inner.fillColor = SKColor.white inner.strokeColor = SKColor.gray addChild(inner) label = SKLabelNode() label!.fontName = UIFont.boldSystemFont(ofSize: 24).fontName label!.fontSize = 24 label!.fontColor = SKColor.black label?.verticalAlignmentMode = .center label?.horizontalAlignmentMode = .center label?.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0 + 1.0) label?.text = text as String addChild(label!) }
updateForSizeChange
ボタンの大きさがに変更がかかった場合こちらが呼ばれ backgroundRect、innerRect、label の大きさと配置が変更される。
処理の初めでこのボタンに背景の background ノードがなかった場合、この処理を抜ける様になっている。
func updateForSizeChange() { guard let background = background else { return } let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height)) background.path = CGPath(ellipseIn: backgroundRect, transform: nil) let innerRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width / 3.0), height: CGFloat(size.height / 3.0)) inner.path = CGPath(ellipseIn: innerRect, transform: nil) label!.position = CGPoint(x: size.width / 2.0 - label!.frame.size.width / 2.0, y: size.height / 2.0 - label!.frame.size.height / 2.0) }
ボタンのサイズ取得
ボタンのサイズ取得様に設定したボタンの大きさを返す。
var innerSize: CGSize { return CGSize(width: CGFloat(size.width), height: CGFloat(size.height)) }
操作の初期化
最初にタッチされたイベントを保持する trackingTouch を空にして、inner の塗りつぶしを元に戻し、delegate の didPress に関数を渡す。
func resetInteraction() { trackingTouch = nil inner.fillColor = SKColor.white delegate!.didPress(self) }
タッチイベント
touchesBegan、touchesEnded、touchesCancelled が設定されている。
touchesBegan では、自身を初めにタッチされたものの保持、inner の fillColor を暗くして押されたh状態にして、willPress 関数に自身を渡す
touchesEnded と touchesCancelled は処理は同じで、resetInteraction を呼び出している。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { trackingTouch = touches.first inner.fillColor = SKColor.black delegate!.willPress(self) } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if touches.contains(trackingTouch!) { resetInteraction() } } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { if touches.contains(trackingTouch!) { resetInteraction() } }
PadOverlay.swift
PadOverlay クラスは SpriteKit の SKNode で設定され A、B ボタンで使い回せる様につくられており、
ButtonOverlay.swift は最終的に GameController で操作できる様にするデリゲート ButtonOverlayDelegate と ButtonOverlay クラスで構成されている。
PadOverlayDelegate
ButtonOverlay 同様にヴァーチャルパッドの操作をデリゲートで渡す関数設定されており、最終的に GameController クラスでこちらが設定されている。
ここでは PadOverlay クラスでタッチイベントからアクセスしている。
func padOverlayVirtualStickInteractionDidStart(_ padNode: PadOverlay) func padOverlayVirtualStickInteractionDidChange(_ padNode: PadOverlay) func padOverlayVirtualStickInteractionDidEnd(_ padNode: PadOverlay)
PadOverlay クラス
クラスの中身を見てゆく。
初期設定
- 全体のサイズ
- ヴァーチャルパッドのスティック部分の初期値。-1 〜 1 の間の値
- デリゲートの設定
- UITouch からタッチの取得
- startLocation で初期位置
- stick 変数で SKShapeNode を使いスティックを設定
- background 変数で SKShapeNode を使いボタンの背景を設定
var size = CGSize.zero { didSet { if size != oldValue { updateForSizeChange() } } } var stickPosition = CGPoint.zero { didSet { if stickPosition != oldValue { updateStickPosition() } } } weak var delegate: PadOverlayDelegate? private var trackingTouch: UITouch? private var startLocation = CGPoint.zero private var stick: SKShapeNode! private var background: SKShapeNode!
イニシャライズ
- サイズを 150 で設定
- 透明度を 0.7
- ユーザー操作を許可
- buildPad 関数でパッドを作成し表示する
init(_ text: NSString) { super.init() size = CGSize(width: CGFloat(150), height: CGFloat(150)) alpha = 0.7 isUserInteractionEnabled = true buildPad() }
buildPad
background で背景として CGPath で円をかいた SKShapeNode が設定されており、 stick にも CGPath で円をかいた SKShapeNode が設定されている。
コメントアウトで #if os( OSX ) が設定されているが macOS ではヴァーチャルパッドが使われることはない。
func buildPad() { let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height)) background = SKShapeNode() background.path = CGPath( ellipseIn: backgroundRect, transform: nil ) background.strokeColor = SKColor.black background.lineWidth = 3.0 addChild(background) var stickRect = CGRect.zero stickRect.size = stickSize stick = SKShapeNode() stick.path = CGPath( ellipseIn: stickRect, transform: nil) stick.lineWidth = 2.0 //#if os( OSX ) stick.fillColor = SKColor.white //#endif stick.strokeColor = SKColor.black addChild(stick) updateStickPosition() }
stickSize
スティックのサイズを全体の 1/3 として返し設定させる。
var stickSize: CGSize { return CGSize( width: size.width / 3.0, height: size.height / 3.0) }
update 関連
ボタンと同様にサイズが変更されると各パーツが変更される。
また updateStickPosition はスティックの移動と位置の変更値を stickPosition に渡す。
- updateForSizeChange は全体と背景となる円。
- updateStickPosition はスティックの SKNode の初期位置
- updateStickPosition はスティック stickPosition 変数が変更される
func updateForSizeChange() { guard let background = background else { return } let backgroundRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height)) background.path = CGPath( ellipseIn: backgroundRect, transform: nil) let stickRect = CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width / 3.0), height: CGFloat(size.height / 3.0)) stick.path = CGPath( ellipseIn: stickRect, transform: nil) } func updateStickPosition() { let stickSize: CGSize = self.stickSize let stickX = size.width / 2.0 - stickSize.width / 2.0 + size.width / 2.0 * stickPosition.x let stickY = size.height / 2.0 - stickSize.height / 2.0 + size.width / 2.0 * stickPosition.y stick.position = CGPoint(x: stickX, y: stickY) } func updateStickPosition(forTouchLocation location: CGPoint) { var l_vec = vector_float2( x: Float( location.x - startLocation.x ), y: Float( location.y - startLocation.y ) ) l_vec.x = (l_vec.x / Float( size.width ) - 0.5) * 2.0 l_vec.y = (l_vec.y / Float( size.height ) - 0.5) * 2.0 if simd_length_squared(l_vec) > 1 { l_vec = simd_normalize(l_vec) } stickPosition = CGPoint( x: CGFloat( l_vec.x ), y: CGFloat( l_vec.y ) ) }
操作の初期化
スティックとスティックの初期位置
func resetInteraction() { stickPosition = CGPoint.zero trackingTouch = nil startLocation = CGPoint.zero delegate!.padOverlayVirtualStickInteractionDidEnd(self) }
タッチイベント: タッチ
ヴァーチャルパッドの触った際の処理
- trackingTouch に最初のタッチを保持する
- trackingTouch からタッチされた位置を取り、startLocation に渡す
- 中心が円の真ん中にあるため、タッチ位置を中心にせってするため width と height 割った数を startLocation の X、Y へ渡す。
- updateStickPosition 関数に位置を渡し stickPosition を更新する
- padOverlayVirtualStickInteractionDidStart に自身を設定する。
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { trackingTouch = touches.first startLocation = trackingTouch!.location(in: self) startLocation.x -= size.width / 2.0 startLocation.y -= size.height / 2.0 updateStickPosition(forTouchLocation: trackingTouch!.location(in: self)) delegate!.padOverlayVirtualStickInteractionDidStart(self) }
タッチイベント: ムーブ
ヴァーチャルパッドのスティックを移動させた際の処理。
タッチイベントの touches に trackingTouch があれば、updateStickPosition を実行し trackingTouch の位置を渡している。
updateStickPosition で startLocation と比較し移動の値を渡すことになる。
padOverlayVirtualStickInteractionDidChange に自身を設定する。
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { if touches.contains(trackingTouch!) { updateStickPosition(forTouchLocation: trackingTouch!.location(in: self)) delegate!.padOverlayVirtualStickInteractionDidChange(self) } }
タッチイベント: エンド、キャンセル
エンド、キャンセル共に処理は同じ。
タッチイベントの touches に trackingTouch があれば、resetInteraction 関数を実行し、スティック等をリセットする。
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if touches.contains(trackingTouch!) { resetInteraction() } } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { if touches.contains(trackingTouch!) { resetInteraction() } }
ControlOverlay.swift
ControlOverlay クラスのみのファイル。
ボタンのマージンを 25 として、
キャラクターの移動を leftPad と カメラ視点変更を rightPad として PadOverlay クラスに設定。
ジャンプを buttonA、攻撃を buttonB として ButtonOverlay をボタンとして設定している。
イニシャライズは画面サイズを引数として取り、それを元に上記の leftPad、rightPad、buttonA、buttonB の位置を設定している。
class ControlOverlay: SKNode { let buttonMargin = CGFloat( 25 ) var leftPad = PadOverlay() var rightPad = PadOverlay() var buttonA = ButtonOverlay("A") var buttonB = ButtonOverlay("B") init(frame: CGRect) { super.init() leftPad.position = CGPoint(x: CGFloat(20), y: CGFloat(40)) addChild(leftPad) rightPad.position = CGPoint(x: CGFloat(frame.size.width - 20 - rightPad.size.width), y: CGFloat(40)) addChild(rightPad) let buttonDistance = rightPad.size.height / CGFloat( 2 ) + buttonMargin + buttonA.size.height / CGFloat( 2 ) let center = CGPoint( x: rightPad.position.x + rightPad.size.width / 2.0, y: rightPad.position.y + rightPad.size.height / 2.0 ) let buttonAx = center.x - buttonDistance * CGFloat(cosf(Float.pi / 4.0)) - (buttonB.size.width / 2) let buttonAy = center.y + buttonDistance * CGFloat(sinf(Float.pi / 4.0)) - (buttonB.size.height / 2) buttonA.position = CGPoint(x: buttonAx, y: buttonAy) addChild(buttonA) let buttonBx = center.x - buttonDistance * CGFloat(cosf(Float.pi / 2.0)) - (buttonB.size.width / 2) let buttonBy = center.y + buttonDistance * CGFloat(sinf(Float.pi / 2.0)) - (buttonB.size.height / 2) buttonB.position = CGPoint(x: buttonBx, y: buttonBy) addChild(buttonB) } override init() { super.init() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } }
これで一応、Fox2 のプロジェクトの設定はおしまい。
WWDC 2018 後、しばらくしてから、シーンファイルのノードに設定されている Shader Modifier について解説するかもしれない。