- 投稿日:2021-01-03T22:38:13+09:00
【Swift】Scroll Viewを設置したあと
- 投稿日:2021-01-03T21:40:20+09:00
ARKit+GameplayKit で 例の飛行機の編隊飛行
ARKit+GameplayKit の動きを確認するため Xcode の ARKitプロジェクトに用意されている飛行機を空に飛ばしてみた。
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) }飛行機の位置・姿勢の更新処理
飛行機の位置・姿勢は
GKAgentDelegate
のagentDidUpdate(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_float4x4
やSCNMatrix4
の transformプロパティがない。position(vector_float3型。simd_float3のエイリアス)と rotation(matrix_float3x3型。simd_float3x3のエイリアス)を持っているので、この情報からsimd_float4x4
型の値を生成する処理をGKAgent3D
のextensionに実装している。Scene Editor の設定
shipの子ノードをY軸90度回転しておく必要がある(こうしないと右を向いて飛んでいく)。
これも理由は不明。
この飛行機のジオメトリはもともとZ軸のプラス側を向いている。これを90度回転してX軸のプラス側を向くようにする。GameplayKitの3Dでの進行方向正面はX軸プラスということらしい。GKAgent3Dクラスの rotation もそうだが(ドキュメントのどこかに書いてあるのだろうが)Appleにはわかりやすい場所に説明・リンクを用意して欲しい。。。最後に
謎な部分は残っているが、この短いコードで派手な動きができたと思う。
GameplayKit はSceneKitよりもさらにマイナーなフレームワークだと思うが、頭の引き出しに入れておくと、役立つことがあるかもしれない。全体ソースコード
ViewController.swiftimport 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)) } }
- 投稿日:2021-01-03T21:22:25+09:00
[RxSwift] Observableの定義を追って理解を深めたい
はじめに
RxSwiftを勉強しているときに
Observable
クラスの定義を見ていると、プロトコルが(自分にとっては)複雑すぎて頭が混乱してしまいました。
なのでObservable
クラスと、プロトコルの勉強を兼ねて、定義についてまとめていきたいと思います。
※RxSwiftについて多少なりとも理解している方には参考にならないと思います。プロトコルをしっかり理解している人も実際の定義を見た方が早いと思います!Observableクラス
public class Observable<Element> : ObservableType { 省略 }
- 監視(Observe)される
- next, error, completed の3つを通知できる(後で説明)
- 任意の型を扱える
- ObservableTypeプロトコルに準拠している
- そして、ObservableTypeはObservableConvertibleTypeプロトコルを継承している
以下では
ObservableType
やObservableConvertibleType
について説明していきます。ObservableType
public protocol ObservableType: ObservableConvertibleType { 省略 }
Observable
に準拠しているプロトコル。
何が定義されているかというと、、、
- subscribe(メソッド)
- asObservable(メソッド)
subscribe
はObservableType
プロトコル内で定義されていて
asObservable
はObservableConvertibleType
プロトコルで定義されています。subscribe
public func subscribe<Observer: ObserverType>(_ observer: Observer) -> Disposable where Observer.Element == Element引数:
ObserverType
プロトコルのassociatedtypeであるElement
と、ObservableConvertibleType
プロトコルのassociatedtypeであるElement
が等しい値
戻り値:Disposable
プロトコルに準拠した値
※Disposable
はsubscribe(購読)の解除を行うためのプロトコルです。observable.subscribe(onNext: { print($0) })実際に使用する際は、
Observable
を持つ値に対して使います。
onNext
でイベントが流れてきたときにprint($0)
が実行されます。
onNext
の他にはonError
,onCompleted
があります。asObservable
public func asObservable() -> Observable<Element> { return Observable.create { o in return self.subscribe(o) } }引数: なし
戻り値: 任意の型Element
をもつObservable
を返す監視したいけれど、
Observable
じゃない。ってときにこのメソッドを使うとObservable
として扱える感じです。
ただし、ObservableType
にしか使えないです。ObserverType
public protocol ObserverType { associatedtype Element @available(*, deprecated, renamed: "Element") typealias E = Element func on(_ event: Event<Element>) } extension ObserverType { public func onNext(_ element: Element) { self.on(.next(element)) } public func onCompleted() { self.on(.completed) } public func onError(_ error: Swift.Error) { self.on(.error(error)) } }
subscribe
の型のところで新しく出てきたObserverType
について説明する前にEvent
について軽く説明します。public enum Event<Element> { case next(Element) case error(Swift.Error) case completed }定義を見るとこのようになっており、
next
はObserverに値の更新を通知します。error
はエラー時にエラーを通知します。completed
はイベントが終わったときに完了を通知します。func on(_ event: Event<Element>)このメソッドを実際に使う際は、以下の様にどちらを使っても構いません。
observer.on(.next(element)) observer.onNext(element)ObservableConvertibleType
public protocol ObservableConvertibleType { associatedtype Element @available(*, deprecated, renamed: "Element") typealias E = Element func asObservable() -> Observable<Element> }
Observable
クラスに準拠しているObservableType
プロトコルが継承しているObservableConvertibleType
プロトコルについてみていきます(ややこしいな...)func asObservable() -> Observable<Element>このメソッドについては
ObservableTypeのasObservableを見てください。このプロトコルでは難しいことはないのでよかったです。
終わりに
自分の思考を整理するために書いていたところが大きかったため、あまり理解しやすい内容とはいえないかもしれませんが、個人的にはだいぶ理解が深まったなと思っています。
やはり、定義にジャンプしてコードを追うことは(脳に抵抗がありますが)大事だと思いました。
- 投稿日:2021-01-03T20:24:18+09:00
【Unity】iOSネイティブプラグインをSwiftで実装する際には、2019.3前後で設定方法が変わる
Unity 2019.3
から新機能として「Unity as a Library」が入った影響で、iOS向けビルド後の.xcodeproj
の構成に大きく変更が掛かりました。その影響としてか、例えば以下の記事にある方法でSwiftコードを実装しようとしてもビルドが通らない状況となっていたので、今回は従来の方法と照らし合わせつつ解決策諸々を備忘録序に纏めていければと思います。
※以降、以下に記事にある2019.2までのやり方を従来のやり方
と表記する形で解説していきます。ひょっとしたら「実はこのパターンが出来なかった」といった見落としがあるかもしれないので、そちらについては分かり次第随時追記予定。
(もし「ここの内容だとあれが出来ない」と言った情報があれば、コメントや編集リクエストなどで教えて頂けると幸いです![]()
)
バージョン
- Unity
- 2018.4.30f1
- 従来のやり方の検証に利用
- 2019.4.17f1
- 2019.3からのやり方の検証に利用
- Xcode 12.3
公開リポジトリ
今回検証に用いたプロジェクト一式をGitHubにて公開してます。
「従来のやり方を適用したプロジェクト」と「2019.3からのやり方を適用したプロジェクト」の2つを用意してます。2019.3からどの様な変更が掛かったのか?
※一応補足として記載しておきます。「ここまでは知ってるので、早くSwiftコードを実装する方法を知りたい」という方は読み飛ばして下さい。
従来は
Unity-iPhone
と言うターゲットに全てのコードやデータなどが集約される形となっておりましたが、2019.3からは新たに「UnityFramework」と言うフレームワークのターゲットが追加されており、Classes
やLibraries 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 Header
にUnitySwift-Bridging-Header.h
を設定
- ※
UnitySwift-Bridging-Header.h
はプロジェクト中に事前にプラグインとしてインポートされている物が設定されるように適用Objective-C Generated Interface Header Name
にunityswift-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.h
とUIKit.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#側から渡された文字列をcallUnityMethod
はUnitySendMessage
を呼び出してC#側に"Hello, Unity!"と言う文字列を送ります。Exsample.swiftimport 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側のcallSwiftMethod
とcallUnityMethod
を呼び出せるようにしてます。なお、
callUnityMethod
についてはSendMessageで結果を受け取る都合上、GameObject名やメソッド名を合わせる形にして受け取った文字列をログ出力するようにしてます。
Exsample.cs
(クリックで展開)Exsample.csusing 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.cssealed 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.swiftimport 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#側の呼び出しコードに変更は無いので割愛。
参考リンク
- FrameworkでSwiftとObjective-C混ぜるのはやばい
- UnityのiOSネイティブプラグインをSwiftで書くためのネイティブプラグイン
- 【Unity】最近のUnityでC#からSwiftを実行する最短実装
- miyabi/unity-swift
UaaL・・・
Unity as a Library
の略称 ↩
- 投稿日:2021-01-03T20:09:33+09:00
アプリの収益化選択肢とアプリ内広告まとめ
<アプリの収益化選択肢>
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円
クリック単価->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
※仮実装でアプリに各サイズのバナーを貼り付けて、イメージにマッチするものを選択するという手順でも結構です。
- 投稿日:2021-01-03T15:28:32+09:00
【Swift】配列から要素をランダムに取り出す
配列 Arrayのプロパティ
randomElement()
でランダムに要素を取り出してくれます。let omikujiResults = ["大吉","中吉","吉","小吉","凶","大凶"] let thisYearLuck = omikujiResults.randomElement() print(thisYearLuck!) //出力結果:大吉実際に自分が試したら、大凶が出ました。?
今年はいい年になりますように。