【Unity】Resourcesの使い方と落とし穴【非推奨】

に公開
使い方と落とし穴シリーズ一覧

非推奨な理由

This chapter discusses the Resources system.
(中略)
3.1. Best Practices for the Resources System
Don't use it.
Unity - Assets, Resources and AssetBundles

  • このように公式から、Resources は使わないのが一番よいと明言されている
  • 理由は
    • 細かいメモリ管理が難しい
    • アプリの起動時間, ビルド時間が増加する
    • プラットフォームごとのリソース選択ができない

非推奨でもこの記事を書く理由

  • 個人的に使う機会がなんやかんやありそうなため
  • 例:unityroom は直接 Addressables に対応していない (2025/06/04時点)
    • 外部サーバーを介したリソース読込など、工夫次第では使用可能だが…
    • unity1week の場合、そこでリソースを割く(Resourcesだけに)のは避けたい

Resourcesを使用するケース

  • 簡単にリソース読込機能を実装したい場合
    • AssetBundle, Addressables よりも手軽

Resourcesの使い方

全体の流れ

  • 以降、各工程に対応した使い方を解説する

Resources フォルダ格納

  • Assets/Resources 以下にリソースを配置する
    • Resources フォルダが無い場合、同名フォルダを自分で作成すればok
  • サブフォルダは特に制限ナシ

リソース読込、利用

  • ファイルパスを指定して読込を行う
    • パスに Assets/Resources を含める必要はない

🔧 Load

string assetPath = "Prefabs/Enemy/Slime";
var    slimeObj  = Resources.Load<GameObject>(assetPath);

🔧 LoadAll

  • 指定フォルダ下のアセットを全て読み込む
  • アセットファイルを指定した場合、アセット単体を配列で返す
string folderPath = "Prefabs";
GameObject[] prefabs  = Resources.LoadAll<GameObject>(folderPath);

🔧 LoadAsync

  • 非同期で読み込む
  • サイズが大きく、処理落ちが起きてしまうようなリソースに有効
string assetPath = "Sounds/TitleBGM";

//UniTask 版 (async, await)
var request = Resources.LoadAsync<AudioClip>(assetPath);
await request;                                 //完了を待機
AudioClip bgm = request.asset as AudioClip;    //取り出し
AudioSource.PlayClipAtPoint(bgm, Vector3.zero);

//Coroutine 版
IEnumerator LoadBgmAsync()
{
    var req = Resources.LoadAsync<AudioClip>(assetPath);
    yield return req;
    var clip = req.asset as AudioClip;
    audioSource.clip = clip;
    audioSource.Play();
}
  • 進捗は request.progress (0〜1) で取得できるのでロードバーに利用可

不要リソースの解放

🔧 UnloadAsset

  • 解放できるのはアセット本体のみ
  • Instantiate で生成したインスタンスを渡しても何も起きない
  • 同アセットを複数回読み込んでいた場合、最後の参照が切れたタイミングで解放される
//読み込んだ Prefab を明示的に解放
var slimePrefab = Resources.Load<GameObject>("Prefabs/Enemy/Slime");

Resources.UnloadAsset(slimePrefab);   //Prefab 自体のメモリを解放
slimePrefab = null;

🔧 UnloadUnusedAssets

  • 参照が0になったアセットをまとめて解放する
  • シーン遷移などで有効
await Resources.UnloadUnusedAssets(); //UniTask 版

yield return Resources.UnloadUnusedAssets(); //Coroutine 版

その他

🔧 FindObjectsOfTypeAll

  • シーン内だけでなく、プロジェクトにロード済みの全オブジェクトを取得
    • 非アクティブ, DontDestroyOnLoad など
  • 読込や解放の状況を確認するのに有効
#if UNITY_EDITOR
//例1:現在メモリに乗っている Texture2D を全部列挙
Texture2D[] loadedTex = Resources.FindObjectsOfTypeAll<Texture2D>();
Debug.Log($"Loaded Texture2D : {loadedTex.Length} 枚");

//例2:シーンに存在しない不要 GameObject を洗い出す
var allObjs = Resources.FindObjectsOfTypeAll<GameObject>();
foreach (var go in allObjs)
{
    // Inspector で選択できるよう ping して確認
    if (!EditorUtility.IsPersistent(go) &&
        go.hideFlags == HideFlags.None)
    {
        Debug.Log($"シーン外でロード中の GO → {go.name}", go);
    }
}
#endif

オイラはこんな落とし穴に出会った

ビルドサイズ肥大化

  • Assets/Resources フォルダ下のアセットは使用の有無に関わらずビルドに含まれる

回避策

  • Resources を使わない
  • 未使用アセットを自動消去してくれるツールの導入もアリ

パスのタイプミス

  • 手入力ではミスが多い & 修正が面倒
  • 同様にパス(アドレス)を指定する Addressable も同様
string path  = "Prefab/Enemy/Slime"; //実際は Prefabs/Enemy/Slime
var slimeObj = Resources.Load<GameObject>(path); //失敗

回避策

  • 定数管理、列挙型 + 拡張メソッド の活用

定数管理

public static class ResourcePath
{
    public const string EnemySlime  = "Prefabs/Enemy/Slime";
    public const string EnemyDragon = "Prefabs/Enemy/Dragon";
    public const string TitleBgm    = "Sounds/TitleBGM";
}

var slime = Resources.Load<GameObject>(ResourcePath.EnemySlime);

列挙型 + 拡張メソッド

public enum PrefabPath
{
    EnemySlime,
    EnemyDragon
}
public static class PrefabPathExt
{
    //Enum → 文字列パス変換
    private static readonly Dictionary<PrefabPath, string> map = new()
    {
        { PrefabPath.EnemySlime,  "Prefabs/Enemy/Slime"  },
        { PrefabPath.EnemyDragon, "Prefabs/Enemy/Dragon" }
    };
    public static string Path(this PrefabPath p) => map[p];
}

var dragon = Resources.Load<GameObject>(PrefabPath.EnemyDragon.Path());

参考

Discussion