フォト画像を端末に保存する機能の実装上のポイント



## はじめに
「[QualiArts Advent Callender 2023](https://qiita.com/advent-calendar/2023/qualiarts)」の4日目の記事になります。

[株式会社QualiArts](https://qualiarts.jp/)でUnityエンジニアをしております、石上です。
今回は、弊社の運用中のタイトルの[IDOLY PRIDE](https://idolypride.jp/game/)にて実装を担当した、
フォト画像の端末保存機能の実装方法と、実装上のポイントについてまとめます。

## フォト画像を端末に保存する機能とは
2023年10月下旬のアップデート（version 3.4.0）で実装された比較的新しい機能です。
ゲーム中の「フォト」を画像として、ゲームプレイ中の端末に保存することができます。


IDOLY PRIDEには撮影機能があり、ライブ中やお仕事中のアイドルの撮影を行うことができます。
撮影した画像は、フィルムを使用することで現像でき、「フォト」として獲得することができます（ギフトとして配られたり、ショップで販売されていたりするフォトもあります）。

獲得したフォトの中から、保存したいフォトの詳細画面に行き、
画面左下の「保存」ボタンを押すことで、
- iOSの写真アプリのアルバム配下
- AndroidのDCIMフォルダ配下

に画像として保存することができます。

「保存」ボタンを初めて押した際には、必要に応じて端末側の権限要求ダイアログが表示され、
プレイヤーが許可することで、画像が保存されます。

|フォトの詳細画面|保存されたフォト画像|
|:---:|:---:|
|<img width = 290 src ="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3487124/dc6b6afb-7e22-0254-b4ae-dd1fb12cb4ce.png">|<img width = 290 src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3487124/bdc01505-7be9-1859-8757-0b8288c51585.png">|



## 画像保存の実装方法

今回の画像保存には、他の画像保存プラグインも検証し、比較検討した結果、
弊社の過去プロジェクトでも導入実績があった[Unity Native Gallery Plugin](https://github.com/yasirkula/UnityNativeGallery)（v1.7.5）を使用させていただきました。

### 保存までの処理の流れ

1. 保存したいフォト画像の`Texture2D`を、フォトの種類に応じて取得
2. 取得した`Texture2D`から読取可能な`Texture2D`を生成
3. Unity Native Gallery Pluginの画像保存メソッドに、読取可能な`Texture2D`を渡して画像を保存


#### 実装例
```cs
// フォト画像の種類に応じて、適切な方法でTexture2Dを取得
var texture = GetPhotoTexture2D(photoData);

// 読取可能なTexture2Dを取得（読取不可のTexture2Dでは端末保存できない）
var readableTexture = CreateReadableTexture2D(texture);

// GUIDを保存ファイル名に使用する
var saveFileName = $"{Guid.NewGuid().ToString()}.png";

try
{
    var permission = NativeGallery.SaveImageToGallery(readableTexture, "IDOLY PRIDE", saveFileName, (isSuccess, savedPath) =>
    {
        if (isSuccess)
        {
            // 保存完了
        }
        else
        {
            // 保存失敗（必要な権限の要求をプレイヤーが拒否した場合など）
        }
    });
    if (permission == NativeGallery.Permission.Denied)
    {
        // 保存のために必要な権限が拒否されている旨のメッセージの表示
    }
}
catch (Exception e)
{
    // 権限拒否以外の原因で、保存失敗
}
finally
{
    // テクスチャを破棄しておく
    Destroy(readableTexture);
}
```

### 実装上のポイント

実装上のポイントとしては、以下の4点があります。
- OSの種類やバージョンによっては、権限要求が必要になる
- `Texture2D`は読取可能である必要がある
- できる限り高解像度なフォト画像が保存できるような`Texture2D`を用意
- 保存ファイル名にはランダム生成された文字列を設定

それぞれ、以下で説明していきます。


#### 1. OSの種類やバージョンによっては、権限要求が必要になる
##### iOS向け
- `NSPhotoLibraryUsageDescription`（フォトライブラリへのアクセス許可）
- `NSPhotoLibraryAddUsageDescription`（フォトライブラリへの書き込み許可）

をアプリの`info.plist`に追加する必要があります。

##### Android向け
Android9以前の端末をサポートする場合、
`WRITE_EXTERNAL_STORAGE`の利用を`AndroidManifest.xml`で宣言しておく必要があります。
（Android10以上は、特に権限不要）

#### 2. `Texture2D`は読取可能である必要がある
実装例の`GetPhotoTexture2D`メソッド（筆者が実装したメソッド）で得られる`Texture2D`は、読取不可な`Texture2D`になっており、このままだとJPGやPNG画像化できないため、
以下のような処理で読取可能な`Texture2D`を生成して、その`Texture2D`を画像保存処理に渡す必要があります。


```cs
private Texture2D CreateReadableTexture2D(Texture2D texture)
{
    var width = texture.width;
    var height = texture.height;

    var midRenderTexture = RenderTexture.GetTemporary(width, height, 0, GraphicsFormat.R8G8B8A8_SRGB);

    Graphics.Blit(texture, midRenderTexture);
    var previous = RenderTexture.active;
    RenderTexture.active = midRenderTexture;

    var readableTexture = new Texture2D(width, height);
    readableTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
    readableTexture.Apply();
    RenderTexture.active = previous;
    RenderTexture.ReleaseTemporary(midRenderTexture);
    return readableTexture;
}
```


#### 3. できる限り高解像度なフォト画像が保存できるような`Texture2D`を用意
IDOLY PRIDEで獲得できるフォトを大別すると、
- ショップでの購入やギフト配布で入手できるフォト
- プレイヤーがゲーム内で撮影したフォト

に分けられます。

ショップでの購入やギフト付与で入手できるフォトの画像サイズが一定であるのに対し、
プレイヤーがゲーム内で撮影したフォトは、
撮影時の端末サイズ・ゲーム内の解像度設定によって異なるサイズで、
クラウドストレージ上に保存されています。
（撮影時の端末サイズが大きく、ゲーム内の解像度設定が高いほど、高解像度で保存されています。）

できるだけ高解像度の画像を端末に保存していただくために、
クラウドストレージに保存されている画像サイズと同じサイズの`Texture2D`を生成し、
同じサイズの画像を端末に保存できるようにしました。


#### 4. 保存ファイル名にはランダム生成された文字列を設定
保存の都度、ランダムなファイル名で画像を保存するようにしました。
GUIDとして、ランダムな文字列を生成し、それをファイル名として用いています。

```cs
var saveFileName = $"{Guid.NewGuid().ToString()}.png";
```

そうすることで、以下の恩恵があります。
- 同じ画像を複数回端末に保存できるようになる（プレイヤーメリット）
- ゲーム内で使われているフォトの画像名を特定できないようにする（プロジェクト都合）

## おわりに
今回は、IDOLY PRIDEに約1ヶ月前のアップデート（version 3.4.0）で実装された、フォト画像を端末に保存する機能について、実装上のポイントを解説しました。
今回の機能はネイティブ周りが絡んできたり、画像の編集を扱ったりということで、かなり試行錯誤をして最終的な実装を行いました。

全体として、プレイヤーの皆様からの反響も良く、リリースできて良かったです。
今後もより良いものが届けられるように、私自身も引き続き精進していきます。

ここまでお読みいただき、ありがとうございました。