では今回のメインディッシュを作りたいと思います。やや長いプログラムになりますので段階的に処理を書いていきたいと思います。実際クラス単位で処理をまとめられるのもステートパターンのいいところなので、そのあたりも実感していただけると幸いです!
共通部分と各状態のクラス準備
まずは共通になる箇所や各状態のクラスを順番に用意していこうと思います。
共通処理
ここでは各ボタンや入金された値を管理するための変数などを用意します。
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
public class VenderMachine : StateMachineBase<VenderMachine>
{
#region 共通部分
private JuiceButton[] m_juiceButtonArr;
private ChangeButton m_changeButton;
public Text m_txtCoin;
public int m_iCoinValue;
private class UnityEventInt : UnityEvent<int> { }
private UnityEventInt OnAddCoin = new UnityEventInt();
private void showCoinText( int _iCoin)
{
m_txtCoin.text = _iCoin.ToString();
}
private void addCoin(int _iAdd)
{
m_iCoinValue += _iAdd;
showCoinText(m_iCoinValue);
}
public void AddCoin( int _iAdd)
{
OnAddCoin.Invoke(_iAdd);
}
#endregion 共通部分
}
各状態のクラスを作成する
続いて各状態のクラスを作りましょう。Startメソッドではお金のリセットと、Neutralに遷移する処理も合わせて追加します。また、Payout状態では何を購入したか合わせて引数に渡します。
public class VenderMachine : StateMachineBase<VenderMachine>
{
#region 共通部分
#endregion 共通部分
private void Start()
{
m_iCoinValue = 0;
showCoinText(0);
m_juiceButtonArr = FindObjectsOfType<JuiceButton>();
m_changeButton = FindObjectOfType<ChangeButton>();
ChangeState(new VenderMachine.Neutral(this));
}
private class Neutral : StateBase<VenderMachine>
{
public Neutral(VenderMachine _machine) : base(_machine)
{
}
}
private class Selecting : StateBase<VenderMachine>
{
public Selecting(VenderMachine _machine) : base(_machine)
{
}
}
private class Payout : StateBase<VenderMachine>
{
private JuiceModel m_juiceModel;
public Payout(VenderMachine _machine , JuiceModel _juice ) : base(_machine)
{
m_juiceModel = _juice;
}
}
private class Changing : StateBase<VenderMachine>
{
public Changing(VenderMachine _machine) : base(_machine)
{
}
}
}
各状態(クラス)の編集を行う
各状態のクラスを作りましたので、一つずつ完成させて行きましょう。クラスごとに何の要素があるのかを交えながらコーディングしていきましょう。
Neutral状態
ニュートラル状態では、基本的に入金されるのを待ちます。ただ、入金前には商品をしっかりと見せたいので、ジュースの表示をはっきりさせるための処理も行います。ShowJuiceには購入するのに必要な金額を入れることではっきりした表示になることを利用しています。
private class Neutral : StateBase<VenderMachine>
{
public Neutral(VenderMachine _machine) : base(_machine)
{
}
public override void OnEnterState()
{
machine.OnAddCoin.AddListener((value)=>
{
machine.addCoin(value);
machine.ChangeState(new VenderMachine.Selecting(machine));
});
foreach (JuiceButton button in machine.m_juiceButtonArr)
{
button.ShowJuice(button.model.price);
}
machine.GetComponent<SpriteRenderer>().color = new Color(1f, 1f, 1f, 0.75f);
}
public override void OnExitState()
{
machine.OnAddCoin.RemoveAllListeners();
machine.GetComponent<SpriteRenderer>().color = Color.white;
}
}
Selecting状態
Selecting状態やることは以下
- 入金されている金額に応じてジュースボタンの表示を切り替える
- 再入金することが出来る
- 購入可能なジュースボタンが押されたら払い出しを行う
- お釣り払い出しボタンが押されたらお釣りの払い出しを行う
- 30秒経ったら自動的にお釣り払い出しを行う
private class Selecting : StateBase<VenderMachine>
{
private float m_fTimer = 0f;
public Selecting(VenderMachine _machine) : base(_machine)
{
}
public override void OnEnterState()
{
machine.OnAddCoin.AddListener((value) =>
{
machine.addCoin(value);
machine.ChangeState(new VenderMachine.Selecting(machine));
});
foreach ( JuiceButton button in machine.m_juiceButtonArr)
{
bool bAbleBuy = button.model.price <= machine.m_iCoinValue;
button.ShowJuice(machine.m_iCoinValue);
if ( bAbleBuy)
{
button.m_eventJuice.AddListener((value) =>
{
machine.ChangeState(new VenderMachine.Payout(machine, value));
});
}
}
machine.m_changeButton.m_eventPush.AddListener(() =>
{
machine.ChangeState(new VenderMachine.Changing(machine));
});
}
public override void OnUpdate()
{
m_fTimer += Time.deltaTime;
if( 30f < m_fTimer) {
machine.ChangeState(new VenderMachine.Changing(machine));
}
}
public override void OnExitState()
{
machine.OnAddCoin.RemoveAllListeners();
foreach (JuiceButton button in machine.m_juiceButtonArr)
{
button.m_eventJuice.RemoveAllListeners();
}
machine.m_changeButton.m_eventPush.RemoveAllListeners();
}
}
Payout
ジュースの払い出しでやることは以下
- 購入したジュースが上から降ってくる
- 投入金額の金額を減らす
- 1秒間なにもしない
- 残高がある場合はSelectingへ
- 残高がない場合はNeutralへ
降ってくるジュースはプレファブを利用しても良かったんですが、スクリプトのみで行えるようにしました。
private class Payout : StateBase<VenderMachine>
{
private JuiceModel m_juiceModel;
private float m_fTimer;
public Payout(VenderMachine _machine , JuiceModel _juice ) : base(_machine)
{
m_fTimer = 0f;
m_juiceModel = _juice;
}
public override void OnEnterState()
{
// 作る
GameObject objJuice = new GameObject();
objJuice.transform.position = new Vector3(Random.Range(-5f, 5f), 7f, 0f);
SpriteRenderer sr = objJuice.AddComponent<SpriteRenderer>();
sr.sprite = m_juiceModel.sprite;
sr.sortingOrder = 15;
objJuice.AddComponent<Rigidbody2D>();
Destroy(objJuice, 5f);
machine.addCoin(-1 * m_juiceModel.price);
}
public override void OnUpdate()
{
m_fTimer += Time.deltaTime;
if( 1f < m_fTimer)
{
if( 0 < machine.m_iCoinValue)
{
machine.ChangeState(new VenderMachine.Selecting(machine));
}
else
{
machine.ChangeState(new VenderMachine.Neutral(machine));
}
}
}
}
Changing状態
お釣りの払い出しは配列とforeach文を使ったループ処理があります。硬貨の種類の配列を利用して支払い出来る硬貨を大きい順に払い出しています。メンバー変数の動きなどをしっかり把握してください!
private class Changing : StateBase<VenderMachine>
{
private readonly int[] CoinValues = new int[4] { 500, 100, 50, 10 };
private int m_iTempCoinValue;
private float m_fTimer = 0f;
public Changing(VenderMachine _machine) : base(_machine)
{
}
public override void OnEnterState()
{
m_iTempCoinValue = machine.m_iCoinValue;
}
public override void OnUpdate()
{
m_fTimer += Time.deltaTime;
if (1f < m_fTimer)
{
m_fTimer -= 1f;
if (0 < m_iTempCoinValue)
{
for (int i = 0; i < CoinValues.Length; i++)
{
if (CoinValues[i] <= m_iTempCoinValue)
{
m_iTempCoinValue -= CoinValues[i];
machine.showCoinText(m_iTempCoinValue);
break;
}
}
}
else
{
machine.ChangeState(new VenderMachine.Neutral(machine));
}
}
}
public override void OnExitState()
{
machine.m_iCoinValue = 0;
}
}
仕上げ!シーン内で設定を行う
スクリプトの用意が出来たら、あとはゲーム内に配置・設定を行って自動販売機を完成させましょう
スクリプトのアタッチ
スクリプトをアタッチする場所は自動販売機本体にくっつけます。画像をドラッグ・アンド・ドロップしたならば「vendermachine」になっていると思います。
そのGameObjectに先程作ったスクリプトのVenderMachineを付けます
インスペクターにセット
今回のインスペクターのセットは少なめにしてます。これもスクリプトで解決出来ましたが、インスペクターでセットを行います。投入中の金額を表示しているテキストをインスペクターのTxt Coinにセットします。
金額投入ボタンを紐付ける
金額投入ボタンは4つあります。OnClickのイベントを追加して、VenderMachineスクリプトがくっついているゲームオブジェクトをセットしてください。メソッドはVenderMachine/AddCoinを指定します。
AddCoinはint型の引数を指定することが出来ます。各ボタンの金額に対応した数値を設定してください
動作チェック!
ここまで出来たらあとは動かしてチェックしてみましょう。うまく行っていれば左側のボタンを押して金額が増えたり、点灯したジュースをクリックして上から降ってきたり、お釣りボタンを押して払い出しがされたら成功です!
コメント
コメント一覧 (5件)
動画も合わせて分かりやすくとても勉強になりました。
public Changing(VenderMachine _machine) : base(_machine)
{
}
ただ一つこの部分が自分の勉強不足で理解できなかったのですが、このコンストラクタはどういう働きをしているのでしょうか?
お忙しいとは思いますが、教えていただけないでしょうか。よろしくお願いします。
動画も見ていただきありがとうございます。
コンストラクタというのはクラスが生成されたときに呼び出されるクラスの初期化内容を定義するものです。
Changingクラスが生成された時(new VenderMachine.Changing(machine))にコメント頂いたコンストラクタが呼ばれています。
こちらChangingクラス自体の初期化は何も行われておりませんが、base(_machine)というところが重要で、
基本クラス(StateBaseクラス)側でのコンストラクタも合わせて呼んでいます。
各状態ごとにmachineドットでいろんなものにアクセスしているのは各状態に切り替わるたびにmachineをリレーしているおかげでどの状態でもVenderMachineの共通部分にアクセスすることが出来るようになっています。
完全に理解するにはコンストラクタ以外にC#の継承という機能も必要と少しむずかしい内容かも知れません。
ただ、こういった流し気味の内容に疑問を持つのはとてもプログラマー向きなタイプだと思います。
(まぁその分立ち止まる時間も多くなって気苦労多いかもしれませんが・・・)
コンストラクタにDebug.Logを入れて呼ばれるタイミングを観測したり、メンバー変数の初期化をしてみたり試してみると実感ベースで理解できると思います。
最近はたまーにTwitchなどでも配信してますので、これに限らずゲーム開発で気になることがあるなら、タイミング合えば直接質問していただいても大丈夫ですよ。
Youtubeは来月ぐらいから配信する・・・かもです。
丁寧なコメントありがとうございます。
「継承」を勉強するのに時間がかかり、返信が遅れて申し訳ありませんでした。
つまり、ChangeStateしたタイミングで「StateBase」のコンストラクタを使い、NeutralやSelectingなどの参照を「machine」変数に代入している・・・と言う理解でよかったでしょうか?
あってます。
一番最初にthis(VenderMachine自身)を代入して以降は各状態のmachine変数を受け渡し続けれいます
ありがとうございました。とても勉強になりました。
これからも動画で勉強させてもらいます。