Unity iOS/Android で端末内の画像を使うためのプラグインを書いた
Unity で作ってるスマホゲームアプリで、スマホ内の画像を使いたいシチュエーションがあったので、画像を選択できるネイティブプラグインを作ってみた。 はまったところを中心に、メモを残しておく。
概要
- Android は
Fragment
からIntent
を投げて、画像の選択結果を受け取る - iOS は
UIImagePickerController
を開いて、画像の選択結果を受け取る - 画像を選択できたら、画像のファイルパスを C# 側にコールバックする
- オリジナルのファイルパスは取得できないので、自分で読み書きできる場所に画像をコピーして、そのパスを渡す
成果物
thedoritos/unimgpicker: Image picker for Unity iOS/Android にある。
以下のソースは、説明のため、部分的に示したりエラー処理を省略したりしているので、詳しくはリポジトリの方を参照する。
Android 版
Android 版では、画像ファイルを開く Intent を投げて、外部アプリで画像を開いてもらい、結果を受け取る方針にする。
Fragment でコールバックを受け取る
Intent は startActivityForResult
メソッドで投げて、結果を onActivityResult
メソッドで受け取る。
各メソッドは Activity
にも Fragment
にもあり、どちらから投げるかによってどちらにコールバックされるかが決まる。
別の話として、Unity のネイティブプラグインを作るときに、UnityPlayerActivity
の継承問題がある。
各々のプラグインが UnityPlayerActivity
を継承する設計になってしまっているために、ソースが競合したり、マージが必要になったりしてつらいことがある。
そこで、本プラグインでは、Fragment
を使うことで既存の UnityPlayerActivity
には手を入れないようにしている。
画像ファイルをコピーする
画像が選択できると onActivityResult
が呼ばれる。画像の URI は引数の Intent
から簡単に取得することができる。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Uri uri = data.getData();
}
ただし、この URI は content://
という特殊な形式になっている。
これは FileProvider | Android Developers という仕組みで、実際のファイルパスを隠蔽しながらファイルを共有しているため。
FileProvider でファイルを共有する側は、以前 Unity Android で FileProvider を使用する でやった。
この content://
という URI をそのまま C# 側に渡しても、ファイルを読みに行くことができない。
そこで、Android プラグイン側でファイルを読み、C# 側から読める場所にコピーして、そのパスを返してあげるようにしている。
InputStream inputStream = context.getContentResolver().openInputStream(uri);
FileOutputStream outputStream = context.openFileOutput(mOutputFileName, Context.MODE_PRIVATE);
byte[] buf = new byte[8192];
while (true) {
int r = inputStream.read(buf);
if (r == -1) {
break;
}
outputStream.write(buf, 0, r);
}
iOS 版
iOS 版では、UIImagePickerController
を使う方針にする。
NSPhotoLibraryUsageDescription
Info.plist
に NSPhotoLibraryUsageDescription
を定義していないと、クラッシュするロックな仕様なので気をつける。
本プラグインでは PostProcessBuild
で自動で設定するようにしている。
画像ファイルをコピーする
UIImagePickerControllerDelegate
は、メモリに読み込まれた画像を受け取るようになっていて、こちらも実際のファイルパスは隠蔽されている。
ということで、画像を C# 側から読める場所に書き込んで、そのパスを返してあげるようにしている。
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
UIImage *image = info[UIImagePickerControllerOriginalImage];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
// 拡張子がないとだめだから
NSString *imageName = self.outputFileName;
if ([imageName hasSuffix:@".png"] == NO) {
imageName = [imageName stringByAppendingString:@".png"];
}
NSString *imageSavePath = [(NSString *)[paths objectAtIndex:0] stringByAppendingPathComponent:imageName];
NSData *png = UIImagePNGRepresentation(image);
BOOL success = [png writeToFile:imageSavePath atomically:YES];
}
上記の例はがっつり省略しているので、詳しくはリポジトリの方を参照する。
Unity で画像を読み込む
プラグインから受け取ったファイルパスに file://
をつけて URL を作って、UnityEngine.WWW
で読みにいけば良い。
まとめ・感想
- Android は
Fragment
からIntent
を投げて、画像の選択結果を受け取った-
Fragment
を使うことでUnityPlayerActivity
継承のコンフリクトを避けることができた
-
- iOS は
UIImagePickerController
を開いて、画像の選択結果を受け取った-
NSPhotoLibraryUsageDescription
の設定が必要がだったので、スクリプトで自動化した
-
- 画像を選択できたら、画像のファイルパスを C# 側にコールバックした
- オリジナルのファイルパスは取得できないので、自分で読み書きできる場所に画像をコピーして、そのパスを渡した
ひさびさに Objective-C 書いたらつらかった
特に I/O 周りだと nil チェックが必要なことが多くて、Swift では if let
とか guard
で綺麗に書けるところが if (hoge == nil)
の早期リターンだらけになってつらかった。
あと、ググるときにわざわざ「Objective-C」を指定しないとドキュメントが見つけられないのが、タイプ数的にも精神的にもつらかった。
プラグインを Swift で書くのは Unity iOS のネイティブプラグインを Swift で書いた でやっているので、今回も Swift にしても良かったかもしれない。 というか早く iOS ネイティブプラグインも Swift で書くのがデファクトになってほしい
参考
- android - onActivityResult is not being called in Fragment - Stack Overflow
- FileProvider | Android Developers
- Storage Access Framework | Android Developers
- [objective c - iOS 10 error [access]
when using UIImagePickerController - Stack Overflow](https://stackoverflow.com/questions/38236723/ios-10-error-access-private-when-using-uiimagepickercontroller)
補足
Texture をリサイズしたい場合に使えそうな情報。