カテゴリー: iOS

UnityのiOSネイティブをSwiftで書く

はじめに

初めまして。カイザーです。
今回から、月一で寄稿させて頂くことになりました。
日頃の開発で詰まったところなど、記事を通してナレッジをシェアしていきたいと思います!
よろしくお願いします!!

UnityのiOSネイティブとは?

Unityはクラスプラットフォームのゲームエンジンなので、1つのプロジェクトで、同じゲーム等を様々なOSや端末で動作させることができます。
ですが、中にはOSに依存する機能が必要な場合があります。例えば、下記のようなものが挙げられます。

  • 課金
  • SNSシェア
  • WebView
  • マップ表示(MapKit等)

これらを実現するには「ネイティブ実装」を行うことが必要ですが、Unity本体やAssetStoreで入手できるプラグインによって実現できるものもあります。
ですが、実現したい内容によっては自分で実装しなければならない場合もあります。
そこで、今回はSwiftを使って、iOSのMapKitをUnityから呼び出してみたいと思います。

今回作るもの


このようなUnityのシーンにApple標準(MapKit)のマップを表示するサンプルアプリを題材に説明します。
なお、表示の際には、マップの表示位置・サイズと、ピン情報を指定できるものとします。

必要な構成

UnityのiOSネイティブはCで呼ばれるため、その部分はObjectiveC++で開発し、そこからさらにSwiftを呼ぶ形となります。
どのように呼び出されるのか図にまとめました。

この構造のため、SwiftはNSObjectを継承する必要があり、完全にSwiftで記述することはできないという欠点はあるものの、Optionalやstructなど、Swiftの恩恵は受けることができます。

unity-swiftの導入

Unityで書き出されるiOSプロジェクトは、Objective-Cベースなので、それにSwiftを共存させる設定が追加で必要になります。
しかし、Unityから出力されるプロジェクトにはそのような設定はされておらず、特にBuild SettingsのRuntime Search PathsはUnityからのビルド時に毎度設定する必要があります。

そこで、下記のAssetを使用することにより、それらの設定を自動化することができます。

https://github.com/miyabi/unity-swift

今回は、こちらのAssetを使用していきます。

「Clone or Download」からダウンロードし、unity-swift.unitypackageを開き、プロジェクトに追加しておきます。

必要な機能の実装

ここからは、実際に必要な機能をプログラミングしていきます。

Swiftクラスの作成

Swiftクラスは、Assets内であればどこに作成しても構いません。
呼び出されると、指定された情報でMKMapViewを生成し、addSubViewするメソッドを作成しました。

import Foundation
import MapKit

class UnityMapKit: NSObject {
    // @objcをつけて置かないと、Objective-C++から呼び出せないので、必ずつけておく。
    @objc static func showMap(frame:CGRect, placeJson: String) {
        // UnityGetGLView()でUnityで使用しているUIViewを取得できるので、そこにaddSubViewできる。
        guard let view = UnityGetGLView() else {
            return
        }
        let mapView = MKMapView(frame: frame)
        
        // Unityから送られてくるピン情報はJSONなので、パースする。
        let jsonData = placeJson.data(using: .utf8)!
        let decoder = JSONDecoder()
        let placeList = try! decoder.decode(PlaceList.self, from: jsonData)
        
        placeList.places.forEach { // ピンを配置する
            let annotation = MKPointAnnotation()
            annotation.coordinate = CLLocationCoordinate2DMake($0.latitude, $0.longitude)
            annotation.title = $0.title
            mapView.addAnnotation(annotation)
        }
        
        view.addSubview(mapView)
        
    }
}

メソッドを作成する際、@objcをつけておかないと、Objective-C++から呼び出せませんので、注意してください。

また、JSONパースにCodableを使用していますので、structでモデルを作成します。

/// ピン情報のリスト (UnityのJsonUtilityが配列がルートのJSONに対応していないため
struct PlaceList: Codable {
    let places: [Place]
}

/// ピン情報のモデル
struct Place: Codable {
    let id: Int
    let title: String
    let latitude: Double
    let longitude: Double
}

Codableについては、下記の過去記事に詳しく書いてありますので、興味のある方は読んでみてください。

https://re-engines.com/2018/09/13/2650/

Objective-C++でUnityからの呼び出しをSwiftに受け流す

先述の通り、Unityから直にSwiftを呼び出すことはできず、Objective-C++(.mmファイル)もしくはcppへの呼び出しになります。
Objective-C++が使用できれば、Swiftで書かれたメソッドを呼び出すこともできますので、今回はその方式で実装していきます。
なお、Objective-C++についてもどこに配置しても構いませんが、今回はSwiftファイルと同じ階層である「Assets/Scripts」に配置しました。

// 必ずimportする
#import "unityswift-Swift.h"

extern "C" {
    // メソッド名、引数の型・引数の個数は、C#のインタフェース(後述)と合わせる
    void showMap(float x, float y, float width, float height, char *placeJson) {
        // ここではSwiftのメソッドを呼ぶだけ
        [UnityMapKit showMapWithFrame:CGRectMake(x, y, width, height) placeJson:[NSString stringWithUTF8String:placeJson]];
    }
}

C#クラスの作成

最後に、C#クラスを作成していきます。これは、Unityでクラスを作成するいつも通りの方法で問題ありません。
先ほど作成したCの関数と同じインタフェースをC#で記述することで、C#から呼出可能になります。
それでは、C#でのインタフェース定義と、呼び出しを両方同時に見てみましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Runtime.InteropServices;

public class UnityMapKit : MonoBehaviour
{
    public PlaceList placeList; // マップに表示するピンの情報

    // Objective-C++に記述した関数をC#クラスにも記載することで、C#から呼び出し可能になる。
    // 必ず[DllImport("__Internal")]を設定しておかないと、Objective-C++の関数と紐づかない
    // また、今回IOSのネイティブコードであるため、IOS以外(エディタ含む)はエラーとなってしまうため、#ifで実行させないようにする
    #if UNITY_IOS && !UNITY_EDITOR
    [DllImport("__Internal")]
    private static extern void showMap(float x, float y, float width, float height, string placeJson);
    #endif

    void Start()
    {
        // JsonUtilityを使ってピン情報をJSONに変換
        var json = JsonUtility.ToJson(placeList);

        // 上記のインタフェース経由でネイティブコードを呼び出すが、こちらもIOS専用としてある
        #if UNITY_IOS && !UNITY_EDITOR
        showMap(0f, 0f, 500f, 500f, json);
        #endif
    }
}

最後に、ピンの情報を持つモデルを作成します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class Place {
    public int id;
    public string title;
    public double latitude;
    public double longitude;
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class PlaceList
{
    public Place[] places;
}

UnityのJsonUtilityでは、配列がルートのJSONを取り扱うことができないため、ルートをJSONObjectにするためのPlaceListというモデルを作成しました。

シーンへの配置

「UnityMapKit.cs」をシーンに配置して、マップピンの情報を予めセットしてみました。

ビルドして実機で確認

これで完了です。ビルドするとシーンにマップが表示されます。

Swiftのバージョンでエラーになってしまうときは

Unityが出力するiOSプロジェクトでは、Swiftバージョンの指定がないため、エラーとなってしまいます。
その場合、出力されたプロジェクトを開き、Target「Unity-iPhone」> Build Settings > Swift Language Versionを「Swift 4.0」等に変更することで解消できます。

おまけ:SwiftからUnityを呼び出すには?

Swiftが呼び出された時、すぐ値を返却したいのであれば、Cの関数の返却をvoidではなく返り値を設定すれば良いのですが、後からUnityを呼び出したい場合は、下記の構文を使用することができます。

UnitySendMessage("GameObjectName1", "MethodName1", "Message to send")

第一引数: 呼び出すGameObjectのname
第二引数: 呼び出すGameObjectにアタッチされているスクリプトのメソッド名
第三引数: 上記メソッドの第一引数に渡す情報(文字列のみ)

この場合、呼び出されるC#側のメソッドは下記になります。

void MethodName1(string message)

第三引数が文字列固定のため、複数データを渡すときは、JSONでやり取りすることがオススメです。

さいごに

UnityからSwiftを呼び出すまで、複数の仕組みや言語を経由する必要があり、面倒ではありますが、ネイティブをObjective-CではなくSwiftで書けることはとてもありがたいです。

UnityのiOSネイティブを書く機会があれば、Swift一択ですね!

参考

カイザー

シェア
執筆者:
カイザー
タグ: SwiftC#Unity

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前