Learn how create 2D Idle Clicker Game in Unity Section3 Shaftの動的作成 学習メモ

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

# 開発環境
IDE:Rider
Unity:2019.4.40
OS:Windows10

# 図

## gifアニメ
![Section3sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/2652538f-a973-9b80-c06a-dd17f0c66a4c.gif)


今回実装する機能
newShaftボタンを押すと、新しいshaftが追加される。
MinerオブジェクトをクリックするとMinerが動き出す。

## クラス図

### Shaft
新しいshaftを追加するためののクラス図です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/2fb240de-b5d4-906f-88cc-fcc8ba28a083.png)


クラス名|説明
----|----
Shaft|Shaftオブジェクトの子オブジェクトを定義。Shaftの子オブジェクトにMinerとDepositを加える。
ShaftUI|ShaftのUI部分に関するクラス。Shaftクラスの変数idを引数にして対応するShaftのUIを操作している。
Singleton| シングルトン 他クラスに適用しやすくするためにジェネリック型を使っている
ShaftManager| Shaftたちを管理するクラス。Shaftの追加処理のみ



### Miner
Minerをクリックしたときに動かせるために実装したクラス　メソッド
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/79b168fb-0cfe-d052-3c85-93a35741e6ae.png)




クラス名|説明
----|----
IClickable|新規作成 インターフェイス
BaseMiner|OnClickとMouseDownを追加 OnClickの実装はShaftMinerで行うので宣言のみ
ShaftMiner|OnClickを追加　処理を実装している。

※MouseDownメソッドはStartやUpdateのようなUnity Event Function
OnClickは非Unity依存の関数

# 実装
## UnityEditorの設定
起動した時のシーンの画像です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/e06948e4-3740-a7f9-b13e-8590d4e41977.png)

オブジェクト名|説明
----|----
ShaftManager|ShaftManagerスクリプトがアタッチされている。
Shaft|親オブジェクト。Shaft,ShaftUIスクリプトがアタッチされている。
Canvas|UI部分、ShaftのID、Depositの金額、Upgradeレベル、追加でShaftを追加する`ボタン`の４点
MiningLocation|Minerが動ける右端　到達すると採掘を開始する　コンポーネントのアイコンを使っているのでゲーム画面には表示されない
DepositLocation|Minerが動ける左端　到達するとMinerの所持金をDepositに移す　コンポーネントのアイコンを使っているのでゲーム画面には表示されない
DepositCreationLocation|Depositが生成される座標 
Miner|Prefab。 DepositPostionの座標に生成 ShaftMinerスクリプトがアタッチされている。
Deposit|Prefab。 DepositCreationLocationの座標に生成 Depositスクリプトがアタッチされている。


起動させると動的オブジェクト(MinerとDeposit)が生成されます。

## Shaftのスクリプト

### Shaft
```cs:Shaft.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;

public class Shaft : MonoBehaviour
{
    [Header("Prefab")] 
    [SerializeField] private ShaftMiner minerPrefab;
    [SerializeField] private Deposit depositPrefab;

    [Header("Locations")] 
    [SerializeField] private Transform miningLocation;
    [SerializeField] private Transform depositLocation;
    [SerializeField] private Transform depositCreationLocation;

    public int ShaftID { get; set; }
    public Transform MiningLocation => miningLocation;
    public Transform DepositLocation => depositLocation;
    public Deposit ShaftDeposit { get; set; }
    public ShaftUI ShaftUI { get; set; }

    private void Awake()
    {
        Debug.Log("<color=yellow>ShaftのAwakeが実行されました。</color>");
        //　使われていない。
        ShaftUI = GetComponent<ShaftUI>();
    }

    private void Start()
    {
        Debug.Log("<color=yellow>ShaftのStartが実行されました。</color>");
        CreateMiner();
        CreateDeposit();
    }

    private void CreateMiner()
    {
        Debug.Log("<color=yellow>ShaftのCreateMinerが実行されました。</color>");
         ShaftMiner newMiner = Instantiate(minerPrefab, depositLocation.position, quaternion.identity);
         newMiner.CurrentShaft = this;
         newMiner.transform.SetParent(transform);
    }

    private void CreateDeposit()
    {
        Debug.Log("<color=yellow>ShaftのCreateDepositが実行されました。</color>");
        ShaftDeposit = Instantiate(depositPrefab, depositCreationLocation.position, quaternion.identity);
        ShaftDeposit.transform.SetParent(transform);
    }
}

```


### ShaftUI
```cs:ShaftUI.cs
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class ShaftUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI depositGold;
    [SerializeField] private TextMeshProUGUI shaftID;
    [SerializeField] private TextMeshProUGUI shaftLevel;
    [SerializeField] private TextMeshProUGUI newShaftCost;
    [SerializeField] private GameObject newShaftButton;
    
    private Shaft _shaft;
    
    // Start is called before the first frame update
    private void Awake()
    {
        Debug.Log("<color=green>ShaftUIのAwakeが実行されました。</color>");
        _shaft = GetComponent<Shaft>();
    }

    // Update is called once per frame
    private void Update()
    {
        depositGold.text = _shaft.ShaftDeposit.CurrentGold.ToString();
    }

    // NewShaft-buttonにアタッチされている。
    public void AddShaft()
    {
        Debug.Log("<color=green>ShaftUIのAddShaftが実行されました。</color>");
        ShaftManager.Instance.AddShaft();
        newShaftButton.SetActive(false);
    }

    public void SetShaftUI(int ID)
    {
        Debug.Log("<color=green>ShaftUIのSetShaftUIが実行されました。</color>");
        _shaft.ShaftID = ID;
        shaftID.text = (ID + 1).ToString();
    }

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

```

### ShaftManager
```cs:ShaftManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShaftManager : Singleton<ShaftManager>
{
    [SerializeField] private Shaft shaftPrefab;
    [SerializeField] private float newShaftYPosition;
    [SerializeField] private float newShaftCost = 5000;
    [SerializeField] private float newShaftCostMultiplier = 10;
    
    [SerializeField] private List<Shaft> shafts;

    public float ShaftCost { get; set; }
    
    private int _currentShaftIndex;

    private void Start()
    {
        Debug.Log("<color=purple>ShaftManagerのStartが実行されました。</color>");
        ShaftCost = newShaftCost;
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.N))
        {
            Debug.Log("<color=purple>ShaftManagerのUpdateが実行されました。</color>");
            AddShaft();
        }
    }

    public void AddShaft()
    {
        Debug.Log("<color=purple>ShaftManagerのAddStartが実行されました。</color>");
        Transform lastShaft = shafts[_currentShaftIndex].transform;
        Vector3 newShaftPos = new Vector3(lastShaft.position.x, lastShaft.position.y - newShaftYPosition);
        Shaft newShaft = Instantiate(shaftPrefab, newShaftPos, Quaternion.identity);

        _currentShaftIndex++;
        ShaftCost *= newShaftCostMultiplier;
        
        newShaft.ShaftUI.SetShaftUI(_currentShaftIndex);
        newShaft.ShaftUI.SetNewShaftCost(ShaftCost);
        
        shafts.Add(newShaft);
    }
}

```

### Singleton
```cs:Singleton.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : Component
{
    private static T instance;
    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                instance = FindObjectOfType<T>();
                if (instance == null)
                {
                    GameObject newGO = new GameObject();
                    instance = newGO.AddComponent<T>();
                }
            }

            return instance;
        }
    }

    protected virtual void Awake()
    {
        instance = this as T;
    }
}
```

## Minerのスクリプト

### IClickable
```cs:IClickable.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IClickable
{
    void OnClick();
}

```

### BaseMiner
```diff_c_sharp:BaseMiner.cs
using System;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;

public class BaseMiner : MonoBehaviour, IClickable
{
    [Header("Initial Values")] 
    [SerializeField] private float initialCollectCapacity;
    [SerializeField] private float initialCollectPerSecond;
    [SerializeField] protected float moveSpeed;
    
    public float CurrentGold { get; set; }
    public float CollectCapacity { get; set; }
    public float CollectPerSecond { get; set; }
    public bool IsTimeToCollect { get; set; }
+    public bool MinerClicked { get; set; }
    
    protected Animator _animator;
    
    private void Start()
    {
        _animator = GetComponent<Animator>();
        IsTimeToCollect = true;

        CollectCapacity = initialCollectCapacity;
        CollectPerSecond = initialCollectPerSecond;
    }

+    private void OnMouseDown()
+    {
+        if (!MinerClicked)
+        {
+            OnClick();
+            MinerClicked = true;
+        }
+    }

+    public virtual void OnClick()
+    {
+        
+    }
    
    protected virtual void MoveMiner(Vector3 newPosition)
    {
        transform.DOMove(newPosition, moveSpeed).SetEase(Ease.Linear).OnComplete((() =>
        {
            if (IsTimeToCollect)
            {
                CollectGold();
            }
            else
            {
                DepositGold();
            }

        })).Play();
    }
    
    protected virtual void CollectGold()
    {
        
    }

    protected virtual IEnumerator IECollect(float gold, float collecTime)
    {
        yield return null;
    }
    
    protected virtual void DepositGold()
    {
        
    }

    protected void ChangeGoal()
    {
        IsTimeToCollect = !IsTimeToCollect;
    }

    protected void RotateMiner(int direction)
    {
        if (direction == 1)
        {
            transform.localScale = new Vector3(1, 1, 1);
        }
        else
        {
            transform.localScale = new Vector3(-1, 1, 1);
        }
    }
}

```

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

public class ShaftMiner : BaseMiner
{
    public Shaft CurrentShaft { get; set; }

    public Vector3 DepositLocation => new Vector3(CurrentShaft.DepositLocation.position.x, transform.position.y);
    public Vector3 MiningLocation => new Vector3(CurrentShaft.MiningLocation.position.x, transform.position.y);
    
    private int walkAnimation = Animator.StringToHash("Walk");
    private int miningAnimation = Animator.StringToHash("Mining");

+    public override void OnClick()
+    {
+        MoveMiner(MiningLocation);
+    }

    protected override void MoveMiner(Vector3 newPosition)
    {
        base.MoveMiner(newPosition);
        _animator.SetTrigger(walkAnimation);
    }

    protected override void CollectGold()
    {
        _animator.SetTrigger(miningAnimation);
        float collectTime = CollectCapacity / CollectPerSecond;
        StartCoroutine(IECollect(CollectCapacity, collectTime));
    }

    protected override IEnumerator IECollect(float gold, float collectTime)
    {
        yield return new WaitForSeconds(collectTime);

        CurrentGold = gold;
        ChangeGoal();
        RotateMiner(-1);
        MoveMiner(DepositLocation);
    }

    protected override void DepositGold()
    {
        CurrentShaft.ShaftDeposit.DepositGold(CurrentGold);
        
        CurrentGold = 0;
        ChangeGoal();
        RotateMiner(1);
        MoveMiner(MiningLocation);
    }
}

```


# 動かして確認
## Shaftを追加した時の動き
### gifアニメ
shaftを追加後、StaffManagerの変数shaftsが変化していることを確認できます。
次に各Shaftをインスペクター上で確認し、Shaftクラスの変数ShaftIDが変化していることを確認できます。

![Section3newShaft.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/7d82adf8-c832-071f-231a-54dee64ef3ba.gif)

### コンソール上のログ
新しくShaftを追加したとき、以下の順でメソッドが実行されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/afb1e5b6-1697-2dac-cfef-37d4dd71d436.png)



## Minerを動かした時の処理

### gifアニメ
Minerをクリックしたとき継承元のクラスBaseMinerの変数MinerClickedがfalseからtrueに変化します。

![Section3WokerClickf.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/f4d34ee1-cc60-61d1-9e85-883e1a734f86.gif)


### コンソール上のログ
Minerを追加したとき　以下の順でメソッドが実行されています。
尚、Section2で行ったMoveMiner以降の処理は追ってません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710623/e876188a-3af3-5d5b-7398-84270ee0c8d8.png)





# 気になったこと
GameObjectとuGuiのクリック処理は実装方法が違う
GameObject OnMouseDownメソッドに処理を追加する

# 感想
Section2のときと比べてクラスが増えて処理を追うのが難しくなりました。
特にC#の文法的に理解が怪しい部分がでてきました。（ジェネリック型　インターフェイス型）
が、一つ一つ分解して確認していけばギリ把握できます。

uGuiとGameObjectでスクリプトを分ける方法は勉強になりました。


# 参考
Section3 Shaft

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

ジェネリッククラス

https://blog.websandbag.com/entry/2018/08/11/230001