この投稿で得られること
- 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/公開中のアプリ一覧/
コメント・ストック・LGTM大歓迎!
「自分も似た経験ある」「こうやって回避してるよ」などあれば気軽にコメントください!
ストックやLGTMも励みになります!