Apple Engine

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

Xcode 10、SceneKit の Cross-platform Game テンプレートを自力でつくる

f:id:x67x6fx74x6f:20190402163858p:plain

Xcode 9 までは Cross-platform Game テンプレートは SpriteKit しかなかったのだが、Xcode 10 から SceneKit と Metal が増えた。
今回はこちらがどのような構造になっているのかというのと、このテンプレートのつくり方を探ってみる。

特に何かなければ、自力でつくらずに Cross-platform Game テンプレートをつくり、watchOS などいらないものを消していったほうが楽かと思われる。

Command + Shift + N で新規プロジェクトを作成し、Cross-platform タブの Cross-platform Game テンプレートを選択して Game technology を SceneKit にするだけ。

f:id:x67x6fx74x6f:20190402163948p:plain

f:id:x67x6fx74x6f:20190402164002p:plain

 

構造

f:id:x67x6fx74x6f:20190402164057p:plain

1つのプロジェクトファイルを起点に iOS、iOS のエクステンションとなる watchOS、macOS、tvOS がターゲットとして設定されており、共有するアセットやコードを1つにまとめている。
また、watchOS では Notification や Complication のアイコンなどが設定されていないので必要であれば設定する必要がある。

テンプレートであるため、プロジェクトファイルに対してターゲットが付加されているが、 Cocoa Pods のようにワークスペースをつくり(Command + Control + N)に iOS + watchOS、macOS、tvOS のプロジェクトを設定してもよいかもしれない。

共有するデータを誤って消したり外れたりする場合さえ注意すれば、こちらの管理の方が楽そうではある。
(各プラットフォームプロジェクト単位で他の人が触れるため)

 

プロジェクトをつくる流れ

  • iOS のプロジェクトをつくる(どのプロジェクトでも構わないがここでは iOS)
  • ターゲットから macOS、tvOS、watchOS のプロジェクトをつくる
  • アセットをひとつにまとめる 

 

ひとまず、全てのプラットフォームで動くプロジェクトをつくる

新規プロジェクトで iOS タブの Game テンプレートを選択しプロジェクトを作成。

f:id:x67x6fx74x6f:20190402164146p:plain
iOS の Game テンプレートを作成

 

Xcode のアプリケーションメニュー「File」>「New」>「Target」から、 macOS のタブを選択し、Game のテンプレートを選択して保存する。

f:id:x67x6fx74x6f:20190402164238p:plain
macOS の Game テンプレートを追加

 

同じように、Target から tvOS と watchOS の Game のテンプレートを選択して保存を繰り返す。

あとは、各プラットフォームのスキーマを変更してビルドするだけ。

f:id:x67x6fx74x6f:20190402164327p:plain
全てのプラットフォームをターゲットとして追加した状態

 

シーンファイルや画像、アイコンなどのリソースを1つにまとめる

流れ

  • 新規でグループを作成する
  • シーンファイルを含む art.scnassets フォルダをコピーする
  • scnassets のターゲットを変更する
  • xcassets で各プラットフォームのアイコンを設定する
  • xcassets のターゲットを変更する
  • 各プラットフォームでアイコンや起動画像の参照先を変更する
  • 各プラットフォームの scnassets や xcassets を削除する

 

シーンファイルが格納されている scnassets を全てのプラットフォームで使用できるように設定する

まず、共有するため適当に新規でグループを作成。ここでは「Shared」として作成した。
そして、iOS から art.scnassets のフォルダを移動させる。

f:id:x67x6fx74x6f:20190402165710p:plain
scnassets を共有するフォルダに移動する

 

そのままだと、Targets Membership で iOS しかこのリソースを読み込むことができないので、macOS、tvOS、watchOS の Extension にチェックを入れる。

f:id:x67x6fx74x6f:20190402165802p:plain
各プラットフォームの Target にチェックを入れる

 

(watchOS のアプリ側ではなく Extension にチェックを入れているのは、Extension がコードやリソースを持つため)

 

必要であれば、アイコン画像と起動画面を共有する

アイコン画像と起動画面はサイズが異なり基本的には共有できないため、 個人的には各プラットフォームのフォルダにアイコン画像等があった方が良い気はするので必要なければ読み飛ばしてかまわない。

 

設定してみる

新規で xcassets を作成するか、各プラットフォームから移動させる。

f:id:x67x6fx74x6f:20190402165944p:plain
xcassets を共有フォルダに追加

 

新規した場合 xcassets を開き左側で右クリックか下のプラスボタンクリックから「App Icons & Launch Images」から以下の Image Set を設定する

  1. New iOS App Icon
  2. New macOS App Icon
  3. New tvOS App Icon and Top Shelf Image
  4. New tvOS Launch Image
  5. New watchOS App Icon

f:id:x67x6fx74x6f:20190402170309p:plain
App Icons & Launch Images

 

設定が完了したらプロジェクトの「General」>「TARGETS」から各プラットフォームの Image Set を関連づける。

watchOS の Extension 側ではなくアプリの方を開く。

OS 設定箇所 上で作成したリスト番号で設定する
iOS App Icons & Launch Images > App Icons Source 1
macOS App Icons > Source 2
tvOS App Icons & Launch Images > App Icons Source 3
tvOS App Icons & Launch Images > Launch Images Source 4
watchOS App Icons > App Icons Source 5

f:id:x67x6fx74x6f:20190402174313p:plain
iOS
f:id:x67x6fx74x6f:20190402174329p:plain
macOS
f:id:x67x6fx74x6f:20190402174343p:plain
tvOS
f:id:x67x6fx74x6f:20190402174402p:plain
watchOS

 

xcassets は作成したプラットフォームしか読み込まれないので、 Targets Membership で、macOS、tvOS、watchOS にチェックを入れる。

 

f:id:x67x6fx74x6f:20190402170152p:plain
各プラットフォームの Target にチェックを入れる

 

汎用的なコードを一つにまとめる

Cross-platform Game テンプレートでは共有するフォルダに GameController.swift というファイルがある。

GameController クラスは NSObject の派生クラスで、SCNSceneRendererDelegate のプロトコルが設定されており、このクラスのイニシャライズで SCNSceneRenderer(SCNView) が参照渡しされる。

参照している SCNView に SCNScene と SCNSceneRenderer の delegate と関数を設定。

そして、タッチされた際にシーンのジオメトリのマテリアルの色を変える関数を設定している。

先頭に watchOS の場合「import WatchKit」を使用する分岐と macOS の場合に NSColor を使用する SCNColor という typealias が設定されている。

 

なぜこのようなつくりになっているのか

SCNView を NSObject のクラスに参照させ、このクラスでシーンの処理をしている。
元となる ViewController 側では SCNView の初期設定と View へのタッチやクリックイベント、ゲームコントローラーなどを設定するだけで表示部分や何らかの操作からの表示変更は各プラットフォーム共通して使用することができる。

厳密には watchOS では荷が重い処理があったりするので分岐や調整は必要。 

また、A8 より前と A9 からでは SceneKit / Metal で使える機能が増えているためそこら辺も注意。

 

GameController.swift のコード

import SceneKit

#if os(watchOS)
    import WatchKit
#endif

#if os(macOS)
    typealias SCNColor = NSColor
#else
    typealias SCNColor = UIColor
#endif

class GameController: NSObject, SCNSceneRendererDelegate {

    let scene: SCNScene
    let sceneRenderer: SCNSceneRenderer
    
    init(sceneRenderer renderer: SCNSceneRenderer) {
        sceneRenderer = renderer
        scene = SCNScene(named: "Art.scnassets/ship.scn")!
        
        super.init()
        
        sceneRenderer.delegate = self
        
        if let ship = scene.rootNode.childNode(withName: "ship", recursively: true) {
            ship.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: 2, z: 0, duration: 1)))
        }
        
        sceneRenderer.scene = scene
    }
    
    func highlightNodes(atPoint point: CGPoint) {
        let hitResults = self.sceneRenderer.hitTest(point, options: [:])
        for result in hitResults {
            // get its material
            guard let material = result.node.geometry?.firstMaterial else {
                return
            }
            
            // highlight it
            SCNTransaction.begin()
            SCNTransaction.animationDuration = 0.5
            
            // on completion - unhighlight
            SCNTransaction.completionBlock = {
                SCNTransaction.begin()
                SCNTransaction.animationDuration = 0.5
                
                material.emission.contents = SCNColor.black
                
                SCNTransaction.commit()
            }
            
            material.emission.contents = SCNColor.red
            
            SCNTransaction.commit()
        }
    }
    
    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        // Called before each frame is rendered
    }

}

 

ViewController 側のコード

iOS と tvOS はほぼ同じ、macOS は UIColor > NSColor、タッチイベントをクリックイベントに変える。

watchOS は Storyboard で Tap Gesture Recognizer が設定されている。

(テンプレートでは @IBAction が設定されていないので注意)

 

iOS / tvOS (GameController.swift)
var gameView: SCNView {
    return self.view as! SCNView
}

var gameController: GameController!

override func viewDidLoad() {
    super.viewDidLoad()
    
    self.gameController = GameController(sceneRenderer: gameView)
    
    // Allow the user to manipulate the camera
    self.gameView.allowsCameraControl = true
    
    // Show statistics such as fps and timing information
    self.gameView.showsStatistics = true
    
    // Configure the view
    self.gameView.backgroundColor = UIColor.black
    
    // Add a tap gesture recognizer
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
    var gestureRecognizers = gameView.gestureRecognizers ?? []
    gestureRecognizers.insert(tapGesture, at: 0)
    self.gameView.gestureRecognizers = gestureRecognizers
}

@objc
func handleTap(_ gestureRecognizer: UIGestureRecognizer) {
    // Highlight the tapped nodes
    let p = gestureRecognizer.location(in: gameView)
    gameController.highlightNodes(atPoint: p)
}

 

macOS (GameController.swift)
var gameView: SCNView {
    return self.view as! SCNView
}

var gameController: GameController!

override func viewDidLoad() {
    super.viewDidLoad()
    
    self.gameController = GameController(sceneRenderer: gameView)
    
    // Allow the user to manipulate the camera
    self.gameView.allowsCameraControl = true
    
    // Show statistics such as fps and timing information
    self.gameView.showsStatistics = true
    
    // Configure the view
    self.gameView.backgroundColor = NSColor.black
    
    // Add a click gesture recognizer
    let clickGesture = NSClickGestureRecognizer(target: self, action: #selector(handleClick(_:)))
    var gestureRecognizers = gameView.gestureRecognizers
    gestureRecognizers.insert(clickGesture, at: 0)
    self.gameView.gestureRecognizers = gestureRecognizers
}

@objc
func handleClick(_ gestureRecognizer: NSGestureRecognizer) {
    // Highlight the clicked nodes
    let p = gestureRecognizer.location(in: gameView)
    gameController.highlightNodes(atPoint: p)
}

 

watchOS (InterfaceController.swift)
@IBOutlet var scnInterface: WKInterfaceSCNScene!
var gameController: GameController!

override func awake(withContext context: Any?) {
    super.awake(withContext: context)
    gameController = GameController(sceneRenderer: scnInterface)
}

@IBAction func handleTap(_ gestureRecognizer: WKTapGestureRecognizer) {
    // Highlight the tapped nodes
    let p = gestureRecognizer.locationInObject()
    gameController.highlightNodes(atPoint: p)
}

 

まとめ

自力でつくる必要はないのだが、全てのプラットフォームでの SceneKit アプリの作成と各プラットフォームでの差があまりないことが分かったと思われる。

ゲーム系アプリとなるとフルスクリーン表示となり、 操作系以外はわりと iOS と tvOS の違いは少ないため移植は簡単かつ、tvOS のアプリ自体が少ないため狙いどころではある。