【Unity】ローカルデータの保存

はじめに

こんにちは、Unityエンジニアの清水です。
この記事はカヤックUnityアドベントカレンダー2016の18日目の記事になります。

今日はローカルデータの保存についてお送りします。

データの保存について

サーバの存在するゲームでは大抵のユーザデータはサーバ上に保存されると思います。
サーバにデータが置いてあればユーザが任意に書き換えられないのでチート対策になりますし、アプリをインストールしている端末を壊してしまったときに新しい端末とサーバのデータを紐付け直せば復帰できるかもしれません。
しかし、端末上には何もデータを保存しないかといえば、そんなことはありません。
例えば、サーバ上のデータと紐付けるためのIDのような値、通信や処理を減らすためのキャッシュデータ、書き変えられたり失われたりしてもあまり問題ない音量や表示の設定値といったデータは端末上に保存されるかもしれません。
そこで、この回では端末上にデータを保存するときにどのような選択肢があるのかと、それらを扱う際の注意点を書いていきます。
端末は主にiOSとAndroidを対象としています。

保存領域

まずは、よく使われる保存領域とその性質についてです。

PlayerPrefs

PlayerPrefsはUnityEngineが提供しているデータの保存方法で、もっとも手軽に使えます。
key-value形式で、int、float、stringを保存できます。
公式ドキュメント

使用するキーは、重複やタイポによるバグを防ぐためにも、定数化して1箇所にまとめて管理すると良いでしょう。
また、プロジェクトに組み込んだプラグインやSDKがPlayerPrefsを使っている場合、それらで定義しているキーと重複する可能性があるので、キーにはプロジェクト固有のprefixをつけると安全です。

public static class PlayerPrefsKey
{
    public const string bgmVolume = "kayac_bgm_volume";
    public const string seVolume = "kayac_se_volume";
}

PlayerPrefsの変更はアプリ終了時にディスクへ保存されますが、アプリがクラッシュすると保存されないため、変更後キリの良いタイミングで PlayerPrefs.Save() を実行するようにします。

using UnityEngine;

public class Sample
{
    public void SaveVolumes(float bgmVolume, float seVolume)
    {
        PlayerPrefs.SetFloat(PlayerPrefsKey.bgmVolume, bgmVolume);
        PlayerPrefs.SetFloat(PlayerPrefsKey.seVolume, seVolume);
        PlayerPrefs.Save();
    }
}

実際に保存される領域ですが、iOSはUserDefaults、AndroidはSharedPreferencesが使われているようです。
これらは1つのXMLファイルに書き込まれるため、PlayerPrefsに大量のデータや巨大なデータを入れるのは避けたほうが無難です。
ディスクからメモリに読み込んだりディスクに保存する際にPlayerPrefs全体を読み書きすることになるので、保存する内容が大きくなりすぎるとパフォーマンスの低下を引き起こすためです。
PlayerPrefsの使用用途としては、Prefsの名前の通り、音量設定などの単純な設定値の保存が適しています。
サイズが大きいデータは後述する方法で個別にファイルとして保存しましょう。

また、少し変わったPlayerPrefsの使い道としては、保存場所がUserDefaults、SharedPreferencesであることを利用して、ネイティブプラグインとの値のやり取りに使うこともできます。

データ用ディレクトリ

ファイルとしてデータを保存する場合、UnityEngine.Application.persistentDataPath で取得したディレクトリ以下にSystem.IOのStreamWriterやFileStreamなどを使って書き込みます。
保存データの読み込みにはStreamReaderやFileStreamなどを使います。
オブジェクトの内容をそのまま保存したい場合は、jsonやxml、BinaryFormatterなどでシリアライズして保存し、読み込み時にデシリアライズします。

iOSではサーバから再取得可能なキャッシュデータをここに置いておくとガイドライン違反でリジェクトされる可能性があるので、そのようなデータは後述のキャッシュ用ディレクトリに保存します。
そのため、AssetBundleのキャッシュシステムを自前で用意する場合などは、iOSの保存場所はキャッシュ用ディレクトリを使ったほうが無難でしょう。

また、ここに保存した内容はiCloudへバックアップされるので、あまり容量が大きくならないように注意します。
iCloudへのバックアップが必要無いなら、UnityEngine.iOS.Device.SetNoBackupFlag を使って除外することもできます。

キャッシュ用ディレクトリ

UnityEngine.Application.temporaryCachePath でキャッシュ用のディレクトリが取得できます。
ファイルの読み書き方法はデータ用のディレクトリと同じですが、以下のような違いがあります。

  • ディスク容量が足りない時はアプリ起動中であってもOSによって削除される
  • Androidは設定の「アプリ」の「キャッシュを消去」で削除される
  • iOSでiTunesやiCloudのバックアップ対象にならない

どのタイミングで削除されてもおかしくないので、その前提で使う必要があります。
特にAndroidでは容量確保のためにユーザ操作で頻繁に削除するケースが多いです。
そのためAndroidではiOSと逆で、AssetBundleの保存場所には使うべきではありません。

(2018.12.20追記) 最近のiOSはキャッシュ用ディレクトリがOSによって削除されやすくなっているようなので、AssetBundleの置き場所としてキャッシュ用ディレクトリは避けた方が良さそうです。

Keychain (iOS)

iOSではデータの保存先としてKeychainも候補になります。
UnityのAPIは用意されていないのでネイティブプラグインが必要にはなりますが、Keychainに保存した情報はアプリをアンインストールしても失われないという特徴があります。
これを利用して、ユーザを識別する値を保存しておくことで、アプリを再インストールした際にも端末とサーバ上のデータの紐付けを復活させるといったことができます。
Keychainに保存した内容は通常のバックアップでは他の端末に引き継がれませんが、暗号化ありのバックアップから復元した場合は引き継がれます。
保存時の設定によっては暗号化ありでも引き継げなくできます。

SQLite

上記の他にも、大量のデータを管理するような場合にローカルでSQLiteを使うという手がありますが、弊社では使っているプロジェクトが無かったので割愛します。

データの保存に関する注意点

暗号化

ローカルのディスクに保存したデータはユーザが見ようと思えば見ることができるし、編集もできます。
そのため、見られたくない、編集されたくないデータは暗号化して保存するべきです。
ただし、暗号化していれば絶対に大丈夫とも言えないため、重要なデータはできる限りサーバ上に保存しておくのが安全です。

SDカード (Android)

Androidではデータの保存先として内部ストレージと外部ストレージ(SDカード)の2種類があります。
PlayerSettingsのWrite AccessがInternal OnlyならApplication.persistentDataPathは常に内部ストレージを返しますが、Externalの場合はSDカードが刺さっているときはSDカード、刺さっていないときは内部ストレージを返します。
f:id:shimiu:20161216112000p:plain
外部ストレージにデータを保存するようにしていると、SDカードを抜いたときに保存データを取得できなくなるので注意が必要です。

例外処理

ディスク容量が足りないなどでデータの保存に失敗したり、読み込もうとしたデータが破損していて読み込みに失敗したりすることがあります。
そのため、ファイルの読み書き時の例外処理は必須になります。
特にディスク容量不足の場合は、ディスク容量を空けるように促すメッセージをユーザに対して表示すると良いでしょう。

また、ファイルの上書き保存中に処理が止まると、変更前後の両方のデータが失われることになるので対策が必要です。
上書き保存の際は一旦別のファイルとして保存して、保存完了後に元ファイルを新しいファイルで置き換えるといった方法をとると安全です。

バージョンアップ

できれば避けたいパターンですが、アプリのバージョンアップでローカルに保存しているデータの形式を変えるような場合は、既存データを考慮する必要があります。
例えば、シリアライズして保存するオブジェクトのフィールドを変更した場合に、データをデシリアライズできなくなってエラーが発生するかもしれません。
既存データは消して良いのか、それとも読み込んで新形式に変換する必要があるのか、検討して適切な処理を書きましょう。

おわりに

今日はデータの保存についてお送りしました。
明日はによる通信周りの話の予定となります。

【Unity】Audioの基本と扱いについて

今回はAudioの扱い方です。 3Dサウンドについては触れず、社内で扱う事の多い2Dサウンドを前提にして書いていきます。

こんにちは、ソーシャルゲーム事業部所属エンジニアのぴーちんです!!宜しくお願いしますヾ(@⌒ー⌒@)ノ

この記事はカヤックUnityアドベントカレンダー2016の17日目の記事になります。

Audioの扱いについて

Unityで音を鳴らす仕組みの基本

Unityの音は AudioClip, AudioSource, AudioListener, AudioMixer などのComponentによって鳴らされています。

AudioClipをAudioSourceが再生し、AudioListnerがそれを受信して、物理的なスピーカーなどを通じて私達へ音を届けてくれます。 この関係は公式マニュアルのAudioOverViewで詳しく説明されています。

また、AudioListnerが音を受信する前にAudioMixerを介する事で、音にエフェクトをかけたり音量を調整したりする事が可能です。 この関係は公式マニュアルのAudioMixerOverviewで詳しく説明されています。

AudioMixerの他にも、AudioSourceやAudioListnerが付いてるGameObjectに AudioFIlter 関連のComponentを付けて音を調整する方法もあります。

Unityで扱える音声フォーマットの種類について

Unityでは以下のフォーマットがサポートされています。 理由は後述しますが、 社内では基本的に .wav などの非圧縮フォーマットを扱う場合が多いです。

https://docs.unity3d.com/ja/current/Manual/AudioFiles.html

AudioFileとAudioFormatの関係

UnityにimpotしたAudioFile( .mp3 , .wav など)は、そのままゲームに使われるわけではありません。 UnityへimportしたAudioFileは AudioClip としてUnityに取り込まれ、ゲーム内で扱われるサウンドはAudioClipのInspectorで設定されたものが再生される事になります。

f:id:pchin:20161216231112p:plain

なので、元のオーディオファイルがそのまま再生されるわけでなく、インポート時に指定形式に変換されたサウンドがUnity内で扱われる事になります。

この仕組みについては10日目の『Texture File と Texture Formatの関係』でも触れられていますので、詳細は割愛します。

音声ファイルの品質について

.ogg.mp3 などの非可逆圧縮されるフォーマットをUnityへimportする際の注意点があります。 現時点でUnityはデフォルトで非可逆圧縮フォーマットの Vorbis 形式(.ogg) に設定されてしまいます。 非可逆圧縮形式の音声を再圧縮してしまう場合、再生時の劣化が多くなってしまいます。

どうやらデフォルトの圧縮設定はUnity5から変更された様です。 詳しくは公式マニュアルを参照してください。

一方で .wavaiff などの無圧縮フォーマットをUnityへimportする際には、上記の様な心配事はありません。 AudioClipのInspectorの設定次第で、生データのまま扱う事も非可逆圧縮形式に設定する事も可能です。

基本的に社内のモバイルゲーム開発ではBGM/SE両方で .wav を使用する場合が多いです。 理由としては、Unityへimport後に圧縮設定が行えるので、わざわざ圧縮されたファイルをUnityへimportする必要が無いからです。

AssetPostprocessor

先程はAudioClipのInspectorから、圧縮の設定や読み込み方法を設定可能な事について触れました。 基本的にチームで開発する際にAudioClipの設定は、設定漏れやミスをを避ける為に自動で処理します。 UnityにはAssetの変更時などイベントをHookする機能が提供されています。

AudioのimportをHookするには OnPostprocessAudioを使います。

設定例

using UnityEditor;
using UnityEngine;

public class AssetPostprocessAudio : AssetPostprocessor
{
    private void OnPreprocessAudio()
    {
        var importer = assetImporter as AudioImporter;

        importer.forceToMono = false;
        importer.loadInBackground = false;
        importer.preloadAudioData = true;

        var settings = new AudioImporterSampleSettings()
        {
            compressionFormat = AudioCompressionFormat.MP3, // mp3に圧縮します
            loadType = AudioClipLoadType.CompressedInMemory, // 圧縮された状態でメモリに保持されます
            quality = 0.15f, // 品質の設定
            sampleRateOverride = 44100, // AudioClipのsamplingRateを44,1kHzに設定 ※1
        };
        importer.SetOverrideSampleSettings("iOS", settings);
        importer.SetOverrideSampleSettings("Android", settings);
    }
}

AudioClipのsamplingRateを44,1kHzに設定 ※1

AudioManagerから音出力時のSamplingRateを指定出来るので、もしUnityへimportした音データのSamplingRateがそこで設定された値以上の場合には無駄になるので、import時に調整してあげましょう。

AudioManagerから音出力時のSamplingRateを指定出来る

先ほど紹介したのはAudioClipのSamplingRateについてですが、音を出力する際のSamplingRateをグローバルに設定する事が可能です。 それらを以下で紹介します。

AudioManager

UnityEditorのEdit->ProjectSettings->Audioから開ける設定画面です。

ここでのSamplingRateの設定が0の場合に、iOS/Androidだとデフォルトで出力時のSamplingRateが24kHzになってしまいます。 困る場合にはゲームに適したSamplingRateを設定しておきましょう。 社内ではプロジェクトによりますが、44,1kHzに設定しておく事が多いです。

このTipsはUnityの公式ブログにて紹介されています。

その他の詳しい情報は公式マニュアルをご覧ください

AudioSettings

Unity5から AudioSettingsというランタイムのみに適用される、グローバルな設定を管理可能なAPIが提供されています。

使うケースですが、ゲームの設定画面にて AudioConfiguration の設定項目をユーザーに設定させて、その場で設定を反映する。

具体的なコードを交えた使用例はAudioSettings.ResetのScriptReferenceに例が記載されてるので、割愛します。

SEとBGMの音の鳴らし方の違い

ゲームでは基本的にSE/BGMの2種類の音を扱います。 この2種類の音を再生する場合の違いを紹介します。

BGM

BGMは基本的に『途切れずに鳴らせる』設計にするべきです。 なので、BGMを鳴らす専用のAudioSourceを最低でも2つ用意します。 理由としては、ゲームの特性によりますが、多くのゲームでBGMをクロスフェードさせる場合が多いです。

コード例としては、以下の様な感じになります。 最近はDOTweenだけで簡単なCrossFade処理なら書ける様です、便利ですね。

ちなみに DOTweenについては『【Unity】Tween アニメーション(DOTween)の話』で紹介されていますので、合わせてお読み下さい。

using UnityEngine;
using DG.Tweening;

public class BGMCrossFadeSample : MonoBehaviour
{
    // BGM再生用のAudioSourceを2つ用意
    [SerializeField]
    private AudioSource _source0;

    [SerializeField]
    private AudioSource _source1;


    public void CrossFade(float maxVolume, float fadingTime)
    {
        var fadeInSource =
            _source0.isPlaying ?
            _source1 :
            _source0;

        var fadeOutSource =
            _source0.isPlaying ?
            _source0 :
            _source1;

        fadeInSource.Play();
        fadeInSource.DOKill();
        fadeInSource.DOFade(maxVolume, fadingTime);

        fadeOutSource.DOKill();
        fadeOutSource.DOFade(0, fadingTime);
    }

}

SE

SEはBGMと違い、継続して鳴らさなくて良いです。 しかしSEは『同時再生可能』な様に設計しなければいけません。 それはゲームの特性によって変わるので固定でAudioSourceを用意するも良いですし、可変にAudioSourceのinstanceを増やす仕組みを作るという選択肢もあります。

コード例は少し大変だったので割愛します。インターネットで検索すると書いてる人が多いので参考にして下さい。

AudioMixer

Unity5から音をMixingする為のEditorが公式でサポートされました。 サウンドのミキシング、エフェクト、マスタリングなどの機能があります。

詳しくはUnityのマニュアル動画のチュートリアルを見ると良いでしょう。

(ちなみにこの記事の時点では、まだ弊社では本格的に使った事例はありません)

調整した設定は .mixer ファイルとして保存できるので、サウンドの調整をディレクターなどと分業する事がしやすくなります。 .mixer ファイルにはゲーム内で扱うグローバルな設定をするのが良さそうです。 特別な設定が必要な時はScriptなどで済ますのが良さそうです。

使用例

  • ストーリー画面でBGMとキャラボイスが混在する場合、ボイスの音が発生されたタイミングではBGMの音量を下げたい時(Ducking)
  • 洞窟にキャラクターが歩いてる時に音が響く様にしたい時(Reverb)

おわりに

明日はローカルデータの保存についての記事になります。 担当はしみーさんになります。 お楽しみに。