見出し画像

【Unity】3Dローグライクゲームの作り方〜Step9&10-4〜

前回の記事はこちら
前回はインベントリウィンドウを作成し、それを開いたり閉じたりしました。

Itemクラスの実装

キャラクターにアイテムを持たせられるようにするために、まずはItemクラスから実装していきます。
「Item」スクリプトを作成して、以下を記述しましょう。

[System.Serializable]
public class Item
{
   public int id;
   public string name;
}

取り敢えずはこれだけです。

ItemParamsControllerクラスの実装

ついでにItemクラスを操作するクラスも作っておきましょう。
「ItemParamsController」スクリプトを作成し、以下を記述して下さい。

using UnityEngine;

public class ItemParamsController : MonoBehaviour
{
   public Item parameter;
}

今はメソッドは書きません。
ビルドしたら、Prefabsフォルダ内のItem1にこのスクリプトをアタッチしておきましょう。

Inventoryクラスの実装

それでは次に、キャラクターの持っているアイテムの情報を保管するInventoryクラスを作成しようと思います。「Inventory」スクリプトを作成し、以下を記述して下さい。

using System.Collections.Generic;
using UnityEngine;

public class Inventory : MonoBehaviour
{
   public int itemNumMax = 10;
   public List<Item> items = new List<Item>();

   /**
   * もし所持可能数を超えていなければ、アイテムをインベントリに加える
   */
   public bool Add(Item it)
   {
       if(items.Count < itemNumMax)
       {
           items.Add(it);
           return true;
       }
       return false;
   }

   /**
   * インベントリから指定したインデックスのアイテムを返す
   */
   public Item Get(int i)
   {
       if (items.Count > i && i >= 0) return items[i];
       return null;
   }
}

ビルドしたら、PlayerとEnemy1にそれぞれアタッチしておきます。
itemNumMaxパラメーターはアイテムを持たせられる数を表しています。もしもっとプレイヤーにアイテムを持たせたいだとか敵にアイテムを持たせたくないだとか思ったら、この値を変えてあげるといいです。

インベントリウィンドウにアイテムスロットを表示する

それでは次に、インベントリに所持可能数だけアイテムスロットを表示させようと思います。
しかしInventoryActionクラスには記述したくないので、別に「ItemSlotDisplay」スクリプトを作成し、そこに処理を書いていくことにします。

using UnityEngine;
using UnityEngine.UI;

public class ItemSlotDisplay : MonoBehaviour
{
   public Inventory inventory;
   public GameObject content;
   public GameObject itemSlot;

   /**
   * インベントリを参照し、アイテムスロットを表示する
   */
   public void Show()
   {
       for (int i = inventory.itemNumMax; i < content.transform.childCount; i++)
           content.transform.GetChild(i).gameObject.SetActive(false);
       for (int i = content.transform.childCount; i < inventory.itemNumMax; i++)
           Instantiate(itemSlot, content.transform);
       for (int i = 0; i < inventory.itemNumMax; i++)
       {
           Transform slot = content.transform.GetChild(i);
           slot.gameObject.SetActive(true);
           Item it = inventory.Get(i);
           if (it != null) slot.GetComponentInChildren<Text>().text = it.name;
       }
   }
}

次にInventoryActionスクリプトを開いて、以下のように変更して下さい。

// 次のパラメーターを追加
public ItemSlotDisplay display;

// 次のように変更
private void MoveBegin()
{
   isOpen = !isOpen;
   if (isOpen)
   {
       display.Show();
       gameObject.SetActive(true);
   }
   action = EAct.Move;
}

ビルドしたら、ItemSlotDisplayスクリプトはInventoryWindowにアタッチします。このコンポーネントのコンテンツにはScroll View階層下にあるContentを指定してあげましょう。
テストしてみます。以下のようになればOKです。

スクリーンショット 2020-05-09 7.34.54

スクロールできるようにする

今回はスクロールビューなどを使っていないので、自ら作成しないといけません。これから作っていきたいと思います。
先に「ScrollItem」スクリプトを作成して、以下のように記述しましょう。

public class ScrollItem : MonoBehaviour
{
   public Color[] color;
   public float paddingX = 20;
   private bool isSelected = false;

   /**
   * このスクリプトがアタッチされているオブジェクトが隠れている場合は
   * 移動するべき位置までの距離を返す
   */
   public Vector3 GetMoveDistance(RectTransform viewport)
   {
       RectTransform transform = GetComponent<RectTransform>();
       float x1 = transform.rect.x + transform.position.x;
       float x2 = x1 + transform.rect.width;
       float vx1 = viewport.rect.x + viewport.position.x;
       float vx2 = vx1 + viewport.rect.width;
       if (x1 < vx1) return new Vector3(vx1 - x1 + paddingX, 0);
       if (x2 > vx2) return new Vector3(vx2 - x2 - paddingX, 0);
       return new Vector3(0, 0);
   }

   /**
   * 選択状態を返す
   */
   public bool GetSelected() => isSelected;

   /**
   * 選択状態を設定する
   */
   public void SetSelected(bool s, bool isForce = false)
   {
       if (s)
       {
           foreach (var item in transform.parent.GetComponentsInChildren<ScrollItem>())
               item.SetSelected(false, true);
           isSelected = true;
           GetComponent<Image>().color = color[1];
       }
       else if(isForce)
       {
           isSelected = false;
           GetComponent<Image>().color = color[0];
       }
   }
}

それでは「ScrollView」スクリプトを作成して下さい。そこに、以下のように記述します。

using UnityEngine;

public class ScrollView : MonoBehaviour
{
   public GameObject viewPort;
   public GameObject content;
   public float inputHoldDelay = 0.5f;

   private int selectItemIndex = 0;
   private float time = 0;

   // Start is called before the first frame update
   void Start()
   {
      ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
      if (contents.Length > 0)
           contents[selectItemIndex].SetSelected(true);
   }

   // Update is called once per frame
   void Update()
   {
       if (!transform.parent.gameObject.activeInHierarchy) return;
       ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
       if (contents.Length < 1) return;
       Vector3 moveDistance;
       if (Input.anyKeyDown)
       {
           if (Input.GetKeyDown(KeyCode.RightArrow))
           {
               selectItemIndex = (selectItemIndex + 1) % contents.Length;
           }
           if (Input.GetKeyDown(KeyCode.LeftArrow))
           {
               selectItemIndex = (selectItemIndex + contents.Length - 1) % contents.Length;
           }
           moveDistance = contents[selectItemIndex].GetMoveDistance(viewPort.GetComponent<RectTransform>());
           content.transform.position += moveDistance;
           contents[selectItemIndex].SetSelected(true);
           time = 0;
           return;
       }
       time += Time.deltaTime;
       if (time < inputHoldDelay) return;
       if (!Input.anyKey) return;
       if (Input.GetKey(KeyCode.RightArrow))
       {
           selectItemIndex = (selectItemIndex + 1) % contents.Length;
       }
       if (Input.GetKey(KeyCode.LeftArrow))
       {
           selectItemIndex = (selectItemIndex + contents.Length - 1) % contents.Length;
       }
       moveDistance = contents[selectItemIndex].GetMoveDistance(viewPort.GetComponent<RectTransform>());
       content.transform.position += moveDistance;
       contents[selectItemIndex].SetSelected(true);
       time = 0;
   }
   
   /**
   * 選択されているアイテムのインデックスを返す
   */
   public int GetSelectItemIndex() => selectItemIndex;
}

(ゲームとは直接関係ないスクリプトなので説明は割愛します)
ビルドしたら、ScrollViewにScrollViewスクリプトを、ItemSlotにScrollItemスクリプトをそれぞれアタッチして下さい。また、ScrollItemコンポーネントにて色パラメーターのサイズを2にし、要素0に選択されていないときの色、要素1に選択されているときの色をそれぞれ指定しましょう。
(※R2.10/25追記 なお、色パラメーターの初期値は透明度が0になっている為そのままだと表示されません。お気をつけ下さい!)
以下のように10番目のアイテムスロットが選択されればOKです。

スクリーンショット 2020-05-10 4.28.22

スクロール時にアニメーションさせる

しかし今のままだと、本当に最後の要素が選択されているかわかりにくいですね。これを改善するために、スクロール時にアニメーションを加えることで対処しようと思います。
ScrollViewスクリプトを開いて、以下に書き換えて下さい。

using UnityEngine;

public class ScrollView : MonoBehaviour
{
   public GameObject viewPort;
   public GameObject content;
   public float inputHoldDelay = 0.5f;
   public float maxPerFrameScroll = 0.3f;

   private int selectItemIndex = 0;
   private float time = 0;
   private float prevPosX;
   private float nextPosX;
   private int frame = 0;

   // Start is called before the first frame update
   void Start()
   {
       ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
       if (contents.Length > 0)
           contents[selectItemIndex].SetSelected(true);
       nextPosX = prevPosX = content.transform.position.x;
   }

   // Update is called once per frame
   void Update()
   {
       if (!transform.parent.gameObject.activeInHierarchy) return;
       ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
       if (contents.Length < 1) return;
       if (frame > 0) Move(nextPosX, maxPerFrameScroll);
       Vector3 moveDistance;
       if (Input.anyKeyDown)
       {
           /*       省略       */
           moveDistance = contents[selectItemIndex].GetMoveDistance(viewPort.GetComponent<RectTransform>());
           nextPosX = content.transform.position.x + moveDistance.x;
           Move(nextPosX, maxPerFrameScroll);
           contents[selectItemIndex].SetSelected(true);
           time = 0;
           return;
       }
       /*       省略       */
       moveDistance = contents[selectItemIndex].GetMoveDistance(viewPort.GetComponent<RectTransform>());
       nextPosX = content.transform.position.x + moveDistance.x;
       Move(nextPosX, maxPerFrameScroll);
       contents[selectItemIndex].SetSelected(true);
       time = 0;
   }

   /**
   * 選択されているアイテムのインデックスを返す
   */
   public int GetSelectItemIndex() => selectItemIndex;

   /**
   * 補完で計算してアニメーションさせる
   */
   private bool Move(float p2x, float maxPerFrame)
   {
       frame += 1;
       float c = maxPerFrame / Time.deltaTime;
       float t = frame / c;
       content.transform.position = new Vector3(prevPosX + (p2x - prevPosX) * t, content.transform.position.y);
       if (c <= frame)
       {
           frame = 0;
           content.transform.position = new Vector3(p2x, content.transform.position.y);
           prevPosX = p2x;
           return true;
       }
       return false;
   }
}

こんな感じになればOKです。

スクロールアニメ

アイテムスロットを使い回す

アイテムスロットが10個程度であれば問題なく動きます。しかしこれから先、もし20、30......50個と持てるアイテムの数が増えたとき、その度にアイテムスロットの数を増やしていたら、どんどん処理が重たくなっていきます。それを防ぐために、隠れたものを先頭もしくは最後に持ってくることで、アイテムスロットを使い回したいと思います。
(正直コードをいじり過ぎて筆者もどこを変えたのか分からなくなったので、変更したスクリプトを全て掲載したいと思います。すみません......!)
まずScrollItemスクリプトを以下のように書き換えます。

using UnityEngine;
using UnityEngine.UI;

public class ScrollItem : MonoBehaviour
{
   public Color[] color;
   public float paddingX = 20;
   private bool isSelected = false;

   /**
   * このスクリプトがアタッチされているオブジェクトが隠れている場合は
   * 移動するべき位置までの距離を返す
   */
   public Vector3 GetMoveDistance(RectTransform viewport)
   {
       RectTransform transform = GetComponent<RectTransform>();
       float x1 = transform.rect.x + transform.position.x;
       float x2 = x1 + transform.rect.width;
       float vx1 = viewport.rect.x + viewport.position.x;
       float vx2 = vx1 + viewport.rect.width;
       if (x1 < vx1) return new Vector3(vx1 - x1 + paddingX, 0);
       if (x2 > vx2) return new Vector3(vx2 - x2 - paddingX, 0);
       return new Vector3(0, 0);
   }

   /**
   * このスクリプトがアタッチされているオブジェクトが
   * 戻るべき距離を返す
   */
   public Vector3 GetBackDistance(bool isRight) {
       RectTransform transform = GetComponent<RectTransform>();
       if (isRight) return new Vector3(transform.rect.width + paddingX, 0);
       return new Vector3(-transform.rect.width - paddingX, 0);
   }

   /**
   * 選択状態を返す
   */
   public bool GetSelected() => isSelected;

   /**
   * 選択状態を設定する
   */
   public void SetSelected(bool s, bool isForce = false)
   {
       if (s)
       {
           foreach (var item in transform.parent.GetComponentsInChildren<ScrollItem>())
               item.SetSelected(false, true);
           isSelected = true;
           GetComponent<Image>().color = color[1];
       }
       else if(isForce)
       {
           isSelected = false;
           GetComponent<Image>().color = color[0];
       }
   }
}

次にScrollViewスクリプトを開きましょう。以下のコードに書き換えて下さい。

using UnityEngine;

public class ScrollView : MonoBehaviour
{
   public GameObject viewPort;
   public GameObject content;
   public float inputHoldDelay = 0.5f;
   public float maxPerFrameScroll = 0.3f;

   private int selectItemIndex = 0;
   private float time = 0;
   private float prevPosX;
   private float nextPosX;
   private int frame = 0;

   // Start is called before the first frame update
   void Start()
   {
       ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
       if (contents.Length > 0)
           contents[selectItemIndex].SetSelected(true);
       nextPosX = prevPosX = content.transform.position.x;
   }

   // Update is called once per frame
   void Update()
   {
       if (!transform.parent.gameObject.activeInHierarchy) return;
       ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
       if (contents.Length < 1) return;
       if (frame > 0) Move(nextPosX, maxPerFrameScroll);
       selectItemIndex = GetSelectItemIndexFor(contents);
       Vector3 moveDistance;
       if (Input.anyKeyDown)
       {
           if (Input.GetKeyDown(KeyCode.RightArrow))
           {
               selectItemIndex = (selectItemIndex + 1) % contents.Length;
           }
           if (Input.GetKeyDown(KeyCode.LeftArrow))
           {
               selectItemIndex = (selectItemIndex + contents.Length - 1) % contents.Length;
           }
           moveDistance = contents[selectItemIndex].GetMoveDistance(viewPort.GetComponent<RectTransform>());
           nextPosX = content.transform.position.x + moveDistance.x;
           Move(nextPosX, maxPerFrameScroll);
           contents[selectItemIndex].SetSelected(true);
           time = 0;
           return;
       }
       time += Time.deltaTime;
       if (time < inputHoldDelay) return;
       if (!Input.anyKey) return;
       if (Input.GetKey(KeyCode.RightArrow))
       {
           selectItemIndex = (selectItemIndex + 1) % contents.Length;
       }
       if (Input.GetKey(KeyCode.LeftArrow))
       {
           selectItemIndex = (selectItemIndex + contents.Length - 1) % contents.Length;
       }
       moveDistance = contents[selectItemIndex].GetMoveDistance(viewPort.GetComponent<RectTransform>());
       nextPosX = content.transform.position.x + moveDistance.x;
       Move(nextPosX, maxPerFrameScroll);
       contents[selectItemIndex].SetSelected(true);
       time = 0;
   }

   /**
   * 選択されているアイテムのインデックスを返す
   */
   public int GetSelectItemIndex() => selectItemIndex;

   /**
   * 補完で計算してアニメーションさせる
   */
   private bool Move(float p2x, float maxPerFrame)
   {
       frame += 1;
       float c = maxPerFrame / Time.deltaTime;
       float t = frame / c;
       content.transform.position = new Vector3(prevPosX + (p2x - prevPosX) * t, content.transform.position.y);
       if (c <= frame)
       {
           frame = 0;
           content.transform.position = new Vector3(p2x, content.transform.position.y);
           prevPosX = p2x;
           return true;
       }
       return false;
   }

   /**
   * 配列から選択されているアイテムのインデックスを返す
   */
   private int GetSelectItemIndexFor(ScrollItem[] contents)
   {
       for (int i = 0; i < contents.Length; i++)
       {
           if (contents[i].GetSelected()) return i;
       }
       return 0;
   }

   /**
   * コンテンツオブジェクトの位置を移動する
   */
   public void MoveContentPosition(Vector3 p)
   {
       content.transform.position += p;
       prevPosX += p.x;
       nextPosX += p.x;
   }
}

最後にItemSlotDisplayスクリプトを開いて下さい。以下のように書き換えます。

using UnityEngine;
using UnityEngine.UI;

public class ItemSlotDisplay : MonoBehaviour
{
   public Inventory inventory;
   public GameObject content;

   private ScrollView view;
   private int prevViewSelectItemIndex;
   private int selectItemIndex;
   private int leadShowItemIndex = 0;
   private int maxShowItemNum;

   // Start is called before the first frame update
   void Start()
   {
       view = GetComponentInChildren<ScrollView>();
       selectItemIndex = prevViewSelectItemIndex = view.GetSelectItemIndex();
   }

   // Update is called once per frame
   void Update()
   {
       if (!gameObject.activeInHierarchy) return;
       int viewSelectItemIndex = view.GetSelectItemIndex();
       if (viewSelectItemIndex == prevViewSelectItemIndex) return;
       ScrollItem[] contents = content.GetComponentsInChildren<ScrollItem>();
       if (viewSelectItemIndex == prevViewSelectItemIndex + 1)
       {
           if (viewSelectItemIndex == maxShowItemNum - 1 && selectItemIndex < inventory.itemNumMax - 2)
           {
               contents[0].transform.SetParent(null);
               contents[0].transform.SetParent(content.transform);
               leadShowItemIndex++;
               Vector3 moveDistance = contents[viewSelectItemIndex].GetBackDistance(true);
               view.MoveContentPosition(moveDistance);
               ShowItemInfo();
           }
       }
       if (prevViewSelectItemIndex == maxShowItemNum - 1 && viewSelectItemIndex == 0)
       {
           leadShowItemIndex = 0;
           ShowItemInfo();
       }
       if (viewSelectItemIndex == prevViewSelectItemIndex - 1)
       {
           if (viewSelectItemIndex == 0 && selectItemIndex > 1)
           {
               for (int i = 0; i < maxShowItemNum - 1; i++)
                   contents[i].transform.SetParent(null);
               for (int i = 0; i < maxShowItemNum - 1; i++)
                   contents[i].transform.SetParent(content.transform);
               leadShowItemIndex--;
               Vector3 moveDistance = contents[viewSelectItemIndex].GetBackDistance(false);
               view.MoveContentPosition(moveDistance);
               ShowItemInfo();
           }
       }
       if (prevViewSelectItemIndex == 0 && viewSelectItemIndex == maxShowItemNum - 1)
       {
           leadShowItemIndex = inventory.itemNumMax - maxShowItemNum;
           ShowItemInfo();
       }
       selectItemIndex = leadShowItemIndex + viewSelectItemIndex;
       prevViewSelectItemIndex = viewSelectItemIndex;
   }

   /**
   * インベントリを参照し、アイテムスロットを表示する
   */
   public void Show()
   {
       maxShowItemNum = content.transform.childCount;
       for (int i = inventory.itemNumMax; i < maxShowItemNum; i++)
           content.transform.GetChild(i).gameObject.SetActive(false);
       maxShowItemNum = maxShowItemNum > inventory.itemNumMax ? inventory.itemNumMax : maxShowItemNum;
       ShowItemInfo();
   }

   /**
   * アイテムスロットに情報を付け足す
   */
   private void ShowItemInfo()
   {
       for (int i = 0; i < maxShowItemNum; i++)
       {
           Transform slot = content.transform.GetChild(i);
           slot.gameObject.SetActive(true);
           Item it = inventory.Get(i + leadShowItemIndex);
           if (it == null)
               slot.GetComponentInChildren<Text>().text = "";
           else
               slot.GetComponentInChildren<Text>().text = it.name;
       }
   }
}

ビルドしたら、ItemSlotを表示する分だけContentオブジェクト階層下に入れます(筆者は11個必要でした)。
インベントリの所持可能数を増やしたりして、ちゃんと動くかどうかテストしておきましょう。なお、端から端へのアニメーション時、少しだけアイテムスロットの中身の表示がおかしくなるかもしれませんが、仕様です。動作には問題ないのでそのままにしておきます(直せる方は直しておくと良いと思います)。

という訳で、長くなり過ぎたので今回はここまでに致します。
次回はキャラクターがアイテムを拾うようにしたいと思います。


この記事が気に入ったらサポートをしてみませんか?