Apple Engine

Apple, iPhone, iOS, その周辺のことについて

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 について解説するかもしれない。