見出し画像

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

前回の記事はこちら
前回はやられモーションを実装しました。

インベントリソートの実装

今回はインベントリソート機能を実装するところから始めたいと思います。
今回のインベントリソートは主に以下のような形を想定しています。​

・インベントリを開いた時にRキーを押すとソートされる
・アイテムインベントリの場合はアイテムID順に昇順ソート、ただし装備中のアイテムがある場合はそれを一番前に持ってくる
・魔石インベントリの場合は+値の順に昇順ソート
・ソート後にRキーをもう一度押すと順番が逆になる

早速実装していきましょう。
まず最初にItemクラスを変更して下さい。

// publicにする
public int extPoint { get { return extData.GetPlus(); } }

次にInventoryクラスを変更して下さい。

// 新しいパラメーターとメソッドを追加
private bool isSort = false;

/**
* インベントリ内をアイテムID->+値でソート
* ただし装備中アイテムは前に持ってくる
*/
public void Sort()
{
   System.Comparison<Item> s = (a, b) =>
   {
       int i = isSort ? -1 : 1;
       if (!a.isEquip && b.isEquip) return i;
       if (a.isEquip && !b.isEquip) return -i;
       if (a.id == b.id) return (a.extPoint - b.extPoint) * i;
       return (a.id - b.id) * i;
   };
   items.Sort(s);
   isSort = !isSort;
}

// Addメソッドに追記
public bool Add(Item it)
{
   if (items.Count < itemNumMax)
   {
       items.Add(it);
       isSort = false; // これを追記
       return true;
   }
   return false;
}

次にInventoryActionクラスに追記します。

private void KeyInput()
{
   if (Input.anyKeyDown)
   {
       if (!isOpen && Input.GetKeyDown(KeyCode.R))
       {
           /*   省略   */
       }
       // 以下5行を追記
       else if (Input.GetKeyDown(KeyCode.R))
       {
           display.inventory.Sort();
           display.Show();
       }
       else if (Input.GetKeyDown(KeyCode.E))
       {
           /*   省略   */
       }
   }
}

実行してみます。

スクリーンショット 2020-10-27 3.07.12

この状態でスタート

スクリーンショット 2020-10-27 3.07.38

装備していない時にソート

スクリーンショット 2020-10-27 3.06.57

装備中にソート

スクリーンショット 2020-10-27 3.08.33

魔石インベントリ表示中にRキーを2回押した場合

もしアイテムIDではなく独自のソート順で並べ替えたい場合は、アイテムデータにソート値(例えばsortなど)を追加し、上記のSortメソッドのidをsortに置き換えることで可能です。

敵の無限湧き

これだけだと味気ないので敵が少なくなったら増やす機能もつけたいと思います。
Fieldクラスに新しいメソッドとパラメーターを追加します。

// パラメーターを追加
public int enemyNumMin;

// メソッドを追加
/*
* 敵の数が規定数より少なくなりそうなら、プレイヤーのいない範囲で追加する
*/
public void AddEnemy()
{
   if (enemyNumMin < enemies.transform.childCount) return;
   int x1 = playerMovement.grid.x - 8;
   int x2 = playerMovement.grid.x + 8;
   int z1 = playerMovement.grid.z - 5;
   int z2 = playerMovement.grid.z + 5;
   List<ObjectPosition> croom = new List<ObjectPosition>();
   foreach (var room in rooms.GetComponentsInChildren<ObjectPosition>())
   {
       int lx = room.grid.x + room.range.left;
       int rx = lx + room.range.width;
       int tz = room.grid.z + room.range.top;
       int bz = tz + room.range.height;
       if (x1 < rx && lx < x2 && z1 < bz && tz < z2) continue;
       croom.Add(room);
   }
   while (true)
   {
       int roomIdx = Random.Range(0, croom.Count);
       ObjectPosition r = croom[roomIdx];
       int xmin = r.grid.x + r.range.left;
       int xmax = xmin + r.range.width;
       int zmin = r.grid.z + r.range.top;
       int zmax = zmin + r.range.height;
       int x = Random.Range(xmin, xmax + 1);
       int z = Random.Range(zmin, zmax + 1);
       if (IsCollide(x, z) || GetExistActor(x, z) != null) continue;
       SetObject("Random", "Enemy", x, z, 1, 1);
       break;
   }
}

敵の数が規定数より少なくなりそうなら、敵をランダムに追加します。
ただしプレイヤーがいる範囲で突然現れたりしたら不自然なので、画面に表示されていないだろう範囲の部屋内に敵を追加しています。
もしその範囲を狭めたかったり、広げたりしたい場合は最初の変数x1、x2、z1、z2の代入式の末尾の数字(8、5)を変更すると良いです。
また、全てのフロアで同じ規定数を用いる場合は直接コンポーネントのEnemy Num Minに入力するだけで良いですが、マップファイル側から変えられるようにする場合は、LoadFieldMapクラスに追記します。

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;
           info.isStartEnd = dungeonInfo.isStartEnd;
           foreach (var prop in group.Element("properties").Elements("property"))
           {
               /*   省略   */
           }
           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;
}

さて、メソッドを作ったのは良いですが、呼び出さなければ意味はないですね。Updateメソッドで呼んであげるのも良いですが、毎回判定するのは大変だろうということで、敵が倒れた時にこのメソッドを呼び出すことにします。
ActorParamsControllerクラスのDeathJudgementメソッドを変更します。

private bool DeathJudgment()
{
   if (parameter.hp <= 0)
   {
       Field field = GetComponentInParent<Field>();
       if (parameter.id > EActor.PLAYER)
       {
           /*   省略   */
           field.AddEnemy();
       }
           
       field.GetComponent<EffectManager>().Play(EffectManager.EType.Death, field.gameObject, transform.position);
       Destroy(gameObject);
       return true;
   }
   return false;
}

実行してみます。敵が規定数以下になりそうな時はプレイヤーから離れた部屋で新たな敵が出現するはずです。

一定ターン経過後にイベントを起こす

敵の無限湧きを実装すると、そのフロアに居続けることでいくらでもレベル上げができてしまいます。これはある意味問題なので、何らかのイベントを起こすことで対処したいと思います。
この場合のイベントとして、以下が考えられます。

・強い敵を出現させる
・触れると強制的に上のフロアに進む敵またはオブジェクトを出現させる
・強制的に上のフロアに進ませる

今回は一番下の案を採用しようと思います。
まずは何ターン経過したかスクリプト側から分かるようにしましょう。
SequenceManagerクラスを以下のように変更して下さい。

// パラメーターを追記
public int elapsedTurn = 0;

// UpdateSequenceメソッドの該当箇所に追記
if (pAct == EAct.TurnEnd)
{
   AllOperatedProc(iAct == EAct.TurnEnd);
   ActorCondition();
   DecreaseFood();
   elapsedTurn++; // これを追記
   isEnemyDeterminedBehaviour = false;
   operatedEnemies.Clear();
   return;
}

これで一旦ビルドし、実行してみます。インスペクタータブ上で確認しながら、アイテムを投げたりなどもしてみて、正しくターン経過しているか確認して下さい。
特に問題はなさそうなので、elapsedTurnパラメーターを以下のように変更します。

public static int elapsedTurn = 0;

これで現在のターン数がどのスクリプトからも取得できるようになりました。
Fieldクラスを変更します。

// Resetメソッドの最後に以下を追記
SequenceManager.elapsedTurn = 0;

// メソッドを追記
/**
* 指定した部屋にオブジェクトをランダムに配置する
*/
public void RandomSetObjectInRoom(ObjectPosition room, string name, string type)
{
   while (true)
   {
       int xmin = room.grid.x + room.range.left;
       int xmax = xmin + room.range.width;
       int zmin = room.grid.z + room.range.top;
       int zmax = zmin + room.range.height;
       int x = Random.Range(xmin, xmax + 1);
       int z = Random.Range(zmin, zmax + 1);
       if (IsCollide(x, z) || GetExistActor(x, z) != null) continue;
       SetObject(name, type, x, z, 1, 1);
       return;
   }
}

// メソッドを変更
public void AddEnemy()
{
   if (enemyNumMin < enemies.transform.childCount) return;
   int x1 = playerMovement.grid.x - 8;
   int x2 = playerMovement.grid.x + 8;
   int z1 = playerMovement.grid.z - 5;
   int z2 = playerMovement.grid.z + 5;
   List<ObjectPosition> croom = new List<ObjectPosition>();
   foreach (var room in rooms.GetComponentsInChildren<ObjectPosition>())
   {
       int lx = room.grid.x + room.range.left;
       int rx = lx + room.range.width;
       int tz = room.grid.z + room.range.top;
       int bz = tz + room.range.height;
       if (x1 < rx && lx < x2 && z1 < bz && tz < z2) continue;
       croom.Add(room);
   }
   int roomIdx = Random.Range(0, croom.Count);
   ObjectPosition r = croom[roomIdx];
   RandomSetObjectInRoom(r, "Random", "Enemy");
}

LoadFieldMapクラスを変更します。

// メソッドを変更
public void Load(bool isPlayerPositionRandom = false)
{
   field.Reset();
   Array2D mapdata = ReadMapFile(mapName);
   if (mapdata != null)
   {
       field.Create(mapdata);
       ObjectPosition[] croom = field.rooms.GetComponentsInChildren<ObjectPosition>();
       int roomIdx = Random.Range(0, croom.Length);
       if (isPlayerPositionRandom)
           field.RandomSetObjectInRoom(croom[roomIdx], "StartPoint", "StartPoint");
   }
}

StairsMenuActionクラスを変更します。

// パラメーターを追加
private bool isBlowOff = false;

// メソッドを変更
public void Proc(bool isMove = true)
{
   switch (action)
   {
       case EAct.KeyInput: KeyInput(isMove); break;
       case EAct.ActBegin: ActBegin(); break;
       case EAct.Act: Act(); break;
       case EAct.ActEnd: ActEnd(); break;
       case EAct.TurnEnd: TurnEnd(); break;
   }
}

private void KeyInput(bool isMove)
{
   if (isMove) Show(field.PlayerOnStairs());
   if (SequenceManager.exitTurn <= SequenceManager.elapsedTurn + 1)
   {
       string str = (Field.floorNum + (field.firstStartStairs.Contains("Down") ? 1 : -1)) + "F";
       fade.Fade(true, str);
       isBlowOff = true;
       action = EAct.Act;
   }
   else isBlowOff = false;
}

private void Act()
{
   if (fade.Fade(true))
   {
       if (Input.anyKeyDown && Input.GetKeyDown(KeyCode.Space))
       {
           string method = field.firstStartStairs.Contains("Up") ? "Down" : "Up";
           if(!isBlowOff) method = subMenu.GetSelectItemMethod();
           ChangeFloor(method);
           fade.Fade(false);
           action = EAct.ActEnd;
       }
   }
}

private void ChangeFloor(string stairs)
{
   if (stairs.Contains("Down"))
   {
       stairs = "Up";
       Field.floorNum--;
   }
   else
   {
       stairs = "Down";
       Field.floorNum++;
   }
   field.startStairs = stairs;
   loadFieldMap.Load(isBlowOff);
}

private void TurnEnd()
{
   if (fade.Fade(false))
   {
       if (isBlowOff)
       {
           Message.Add(37);
           isBlowOff = false;
       }
       action = EAct.KeyInput;
   }
}

最後にSequenceManagerクラスを変更します。

public const int exitTurn = 50;

// メソッドを追加
/**
* ターン経過する
*/
private void ElapseTurn()
{
   elapsedTurn++;
   if (elapsedTurn >= exitTurn / 2 && elapsedTurn < exitTurn / 2 + 1)
       Message.Add(35);
   if (elapsedTurn >= exitTurn * 3 / 4 && elapsedTurn < exitTurn * 3 / 4 + 1)
       Message.Add(36);
}

// UpdateSequenceメソッドの該当箇所を変更
if (pAct == EAct.TurnEnd)
{
   AllOperatedProc(iAct == EAct.TurnEnd);
   ActorCondition();
   DecreaseFood();
   ElapseTurn(); // ここ
   isEnemyDeterminedBehaviour = false;
   operatedEnemies.Clear();
   return;
}

if (actEnemies.Count < 1 && moveEnemies.Count < 1 && pAct == EAct.ActEnd)
{
   stairsMenuAction.Proc(false);
   EAct sAct = stairsMenuAction.GetAction();
   if (sAct == EAct.KeyInput) AllOperatedProc(iAct == EAct.ActEnd);
   else
   {
       ActorAction.runSpeedRate = 1;
       playerAction.StopWalkingAnimation();
       AllEnemyStopWalkingAnimation();
   }
   return;
}​

今回追加したメッセージは以下の通りです。

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

今回は上のフロアの(階段の位置ではなく)ランダムな位置に飛ばすことにしました。
実行してみます(exitTurnを10にしています)。

ターン経過

10ターン経過後に無事上のフロアに飛ばされました。

ということで今回はここまでです。
次回は宝箱の実装などしていこうと思います。
(罠の実装はアイテムぐらい手間がかかりそうなので、後回しにします。ご了承下さい)

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