抽象クラスからの継承を使ったUnityでの移動方法の学習

2021年12月7日

unityでの移動(瞬間移動)には、2種類があります。今回は、抽象クラスも使ってこれを切り替えて実行するプログラムに挑戦してみましょう
瞬間移動とは、例えば、将棋の駒などボードゲームでの一般的な移動を指します。
対照的に力を加えての移動(物理移動)は、ボールを押して移動させることや重力で移動(落ちる)ことを指します。
今回は、瞬間移動について考えていますが、チャレンジとして物理移動のテストも作ってみられるといいでしょう。

チュートリアル

Unity バージョン 2021.2.5f1

シーンの作成

1つのCubeオブジェクトを作成して、それが移動することを確認することで動作検証します。
学習用として、なるべく単純な構成を考えることが大切です。

作成手順
  • Cubeオブジェクトを追加
  • 各スクリプトの作成
  • CubeオベジェクトへPlayerスクリプトのアタッチ

コード

プレイヤークラス

Cubeオブジェクトにアタッチされて、キー入力から実行される部分を受け持ちます。

全体のコード
using UnityEngine;

public class Player : MonoBehaviour
{
    enum Pattern
    {
        PositionMove,
        TranslateMove,
    }

    [SerializeField]
    Pattern pattern;

    Movable movePattern;

    void Start()
    {
        switch (pattern)
        {
            case Pattern.PositionMove:
                movePattern = new PositionMove(gameObject);
                break;
            case Pattern.TranslateMove:
                movePattern = new TranslateMove(gameObject);
                break;
            default:
                break;
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            movePattern.Move(new Vector3(1, 0, 0));
        }
    }
}
列挙型の部分
enum Pattern
{
    PositionMove,
    TranslateMove,
}

[SerializeField]
Pattern pattern;

列挙型Patternを宣言しています。2つの値を持っています。
[SerializeField]属性を使ってpattern値を宣言すると、インスペクターに値を表示することができます。
列挙型の場合、Unityの機能として、プルダウンで選択できるようになります。

選択の部分
Movable movePattern;

switch (pattern)
{
    case Pattern.PositionMove:
        movePattern = new PositionMove(gameObject);
        break;
    case Pattern.TranslateMove:
        movePattern = new TranslateMove(gameObject);
        break;
    default:
        break;
}

patternの値によってどのように振る舞うか選択します。
case Pattern.PositionMove:のコード行は、「patternの値がPositionMoveの場合は?」と考えます。
movePattern = new PositionMove(gameObject);で、基本クラスであるMovableクラスで宣言されたmovePatternフィールドに派生クラスを代入しています。
これは、ポリモーフィズムの機能を使っているのですが、これで、強制実装されているMoveメソッドの実行が約束されることになります。

実行部分
if (Input.GetKeyDown(KeyCode.Space))
{
    movePattern.Move(new Vector3(1, 0, 0));
}

スペースキーが押されたら実行される部分になります。
movePatternは基本クラスの型(Movable)なので、Moveメソッドの実装が約束されています。
movePatternには、PositionMoveクラスのインスタンスかTranslateMoveクラスのインスタンスのどちらかが代入されています。

基本クラス(抽象クラスで作成)

動くことができるものはこのクラスを継承するようにします。

全体のコード
using UnityEngine;

public abstract class Movable
{
    protected GameObject obj;

    public Movable(GameObject obj)
    {
        this.obj = obj;
        Debug.Log("基本クラスのコンストラクタが実行された");
    }

    public abstract void Move(Vector3 vector);
}
派生クラスで使われるフィールドの宣言
protected GameObject obj;

protected修飾子を使って、派生クラスのみ使えるフィールドとします。
値は次のコンストラクタで代入されます。

コンストラクタの処理
protected GameObject obj;

public Movable(GameObject obj)
{
    this.obj = obj;
    Debug.Log("基本クラスのコンストラクタが実行された");
}

コンストラクタが呼ばれると、フィールドobjに引数の値が代入される部分になります。
テストのため、コンソールに表示されるようにしてあります。

抽象メソッドの追加
public abstract void Move(Vector3 vector);

このコード行では、abstract修飾子が追加されています。
メソッドの形をしていますが、;で終わりになり本体 {…} の部分がありません。
派生クラス側で必ず実装するよう指示します。ここでは、実装しません。

派生クラス(transform.position で直接座標を代入するパターン)

このクラスは、Unity のStart()メソッドを使わないので、MonoBehaviourを継承する必要はありません。
コンポーネントとしてもアタッチすることはできません。(必要がありません)

全体のコード
using UnityEngine;

public class PositionMove : Movable
{
    public PositionMove(GameObject obj) : base(obj)
    {
        Debug.Log("派生クラスのコンストラクタが実行された");
    }

    public override void Move(Vector3 vector)
    {
        obj.transform.position += vector;
        Debug.Log("PositonMove");
    }
}
コンストラクタの処理
public PositionMove(GameObject obj) : base(obj)
{
    Debug.Log("派生クラスのコンストラクタが実行された");
}

引数にゲームオブジェクトを取ります。
渡された引数は、このクラスでは使われず、base(obj)の部分で、基本クラスのコンストラクタに引き渡されます。
基本クラスのコンストラクタが派生クラスの先んじて実行されます。
コンソールの表示で確認しましょう。

移動処理
public override void Move(Vector3 vector)
{
    obj.transform.position += vector;
    Debug.Log("PositonMove");
}

引数の座標分を現在の位置に足します。
このクラスは、Movableクラスを継承しており、Moveメソッドの実装が強制されています。
なので、このメソッドの実装は必須になります。(ないとエラーになり実行できません)

派生クラス(transform.Translateで現在位置からの移動量分移動するパターン)

transform.Translate()メソッドで現在位置からの移動量を引数で渡しています。
このクラスも、MonoBehaviourを継承していないため、コンポーネントとしてアタッチすることはできません。
コンポーネントとしてもアタッチすることはできません。(必要がありません)

コンストラクタと、Moveメソッドの処理は上記と同じため、省略します。

using UnityEngine;

internal class TranslateMove : Movable
{
    public TranslateMove(GameObject obj) : base(obj)
    {
        Debug.Log("派生クラスのコンストラクタが実行された");
    }

    public override void Move(Vector3 vector)
    {
        obj.transform.Translate(vector);
        Debug.Log("TranslateMove");
    }
}

実行

スペースキーを押すたびに、Cubeはx軸方向に1ずつ進みます。
インスペクターのPlayerスクリプトでPatternの項目はプルダウンメニューで動きの方法を選択することができます。
選択は、Start()メソッドで判断されるため、再度実行する必要があります。
再度実行せずに動作するようにするにはどうするか考えてみてもいいでしょう。

UMLクラス図

Playerから直接PositionMoveクラスやTranslateMoveクラスを呼び出すのではなく、Movable抽象クラスへの参照からアクセスできるようにしています。
これによって、依存性を低くする効果があります。

  • PlayerはMonoBehaviourへ依存しています
  • PlayerはMovableを利用しています
  • PositionMoveはMovableへ依存しています
  • TranslateMoveは、Movableへ依存しています

今後、他の手段の移動に関するクラスを実装する場合でも、プログラム更新の影響を低く抑えることができます。
Playerが、実装部分に依存しない(

高度な利用

さらに依存性を下げるには、PlayerクラスでPositionMoveクラスやTranslateMoveクラスが見えないようにするほうがいいです。
ただし、コードの構成が複雑化しますからトレードオフ(適用することによって、余計に使いづらくならないか)を考えましょう。

依存性を下げる方法として、インスタンスの作成を他のクラスに任せることです。
今回の場合、Factoryクラスにその処理を任せています。Playerクラスは出来上がった(new でインスタンスを作成された)インスタンスを受け取って利用しています。
いよいよ、Playerクラスからは、実装クラス(PositionMoveとTranslateMove)が見えなくなっています。
このように作り方をファクトリーパターンといいます。

Playerクラス(C#9以降のnew()式を利用しています)

using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField]
    Pattern pattern;

    Movable movePattern;

    void Start()
    {
        Factory factory = new();
        movePattern = factory.Create(pattern, gameObject);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            movePattern.Move(new Vector3(1, 0, 0));
        }
    }
}

Pattern列挙型(Patternスクリプトに記述しています)

public enum Pattern
{
    PositionMove,
    TranslateMove,
}

Factoryクラス(実際にインスタンスを作ってPlayerクラスに返しています。C#9以降のswitch式を利用しています)

using UnityEngine;

public class Factory
{
    public Movable Create(Pattern pattern, GameObject gameObject)
    {
        return pattern switch
        {
            Pattern.PositionMove => new PositionMove(gameObject),
            Pattern.TranslateMove => new TranslateMove(gameObject),
            _ => null,
        };
    }
}

その他のクラス

Movableクラス、PositionMoveクラス、TranslateMoveはそのまま変更はありません。

クラス図

参考資料

C#,Unity

Posted by hidepon