【Unity】【エディタ】WindowsとMacでパスを正しく取り扱う

Unity2021.2.7

はじめに

マルチプラットフォームでアプリを開発する際には、ファイルパスがよく問題を起こします。
その大きな要因として、WindowsMacディレクトリ区切り文字が異なる点が挙げられます。

UnityではWindowsMacで同じプロジェクトを共有することがよく行われます。
そのため自前のエディタ用スクリプトディレクトリ区切り文字を起因とした不具合が発生しがちです。

本記事ではこのような不具合を防ぐために、改めてUnityにおける正しいパスの使い方を考えます。
なお本文中に書いた通りUnityの仕様も.NETの仕様もここ数年で微妙に変わっていそうなので、
2021年現在における情報としてとらえていただければと思います。

プラットフォームとディレクトリ区切り文字

さてディレクトリを区切るための文字はプラットフォームにより異なります。

Macの場合には、ディレクトリ区切り文字はスラッシュ(/)で表されます。
これに対してWindowsの場合、ディレクトリ区切り文字はバックスラッシュ(\)で表されます。
ただしWindowsはスラッシュにも対応しており、スラッシュ区切りのパスでも読み込めます。

プラットフォーム スラッシュ バックスラッシュ
Windows
Mac ×

プラットフォーム依存問題

さてこのような事情から、ディレクトリ区切り文字はプラットフォーム依存の問題を起こしがちです。
例えば以下のテストはMacで成功しますがWindowsで失敗します。

[Test]
public void Test()
{
    // プラットフォームのデフォルトの区切り文字で区切られたフルパス
    var assetsFolderFullPath = Path.GetFullPath("Assets");

    var contains = assetsFolderFullPath.Contains("/");
    Assert.That(contains); // Mac: 成功 Windows: 失敗
}

これはWindowsではデフォルトの区切り文字がバックスラッシュであるためです。

この例に限らず、等価判定やIndexOfなど他のAPIでも同様の問題が起こります。
またこのように取得したパスを保存して他のプラットフォームで扱うときにも問題が起こります。

UnityのAPIとパス

問題解決のための第一歩は正しい理解ということで、まずUnityのAPIの挙動を正しく理解しています。

最初にAssetDatabaseとResourcesのマニュアルを見てみます。
どちらもディレクトリ区切り文字には常に(Windowsであっても)スラッシュを使うよう書かれています。

docs.unity3d.com

Resourcesに関してはバックスラッシュでは動かないと明確に書かれています。

docs.unity3d.com

この挙動を確かめるためにテストを書いてみます。

[Test]
public void AssetDatabase_Slash()
{
    var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Resources/Tests/Prefabs/FooPrefab.prefab");
    Assert.That(prefab.name, Is.EqualTo("FooPrefab"));
}

[Test]
public void AssetDatabase_Backslash()
{
    var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(@"Assets\Resources\Tests\Prefabs\FooPrefab.prefab");
    Assert.That(prefab.name, Is.EqualTo("FooPrefab"));
}

[Test]
public void AssetDatabase_Mixed()
{
    var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(@"Assets/Resources/Tests/Prefabs\FooPrefab.prefab");
    Assert.That(prefab.name, Is.EqualTo("FooPrefab"));
}

[Test]
public void Resources_Slash()
{
    var prefab = Resources.Load<GameObject>("Tests/Prefabs/FooPrefab");
    Assert.That(prefab.name, Is.EqualTo("FooPrefab"));
}

[Test]
public void Resources_Backslash()
{
    var prefab = Resources.Load<GameObject>(@"Tests\Prefabs\FooPrefab");
    Assert.That(prefab.name, Is.EqualTo("FooPrefab"));
}

[Test]
public void Resources_Mixed()
{
    var prefab = Resources.Load<GameObject>(@"Tests\Prefabs/FooPrefab");
    Assert.That(prefab.name, Is.EqualTo("FooPrefab"));
}

このテストを走らせてみるとわかりますが、実際にはWindowsでもMacでもすべてのケースが成功します。
Resourcesに関してはAndroidiOSの実機でも成功しました。

f:id:halya_11:20210415155937p:plain
すべて成功

つまり、マニュアルではスラッシュを使用するように書いてあるもののバックスラッシュでも読み込めるということになります。
昔のUnityはバックスラッシュは使えなかったような気がしますが、現時点での挙動はこの通りです。

クラス プラットフォーム スラッシュ バックスラッシュ
AssetDatabase Win △(読めるけど公式には×)
AssetDatabase Mac △(読めるけど公式には×)
Resources Win △(読めるけど公式には×)
Resources Mac △(読めるけど公式には×)
Resources iOS △(読めるけど公式には×)
Resources Android △(読めるけど公式には×)

.NETとパス

次に.NETにおけるパスの取り扱いについて考えます。

.NETではPathクラスでパスを取り扱うことができます。
このクラスはパスをそれぞれのプラットフォームに適した形で取り扱います。
したがって前述した例(以下に再掲)のように、得られる結果はプラットフォームにより異なります。

[Test]
public void Test()
{
    // プラットフォームのデフォルトの区切り文字で区切られたフルパス
    var assetsFolderFullPath = Path.GetFullPath("Assets");

    var contains = assetsFolderFullPath.Contains("/");
    Assert.That(contains); // Mac: 成功 Windows: 失敗
}

PathクラスのAPIの挙動は以下の記事のようにバージョンにより微妙に異なります。

light11.hatenadiary.com

いずれにせよPathクラスはプラットフォームに適した形でパスを取り扱うクラスであるといえます。

さてプラットフォームに適した形でパスを取り扱いたい場合はこれを使うとして、
どのプラットフォームでも同じ結果を得たい場合にはどうしたらいいのでしょうか。

以下の.NETのドキュメントにこのあたりの記述があります。

docs.microsoft.com

要点をまとめると以下の通りです。

  • マルチプラットフォームで開発する場合、ディレクトリ区切り文字をハードコーディングするならスラッシュを使うこと
    • MacでもWindowsでも認識される区切り文字だから
  • AltDirectorySeparatorCharはWindowsでもUnixでも同じ文字(スラッシュ)を返すのでこれを使って文字列連結してもOK

Path.Combine()とか使わずに文字列を連結するのが良いようです。

Unityでパスを正しく扱う

さてここまでの情報をまとめると以下のようになります。

API種別 プラットフォーム スラッシュ バックスラッシュ
Unity系 Win △(読めるけど公式には×)
Unity系 Mac △(読めるけど公式には×)
System系 Win
System系 Mac ×

Unityでパスを取り扱う際には以上のことに留意して正しく取り扱う必要があります。
その上で具体的にどうするかという話はそれぞれで良いと思います。いろんなやり方があります。

個人的には、以下のようなルールにするのがシンプルでわかりやすいかなと思います。

  • パスの区切り文字には常にスラッシュを使う
    • ハードコードあるいはPath.AltDirectorySeparatorChar
  • Path.Combine()とかプラットフォーム依存なAPIはできるだけ使わない
    • 全部スラッシュとして取り扱うユーティリティを用意するとか
  • Pathクラスとかを使う場合には必ずその結果のバックスラッシュをスラッシュに置換する

関連

light11.hatenadiary.com

参考

docs.unity3d.com

docs.unity3d.com

docs.microsoft.com