20210103のiOSに関する記事は4件です。

ARKit+GameplayKit で 例の飛行機の編隊飛行

ARKit+GameplayKit の動きを確認するため Xcode の ARKitプロジェクトに用意されている飛行機を空に飛ばしてみた。

<完成イメージ>
demo2.png demo.png
demo2.gif demno.gif

GameplayKit...思いかけずいい動きをしてくれる。
このサンプルは平面検知した後に表示される赤い球にスマホを向けて「発進」ボタンタップで飛行機を飛ばすことができる。

GameplayKit とは

Apple の GameplayKit のAPIドキュメントより

Architect and organize your game logic. Incorporate common gameplay behaviors such as random number generation, artificial intelligence, pathfinding, and agent behavior.
(Google翻訳) ゲームロジックを設計および整理します。 乱数生成、人工知能、経路探索、エージェントの動作など、一般的なゲームプレイの動作を組み込みます。

本記事ではエージェント機能(Agents, Goals, and Behaviors)を使っている。
SceneKitでゲームを作る人は少ないと思われるが、ARKit+SceneKitでAR空間内のキャラクターに動きをつけるのに手軽に使えそうなフレームワークである(ちなみにSceneKitに依存していない)。自力でロジックを構築するのは面倒そうなルート探索機能(Pathfinding)などは用途がありそう。
GameplayKit の全体については次の参考にさせていただいたサイトにサンプルがあるので、そちらをみていただければと思います。
参考サイト:

参考サイトの中に3Dエージェントを利用したものがあったが、エージェントの姿勢(transform)まで設定しているものが見つからなかったので、ARKitとの組み合わせも含めて動作を確認した。

エージェント機能(Agents, Goals, and Behaviors)

エージェント機能は 自律的にキャラクターを動作させるための仕組みを提供する。
今回のサンプルでは、自立して飛行する(ようにみえる)飛行機の位置と姿勢を求めるのに利用している。
飛行機は「ターゲット(勝手に作ったエージェント。本サンプルでは赤い球)」と呼ぶオブジェクトに向かって移動するようにしている。
このターゲットを一定時間で上へ左へと移動させることで、飛行機が上に向かったり、左に向かったり、をさせている。

以下は利用している主なクラス。

GKAgent3Dクラス

自律的に移動するキャラクターを表す。
本サンプルでは「ターゲット」と「飛行機」の2種類。
以下は、飛行機に設定している内容。

// 加速度(m/s/s)
$0.agent.maxAcceleration = Float.random(in: 6...8)
// 最大速度(m/s)
$0.agent.maxSpeed = Float.random(in: 11...12)
// 最初の位置
$0.agent.position = $0.node.simdPosition
// 最初の姿勢。
// rotation のX(right)が進行方向のようなので、1行目がワールド座標のZの奥側(マイナス)を見るように設定
$0.agent.rotation = simd_float3x3(
    SIMD3<Float>( 0, 0,-1),
    SIMD3<Float>( 0, 1, 0),
    SIMD3<Float>( 1, 0, 0))
// 僚機と接触しないようにエージェントの半径を設定。この半径でぶつからないように制御される
$0.agent.radius = 1.0

よくわからないのが rotation(APIドキュメントには「The orientation of the agent in 3D space.
としか記載がない)。
エージェントが最初に向いている向きを設定する必要がある(設定しないと、エージェントが向きを変える動作が最初に入ってしまう)のだが、どこを向けておけばよいか納得して設定できなかった。Agentの動作を確認すると、1行目(right)の方向が移動時のエージェントの正面としているように見える(実際、問題がないように見える)。なので、飛行機からみて赤い球がある方向となるZ軸の奥側(マイナス)を設定している。SceneKitは右手系なので、rightの向きに合わせて2行目(up)はY軸のプラス、3行目(forward)はX軸のプラスを設定で一応動作している。

GKGoalクラス

『任意のエージェントに向かう』、『任意のエージェントから遠ざかる』、『ランダムにうろつく』、『障害物を避ける』、といったエージェントの動作を表す。
このサンプルでは飛行機エージェントで利用していて次の2つのGKGoalを使っている。

// これは各飛行機がターゲットを見つけて向かう、という指定。
GKGoal(toSeekAgent: self.target.agent)
// これは僚機とぶつからないようにする、という指定。
GKGoal(toAvoid: agents, maxPredictionTime: 3)

toAvoid には飛行機エージェントの配列を指定。maxPredictionTime は、どのくらい前からぶつかることを予測し、それを回避するのか、という時間の指定。大きい値を設定すると、ぶつかりにくくなる。どのくらいが適当なのかは、エージェントの移動速度で変わってくると思うが、本サンプルでは 3 くらいでぶつからなかった。

ちなみに、本記事のタイトルを「編隊飛行」としたが、ちょっとバラバラに飛ぶ感じにしたかった(当初はミサイルで 板野サーカス に挑戦していた)ので、編隊飛行にマッチするであろう群れ(Flock)関連のGKGoalは利用しなかった。Flock関連の GKGoal を使うと、複数のエージェントが一定の距離・角度を保ちつつ移動する指定ができる(っぽい。試していない)。
参考サイト:

GKBehavior

GKGoal を取りまとめるクラス。
任意のエージェントに向かいつつ、エージェント間でぶつからないようにする、といった複数のGKGoal
を組み合わせることができる。

// ターゲットに向かいつつ、僚機との接触は避ける
$0.agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: self.target.agent), avoid])

各GKGoalの影響度の重み付けもできるが今回のサンプルでは利用していない。

GKComponentSystem

エージェントの位置・姿勢を定期的に更新するためのクラス。
GKComponentSystem のインスタンスにエージェントを登録して利用する。

var agentSystem = GKComponentSystem(componentClass: GKAgent3D.self)
()
 
// 管理対象のエージェント(ターゲットと各飛行機)を登録しておく
agentSystem.addComponent(gameObject.agent)
()

// 描画フレーム毎にupdateを呼び出す
agentSystem.update(deltaTime: delta)
()

// エージェントの位置・姿勢が更新されて delegateメソッド が呼び出される
func agentDidUpdate(_ agent: GKAgent) {
 ・・・
}

ターゲットの設定・更新処理

サンプルでは「ターゲット」というエージェントを作って、飛行機をターゲットに向かうようにしている。
ターゲットの設定は次のメソッドで行なっている。

private func setupTarget(scene: SCNScene) {
    // 見た目の目的地
    let sphere = SCNSphere(radius: 0.5)
    let material = SCNMaterial()
    material.diffuse.contents = UIColor.red
    sphere.materials = [material]
    let node = SCNNode(geometry: sphere)
    node.simdPosition = self.targetPositions[0].1   // 位置初期化
    scene.rootNode.addChildNode(node)
    // 位置計算用の目的地
    let gameObject = GameObject(node: node)
    gameObject.agent.position = node.simdPosition
    gameObject.agent.radius = 1.0
    // エージェントシステムに登録
    self.agentSystem.addComponent(gameObject.agent)
    self.target = gameObject
}

SceneKitのオブジェクトである SCNNode とGameplayKitの GKAgent3D をセットで管理したいため、GameObject という構造体を用意している。GameplayKit のお作法としては GKEntity + GKComponent で管理するのが正しいのだろうが、非常にシンプルなサンプルなので使わなかった。
ターゲットの見た目は半径50cmの赤い球にしている。ターゲットは見えている必要はないが動作確認しやすいので表示している。

このターゲットは一定時間で移動させている。
時間と位置の対応は次のテーブルで管理している。

// 時間[s]と位置のテーブル
let targetPositions: [(TimeInterval, SIMD3<Float>)] = [
    (2.0, SIMD3<Float>(0, 0, -25)),
    (6.0, SIMD3<Float>(0, 40, -25)),
    (9.0, SIMD3<Float>(30, 2, -10)),
    (13.0, SIMD3<Float>(-100, 2, -10)),
]

このテーブルをARフレーム毎にチェックし、ターゲットの位置を設定。

// ARフレームが更新された
func session(_ session: ARSession, didUpdate frame: ARFrame) {
    guard self.isButtonPressed else { return }

    let delta: TimeInterval
    if self.currentTime == 0 {
        // アニメーション開始
        delta = 0
        self.ships.forEach() {
            $0.node.isHidden = false
        }
    } else {
        delta = frame.timestamp - self.currentTime
    }
    self.currentTime = frame.timestamp

    // 位置テーブルから現在のターゲットの位置を取得
    let targetPosition = self.targetPositions.first(where: { $0.0 > self.animationTime })?.1
    if let pos = targetPosition {
        // ターゲットの位置を更新
        target.setPosition(pos)
    }
    self.animationTime += delta

    agentSystem.update(deltaTime: delta)
}

飛行機の位置・姿勢の更新処理

飛行機の位置・姿勢は GKAgentDelegateagentDidUpdate(GKAgent) メソッドで更新。

class ViewController: UIViewController, ARSessionDelegate, GKAgentDelegate {
    // エージェント情報が更新された
    func agentDidUpdate(_ agent: GKAgent) {
        guard let agent = agent as? GKAgent3D else { return }
        let gameObject = self.ships.first(where: { $0.agent === agent })
        // 見た目の飛行機の位置をエージェントシステムが計算した位置に設定
        gameObject?.node.simdTransform = agent.transform
    }
}

extension GKAgent3D {
    var transform: simd_float4x4 {
        simd_float4x4(simd_float4( rotation.columns.0, 0),
                             simd_float4( rotation.columns.1, 0),
                             simd_float4( rotation.columns.2, 0),
                             simd_float4( position, 1))
    }
}

GKAgent3D には SCNNodeが持っているような simd_float4x4SCNMatrix4 の transformプロパティがない。position(vector_float3型。simd_float3のエイリアス)と rotation(matrix_float3x3型。simd_float3x3のエイリアス)を持っているので、この情報からsimd_float4x4型の値を生成する処理をGKAgent3Dのextensionに実装している。

Scene Editor の設定

shipの子ノードをY軸90度回転しておく必要がある(こうしないと右を向いて飛んでいく)。
image.png
これも理由は不明。
この飛行機のジオメトリはもともとZ軸のプラス側を向いている。これを90度回転してX軸のプラス側を向くようにする。GameplayKitの3Dでの進行方向正面はX軸プラスということらしい。GKAgent3Dクラスの rotation もそうだが(ドキュメントのどこかに書いてあるのだろうが)Appleにはわかりやすい場所に説明・リンクを用意して欲しい。。。

最後に

謎な部分は残っているが、この短いコードで派手な動きができたと思う。
GameplayKit はSceneKitよりもさらにマイナーなフレームワークだと思うが、頭の引き出しに入れておくと、役立つことがあるかもしれない。

全体ソースコード

ViewController.swift
import ARKit
import UIKit
import SceneKit
import GameplayKit

class ViewController: UIViewController, ARSessionDelegate, GKAgentDelegate {

    @IBOutlet weak var scnView: ARSCNView!

    struct GameObject {
        var node: SCNNode!
        var agent = GKAgent3D()

        init(node: SCNNode) {
            self.node = node
        }

        func setPosition(_ position: SIMD3<Float>) {
            self.node.simdPosition = position
            self.agent.position = position
        }
    }

    var target: GameObject!
    var ships: [GameObject] = []
    // 時間[s]と位置のテーブル
    let targetPositions: [(TimeInterval, SIMD3<Float>)] = [
        (2.0, SIMD3<Float>(0, 0, -25)),
        (6.0, SIMD3<Float>(0, 40, -25)),
        (9.0, SIMD3<Float>(30, 2, -10)),
        (13.0, SIMD3<Float>(-100, 2, -10)),
    ]

    var agentSystem = GKComponentSystem(componentClass: GKAgent3D.self)
    var currentTime: TimeInterval = 0
    var animationTime: TimeInterval = 0

    private var isButtonPressed = false

    override func viewDidLoad() {
        super.viewDidLoad()

        let shipScene = SCNScene(named: "art.scnassets/ship.scn")!
        let ship = shipScene.rootNode.childNode(withName: "ship", recursively: true)!

        setupTarget(scene: self.scnView.scene)
        setupShips(ship: ship)
        // AR Session 開始
        self.scnView.session.delegate = self
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        self.scnView.session.run(configuration, options: [.removeExistingAnchors, .resetTracking])
    }

    // ARフレームが更新された
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        guard self.isButtonPressed else { return }

        let delta: TimeInterval
        if self.currentTime == 0 {
            // アニメーション開始
            delta = 0
            self.ships.forEach() {
                $0.node.isHidden = false
            }
        } else {
            delta = frame.timestamp - self.currentTime
        }
        self.currentTime = frame.timestamp

        // 位置テーブルから現在のターゲットの位置を取得
        let targetPosition = self.targetPositions.first(where: { $0.0 > self.animationTime })?.1
        if let pos = targetPosition {
            // ターゲットの位置を更新
            target.setPosition(pos)
        }
        self.animationTime += delta

        agentSystem.update(deltaTime: delta)
    }

    // エージェント情報が更新された
    func agentDidUpdate(_ agent: GKAgent) {
        guard let agent = agent as? GKAgent3D else { return }
        let gameObject = self.ships.first(where: { $0.agent === agent })
        // 見た目の飛行機の位置をエージェントシステムが計算した位置に設定
        gameObject?.node.simdTransform = agent.transform
    }

    // 発射ボタンが押された
    @IBAction func pressButton(_ sender: Any) {
        isButtonPressed = true
    }
}

extension ViewController {
    private func setupTarget(scene: SCNScene) {
        // 見た目の目的地
        let sphere = SCNSphere(radius: 0.5)
        let material = SCNMaterial()
        material.diffuse.contents = UIColor.red
        sphere.materials = [material]
        let node = SCNNode(geometry: sphere)
        node.simdPosition = self.targetPositions[0].1   // 位置初期化
        scene.rootNode.addChildNode(node)
        // 位置計算用の目的地
        let gameObject = GameObject(node: node)
        gameObject.agent.position = node.simdPosition
        gameObject.agent.radius = 1.0
        // エージェントシステムに登録
        self.agentSystem.addComponent(gameObject.agent)
        self.target = gameObject
    }

    private func setupShips(ship: SCNNode) {
        // 1機目
        let ship1 = ship
        self.scnView.scene.rootNode.addChildNode(ship1)
        // 2機目。ジオメトリ は1機目のクローン
        let ship2 = ship1.clone()
        var ship2Position = ship1.simdPosition
        ship2Position.x -= 2
        ship2.simdPosition = ship2Position
        self.scnView.scene.rootNode.addChildNode(ship2)
        // 3機目。ジオメトリ は1機目のクローン
        let ship3 = ship1.clone()
        var ship3Position = ship1.simdPosition
        ship3Position.x += 2
        ship3.simdPosition = ship3Position
        self.scnView.scene.rootNode.addChildNode(ship3)

        self.ships.append(GameObject(node: ship1))
        self.ships.append(GameObject(node: ship2))
        self.ships.append(GameObject(node: ship3))
        let agents = self.ships.map { $0.agent }
        // 飛行機同士が接触しないようにするように、飛行機のリストを作る
        let avoid = GKGoal(toAvoid: agents, maxPredictionTime: 3)

        self.ships.forEach() {
            // 加速度(m/s/s)
            $0.agent.maxAcceleration = Float.random(in: 6...8)
            // 最大速度(m/s)
            $0.agent.maxSpeed = Float.random(in: 11...12)
            // 最初の位置
            $0.agent.position = $0.node.simdPosition
            // 最初の姿勢。
            // rotation のX(right)が進行方向のようなので、1行目がワールド座標のZの奥側(マイナス)を見るように設定
            $0.agent.rotation = simd_float3x3(
                SIMD3<Float>( 0, 0,-1),
                SIMD3<Float>( 0, 1, 0),
                SIMD3<Float>( 1, 0, 0))
            // 僚機と接触しないようにエージェントの半径を設定。この半径でぶつからないように制御される
            $0.agent.radius = 1.0
            $0.agent.delegate = self
            // ターゲットに向かいつつ、僚機との接触は避ける
            $0.agent.behavior = GKBehavior(goals: [GKGoal(toSeekAgent: self.target.agent), avoid])
            // エージェントシステムに登録
            self.agentSystem.addComponent($0.agent)

            $0.node.isHidden = true
        }
    }
}

extension GKAgent3D {
    var transform: simd_float4x4 {
        simd_float4x4(simd_float4( rotation.columns.0, 0),
                             simd_float4( rotation.columns.1, 0),
                             simd_float4( rotation.columns.2, 0),
                             simd_float4( position, 1))
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】iOSネイティブプラグインをSwiftで実装する際には、2019.3前後で設定方法が変わる

Unity 2019.3から新機能としてUnity as a Libraryが入った影響で、iOS向けビルド後の.xcodeprojの構成に大きく変更が掛かりました。

その影響としてか、例えば以下の記事にある方法でSwiftコードを実装しようとしてもビルドが通らない状況となっていたので、今回は従来の方法と照らし合わせつつ解決策諸々を備忘録序に纏めていければと思います。
※以降、以下に記事にある2019.2までのやり方を従来のやり方と表記する形で解説していきます。

ひょっとしたら「実はこのパターンが出来なかった」といった見落としがあるかもしれないので、そちらについては分かり次第随時追記予定。
(もし「ここの内容だとあれが出来ない」と言った情報があれば、コメントや編集リクエストなどで教えて頂けると幸いです :pray: :sweat_drops: )

バージョン

  • Unity
    • 2018.4.30f1
      • 従来のやり方の検証に利用
    • 2019.4.17f1
      • 2019.3からのやり方の検証に利用
  • Xcode 12.3

公開リポジトリ

今回検証に用いたプロジェクト一式をGitHubにて公開してます。
「従来のやり方を適用したプロジェクト」と「2019.3からのやり方を適用したプロジェクト」の2つを用意してます。

2019.3からどの様な変更が掛かったのか?

※一応補足として記載しておきます。「ここまでは知ってるので、早くSwiftコードを実装する方法を知りたい」という方は読み飛ばして下さい。

従来はUnity-iPhoneと言うターゲットに全てのコードやデータなどが集約される形となっておりましたが、2019.3からは新たに「UnityFramework」と言うフレームワークのターゲットが追加されており、ClassesLibraries folders、その他依存関係となっているフレームワーク各種はこちらに切り離されるようになりました。

ここらの詳細については公式ドキュメントの方にも記述があるので、詳しくは以下を御覧ください。

この変更によって大きく影響を受ける箇所としては、恐らくは[PostProcessBuild]で処理されるであろうPBXProjectの更新処理であり、GUIDの向き先を以下の様にUnityFrameworkに向け直す必要が出てきました。

        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

#if UNITY_2019_3_OR_NEWER
            // NOTE: 2019.3からは`UnityFramework`に設定を適用
            var targetGUID = project.GetUnityFrameworkTargetGuid();
#else
            var targetName = PBXProject.GetUnityTargetName();
            var targetGUID = project.TargetGuidByName(targetName);
#endif

            project.AddFrameworkToProject(targetGUID, "MonafuwaLowMemoryWarningFramework.framework", false);
            File.WriteAllText(projectPath, project.WriteToString());
        }

ただ、Swiftを扱う上では↑の様に単純にGUIDの向き先をUintyFrameworkに変えるだけでは解決しないので、具体的な解決策を後述していきます。

従来のSwiftコードの実装方法

先に予備知識として2019.2までの従来の実装方法について記載していきます。

とは言え、やっている事自体はほぼ以下の記事にあるやり方そのままです。
こちらも大体把握されている方は読み飛ばしてしまっても問題有りません。

ここでは以下のプロジェクトにあるExsamplesを元に解説していきます。

PBXProjectの変更点

ほぼ記事にあるやり方のとおりです。
幾つか要点だけ箇条書きで纏めておきます。

  • Objective-C Bridging HeaderUnitySwift-Bridging-Header.hを設定
  • Objective-C Generated Interface Header Nameunityswift-Swift.hを設定
  • Runpath Search Paths@executable_path/Frameworksを設定
  • Swiftのバージョンを5.0に設定
    • ※コメントにも記載しているが、こちらを明示的に指定しないとデフォルトで3.0辺りが設定される?影響で、XcodeによってはUnspecified扱いになることがある
    • ※因みに5.0を指定しているのは最新だからとりあえず指定しているだけであって、特にこれと言った理由などは無い。こちらのバージョンは要件に応じて必要な値を設定すること
XcodePostProcess.cs
    /// <summary>
    /// Swiftを実装するにあたって必要な設定を自動で適用する
    /// </summary>
    sealed class XcodePostProcess
    {
        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

            var targetGuid = project.TargetGuidByName(PBXProject.GetUnityTargetName());
            project.SetBuildProperty(targetGuid, "SWIFT_OBJC_BRIDGING_HEADER", "Libraries/UnitySwift/UnitySwift-Bridging-Header.h");
            project.SetBuildProperty(targetGuid, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "unityswift-Swift.h");
            project.AddBuildProperty(targetGuid, "LD_RUNPATH_SEARCH_PATHS", "@executable_path/Frameworks");

            // Swift version: 5.0
            // NOTE: 明示的に指定しないと3.0ぐらいの古いのが設定されるっぽいので、Xcodeによっては`Unspecified`扱いになる
            project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

            File.WriteAllText(projectPath, project.WriteToString());
        }
    }

UnitySwift-Bridging-Header.hには予めFoundation.hUIKit.hの他、UnityInterface.hをimportしておきます。
(UnitySendMessageなどはUnityInterface.hで宣言されているため、SwiftからUnitySendMessageを呼び出したい場合にはimportしておく必要がある)

UnitySwift-Bridging-Header.h
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "UnityInterface.h"

Swiftのコード

Swiftのコードは以下のように実装してます。

callSwiftMethodはC#側から渡された文字列をprintで出力するだけのものであり、callUnityMethodUnitySendMessageを呼び出してC#側に"Hello, Unity!"と言う文字列を送ります。

Exsample.swift
import Foundation

class Example : NSObject {

    // ObjC++からSwiftのメソッドを呼び出す
    @objc static func callSwiftMethod(_ message: String) {
        print("\(#function) is called with message: \(message)")
    }

    // SwiftのメソッドからSendMessageを呼び出す
    @objc static func callUnityMethod() {
        // Call a method on a specified GameObject.
        UnitySendMessage("CallbackTarget", "OnCallFromSwift", "Hello, Unity!")
    }
}

Objective-C++のコード

SwiftのコードだけだとC#から呼び出せないので、P/Invokeで呼び出せるようにObjC++コードを用意します。
内容はSwift側のメソッドをそのまま呼び出しているだけです。

ExsampleBridge.mm
#import <Foundation/Foundation.h>

// ObjC++からSwiftのクラスにアクセスする際に必要
// NOTE: `unityswift-Swift.h`の実態はビルド時に`DerivedData`以下に自動生成される
#import "unityswift-Swift.h"    // Required

// P/Invoke
extern "C" {

    void callSwiftMethod(const char *message) {
        [Example callSwiftMethod:[NSString stringWithUTF8String:message]];
    }

    void callUnityMethod() {
        [Example callUnityMethod];
    }
}

C#のコード

上記で実装したSwiftコード及びP/Invoke用のObjC++コードの呼び出し処理を実装します。
Exsampleでは画面上にある2つのButtonからSwift側のcallSwiftMethodcallUnityMethodを呼び出せるようにしてます。

なお、callUnityMethodについてはSendMessageで結果を受け取る都合上、GameObject名やメソッド名を合わせる形にして受け取った文字列をログ出力するようにしてます。

Exsample.cs (クリックで展開)
Exsample.cs
using System.Runtime.InteropServices;
using UnityEngine;
using UnityEngine.UI;

namespace UnitySwift.Exsamples
{
    /// <summary>
    /// iOS NativePluginの呼び出しサンプル
    /// </summary>
    public sealed class Exsample : MonoBehaviour
    {
        [SerializeField] Button _callSwiftMethod = default;
        [SerializeField] Button _callUnityMethod = default;

        void Start()
        {
            // SwiftからSendMessageを呼び出す際に指定されているGameObjectの名称を設定
            this.name = "CallbackTarget";

            _callUnityMethod.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                CallUnityMethod();
#endif
            });

            _callSwiftMethod.onClick.AddListener(() =>
            {
#if !UNITY_EDITOR && UNITY_IOS
                CallSwiftMethod("Gorilla");
#endif
            });
        }

        /// <summary>
        /// SwiftからSendMessageで呼び出される措定のメソッド
        /// </summary>
        /// <param name="message"></param>
        void OnCallFromSwift(string message)
        {
            Debug.Log(message);
        }

        /// <summary>
        /// ObjC++からSwiftのメソッドを呼び出す
        /// </summary>
        /// <remarks>[C# -> ObjC++ -> Swift]の流れで呼び出される</remarks>>
        [DllImport("__Internal", EntryPoint = "callSwiftMethod")]
        static extern void CallSwiftMethod(string message);

        /// <summary>
        /// SwiftのメソッドからSendMessageを呼び出す
        /// </summary>
        /// <remarks>※SwiftからUnity側で定義されているメソッドを呼び出したい意図がある</remarks>>
        [DllImport("__Internal", EntryPoint = "callUnityMethod")]
        static extern void CallUnityMethod();
    }
}

2019.3からのSwiftコードの実装方法

上述した従来のやり方のまま.xcodeprojをビルドすると確実にエラーが出て怒られます。

その理由の一つとして、UnityFrameworkの実態であるFrameworkと言うターゲットはBridging-Header.hを設定することが出来ないので、恐らくは早いタイミングでこの旨に関連するエラーが出力されるかと思われます。

この他にも細かい所で幾つか変更が掛かっている箇所があるので、順を追って説明していきます。

ここでは以下のプロジェクトにあるExsamplesを元に解説していきます。

PBXProjectの変更点

先にPBXProjectの変更点についての解説ですが、結論から言うと「Swiftのバージョン指定」以外不要になっているように思われました。

UnitySwift-Bridging-Header.hについてはそもそも設定することが出来ないので、プロジェクトからも削除してます。

XcodePostProcess.cs
    sealed class XcodePostProcess
    {
        /// <summary>
        /// Swiftを実装するにあたって必要な設定を自動で適用する
        /// </summary>
        [PostProcessBuild]
        static void OnPostProcessBuild(BuildTarget target, string path)
        {
            if (target != BuildTarget.iOS) return;

            var projectPath = PBXProject.GetPBXProjectPath(path);
            var project = new PBXProject();
            project.ReadFromString(File.ReadAllText(projectPath));

            // 2019.3からは`UnityFramework`に分離しているので、targetGuidはこちらを指定刷る必要がある。
            // NOTE: 前バージョンと共存させたい場合には「#if UNITY_2019_3_OR_NEWER」で分けることも可能
            var targetGuid = project.GetUnityFrameworkTargetGuid();

            // NOTE:
            // 以前まで設定していた`Bridging-Header.h`の設定の類は2019.3からは不要な模様。
            // 寧ろCocoa touch FrameworkがBridging-Headerに対応していないので、設定していると怒られる。

            // Swift version: 5.0
            // NOTE: 明示的に指定しないと3.0ぐらいの古いのが設定されるっぽいので、Xcodeによっては`Unspecified`扱いになる
            project.AddBuildProperty(targetGuid, "SWIFT_VERSION", "5.0");

            File.WriteAllText(projectPath, project.WriteToString());
        }
    }

Swiftのコード

変更点としては以下の2点が挙げられます。

  • ObjC++に公開するクラス/メソッドのアクセスレベルをpublicに設定
  • UnitySendMessageの代わりにUnityFrameworkに実装されているsendMessageToGOを利用

後者の変更については以下のissue/PRを参考にさせて頂きました。
(記憶だとsendMessageToGOはUaaL1に於ける「Native → Unity」間のやりとり辺りで使われていた覚え)

従来のUnityInterface.hはそもそもターゲットに含まれておらず、参照すら出来ない模様...?

Exsample.swift
import Foundation

// NOTE: ObjCに公開する物はアクセスレベルを`public` or `open`に設定する必要あり

public class Example : NSObject {

    // ObjC++からSwiftのメソッドを呼び出す
    @objc public static func callSwiftMethod(_ message: String) {
        print("\(#function) is called with message: \(message)")
    }

    // SwiftのメソッドからSendMessageを呼び出す
    @objc public static func callUnityMethod() {

        // NOTE: 従来の`UnityInterface.h`にある`SendMessage`は参照でき無さそう?なので、以下のsendMessageToGOを利用する。

        // Call a method on a specified GameObject.
        if let uf = UnityFramework.getInstance() {
            uf.sendMessageToGO(
                withName: "CallbackTarget",
                functionName: "OnCallFromSwift",
                message: "Hello, Unity!")
        }
    }
}

Objective-C++のコード

こちらの主な変更点としては、importするヘッダーであり、対象をunityswift-Swift.hからUnityFramework/UnityFramework-Swift.hに変更してます。

ExsampleBridge.mm
#import <Foundation/Foundation.h>

// 2019.3からはこちらをimportする必要がある
#import <UnityFramework/UnityFramework-Swift.h>

// P/Invoke
extern "C" {

    void callSwiftMethod(const char *message) {
        [Example callSwiftMethod:[NSString stringWithUTF8String:message]];
    }

    void callUnityMethod() {
        [Example callUnityMethod];
    }
}

C#のコード

C#側の呼び出しコードに変更は無いので割愛。

参考リンク


  1. UaaL・・・Unity as a Libraryの略称 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アプリの収益化選択肢とアプリ内広告まとめ

<アプリの収益化選択肢>
https://apps.jp.net/introduction/estimated-income/

有料アプリ販売
->かなりのクオリティを求められる(利便性、エンタメ性)
アプリ内課金
->かなりのクオリティを求められる(利便性、エンタメ性)
アプリ内広告
->1クリック100円で、最低支払額8000円から?

アプリのキャプチャ画像だけで判断してユーザーはアプリをダウンロードする
アプリを消すことはほとんどない
維持費(サーバー通信費用、定期的なメンテナンス、ユーザー同士のコミュニティ、サポート窓口)
維持費0にすると、赤字リスクも0。プラスだけが積み上がる
お金を掛けずにお金を稼ぐことができる

<アプリ内広告サービス:ASP>
アプリの収益は、広告のクリック単価とクリック数によって決まる。
広告のクリック単価は、選択するASPや表示される広告によって変化。

■Admob(アドモブ)
->Googleが提供する広告プラットフォーム。
webページには表示できない
最も収益を見込めるが、広告を運用する設定が細かく、
専門知識や用語を理解する必要があり、ハードル高い
アプリに合わせた広告を設定したり、料金、時間帯などを設定
ダイナミックアロケーション
https://adtech-univ.jp/gam-dynamic-allocation/
https://qiita.com/hikarut/items/0e5a7032f69cb212ba03

■Google Ad Manager
->Admobとの違いは、webページにも広告を表示できること
https://you88.space/408

■Nend
->導入がわりと簡単。webページには表示できない
https://nend.net/faq
https://application.hateblo.jp/entry/2016/07/11/061500
https://qiita.com/nao-otsu/items/21e85e46bea55c17150b
https://apps.jp.net/introduction/nend/

■比較
https://qiita.com/hikarut/items/0e5a7032f69cb212ba03
https://rtbsquare.work/archives/29821
https://apps.jp.net/introduction/estimated-income/

<収益について>
https://apps.jp.net/column/admob/
https://qiita.com/hikarut/items/0e5a7032f69cb212ba03

■Nend
クリック単価->4円〜10円
最低報酬支払額->3,000円

■Google
クリック単価->109円
最低報酬支払額->8,000円

<広告の運用について>
広告の一般用語を理解
->IMP/CPM/純広告など
https://webma.xscore.co.jp/study/cpm/

ASPでの広告設定の操作を理解
->配信する広告の種類/単価/時間などを細かく設定
->独自の用語:在庫、広告ユニット、プレースメント、オーダー、広告申し込み情報、etc
https://marketing-gorilla.jp/posts/35

広告収益アップのための運用知識が必要
->ダイナミックアロケーション、SSP
https://marketing-gorilla.jp/posts/215
https://adtech-univ.jp/gam-dynamic-allocation/

<広告用語>
https://webma.xscore.co.jp/study/cpm/
IMP(インプレッション数)-> 広告が表示された回数
CPM課金(インプレッション課金)-> 広告が1,000回表示されるたびにコストが発生する課金体系
CPC課金(クリック課金)-> 広告がクリックされるたびにコストが発生する課金体系
CPA課金(コンバージョン課金)-> 広告から成果が生まれるたびにコストが発生する課金体系

<Google Ad Managerの実装>
■アプリ実装手順
https://developers.google.com/ad-manager/mobile-ads-sdk/ios/quick-start?hl=ja#update_your_infoplist

■広告管理者から共有してもらう情報
https://support.google.com/admanager/answer/6238692?hl=ja
・アプリID
・広告ユニットID
・バナーのサイズ

■バナーのサイズの選択肢
https://developers.google.com/admob/ios/banner?hl=ja#banner_sizes
※仮実装でアプリに各サイズのバナーを貼り付けて、イメージにマッチするものを選択するという手順でも結構です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【swift5,CocoaPods】同じライブラリが2つinstallされてたら、最初からやり直しましょう

はじめに

CocoaPodsにて、既にライブラリが入ってる状態で、更にライブラリを追加すると遭遇しうる不具合です。
(筆者は既にFireBase系のライブラリが入っている状態にて、2回遭遇しました。)

Error文としては35 duplicate symbols for architecture x86_64とかが出ると思います。

解消方法だけ知りたい場合は「解決策」まで飛ばしてください。

筆者の遭遇した状況

開発初期に、ターミナルから以下のライブラリを導入。

pod 'IQKeyboardManagerSwift'
pod 'Firebase/Analytics'
pod 'Firebase/Auth'
pod 'Firebase/Firestore'
pod 'Alamofire'

1回目の遭遇

ターミナルにてpod updateを実行した直後。
提出先より「ビルドできない」との連絡があり発覚。

2回目の遭遇

アプリにオンボーディングを実装するため、

pod 'Onboard'

を導入。
pod install後前回の不具合を思い出し、Podsファイルを確認したところ発覚。

エラーの原因

当該プロジェクトのPodsファイルを確認してみると、画像のように名前の後ろに「2」が付いているファイルが出てくると思います。
スクリーンショット 2021-01-03 16.32.40.png
「2」が付いているファイルが1つ2つならまだしも、ご覧の通り滅茶苦茶ありますね。
これが今回のErrorの原因です。

解決策

1つ1つダブっているファイルを消していくやり方だと、面倒な上にエラーが起きます。

単純ですが、pod installからやり直しましょう。

手順① プロジェクトにアクセス

Finderやターミナルを使ってプロジェクトにアクセスしましょう。

※バックアップを取っておくのも忘れずに

プロジェクトの上で右クリック。「”(アプリ名)”を圧縮」を選んでzip形式のバックアップをとっておきましょう。
Githubなどの管理ツールを使ってる方は、そちらででもいいですね。

手順② 「Podfile.lock」と「Pods」を削除

プロジェクト下にあるPodfile.lock」と「Pods」のみ削除しましょう。

なお、Podfileは消さないように注意。
もし消しちゃったらゴミ箱から漁って戻すか、ターミナルでpod initして再作成しましょう。

手順③ ライブラリを再インストール

ターミナルを開いて、プロジェクト下に移動してからpod install
ライブラリの再インストールが始まるので、終わったらxcworkspaceから開いて完了です。

最後に

このエラーは、同プロジェクトに対する2回目のライブラリ導入が、pod installpod updateかに関わらず発生し得ます。

この不具合が起きてても、自身の開発環境では普通にビルドが通るので気付きにくいんですよね。
他人にプロジェクトを渡したときにはじめて気づくので、滅茶苦茶焦ると思います。

消して再インストールすれば簡単に治るので、落ち着いて処理しましょう。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む