😅

UnityのScriptableObjectで発生したバグとその解決方法

に公開

この投稿で得られること

  • ScriptableObjectを使う際の意外な落とし穴
  • 「Healのはずが弾が出る」ようなバグの本質
  • Unityで状態管理する際の安全な設計パターン

開発背景

敵キャラの行動管理に ScriptableObject(以下SO)を使っていました。
EnemyData というSOにこんな感じのパラメータを持たせています:

[CreateAssetMenu(fileName = "EnemyData", menuName = "Game/EnemyData")]
public class EnemyData : ScriptableObject
{
    public string enemyName = "Enemy";
    public int maxHP = 100;
    public float attackInterval = 10f;
    public int attackPower = 10;
    public int defense = 5;

    // 実はここが落とし穴
    public EnemyActionType actionType = EnemyActionType.Attack;
}

発生した問題

敵Aが Heal を選んだ

敵Bが Attack を選んだ

→ なぜか 敵Aまで弾を撃ち出した!

見た目は Heal アイコンなのに、弾が出てプレイヤーを攻撃。完全にバグってるように見えました。

原因:SOは「共有される」インスタンスだった

SOは Unity において、アセットベースで共有される参照型です。
そのため、複数の敵が同じSOを参照していると、状態を上書きし合ってしまいます。

enemyA.stats.actionType = Heal;
enemyB.stats.actionType = Attack;
// enemyA も attack 扱いされる(共有されてるため)

解決策:SOを複製して使う!

[SerializeField] private EnemyData statsTemplate; // SOアセット
private EnemyData stats; // 実行時インスタンス

void Start()
{
    stats = Instantiate(statsTemplate); // ランタイムで複製
}

これにより、各敵が個別の stats を持てるようになり、バグは完全解消しました。

ScriptableObjectに持たせてはいけないもの

OK(共有すべき情報)   名前、HP上限、攻撃力など
NG(実行中に変わる情報) 現在HP、状態、行動タイプなど

教訓とベストプラクティス

SOは「読み取り専用設定データ」と割り切る

状態(行動・HP・ターゲット)は必ずインスタンス側で保持

複製には Instantiate() を使う

UnityのSOは便利だけど「安全ではない」こともある

再現チェック用ログ例(抜粋)

[Decision] Drone decided to Buff
[Execute] Drone BUFFS Player

→ 弾が飛ぶ(!?)

[Decision] Drone decided to Attack
[Execute] Drone ATTACKS EnemyController

→ 本来の攻撃ログが混ざってる

まとめ

ScriptableObjectは便利だけど危険な共有変数

状態を扱うには、実行時に複製してインスタンス管理するのがベスト

「バフのはずが攻撃してる…」など直感に反する挙動にはSOの共有性を疑え!

ブログでもUnityや個人開発ネタを発信中!

開発ノウハウやアプリ制作過程、Unity連携のハマりポイントなど
より深掘りした内容をブログでまとめています。
https://syunpp.com

公開中のアプリ一覧はこちら!

Unityで開発・公開済みのアプリ一覧です!
https://syunpp.com/公開中のアプリ一覧/

コメント・ストック大歓迎!

「自分も似たようなバグに遭遇した」
「こんな設計にしてるよ」など、気軽にコメント・ストックしてもらえると励みになります!

Discussion