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