はじめに
こんにちはカピです!
普段はゲーム開発を主に開発しています。
現在、Unityで2D横スクロールアクション『RESONANT SOUL』を個人開発しています。
企画段階で、プレイヤーキャラクターは『剣聖』『魔導士』『守護者』など、複数の「器(ヴェッセル)」を実装する予定でした。
しかし、私には人型キャラクターのドット絵を描くのが壊滅的に苦手という大きな課題がありました。
そこで、以前アセットストアで購入していた「2D Pixel Unit Maker - SPUM」というアセットを使い、この問題を解決することにしました。
作成したキャラクター

作成自体はシンプルで、簡単にキャラクターが作成できました!
ただ、SPUMはパーツを組み合わせてドット絵キャラを作れる素晴らしいアセットですが、独自のスクリプト(VContainerを使ったDI設計)と連携させる際に、いくつかの大きな「壁」にぶつかりました。
この記事は、SPUMと自作のプレイヤースクリプトを連携させるために乗り越えた、3つの不具合について話します。
この記事が役立つかもしれない人:
- SPUMを導入したけど、スクリプトからAnimatorが制御できずに困っている人
-
GetComponentInChildren<Animator>()が期待通りに動かない人 - VContainer(DI)とSPUMのような複雑なプレハブを連携させたい人
開発環境
- エンジン: Unity 6.2
- IDE: Rider
- DIコンテナ: VContainer
- 入力: Unity Input System
- アセット: 2D Pixel Unit Maker - SPUM
不具合①: クラス名の競合
SPUMをPackage Managerからインポートした直後、まず発生したのがクラス名の競合エラーでした。
//エラー:'PlayerState' という名前が重複しています。
SPUMアセットも内部で PlayerState というクラスを使用しており、私がプレイヤーの状態管理のために作成した PlayerState.cs と名前が衝突していました。
解決策としては、単純にリネームです。
これは単純な問題でした。
私たち自身のクラス名を、プロジェクトの企画書に基づいた名前に変更することで即座に解決しましたね。もちろん、このクラスを参照していたスクリプトも、すべて VesselState を参照するように修正しました。
-
PlayerState.cs→VesselState.cs
// 型の名前を PlayerState から VesselState に変更
public class VesselState
{
private readonly Rigidbody2D _rb;
public Vector2 Position => _rb.position;
public float FacingDirection { get; set; } = 1f;
[Inject]
public VesselState(Rigidbody2D rb) // コンストラクタも変更
{
_rb = rb;
}
}
解決に至るまでの試行錯誤
リネームという選択肢を取る前に、
Assembly Definitionで依存関係の最適化させるという方法で改善を図ろうとしていました。
しかし、Assembly Definition自体今回の不具合解消の一環で存在を知ったレベルの認知だったので上手くいかず、エラーがまた新たなエラーを呼ぶという事態に陥ってしまいました。特にR3やDIのような今回の開発で新たに取り入れた技術たちが、知識不足もあり、うまく設定できていなかった気がします。
結果、よりシンプルなリネームを行うという方法に切り替えました。Assembly Definitionは今回はうまく扱えなかったものの、重要技術であることは間違いないので、いつかの機会に改めて学びたいと思います。
不具合②: Animatorがまったく動かない問題
次に、SPUMで作成したプレハブをシーンに配置し、Animator をスクリプトから制御しようとしました。
今回の設計はVContainerを使っているため、最初はGameLifetimeScope.cs でプレイヤーの Animator をDIコンテナに登録し、PlayerMovement.cs がそれを [Inject] 経由で受け取る、という形を取っていました。
// シーンからPlayerBodyを探し...
var playerBody = FindObjectOfType<PlayerBody>();
// そのオブジェクトに付いているAnimatorを自動で取得して登録しようとした
builder.RegisterInstance(playerBody.GetComponent<Animator>());
// DIでAnimatorを受け取る
public PlayerMovement(..., Animator animator)
{
_animator = animator;
}
// Updateのタイミングでパラメータをセット
public void Tick()
{
// ...
_animator.SetBool("IsRunning", true); // 命令を送っているはず...
}
しかし、この設計では予定していた動作をしてくれませんでした…
不具合の詳細は以下の通りです。
- エラーは出ない
- スクリプトは動いており、
Debug.Logで_animatorがnullでないことを確認 - UnityのAnimatorウィンドウは、以下のように正常に動作している
- Animatorウィンドウ上は正常だが、
GameScene上ではIDLEアニメーションが再生され続ける - アニメーションを繋ぐ矢印を選択し、
InspectorウィンドウでアニメーションのPreviewを再生しても再生されず常に静止
エラーがないこともあり、特定にかなり時間を使いました…
そこで友人に相談をしてみると、解決の糸口が見えてきました。
最終的な解決策としては、Animatorの「すれ違い」と「手動アタッチ」でした。
アドバイスから、SPUMのプレハブ構造が問題であることに気づきました。予想より単純なミスで、SPUMのプレハブは、Player(親)オブジェクトの下に、Unitroot という子オブジェクトがあり、「本命」のAnimatorは Unitroot にアタッチされていました。
FindObjectOfType<PlayerBody>().GetComponent<Animator>() というコードでは、Player オブジェクト(親)に(もしあれば)アタッチされている別の(関係ない)Animatorを掴んでしまっていたのです。 まさにスクリプトとAnimatorの「すれ違い」でした。
そこで、自動取得(GetComponent)を諦め、Inspectorから「本命」のAnimatorを「手動で」設定する方法に切り替えました。
変更点
➀ PlayerBody.cs を 「入れ物」に変更
DIコンテナに登録したいコンポーネントを、Inspectorからアタッチするための「入れ物」スクリプトに変更しました。
using UnityEngine;
// Rigidbody2D と Animator への参照を「手動で」保持するための「目印」クラス
public class PlayerBody : MonoBehaviour
{
// Inspectorから手動で設定するスロット
[SerializeField]
private Rigidbody2D _rigidbody2D;
[SerializeField]
private Animator _animator; // ここにUnitrootをアタッチする
// DIコンテナが、このプロパティを通じて参照を取得できるようにする
public Rigidbody2D Rigidbody2D => _rigidbody2D;
public Animator Animator => _animator;
}
➁ GameLifetimeScope.cs で「手動アタッチ」されたものを登録
GameLifetimeScope は、PlayerBody のプロパティから、手動でセットされたコンポーネントを取得して登録するように変更しました。
...
// --- 2. プレイヤーのコンポーネント登録 ---
var playerBody = FindObjectOfType<PlayerBody>();
if (playerBody != null)
{
// PlayerBodyのプロパティから「手動設定された」コンポーネントを取得
var playerRb = playerBody.Rigidbody2D;
var playerAnimator = playerBody.Animator; // 手動設定されたAnimator
if (playerRb != null && playerAnimator != null)
{
// 手動設定された Rigidbody2D と Animator を登録
builder.RegisterInstance(playerRb);
builder.RegisterInstance(playerAnimator);
}
...
➂ Unity Editorで設定
Player オブジェクトの Player Body (Script) コンポーネントに、Rigidbody 2D と、子オブジェクトである Unitroot(Animatorが付いているオブジェクト)をドラッグ&ドロップしました。
これで、スクリプトは確実に「本命」のAnimatorに命令を送れるようになり、アニメーションが正常に再生されました!
不具合③: キャラクターが反転しない
前述の修正でアニメーションは動きましたが、最後の問題が残りました。
キャラクターが進行方向に応じて左右に反転しません。
以前のカプセル(仮置き)では、SpriteRenderer.flipX = true で反転させていました。しかし、SPUMの Unitroot には SpriteRenderer がアタッチされていません(Body や Hair などの子オブジェクトが個別に SpriteRenderer を持っているため)。
解決策として、Unitroot の localScale を変更してみました。
SPUMのようにパーツが分かれているオブジェクトを反転させる正しい方法は、すべてのパーツの親である「視覚的なルート」オブジェクト(=Unitroot)の transform.localScale.x を 1 や -1 に変更することでした。
PlayerMovement.cs が、DIで受け取った Animator(=Unitroot にアタッチされている)の transform を取得し、localScale を変更するように修正しました。
public class PlayerMovement : ITickable, IFixedTickable
{
// ... (DIフィールド) ...
private readonly Transform _visualRoot; // フリップ(反転)させる対象('Unitroot'のTransform)
[Inject]
public PlayerMovement(
Rigidbody2D rb,
PlayerInput input,
VesselState playerState,
Animator animator // SpriteRendererのDIは不要になった
)
{
// ... (他のDI設定) ...
_animator = animator;
// 手動でアタッチした「本命」のAnimatorが乗っているTransformこそが 'Unitroot'
_visualRoot = animator.transform;
}
private void Move()
{
// ... (速度設定) ...
// --- 左右反転ロジック (localScale を使う) ---
// ※SPUMのプレハブがデフォルトで左向き(-1)だったので、値を逆にしています
if (_currentMoveInputX > 0.01f) // 右入力
{
_playerState.FacingDirection = -1f; // SPUMのデフォルトが左向きのため
_visualRoot.localScale = new Vector3(-1f, 1f, 1f); // 右向き(SPUMでは-1)
}
else if (_currentMoveInputX < -0.01f) // 左入力
{
_playerState.FacingDirection = 1f; // SPUMのデフォルトが左向きのため
_visualRoot.localScale = new Vector3(1f, 1f, 1f); // 左向き(SPUMでは1)
}
}
// ...
}
この修正により、無事にプレイヤーが進行方向に応じて反転してくれました!!
結果
不具合の修正により、自分の想定していた動作が実現しました!!
長かった…!

まとめ
今回取り扱ったSPUMは素晴らしいアセットですが、その複雑なプレハブ構造故に、GetComponent系の自動取得とは相性が悪いことが分かりました。
VContainer を使ったDI設計において、PlayerBody.cs のような「手動アタッチ用の入れ物」スクリプトを用意し、そこに「本命」のコンポーネント(AnimatorやRigidbody2D)をInspectorからドラッグ&ドロップで設定し、それを GameLifetimeScope でDIコンテナに登録する、という方法が非常に有効でした。
また、今回の実装では、複雑な不具合だと思っていたけど、ふたを開けたら意外とシンプルな解決策となることが多かったので、もっと全体をシンプルに考えられるようになりたいですね。
次は敵キャラクターの実装に取り組み、今回作成したキャラクターとバトルできる状態を目指します。今後も個人開発関連で記事を作成する機会もあると思うので、その時はまた読んでいただけたら幸いです。
それではまた👋