見出し画像

【Unity】3Dローグライクゲームの作り方〜Step11-14〜

前回の記事はこちら
前回はアクセサリーを実装しました。

敵のアイテムドロップ

今回は敵がアイテムを落とすようにします。
落とすアイテムは以下の通りです。

・魔石(確定でドロップ)
・その敵が拾ったアイテム

お金はドロップしません。代わりに魔石を換金することでお金を手に入れられるようにします(+値で変動)。魔石はインベントリとは別枠で保存します。
また、敵に拾われたアイテムもドロップし、プレイヤーが拾えるようにします。

魔石の用意

EItemとEItemTypeに以下を追記します。

// EItem
STONE01 = 2001

// EItemType
Stone

そしてプレハブを作成します。
次にExcelItemDataファイルに以下のように記述します。

スクリーンショット 2020-10-23 10.23.10

筆者はこれで完了しますが、もしフィールド上にも出現させたければ、ExcelAppearDataにも記述しておきましょう。

魔石用インベントリの作成

それではコードを書いていきます。
まずInventoryクラスの名前を「ItemInventory」にし、その上の基底クラスを改めて「Inventory」クラスにします。
そして作成したInventoryスクリプトに以下のように記述して下さい。

using System.Collections.Generic;
using UnityEngine;

public abstract class Inventory : MonoBehaviour
{
   public int itemNumMax;
   public List<Item> items;

   /**
   * もし所持可能数を超えていなければ、アイテムをインベントリに加える
   */
   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;
   }

   /**
   * もしあれば足下にあるアイテムを返す
   */
   public Item GetFootItem()
   {
       Pos2D grid = GetComponent<ActorMovement>().grid;
       GameObject obj = GetComponentInParent<Field>().GetExistItem(grid.x, grid.z);
       if (obj == null) return null;
       return obj.GetComponent<ItemParamsController>().parameter;
   }

   /**
   * インベントリから指定したアイテムを削除する
   */
   public bool Remove(Item it)
   {
       if (it != null)
       {
           return items.Remove(it);
       }
       return false;
   }

   /**
   * インベントリがいっぱいかどうか
   */
   public bool IsFull() => items.Count >= itemNumMax;

   /**
   * インベントリに入っている全てのアイテムの配列を返す
   */
   public Item[] GetAllItem()
   {
       Item[] itemList = new Item[items.Count];
       for (int i = 0; i < items.Count; i++)
       {
           itemList[i] = items[i].Get();
       }
       return itemList;
   }

   /**
   * インベントリの中身を全て入れ替える
   */
   public abstract void SetAllItem(Item[] itemList, int maxNum);
}

そしてItemInventoryスクリプトには以下のように記述します。

using System.Collections.Generic;

public class ItemInventory : Inventory
{
   /**
   * インベントリの中身を全て入れ替える
   */
   public override void SetAllItem(Item[] itemList, int maxNum)
   {
       List<Item> items = new List<Item>();
       ActorParamsController.Equipment equipment = new ActorParamsController.Equipment();
       int itemCnt = itemList.Length < maxNum ? itemList.Length : maxNum;
       for (int i = 0; i < itemCnt; i++)
       {
           items.Add(itemList[i].Get());
           if (items[i].isEquip)
           {
               if (items[i].type == EItemType.Weapon)
               {
                   if (equipment.weapon == null) equipment.weapon = items[i];
                   else items[i].isEquip = false;
               }
               if (items[i].type == EItemType.Armor)
               {
                   if (equipment.armor == null) equipment.armor = items[i];
                   else items[i].isEquip = false;
               }
               if (items[i].type == EItemType.Accessory)
               {
                   if (equipment.accessory == null) equipment.accessory = items[i];
                   else items[i].isEquip = false;
               }
           }
       }
       itemNumMax = maxNum;
       this.items = items;
       GetComponent<ActorParamsController>().equipment = equipment;
   }
}

「StoneInventory」スクリプトを作成し、以下を記述して下さい。

using System.Collections.Generic;

public class StoneInventory : Inventory
{
   /**
   * もし所持可能数を超えていなければ、魔石をインベントリに加える
   */
   public new bool Add(Item it)
   {
       if (it.type == EItemType.Stone)
       {
           return base.Add(it);
       }
       return false;
   }

   /**
   * インベントリから指定した魔石を削除する
   */
   public new bool Remove(Item it)
   {
       if (it != null && it.type == EItemType.Stone)
       {
           return items.Remove(it);
       }
       return false;
   }

   /**
   * インベントリの中身を全て入れ替える
   */
   public override void SetAllItem(Item[] itemList, int maxNum)
   {
       List<Item> items = new List<Item>();
       int itemCnt = itemList.Length < maxNum ? itemList.Length : maxNum;
       for (int i = 0; i < itemCnt; i++)
       {
           items.Add(itemList[i].Get());
       }
       itemNumMax = maxNum;
       this.items = items;
   }
}

新しく魔石専用のインベントリを用意しました。これをPlayerにアタッチしておきます。
更にInventoryActionクラスも変更します。

// パラメーターを追加
public ItemInventory itemInventory;
public StoneInventory stoneInventory;

// メソッドを変更
private void KeyInput()
{
   if (Input.anyKeyDown)
   {
       if (!isOpen && Input.GetKeyDown(KeyCode.R))
       {
           display.inventory = stoneInventory;
           action = EAct.MoveBegin;
       }
       else if (Input.GetKeyDown(KeyCode.E))
       {
           display.inventory = itemInventory;
           action = EAct.MoveBegin;
       }
   }
}

private void SelectItem()
{
   if (Input.anyKeyDown && Input.GetKeyDown(KeyCode.Space))
   {
       string choiceOrder = "";
       selectItem = display.GetSelectFootItem();
       if (selectItem != null)
       {
           if (selectItem.type == EItemType.Stone)
           {
               if (stoneInventory.IsFull()) return;
               else choiceOrder = "PickUp";
           }
           else
           {
               if (itemInventory.IsFull()) choiceOrder = "PickUpUse, PickUpThrow";
               else choiceOrder = "PickUp, PickUpUse, PickUpThrow";
           }
           if ((int)selectItem.id < 1001) choiceOrder = choiceOrder.Replace("PickUpUse,", "");
           subMenu.SetChoices(choiceOrder);
           subMenu.Show();
           return;
       }
       selectItem = display.GetSelectItem();
       if (selectItem != null)
       {
           if (selectItem.isEquip) choiceOrder = "Remove";
           else
           {
               Item footItem = display.inventory.GetFootItem();
               if (selectItem.type == EItemType.Stone)
               {
                   if (footItem == null) choiceOrder = "Put";
                   else if (footItem.type == EItemType.Stone || !itemInventory.IsFull())
                       choiceOrder = "Replace";
                   else return;
               }
               else
               {
                   if (footItem == null) choiceOrder = "Use, Put, Throw";
                   else if (footItem.type == EItemType.Stone && stoneInventory.IsFull())
                       choiceOrder = "Use, Throw";
                   else choiceOrder = "Use, Replace, Throw";
               }
               if ((int)selectItem.id < 1001) choiceOrder = choiceOrder.Replace("Use", "Equip");
           }
           subMenu.SetChoices(choiceOrder);
           subMenu.Show();
       }
   }
}

Rキーを押すと魔石専用インベントリが表示されるようになりました。
後、ActorUseItemsクラスも一部変更しておきます。

// InventoryをItemInventoryに変更
public ItemInventory inventory;

// メソッドを変更
public void PickUp()
{
   GameObject item = GetComponentInParent<Field>().GetExistItem(move.grid.x, move.grid.z);
   if (item == null) return;
   Item it = item.GetComponent<ItemParamsController>().parameter;
   bool isEnd = false;
   if (it.type == EItemType.Stone)
   {
       StoneInventory si = GetComponent<StoneInventory>();
       if (si == null) isEnd = inventory.Add(it);
       else isEnd = si.Add(it);
   }
   else isEnd = inventory.Add(it);
   if (isEnd)
   {
       Destroy(item);
       Message.Add(8, param.actorName, it.name);
   }
   else
   {
       if (param.parameter.id == 0) Message.Add(9);
       Message.Add(10, param.actorName, it.name);
   }
}

public bool Put(Item it)
{
   Message.Add(11, it.name);
   GameObject item = GetComponentInParent<Field>().GetExistItem(move.grid.x, move.grid.z);
   if (item != null) Destroy(item);
   GameObject items = GetComponentInParent<Field>().items;
   GameObject itemObj = (GameObject)Resources.Load("Prefabs/" + it.prefab);
   item = Instantiate(itemObj, items.transform);
   item.GetComponent<ItemMovement>().SetPosition(move.grid.x, move.grid.z);
   item.GetComponent<ItemParamsController>().SetParams(it);
   if (it.type == EItemType.Stone)
   {
       StoneInventory si = GetComponent<StoneInventory>();
       if (si == null) inventory.Remove(it);
       else si.Remove(it);
   }
   else inventory.Remove(it);
   return true;
}

public bool PickUp(Item it)
{
   bool isEnd = false;
   if (it.type == EItemType.Stone)
   {
       StoneInventory si = GetComponent<StoneInventory>();
       if (si == null) isEnd = inventory.Add(it);
       else isEnd = si.Add(it);
   }
   else isEnd = inventory.Add(it);
   if (isEnd)
   {
       GameObject item = GetComponentInParent<Field>().GetExistItem(move.grid.x, move.grid.z);
       if (item != null) Destroy(item);
       Message.Add(8, param.actorName, it.name);
   }
   return true;
}

public bool Replace(Item it)
{
   Message.Add(11, it.name);
   GameObject item = GetComponentInParent<Field>().GetExistItem(move.grid.x, move.grid.z);
   GameObject items = GetComponentInParent<Field>().items;
   GameObject itemObj = (GameObject)Resources.Load("Prefabs/" + it.prefab);
   GameObject item2 = Instantiate(itemObj, items.transform);
   item2.GetComponent<ItemMovement>().SetPosition(move.grid.x, move.grid.z);
   item2.GetComponent<ItemParamsController>().SetParams(it);
   if (it.type == EItemType.Stone)
   {
       StoneInventory si = GetComponent<StoneInventory>();
       if (si == null) inventory.Remove(it);
       else si.Remove(it);
   }
   else inventory.Remove(it);
   if (item != null)
   {
       it = item.GetComponent<ItemParamsController>().parameter;
       Message.Add(8, param.actorName, it.name);
       if (it.type == EItemType.Stone)
       {
           StoneInventory si = GetComponent<StoneInventory>();
           if (si == null) inventory.Add(it);
           else si.Add(it);
       }
       else inventory.Add(it);
       Destroy(item);
   }
   return true;
}

ビルドしたら、Inventory Actionコンポーネントのインベントリの項目を設定しておきましょう。
これで実行してみます。いろいろ試してみて下さい。
(もし上手く動かないことがあればご連絡下さい)

(オプション)InventoryEditorクラス

折角のエディタ拡張が非表示になってしまったので、直します。

[CustomEditor(typeof(ItemInventory))]
[CanEditMultipleObjects]
public class ItemInventoryEditor : Editor
{
   private ReorderableList list;
   private ItemInventory inventory;
   private EItem id;

   void OnEnable()
   {
       inventory = (ItemInventory)target;
       /*   以下省略   */

​折角なのでランダムで+が付くようにしてみましょう。

private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
   if (isFocused)
   {
       ExcelItemData database = Resources.Load<ExcelItemData>("Datas/ExcelItemData");
       Item item;
       if ((int)id > 1000) item = database.Goods.Find(n => n.id == id).Get();
       else item = database.Equipments.Find(n => n.id == id).Get();
       if (item != null && item.id != inventory.items[index].id)
       {
           SetItem(item, ref item);
           inventory.items[index] = item;
       }
   }
   EditorGUI.LabelField(rect, inventory.items[index].name);
}

​private void SetItem(Item it, ref Item parameter)
{
   parameter = it.Get();
   if (parameter.type != EItemType.Unknown && parameter.type != EItemType.Magic && parameter.type != EItemType.Stone)
   {
       parameter.SetExtParams(Resources.Load<ExcelAppearData>("Datas/ExcelAppearData").ItemPlusMax.Find(n => n.type == it.type));
   }
}

スクリーンショット 2020-10-23 15.57.46

こんな感じになります。

(オプション)StoneInventoryEditorクラス

魔石用のインベントリも自由に魔石を入れられて、+値を選択できたら良いですね。
ただ、今のItemクラスでは拡張パラメーターが入力しづらいので、追記します。

/**
* アイテムデータの+値を設定する
*/
public void SetExtParams(int atk, int def, int hp, int food)
{
   extData.atk = atk;
   extData.def = def;
   extData.hp = hp;
   extData.food = food;
}

それではEditorフォルダ内に「StoneInventoryEditor」スクリプトを作成して下さい。

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

[CustomEditor(typeof(StoneInventory))]
[CanEditMultipleObjects]
public class StoneInventoryEditor : Editor
{
   private ReorderableList list;
   private StoneInventory inventory;
   private int plus;

   void OnEnable()
   {
       inventory = (StoneInventory)target;
       list = new ReorderableList(
           inventory.items,
           typeof(Item)
       );
       list.drawElementCallback += DrawElement;
       list.onCanAddCallback += CanAdd;
       list.drawHeaderCallback += DrawHeader;
   }

   public override void OnInspectorGUI()
   {
       serializedObject.Update();
       inventory.itemNumMax = Mathf.Max(0, EditorGUILayout.IntField("所持可能数", inventory.itemNumMax));
       list.DoLayoutList();
       plus = EditorGUILayout.IntSlider(plus, 0, 10);
       serializedObject.ApplyModifiedProperties();
   }

   private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
   {
       if (isFocused)
       {
           ExcelItemData database = Resources.Load<ExcelItemData>("Datas/ExcelItemData");
           Item item;
           item = database.Goods.Find(n => n.id == EItem.STONE01).Get();
           if (item != null && item.id != inventory.items[index].id)
           {
               SetItem(item, ref item);
               inventory.items[index] = item;
           }
       }
       EditorGUI.LabelField(rect, inventory.items[index].name);
   }

   private bool CanAdd(ReorderableList list)
   {
       return inventory.items.Count < inventory.itemNumMax;
   }

   private void DrawHeader(Rect rect)
   {
       EditorGUI.LabelField(rect, "所持魔石");
   }

   private void SetItem(Item it, ref Item parameter)
   {
       parameter = it.Get();
       parameter.SetExtParams(0, 0, plus, 0);
   }
}

スクリーンショット 2020-10-23 16.24.56

こんな感じで入力できます。

敵を倒すと魔石がドロップ

前置きが随分と長くなってしまいましたが、ようやっと本題です。
まずは拾われたアイテム関係なしに魔石がドロップするようにしましょう。
先にItemMovementクラスに新しいメソッドを加えて下さい。

/**
* ドロップ
*/
public bool Drop()
{
   Field field = GetComponentInParent<Field>();
   GameObject itemObj = field.GetExistItem(grid.x, grid.z);
   if (itemObj == null || itemObj.Equals(gameObject)) return true;
   foreach (EDir d in System.Enum.GetValues(typeof(EDir)))
   {
       if (d == EDir.Pause) continue;
       Pos2D newP = DirUtil.Move(field, grid, d, false);
       if (newP.Equals(grid)) continue;
       if (field.GetExistItem(newP.x, newP.z) == null)
       {
           SetPosition(newP.x, newP.z);
           return true;
       }
   }
   Destroy(gameObject);
   return false;
}

基本的にはアイテムを投げた時に落ちる場所を探すGetFallPositionメソッドと同じですが、キャラクターがいるいないにかかわらず処理するところと、落ちる場所がなければ自然消滅するところが違います。
次にActorParamsControllerクラスを変更します。

// パラメーターを追加
public EItem dropStone = EItem.STONE01;

// メソッドを変更
private bool DeathJudgment()
{
   if (parameter.hp <= 0)
   {
       Field field = GetComponentInParent<Field>();
       if (parameter.id > EActor.PLAYER)
       {
           Message.Add(28, actorName);
           ExcelItemData database = Resources.Load<ExcelItemData>("Datas/ExcelItemData");
           Item item = database.Goods.Find(n => n.id == dropStone).Get();
           item.SetExtParams(0, 0, Random.Range(0, parameter.lv + 1), 0);
           GameObject items = GetComponentInParent<Field>().items;
           GameObject itemObj = (GameObject)Resources.Load("Prefabs/" + item.prefab);
           GameObject dropItem = Instantiate(itemObj, items.transform);
           ActorMovement move = GetComponent<ActorMovement>();
           dropItem.GetComponent<ItemParamsController>().SetParams(item);
           ItemMovement itmove = dropItem.GetComponent<ItemMovement>();
           itmove.SetPosition(move.grid.x, move.grid.z);
           itmove.Drop();
       }
       GameObject effectObj = (GameObject)Resources.Load("Prefabs/DeathEffect");
       GameObject effect = Instantiate(effectObj, field.transform);
       effect.transform.position += transform.position;
       Destroy(gameObject);
       return true;
   }
   return false;
}

dropStoneには落とす魔石の種類を入力します。
DeathJudgementメソッドでの基本的な流れはアイテムのインスタンスを作成し、敵の位置に出現させ、落ちることのできる場所に落とすといった感じです。また、落ちる魔石は敵のLvが上がるごとに大きい+値が出る可能性があるようにしました。
実行してみます。

スクリーンショット 2020-10-23 18.00.54

敵のいた場所にアイテムがない場合

スクリーンショット 2020-10-23 17.50.25

敵のいた場所(リンゴの位置)にアイテムがあった場合

敵を倒すと持っているアイテムも落とす

次に持っているアイテムも周りに散らばるようにしましょう。
ActorParamsControllerクラスを以下のように変更して下さい。

// メソッドを追加
/**
* アイテムを落とす
*/
private bool DropItem(Item it)
{
   GameObject items = GetComponentInParent<Field>().items;
   GameObject itemObj = (GameObject)Resources.Load("Prefabs/" + it.prefab);
   GameObject dropItem = Instantiate(itemObj, items.transform);
   ActorMovement move = GetComponent<ActorMovement>();
   dropItem.GetComponent<ItemParamsController>().SetParams(it);
   ItemMovement itmove = dropItem.GetComponent<ItemMovement>();
   itmove.SetPosition(move.grid.x, move.grid.z);
   return itmove.Drop();
}

// メソッドを変更
private bool DeathJudgment()
{
   if (parameter.hp <= 0)
   {
       Field field = GetComponentInParent<Field>();
       if (parameter.id > EActor.PLAYER)
       {
           Message.Add(28, actorName);
           ExcelItemData database = Resources.Load<ExcelItemData>("Datas/ExcelItemData");
           Item item = database.Goods.Find(n => n.id == dropStone).Get();
           item.SetExtParams(0, 0, Random.Range(0, parameter.lv + 1), 0);
           if (DropItem(item))
           {
               ItemInventory inventory = GetComponent<ItemInventory>();
               foreach (Item it in inventory.GetAllItem())
               {
                   if (!DropItem(it)) break;
               }
           }
       }
       GameObject effectObj = (GameObject)Resources.Load("Prefabs/DeathEffect");
       GameObject effect = Instantiate(effectObj, field.transform);
       effect.transform.position += transform.position;
       Destroy(gameObject);
       return true;
   }
   return false;
}

アイテムをドロップする処理は共通の為メソッド化しました。
また、一度でも落ちる場所がないと判定されれば、それ以上の処理は打ち切ることにしています。
実行してみます。敵に何らかのアイテムを持たせておくと確認が楽です。

スクリーンショット 2020-10-23 18.35.02

プレイヤーと重なっていてわかりづらいですが、リンゴもちゃんと落ちました!

魔石インベントリのセーブ

最後に魔石用インベントリを保存・読み込みできるようにします。
ActorSaveDataスクリプトを以下に変更します。

[System.Serializable]
public class ActorSaveData
{
   public Pos2D grid;
   public EDir direction;
   public Params parameter;
   public ActorParamsController.Condition[] conditions;
   public InventorySaveData itemInventory;
   public InventorySaveData stoneInventory;
}

SaveDataManagerクラスのメソッドを変更します。

private ActorSaveData MakeActorData(Transform actor)
{
   /*   省略   */
   ActorParamsController param = actor.GetComponent<ActorParamsController>();
   actorSaveData.parameter = param.GetParameter();
   actorSaveData.conditions = param.GetConditions();
   actorSaveData.itemInventory = MakeInventoryData(actor.GetComponent<ItemInventory>());
   if (param.parameter.id == EActor.PLAYER)
       actorSaveData.stoneInventory = MakeInventoryData(actor.GetComponent<StoneInventory>());
   return actorSaveData;
}

private void LoadActorData(ActorSaveData data, Transform actor)
{
   /*   省略   */
   ActorParamsController param = actor.GetComponent<ActorParamsController>();
   param.SetParameter(data.parameter);
   param.SetConditions(data.conditions);
   LoadInventoryData(data.itemInventory, actor.GetComponent<ItemInventory>());
   if (data.parameter.id == EActor.PLAYER)
       LoadInventoryData(data.stoneInventory, actor.GetComponent<StoneInventory>());
}

private InventorySaveData MakeInventoryData(Inventory inventory)
{
   InventorySaveData inventorySaveData = new InventorySaveData();
   inventorySaveData.items = inventory.GetAllItem();
   inventorySaveData.maxItemNum = inventory.itemNumMax;
   return inventorySaveData;
}

private void LoadInventoryData(InventorySaveData data, Inventory inventory)
{
   inventory.SetAllItem(data.items, data.maxItemNum);
}

ビルドしたら、セーブして、ロードしてみます。
期待通り動けばOKです。
SaveDataEditorでも確認してみましょう。

スクリーンショット 2020-10-23 19.33.06

スクリーンショット 2020-10-23 19.33.13

ちゃんと取得できています。

という訳で今回はここまでにします。
次回はドリルを作って壁を崩してみましょう。

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