Learn how create 2D Idle Clicker Game in Unity Section6 学習メモ

# 概要
Learn how create 2D Idle Clicker Game in Unity Section6の学習メモです。
動きを把握するため、クラス図を書いたり実際に動かした際の試行錯誤を記録に残しています

# 開発環境

IDE:Rider
Unity:2019.4.40
OS:Windows10

# 図 

## 実装したい動き
![aaaaaaaaaaa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/2d861114-7f7e-fdba-3190-ad3d9e93aae0.gif)



1.Upgrade用のUIを開く。
2.Upgradeボタンを押すとMinerがUpgradeされる。


## クラス図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/23f19110-bd4a-060f-9d65-395c10324819.png)


クラス名|説明
------|----
BaseUpgrade| Upgradeクラス、今後実装予定のElevator Warehouseの継承元にもなります。
ShaftUpgrade| 継承元はBaseUpgrade。オーバライドしている関数はExecuteUpgradeのみ
UpgradePanelInfo| UpgradeUIを構成する画像類を束ねているクラス。ScriptableObjectを作成
UpgradeManager|UpgradeUIを開閉する際に使われるクラス。
ShaftUI|uGui関係のクラス。今回の場合だとUpgradeUIの表示、更新ボタン押した後の画面更新を担当

これで合ってるの？Observerパターンが2つあるから困惑してます。

赤枠のObserverパターン
Upgradeを実行する処理に使われている。
BaseUpgrade（Observale）で現在のレベルの内部処理が行われ、ShaftUI(Observer)でUI側に反映しています。


黄枠のObserverパターン
UpgradeUIを開く処理に使われている。
ボタンを押すと、ShaftUI（Observale）のメソッドが実行され、UpgradeManagerクラス(Observer)で内部処理を行う。

イベント処理(ShaftUI)と内部処理(Upgrade)を分離することが目的と考えられる。
イベント処理と内部処理の依存関係を逆転させるために使っている感じがある。

# 実装

## UnityEditor上の設定
### Upgradesボタン押す前

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/c44232be-bd32-11af-f29e-679efdfcba4a.png)

オブジェクト名| 説明
-------------|-----
UpgradeManager|UpgradeManagerスクリプトがアタッチされている。
Shaft|Shaft,ShaftUI,ShaftUpgradeスクリプトがアタッチされる。
Upgarde-Button|UpgradeUIを開くボタン。ShaftUI.OpenUpgradeContainerメソッドがアタッチされている。



### UPgradesボタン押した後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/c26adcc9-eabb-b48e-79e3-7aeb41fcf02e.png)

オブジェクト名| 説明
-------------|-----
Close-Button |  UIを閉じる。UpgradeManager.OpenCloseUpgradeContainerメソッドがアタッチされている。
Upgarde-Button|Upgradeを実行するボタン。UpgradeManager.Upgradeがアタッチされている。
## コード(とても長いのでスキップ推奨　クラス図を先に見て必要な項目を参照)

### UpgradeManager
```diff_c_sharp:UpgradeManager.cs
using System;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class UpgradeManager : MonoBehaviour
{
    [SerializeField] private GameObject upgradeContainer;
    [SerializeField] private Image panelMinerImage;
    [SerializeField] private TextMeshProUGUI panelTitle;
    [SerializeField] private TextMeshProUGUI level;
    [SerializeField] private TextMeshProUGUI nextBoost;
    [SerializeField] private TextMeshProUGUI upgradeCost;
    [SerializeField] private Image progressBar;

    [Header("Upgrade Buttons")]
    [SerializeField] private GameObject[] upgradeButtons;
    [SerializeField] private Color buttonDisableColor;
    [SerializeField] private Color buttonEnableColor;
    
    [Header("Stat Title")] 
    [SerializeField] private TextMeshProUGUI stat1Title;
    [SerializeField] private TextMeshProUGUI stat2Title;
    [SerializeField] private TextMeshProUGUI stat3Title;
    [SerializeField] private TextMeshProUGUI stat4Title;
    
    [Header("Stat Values")] 
    [SerializeField] private TextMeshProUGUI stat1CurrentValue;
    [SerializeField] private TextMeshProUGUI stat2CurrentValue;
    [SerializeField] private TextMeshProUGUI stat3CurrentValue;
    [SerializeField] private TextMeshProUGUI stat4CurrentValue;

    [Header("Stat Upgrade Values")] 
    [SerializeField] private TextMeshProUGUI stat1UpgradeValue;
    [SerializeField] private TextMeshProUGUI stat2UpgradeValue;
    [SerializeField] private TextMeshProUGUI stat3UpgradeValue;
    [SerializeField] private TextMeshProUGUI stat4UpgradeValue;
    
    [Header("Stat Icon")] 
    [SerializeField] private Image stat1Icon;
    [SerializeField] private Image stat2Icon;
    [SerializeField] private Image stat3Icon;
    [SerializeField] private Image stat4Icon;

    [Header("Panel Info")] 
    [SerializeField] private UpgradePanelInfo shaftMinerInfo;

    public int UpgradeAmount { get; set; }
    
    private Shaft _currentShaft;
    private ShaftUpgrade _currentShaftUpgrade;
    private int _currentActiveButton;
    
    public void OpenCloseUpgradeContainer(bool status)
    {
        Debug.Log("<color=green>UpgradeManagerクラスのOpenCloseUpgradeContainerが実行されました。</color>");
        UpgradeX1();
        upgradeContainer.SetActive(status);
    }

    public void Upgrade()
    {
        if (GoldManager.Instance.CurrentGold >= _currentShaftUpgrade.UpgradeCost)
        {
            _currentShaftUpgrade.Upgrade(UpgradeAmount);
            UpdateShaftPanelValues();
            RefreshUpgradeAmount();
        }
    }

    #region Upgrade Buttons

    public void UpgradeX1()
    {
        ActivateButton(0);
        UpgradeAmount = CanUpgradeManyTimes(1, _currentShaftUpgrade) ? 1 : 0;
        upgradeCost.text = GetUpgradeCost(1, _currentShaftUpgrade).ToString();
    }

    public void UpgradeX10()
    {
        ActivateButton(1);
        UpgradeAmount = CanUpgradeManyTimes(10, _currentShaftUpgrade) ? 10 : 0;
        upgradeCost.text = GetUpgradeCost(10, _currentShaftUpgrade).ToString();
    }
    
    public void UpgradeX50()
    {
        ActivateButton(2);
        UpgradeAmount = CanUpgradeManyTimes(50, _currentShaftUpgrade) ? 50 : 0;
        upgradeCost.text = GetUpgradeCost(50, _currentShaftUpgrade).ToString();
    }
    
    public void UpgradeMax()
    {
        ActivateButton(3);
        UpgradeAmount = CalculateUpgradeCount(_currentShaftUpgrade);
        upgradeCost.text = GetUpgradeCost(UpgradeAmount, _currentShaftUpgrade).ToString();
    }

    private int CalculateUpgradeCount(BaseUpgrade upgrade)
    {
        int count = 0;
        float currentGold = GoldManager.Instance.CurrentGold;
        float currentUpgradeCost = upgrade.UpgradeCost;

        if (GoldManager.Instance.CurrentGold >= currentUpgradeCost)
        {
            for (float i = currentGold; i >= 0; i -= currentUpgradeCost)
            {
                count++;
                currentUpgradeCost *= upgrade.UpgradeCostMultiplier;
            }
        }

        return count;
    }

    private bool CanUpgradeManyTimes(int upgradeAmount, BaseUpgrade upgrade)
    {
        int count = CalculateUpgradeCount(upgrade);
        if (count >= upgradeAmount)
        {
            return true;
        }

        return false;
    }

    private float GetUpgradeCost(int amount, BaseUpgrade upgrade)
    {
        float cost = 0f;
        float currentUpgradeCost = upgrade.UpgradeCost;

        for (int i = 0; i < amount; i++)
        {
            cost += currentUpgradeCost;
            currentUpgradeCost *= upgrade.UpgradeCostMultiplier;
        }

        return cost;
    }
    
    private void ActivateButton(int buttonIndex)
    {
        for (int i = 0; i < upgradeButtons.Length; i++)
        {
            upgradeButtons[i].GetComponent<Image>().color = buttonDisableColor;
        }

        _currentActiveButton = buttonIndex;
        upgradeButtons[buttonIndex].GetComponent<Image>().color = buttonEnableColor;
        upgradeButtons[buttonIndex].transform.DOPunchPosition(transform.localPosition +
                                                              new Vector3(0f, -5f, 0f), 0.5f).Play();
    }

    private void RefreshUpgradeAmount()
    {
        switch (_currentActiveButton)
        {
            case 0:
                UpgradeX1();
                break;
            case 1:
                UpgradeX10();
                break;
            case 2:
                UpgradeX50();
                break;
            case 3:
                UpgradeMax();
                break;
        }
    }
    
    #endregion
    
    private void UpdateUpgradeInfo()
    {
        Debug.Log("<color=green>UpgradeManagerクラスのUpdateUpgradeInfoが実行されました。</color>");
        panelTitle.text = shaftMinerInfo.PanelTitle;
        panelMinerImage.sprite = shaftMinerInfo.PanelMinerIcon;

        stat1Title.text = shaftMinerInfo.Stat1Title;
        stat2Title.text = shaftMinerInfo.Stat2Title;
        stat3Title.text = shaftMinerInfo.Stat3Title;
        stat4Title.text = shaftMinerInfo.Stat4Title;

        stat1Icon.sprite = shaftMinerInfo.Stat1Icon;
        stat2Icon.sprite = shaftMinerInfo.Stat2Icon;
        stat3Icon.sprite = shaftMinerInfo.Stat3Icon;
        stat4Icon.sprite = shaftMinerInfo.Stat4Icon;
    }

    //uGuiの処理を値を更新
    private void UpdateShaftPanelValues()
    {
        Debug.Log("<color=green>UpgradeManagerクラスのUpdateShaftPanelValuesが実行されました。</color>");
        upgradeCost.text = _currentShaftUpgrade.UpgradeCost.ToString();
        level.text = $"Level {_currentShaftUpgrade.CurrentLevel}";
        progressBar.DOFillAmount(_currentShaftUpgrade.GetNextBoostProgress(), 0.5f).Play();
        nextBoost.text = $"Next Boost at Level {_currentShaftUpgrade.BoostLevel}";
        
        // Current Values
        stat1CurrentValue.text = $"{_currentShaft.Miners.Count}";
        stat2CurrentValue.text = $"{_currentShaft.Miners[0].MoveSpeed}";
        stat3CurrentValue.text = $"{_currentShaft.Miners[0].CollectPerSecond}";
        stat4CurrentValue.text = $"{_currentShaft.Miners[0].CollectCapacity}";
        
        // Miner stat
        stat1UpgradeValue.text = (_currentShaftUpgrade.CurrentLevel + 1) % 10 == 0 ? "+1" : "+0";

        // Move Speed
        float minerMoveSpeed = _currentShaft.Miners[0].MoveSpeed;
        float walkSpeedUpgraded = Mathf.Abs(minerMoveSpeed - (minerMoveSpeed * _currentShaftUpgrade.MoveSpeedMultiplier));
        stat2UpgradeValue.text = (_currentShaftUpgrade.CurrentLevel + 1) % 10 == 0 ? $"+{walkSpeedUpgraded}/s" : "+0";

        // Collect Per Second
        float minerCollectPerSecond = _currentShaft.Miners[0].CollectPerSecond;
        float collectPerSecondUpgraded = Mathf.Abs(minerCollectPerSecond - (minerCollectPerSecond * _currentShaftUpgrade.CollectPerSecondMultiplier));
        stat3UpgradeValue.text = $"+{collectPerSecondUpgraded}";
        
        // Collect Capacity
        float minerCollectCapacity = _currentShaft.Miners[0].CollectCapacity;
        float collectCapacityUpgraded = Mathf.Abs(minerCollectCapacity - (minerCollectCapacity * _currentShaftUpgrade.CollectCapacityMultiplier));
        stat4UpgradeValue.text = $"+{collectCapacityUpgraded}";
    }
    
    private void ShaftUpgradeRequest(Shaft selectedShaft, ShaftUpgrade selectedUpgrade)
    {
        Debug.Log("<color=green>UpgradeManagerクラスのShaftUpgradeRequestが実行されました。</color>");
        _currentShaft = selectedShaft;
        _currentShaftUpgrade = selectedUpgrade;
        
        UpdateUpgradeInfo();
        UpdateShaftPanelValues();   
        OpenCloseUpgradeContainer(true);
    }
    
    private void OnEnable()
    {
        Debug.Log("<color=green>UpgradeManagerクラスのOnEnableが実行されました。</color>");
        // 
        ShaftUI.OnUpgradeRequest += ShaftUpgradeRequest;
    }

    private void OnDisable()
    {
        Debug.Log("<color=green>UpgradeManagerクラスのOnDisableが実行されました。</color>");
        ShaftUI.OnUpgradeRequest -= ShaftUpgradeRequest;
    }
}
```

### UpgradePanelInfo
```diff_c_sharp:UpgradePanelInfo.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Upgrade Info")]
public class UpgradePanelInfo : ScriptableObject
{
    public string PanelTitle;
    public Sprite PanelMinerIcon;

    [Header("Stat Title")] 
    public string Stat1Title;
    public string Stat2Title;
    public string Stat3Title;
    public string Stat4Title;
    
    [Header("Stat Icon")] 
    public Sprite Stat1Icon;
    public Sprite Stat2Icon;
    public Sprite Stat3Icon;
    public Sprite Stat4Icon;
}

```
### ShaftUI
```diff_c_sharp:ShaftUI.cs
using System;
using TMPro;
using UnityEngine;

public class ShaftUI : MonoBehaviour
{
    public static Action<Shaft, ShaftUpgrade> OnUpgradeRequest;
    
    [SerializeField] private TextMeshProUGUI depositGold;
    [SerializeField] private TextMeshProUGUI shaftID;
    [SerializeField] private TextMeshProUGUI shaftLevel;
    [SerializeField] private TextMeshProUGUI newShaftCost;
    [SerializeField] private GameObject newShaftButton;
    
    private Shaft _shaft;
+    private ShaftUpgrade _shaftUpgrade;
    
    private void Awake()
    {
        _shaft = GetComponent<Shaft>();
        _shaftUpgrade = GetComponent<ShaftUpgrade>();
    }

    private void Update()
    {
        depositGold.text = _shaft.ShaftDeposit.CurrentGold.ToString();
    }

    public void AddShaft()
    {
        if (GoldManager.Instance.CurrentGold >= ShaftManager.Instance.ShaftCost)
        {
            GoldManager.Instance.RemoveGold(ShaftManager.Instance.ShaftCost);
            ShaftManager.Instance.AddShaft();
            newShaftButton.SetActive(false);
        }
    }

+    public void OpenUpgradeContainer()
+    {
+        Debug.Log("<color=yellow>ShaftUIクラスのOpenUpgradeContainerが実行されました。</color>");
+        OnUpgradeRequest?.Invoke(_shaft, _shaftUpgrade);
+    }
    
    public void SetShaftUI(int ID)
    {
        Debug.Log("<color=purple>ShaftUIクラスのOpenUpgradeContainerが実行されました。</color>");
        _shaft.ShaftID = ID;
        shaftID.text = (ID + 1).ToString();
    }

    public void SetNewShaftCost(float newCost)
    {
        Debug.Log("<color=yellow>ShaftUIクラスのOpenUpgradeContainerが実行されました。</color>");
        newShaftCost.text = newCost.ToString();
    }

+    private void UpgradeCompleted(BaseUpgrade upgrade)
+    {
+        Debug.Log("<color=yellow>ShaftUIクラスのUpgradeCompletedが実行されました。</color>");
+        if (_shaftUpgrade == upgrade)
+        {
+            shaftLevel.text = upgrade.CurrentLevel.ToString();
+        }
+    }
    
+    private void OnEnable()
+    {
+        Debug.Log("<color=yellow>ShaftUIクラスのOnEnableが実行されました。</color>");
+        ShaftUpgrade.OnUpgradeCompleted += UpgradeCompleted;
+    }

+    private void OnDisable()
+    {
+        Debug.Log("<color=yellow>ShaftUIクラスのOnDisableが実行されました。</color>");
+        ShaftUpgrade.OnUpgradeCompleted -= UpgradeCompleted;
+    }
}

```

### ShaftUpgrade
```diff_c_sharp:ShaftUpgrade.cs
using UnityEngine;

public class ShaftUpgrade : BaseUpgrade
{
    protected override void ExecuteUpgrade()
    {
        Debug.Log("<color=red>ShaftUpgradeクラスのExecuteUpgradeが実行されました。</color>");
        if (CurrentLevel % 10 == 0)
        {
            // Levelの数が増えると新しいShaftMinerが１人増える。
            _shaft.CreateMiner();
        }

        foreach (ShaftMiner miner in _shaft.Miners)
        {
            
            miner.CollectCapacity *= CollectCapacityMultiplier;
            miner.CollectPerSecond *= CollectPerSecondMultiplier;
            
            Debug.Log(CollectCapacityMultiplier);
            if (CurrentLevel % 10 == 0)
            {
                miner.MoveSpeed *= MoveSpeedMultiplier;
            }
        }
    }
}
```

### BaseUpgrade
```diff_c_sharp:BaseUpgrade.cs
using System;
using UnityEngine;

public class BaseUpgrade : MonoBehaviour
{
    public static Action<BaseUpgrade> OnUpgradeCompleted;
    
    [Header("Upgrade")]
    [SerializeField] private float collectCapacityMultiplier = 2;
    [SerializeField] private float collectPerSecondMultiplier = 2;
    [SerializeField] private float moveSpeedMultiplier = 2;

    [Header("Cost")]
    [SerializeField] private float initialUpgradeCost = 600;
    [SerializeField] private float upgradeCostMultiplier = 2;

    public int CurrentLevel { get; set; }
    public float UpgradeCost { get; set; }
    public int BoostLevel { get; set; }
    
    public float CollectCapacityMultiplier => collectCapacityMultiplier;
    public float CollectPerSecondMultiplier => collectPerSecondMultiplier;
    public float MoveSpeedMultiplier => moveSpeedMultiplier;
    public float UpgradeCostMultiplier => upgradeCostMultiplier;
    
    protected Shaft _shaft;
    private int _currentNextBoostLevel = 1;
    private int _nextBoostResetValue = 1;

    private void Start()
    {
        _shaft = GetComponent<Shaft>();
        
        CurrentLevel = 1;
        UpgradeCost = initialUpgradeCost;
        BoostLevel = 10;
    }

    public void Upgrade(int amount)
    {
        if (amount > 0)
        {
            for (int i = 0; i < amount; i++)
            {
                UpgradeCompleted();
                ExecuteUpgrade();
            }
        }
    }

    private void UpgradeCompleted()
    {
        Debug.Log("<color=purple>BaseUpgradeクラスのUpgradeCompletedが実行されました。</color>");
        GoldManager.Instance.RemoveGold(UpgradeCost);
        UpgradeCost *= upgradeCostMultiplier;
        CurrentLevel++;
        UpdateNextBoostLevel();
        OnUpgradeCompleted?.Invoke(this);
    }

    protected virtual void ExecuteUpgrade()
    {
        
    }

    protected void UpdateNextBoostLevel()
    {
        Debug.Log("<color=purple>BaseUpgradeクラスのUpgradeNextBoostLevelが実行されました。</color>");
        _currentNextBoostLevel++;
        _nextBoostResetValue++;
        if (_currentNextBoostLevel == BoostLevel)
        {
            _nextBoostResetValue = 1;
            BoostLevel += 10;
        }
    }

    public float GetNextBoostProgress()
    {
        // Levelが10の倍数になったときに実行される
        // progressバーを初期値に戻す
        Debug.Log("<color=purple>BaseUpgradeクラスのGetNextBoostProgressが実行されました。</color>");
        return (float) _nextBoostResetValue / 10;
    }
}

```


# 動かして確認

## Upgradeメニューを開く
### 動き
![upgradeMenuOpen1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/bb86634f-bbdc-e75f-cf55-b0d384b70199.gif)

### ログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/0f13e139-c352-e61d-eb2b-1fca3e3e2a36.png)

各処理の説明
1.ShaftUIクラスのOpenUpgradeContainer
デリデートを使ってOnUpgradeRequestを実行する。（引数も渡している。）
 OnUpgradeRequestにはUpgradeManager.ShaftUpgradeRequestが登録されている。

2.UpgradeManageクラスのUpdateUpgradeInfo
デリケートから渡された変数selectedShaftとselectedUpgradeをprivate変数に渡している。

3.UpgradeManageクラスのUpdateUpgradeInfo
shaftMinerInfoからの情報（アイコン　アイコンのテキスト）を取得。

4.UpgradeManageクラスのUpdateShaftPanelValues
Shaft、ShaftUpgradeクラスの情報（パラメーターの数値など）を取得する。

5.BaseUpgradeクラスのGetNextBoostProgress
ProgressBarの現在値を算出する。

6.UpgradeManageクラスのOpenCloseUpgradeContainer
UpgradeUIをactiveにする。

## Upgradeする
### 動き
![upgradeMenuUpgradeOne.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/a2c7085f-f006-a20a-d6b6-0ee3dd045cbe.gif)


### ログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/97d2e871-415c-5ac2-418c-bd952d937999.png)

1.BaseUpgradeクラスのUpgradeCompleted
所持金の処理
2.BaseUpgradeクラスのUpgradeNextBoostLevel

3.ShaftUpgradeのExecuteUpgrade
Shaft特有のUpgrade処理を実装する。
具体的には採掘スピード、運搬量の改善。
Upgrade数が10の倍数となると従業員の追加、移動速度が改善される。

4.UpgradeManagerのUpdateShaftPanelValues
UIの数値更新

5.BaseUpgradeクラスのGetNextBoostProgress
プログレスバーのUIを更新

## Upgradeする（１０回）

### 動き
![upgradeMenuUpgradeTen.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/b3ea85fe-636c-fda1-b58f-49b824e4acf6.gif)

### ログ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/01695f44-3a87-4794-6165-b56e84adce8c.png)

上のUpgrade処理と類似した処理。
1-3を10回繰り返した後、4,5を実行する


# 感想

クラス図が増えて処理を追うのが難しいですね。ElevatorやWareHouseの場合でも実際に手を動かして習熟する必要があると思います。(Section7, Section8)

UpgradeManagerクラスの責務にuGui要素が含まれているのが気になりましたね。
ShaftUpgradeUI的なクラス設けて分けた方が良いのではと思った。

C#の文法でネックになっている点はActionとコルーチンだと気づきました。
他のUnityの講座でもよく使われている事例が多いので使いこなせたほうがいいですね。

# 参考

Section6 ShaftUpgrade

https://www.udemy.com/course/learn-how-to-create-a-2d-idle-clicker-game-in-unity-2021/learn/lecture/24837826#overview


C# [Design Pattern] C# による Observer パターンの実装

http://blog.shos.info/archives/2012/12/csharp_observerpattern3.html