61
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ゲームプログラム オリジナルデザインパターン紹介 Try-Can-Do 

Last updated at Posted at 2020-08-02

はじめに

RPGでHPクラスにHPを減らすメソッドを作るとき。
this.hp.Decrease(10)と書いたコードが、HPが0以下だとそもそも実行されてなくて困ったってことありませんか?
アニメだけは動いてほしかったのにッ!

Try-Can-Doパターンを導入すれば、この問題を解決できます。

今回は僕がゲームプログラムでメソッドの命名によく使っている Try-Can-Doパターン を紹介します。
Unity環境で開発しているので C# で書きますが、他の言語でもご利用できます。

自己紹介

こんにちは、ウェレイと申します。\うぇ~い/
OverdugeonCraftopia のプログラムを少し書いた人間です。
よろしくお願いします。

基本的な使い方、その前に。

CharaクラスとHPクラスの設計を書いておきますね。

public class Character
{
    private HP hp;

    public void Init(int maxHP)
    {
        this.hp = new HP(maxHP);
    }

    public void TakeDamage(int value)
    {
      this.hp.Decrease(value);
    }
}

public class HP
{
    private int currentValue;
    private int maxValue;

    public HP(int maxValue)
    {
        this.maxValue = maxValue;
        this.currentValue = maxValue;
    }

    //... ここにDecrease系を追記します。
}

基本的な使い方

HPを減らすDecreaseメソッドを最も簡潔に書くと次のようになります。

public class HP
{
    // .. (int currentValueなどは省略してます。)
    
    public Decrease(int value)
    {
        this.currentValue -= value;
    }
}

これを Try-Can-Doパターンを用いて書くと次のようになります。
Decreaseメソッドを、TryDecrease、CanDecrease、DoDecrease の3つに分けただけです。

public class HP 
{
    // Try-Can-Doパターンを使った書き方
    public void TryDecrease(int value)
    {
        if (CanDecrease())
        {
            DoDecrease(value);
        }
    }

    private bool CanDecrease()
    {
        return true;
    }

    private void DoDecrease(int value)
    {
        this.currentValue -= value;
    }
}

これだけだと何が嬉しいかちょっと分からないと思うので実践を見ながら理解を進めましょう。

実践 ~基本編~

基本形は上の通りなのですが、現実ではあとからいろんな仕様が追加されます。
それらをどう対応していくか見ていきましょう。

追加仕様 - HPを減らせなかったときにログを出したい

public class HP 
{
//  public void TryDecrease(int value)
//  {
//      if (CanDecrease())
//      {
//          DoDecrease();
//      }
//  }

    public void TryDecrease(int value)
    {
        if (CanDecrease())
        {
            DoDecrease();
        }
        else
        {
            Debug.Log("HPを減らせなかったよ。(;-;)");
        }
    }
}

追加仕様 - HP値の変更はHPが0より大きくないと行えない

public class HP 
{
//  private bool CanDecrease()
//  {
//      return true;
//  }
    private bool CanDecrease()
    {
        return this.currentValue > 0;
    }
}

追加仕様 - HPを減らせるか外部クラスから取得したい

public class HP 
{
//  private bool CanDecrease()
//  {
    public bool CanDecrease()
    {
}

追加仕様 - HPが0未満にならないようにする

public class HP 
{
//  private void DoDecrease(int value)
//  {
//      this.currentValue -= value;
//  }

    private void DoDecrease(int value)
    {
        this.currentValue = Mathf.Max(this.currentValue - value, 0);
    }
}

実践を終えて

次のようなコードになりました。
最初と比べて複雑になりましたね。

public class HP 
{
    private int currentValue;
    private int maxValue;

    public HP(int maxValue)
    {
        this.maxValue = maxValue;
        this.currentValue = maxValue;
    }

    public void TryDecrease(int value)
    {
        if (CanDecrease())
        {
            DoDecrease(value);
        }
        else
        {
            Debug.Log("HPを減らせなかったよ。(;-;)");
        }
    }

    public bool CanDecrease()
    {
        return this.currentValue > 0;
    }

    private void DoDecrease(int value)
    {
        this.currentValue = Mathf.Max(this.currentValue - value, 0);
    }
}

Try、Can、Doの役割について理解が少し進んだところで言葉でまとめます。

  • Tryメソッドは、「しようとするメソッド」です。
  • 必ずしも実行しません。安全なので外部からの受付は基本的にこの子が担当します。普段はpublicです。
  • Canメソッドは、「できるかどうかを返すだけのメソッド」です。
  • 戻り値はbool。副作用がなく安全なので常にpublicで大丈夫です。
  • Doメソッドは、「実行のみが記述されているメソッド」です。
  • 必ず実行されるので危ない子です。普段はprivateにしてください。

表でまとめると次のようになります。

接頭語 アクセス修飾子 概要
Try public ~~をしようとするメソッド。条件によっては何も実行しません。
Can public ~~ができるかどうかを返すメソッド。(bool)
Do private 実行のみが記述されていて、必ず~~を実行するメソッド。

実践 ~応用編~

現実ではまだまだいろんな仕様が追加されます。
頑張って対応しましょう!

追加仕様 - 他のクラスにも失敗したか知らせたい。

public class HP 
{
//  public void TryDecrease(int value)
//  {
//      if (CanDecrease())
//      {
//          DoDecrease(value);
//      }
//      else
//      {
//          Debug.Log("HPを減らせなかったよ。(;-;)");
//      }
//  }
    public bool TryDecrease(int value)
    {
        if (CanDecrease())
        {
            DoDecrease(value);
            return true;
        }
        else
        {
            Debug.Log("HPを減らせなかったよ。(;-;)");
            return false;
        }
    }
}

追加仕様 - 0以下の値が渡されたら無効にしたい。

public class HP 
{
//  public bool TryDecrease(int value)
//  {
//      if (CanDecrease())
//      {
//      ..
//  }
    public void TryDecrease(int value)
    {
        if (CanDecrease(value))
        {
//      ..
    }

//  public bool CanDecrease()
//  {
//      return this.currentValue > 0;
//  }
    public bool CanDecrease(int value)
    {
        return this.currentValue > 0 && value > 0;
    }
}

追加仕様 - 他のクラスから条件を無視して値を渡したい。

例えば0以下の値をどうしても渡したいケース。
(危険だからあんまりやらないでね)

public class HP 
{
//  private void DoDecrease(int value)
//  {
    public void DoDecrease(int value)
    {
}

実践~応用編~を終えて

アクセス修飾子、引数、戻り値などは自由に変えて構わないよ、という例でした。
次のようなコードになりました。

public class HP 
{
    private int currentValue;
    private int maxValue;

    public HP(int maxValue)
    {
        this.maxValue = maxValue;
        this.currentValue = maxValue;
    }

    public bool TryDecrease(int value)
    {
        if (CanDecrease(value))
        {
            DoDecrease(value);
            return true;
        }
        else
        {
            Debug.Log("HPを減らせなかったよ。(;-;)");
            return false;
        }
    }
    
    public bool CanDecrease(int value)
    {
        return this.currentValue > 0 && value > 0;
    }

    public void DoDecrease(int value)
    {
        this.currentValue = Mathf.Max(this.currentValue - value, 0);
    }
}

こういうメソッドに使おう(中級)

Try-Can-Doはすべてのメソッドに使うわけではありません。
そのメソッドが内容を実行しない可能性があるときが使い所です。
例:現在HPが1以上なら HP.TryDecrease(10);は呼んだらHPを変更させますが、HP.TryDecrease(-10); は呼んでもHPを変更させません。

次の例は内容を実行しない可能性があるので、Try-Can-Doで書きましょう。

public class Magic
{
    public void Cast(Character character) //魔法を唱えるぞ
    {
        if (character.mp.GetValue() > this.requestMP)
        {
            character.mp.TryDecrease(this.requestMP);
            // ..
            // 魔法発動の処理
            // ..
        }
    }
}

Try-Can-Doを使って、次のようにしましょう。
あとでDoCastを直接呼ぶようになるかもしれませんよ。
(例えば死んだ時に必ず魔法を発動させる敵を作りたくなったとかでね!)

public class Magic
{
    public void TryCast(Character character)
    {
        if (CanCast(character))
        {
            DOCast(character);
        }
    }

    public bool CanCast(Character character)
    {
        return character.mp.GetValue() > this.requestMP;
    }

    private void DoCast(Character character)
    {
        character.mp.TryDecrease(this.requestMP);
        // ..
        // 魔法発動の処理
        // ..
    }
}

次のようなものは 必ず実行するので Try-Can-Do を使うのはやめましょう。

public class Magic
{
    public int GetRequestMP()
    {
        return this.requestMP;
    }
}

仮にMPが半減するようなルールが増えたしても、必ず実行することは変わりません。
Try-Can-Do を使うのはやめましょう。

public class Magic
{
    public int GetRequestMP()
    {
        if (Field.currentField.CanReduceRequestMP()) // 魔法フィールドがあると消費MP半減!
        {
            return (int)(this.requestMP * 0.5f);
        }
        else
        {
            return this.requestMP;
        }
    }
}

慣れてきたら、省略してみよう(上級)

最初のうちは必ずTry-Can-Doをセットで使ってください。
慣れてきたらCanやDoがそれぞれ一行しか無いような場合は、省略してTryだけ書くのもOKです。
仕様の追加に応じてあとからCanやDoを増やせば大丈夫です。

public class MenuWindow
{
    public void TrySetActive()
    {
        if (!this.gameObject.IsActive())
        {
            this.gameObject.SetActive(true);
        }
    }
}

Tryの中にCanやDoを2種類以上書いてみよう(上級)

今までは TryDecrease CanDecrease DoDecrease のように後ろの文字が一致していましたが、後ろの文字が一致しないことがあります。
例を見てみましょう。

public class Enemy
{
    // ランダムなアクションをRun(実行)する!
    public void TryRunAction_Randomly()
    {
        int random = (int)(Random.value * 2);
        if (random == 0)
        {
            if (CanAttack())
            {
                DoAttack();
            }
        }
        else if (random == 1)
        {
            if (CanJump())
            {
                DoJump();
            }
        }
    }
}

例のように、Tryは複数のCan-Doを書いてもオッケーです。
ちなみに、Do(条件チェックせずに実行)はCan(条件チェック)とセットなので、Doの後ろの文字はCanと必ず一致します。

最後に

ここまで読んでいただきありがとうございました。
今回が初投稿なので色々不安です。至らないところがあればご指摘ください。
でも頑張って書いたので、たくさん褒めていただけたら嬉しいです!

では、TrySubmit(this.article)~~!!

募集1

tryキーワードは多くの言語で使われているので Try という接頭語はあまり良いとは言えません。
一覧性の高さを保証するために4文字以下に収めたいのですが、類語のAttemptなどはその点が達成できないので結局Tryを使っています。
もしほかのよい単語が見つかったらお伝え下さい。

募集2

ほとんど独学で開発してきたこともあり、これをデザインパターンと呼んでいいのか分からないです。
もっとよい呼び方を知っていたら教えて下さいませ。

ありがとうございました!

61
66
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
66

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?