見出し画像

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

前回の記事はこちら
前回はインベントリソートなどを実装しました。

宝箱の実装

今回は宝箱を実装したいと思います。
仕様はこんな感じです。

・動かない
・宝箱に向かって攻撃すると消える
・消える時にExcelAppearDataに沿ってランダムにアイテムをばら撒く
・最大5個までアイテムが入っている
・鍵などはいらない(今のところは)

宝箱のプレハブの用意

とりあえず宝箱がないと始まりませんね。宝箱のモデルを用意します。
筆者は前のステップで紹介したものを使わせて頂きます。

スクリーンショット 2020-05-16 1.47.59

まだアセットストアでダウンロードできるかは微妙なところなので、他のアセットを使った方が良いかもしれません。
モデルをインポートしたら、ヒエラルキータブ内のEnemiesのところにモデルを入れて、名前を「Chest1」にします。
そして以下のスクリプトをアタッチして下さい。

・ActorMovement
・ActorParamsController
・ItemInventory(所持可能数を5にしておく)

それができたらプレハブ化し、ヒエラルキータブ内からは削除します。

EActorに追記

今回宝箱は一種の敵キャラクターとして扱う予定なので、EActorに追記します。

CHEST = 1001

データファイルの用意

ExcelActorDataファイルに以下のように追記します。
まずはDataシートです。

スクリーンショット 2020-10-28 22.31.12

次に「Chest1」シートを作成し、データを記述します。

スクリーンショット 2020-10-28 22.40.55

筆者は上記のようになりました。攻撃したらすぐに開けられるようにしたい場合はhpを1などにし、宝箱を開けた時も経験値を得られるようにする場合はxpに0より大きい値を入力して下さい。
それができたら、取り込む前にExcelActorDataスクリプトに以下を追記して下さい。

public List<Params> Chest1;

次にExcelAppearDataファイルに「ChestAppear」シートを作成し、以下を記述します。

スクリーンショット 2020-10-29 0.28.16

nummin1には宝箱の中身の最少数、nummax1には最大数、rate1には宝箱が出現する確率(0で確実に出現せず100で確実に出現)を記述します。
ExcelAppearDataファイルを取り込む前に同名のスクリプトに以下を追記して下さい。

// パラメーター
public List<ChestAppearData> ChestAppear;

// 内部クラス
[System.Serializable]
public class ChestAppearData
{
   public int floor;
   public EActor id1;
   public int lvmin1;
   public int lvmax1;
   public int rate1;
   public int nummin1;
   public int nummax1;
}

宝箱を出現させる:固定マップ編

それではコードを書いてみます。
Fieldクラスを変更します。

// メソッドを追加
/**
* フィールドに宝箱を設置する
* nameに「RandomAppear」が指定されていた場合はその場所に宝箱を置くかどうか判定してから設置する
* nameにコンマ区切りでアイテムIDが指定されていればその分だけアイテムが入る
* (例:「FOOD01,WEAPON01」ならリンゴとブーツが入る)
* ただし宝箱のインベントリの所持可能数以上は入らず、仕様上6つ以上は消える
* アイテムIDではなく「Random」であればランダムなアイテムが入る
*/
private void SetChest(string name, int xgrid, int zgrid)
{
   EActor actorId = EActor.CHEST;
   string[] itemIds;
   ExcelAppearData.ChestAppearData c =
           appearDatabase.ChestAppear.Find(n => n.floor == floorNum);
   if (name.Equals("RandomAppear"))
   {
       int p = Random.Range(0, 100);
       if (p >= c.rate1) return;
       actorId = c.id1;
       p = Random.Range(c.nummin1 - 1, c.nummax1 + 1);
       name = "Random";
       for (int i = 0; i < p; i++) name += ",Random";
   }
   ExcelActorData.ActorData actorData = actorDatabase.Data.Find(n => n.id == actorId);
   if (actorData == null) return;
   int lv = 1;
   if (actorId == c.id1) lv = Random.Range(c.lvmin1, c.lvmax1 + 1);
   GameObject chestObj = (GameObject)Resources.Load("Prefabs/" + actorData.prefab);
   GameObject chest = Instantiate(chestObj, enemies.transform);
   chest.GetComponent<ActorMovement>().SetPosition(xgrid, zgrid);
   chest.GetComponent<ActorParamsController>().SetLv(lv);
   name = name.Replace(" ", "");
   itemIds = name.Split(',');
   for (int i = 0; i < itemIds.Length; i++)
   {
       Item itemData = GetItem(itemIds[i]);
       if (itemData == null) continue;
       chest.GetComponent<ItemInventory>().Add(itemData);
   }
}

/**
* アイテムIDに応じたアイテムを返す
* nameに「Random」が指定されていた場合はランダムなアイテムを返す
*/
private Item GetItem(string name)
{
   Item itemData;
   EItem itemId = EItem.NONE;
   if (name.Equals("Random"))
   {
       List<ExcelAppearData.ItemAppearData> cItems =
           appearDatabase.ItemAppear.FindAll(n => !n.shoponly && n.start <= floorNum && n.end >= floorNum);
       int maxRate = 0;
       foreach (var cItem in cItems) maxRate += cItem.rate;
       int p = Random.Range(1, maxRate + 1);
       foreach (var cItem in cItems)
       {
           p -= cItem.rate;
           if (p <= 0)
           {
               itemId = cItem.id;
               break;
           }
       }
   }
   else if (!System.Enum.TryParse(name, out itemId)) return null;
   if ((int)itemId > 1000) itemData = itemDatabase.Goods.Find(n => n.id == itemId).Get();
   else itemData = itemDatabase.Equipments.Find(n => n.id == itemId).Get();
   if (itemData == null) return null;
   itemData.SetExtParams(appearDatabase.ItemPlusMax.Find(n => n.type == itemData.type));
   return itemData;
}

// メソッドを変更
private void SetItem(string name, int xgrid, int zgrid)
{
   Item itemData = GetItem(name);
   if (itemData == null) return;
   GameObject itemObj = (GameObject)Resources.Load("Prefabs/" + itemData.prefab);
   GameObject item = Instantiate(itemObj, items.transform);
   item.GetComponent<ItemMovement>().SetPosition(xgrid, zgrid);
   item.GetComponent<ItemParamsController>().SetParams(itemData);
}

public void SetObject(string name, string type, int xgrid, int zgrid, int width, int height)
{
   switch (type)
   {
       /*   省略   */
       case "Enemy":
           SetEnemy(name, xgrid, zgrid);
           break;
       case "Item":
           SetItem(name, xgrid, zgrid);
           break;
       // 以下3行を追記
       case "Chest":
           SetChest(name, xgrid, zgrid);
           break;
   }
}

ActorActionクラスも変更します。

private void ActBegin()
{
   actorAttack.Attack();
   Pos2D grid = DirUtil.GetNewGrid(actorMovement.grid, actorMovement.direction);
   GameObject actor = GetComponentInParent<Field>().GetExistActor(grid.x, grid.z);
   // 以下1行を変更
   if(actor != null && actor.GetComponent<ActorParamsController>().parameter.id < EActor.CHEST)
   {
       actor.GetComponent<ActorMovement>().TurnAround(actorMovement.direction);
       actor.GetComponent<ActorAttack>().Damaged();
   }
   action = EAct.Act;
}

ビルドしたら、マップデータファイルの固定マップ階に以下のようなオブジェクトを置いてみて下さい。

Typeは全て「Chest」とする
・「RandomAppear」という名前のオブジェクト
・「Random,Random,Random」という名前のオブジェクト
・「FOOD01,WEAPON01」という名前のオブジェクト

スクリーンショット 2020-10-29 1.39.56

実行してみます。

スクリーンショット 2020-10-29 1.55.03

左上は確率が低すぎて出現していない

スクリーンショット 2020-10-29 1.55.55

宝箱を開けてみた

攻撃して開けてみると分かるのですが、これには問題点が二つあります。
一つは宝箱を開けた際にも魔石が出現してしまうこと。
もう一つは「宝箱を倒した」というメッセージが表示されてしまうこと。
これらを改善するには実際に処理をしているActorParamsControllerクラスのDeathJudgementメソッドを変更すると良いでしょう。

private bool DeathJudgment()
{
   if (parameter.hp <= 0)
   {
       Field field = GetComponentInParent<Field>();
       if (parameter.id >= EActor.CHEST)
       {
           Message.Add(38, actorName);
           ItemInventory inventory = GetComponent<ItemInventory>();
           foreach (Item it in inventory.GetAllItem())
           {
               if (!DropItem(it)) break;
           }
       }
       else if (parameter.id > EActor.PLAYER)
       {
           /*   省略   */
       }
           
       field.GetComponent<EffectManager>().Play(EffectManager.EType.Death, field.gameObject, transform.position);
       Destroy(gameObject);
       return true;
   }
   return false;
}

追加したメッセージはこちら。

スクリーンショット 2020-10-29 2.14.00

実行してみます。

スクリーンショット 2020-10-29 2.16.29

魔石が落ちなくなり、メッセージも専用のものになりました。

宝箱を出現させる:ランダム生成マップ編

最後にランダム生成マップでも宝箱が出現するようにしてみましょう。
LoadFieldMapクラスを変更して下さい。

[System.Serializable]
public class DungeonInfo
{
   public int roomSizeMax = 5;
   public int roomSizeMin = 3;
   public int outerMargin = 3;
   public int posMargin = 2;
   public int connectRate = 50;
   public int enemyNumMin = 0;
   public int itemNumMin = 0;
   public int enemyNumMax = 0;
   public int itemNumMax = 0;
   // 以下1行を追記
   public int chestNumMax = 0;
   public int isStartEnd = -1;
}

private Array2D ReadMapFile(string path)
{
   try
   {
       /*   省略   */
       if (data == null)
       {
           DungeonInfo info = new DungeonInfo();
           info.roomSizeMin = dungeonInfo.roomSizeMin;
           info.roomSizeMax = dungeonInfo.roomSizeMax;
           info.outerMargin = dungeonInfo.outerMargin;
           info.posMargin = dungeonInfo.posMargin;
           info.connectRate = dungeonInfo.connectRate;
           info.enemyNumMin = dungeonInfo.enemyNumMin;
           info.enemyNumMax = dungeonInfo.enemyNumMax;
           info.itemNumMin = dungeonInfo.itemNumMin;
           info.itemNumMax = dungeonInfo.itemNumMax;
           // 以下1行を追記
           info.chestNumMax = dungeonInfo.chestNumMax;
           info.isStartEnd = dungeonInfo.isStartEnd;
           foreach (var prop in group.Element("properties").Elements("property"))
           {
               int num = int.Parse(prop.Attribute("value").Value);
               switch (prop.Attribute("name").Value)
               {
                   case "RoomSizeMin":
                       info.roomSizeMin = num;
                       break;
                   case "RoomSizeMax":
                       info.roomSizeMax = num;
                       break;
                   case "OuterMargin":
                       info.outerMargin = num;
                       break;
                   case "PosMargin":
                       info.posMargin = num;
                       break;
                   case "ConnectRate":
                       info.connectRate = num;
                       break;
                   case "EnemyNumMin":
                       info.enemyNumMin = num;
                       break;
                   case "EnemyNumMax":
                       info.enemyNumMax = num;
                       break;
                   case "ItemNumMin":
                       info.itemNumMin = num;
                       break;
                   case "ItemNumMax":
                       info.itemNumMax = num;
                       break;
                   // 以下3行を追記
                   case "ChestNumMax":
                       info.chestNumMax = num;
                       break;
                   case "IsStartEnd":
                       info.isStartEnd = num;
                       break;
               }
           }
           field.enemyNumMin = info.enemyNumMin;
           data = dungeon.Create(w, h, field, info);
       }
       return data;
   }
   catch (System.Exception i_exception)
   {
       Debug.LogErrorFormat("{0}", i_exception);
   }
   return null;
}

更に内部クラスのRandomDungeonにも追記します。

private void SetObject(string name, string type, Field field, Array2D data)
{
   // 以下1行を追記
   int c = data.width * data.height;
   while (true)
   {
       // 以下2行を追記
       c--;
       if (c < 0) break;
       int areaIdx = Random.Range(0, areas.Count);
       Rect2D room = areas[areaIdx].room;
       int x = Random.Range(room.left, room.right + 1);
       int y = Random.Range(room.top, room.bottom + 1);
       if (data.Get(x, y) != 0) continue;
       data.Set(x, y, 1);
       field.SetObject(name, type, x, y, 1, 1);
       break;
   }
}

private void SetObjects(Field field)
{
   Array2D tmpData = new Array2D(data.width, data.height);
   for (int x = 0; x < data.width; x++)
   {
       /*   省略   */
   }
   SetObject("UpStairs", "Stairs", field, tmpData);
   SetObject("DownStairs", "Stairs", field, tmpData);
   if (info.isStartEnd == 0)
   {
       /*   省略   */
   }
   int num = Random.Range(info.enemyNumMin, info.enemyNumMax + 1);
   for (int i = 0; i < num; i++)
       SetObject("Random", "Enemy", field, tmpData);
   num = Random.Range(info.itemNumMin, info.itemNumMax + 1);
   for (int i = 0; i < num; i++)
       SetObject("Random", "Item", field, tmpData);
   // 以下2行を追記
   for (int i = 0; i < info.chestNumMax; i++)
       SetObject("RandomAppear", "Chest", field, tmpData);
}

chestNumMaxは宝箱の出現最大数を表します。
また、今回からオブジェクトを配置する際に試行回数上限を設けました。
こうすることでループから抜け出せないという事態を防ぐことができます。
ビルドしたら、コンポーネントのChest Num Maxに1以上の値を入力するか、マップデータファイルのカスタムプロパティに「ChestNumMax」を追加し、1以上の値にして下さい。
実行してみます。下の画像のように宝箱が出てくればOKです。

スクリーンショット 2020-10-29 2.57.48

宝箱が3つ出現した

もし何度やっても出てこないという方は、Chest Num Maxの値をもっと増やしたり、敵やアイテムの数を減らしたり、フィールドを大きくしたりしてみて下さい。
それでも駄目なら、ExcelAppearDataファイルのChestAppearシートのrate1に100以上の値を入力してみましょう。確実に出現するはずです。

ということで今回はここまでです。
次回はターン数のセーブ処理の実装から始めたいと思います。

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