見出し画像

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

前回の記事はこちら
前回は中断セーブ機能などを作成しました。

スタート時一時保存のデータがあったら

この時はデータを削除して一から開始したいと思います。
データ削除前に確認画面があった方がいいですね。
ということでそのように実装します。まずは外形から。

スクリーンショット 2020-11-02 22.07.31

スクリーンショット 2020-11-02 22.18.54

スクリプトを書いていきます。まずSaveDataManagerクラスにメソッドを追加して下さい。

/**
* 中断データを削除する
*/
public void DeleteFlashData() => File.Delete(Application.dataPath + flashSaveFilePath);

StartButtonスクリプトを以下のように変更しましょう。

// スクリプトの最初に記述
using UnityEngine.UI;

// パラメーターを追加
public GameObject startCautionPanel;
public Button startButton;

// メソッドを追加
private void Start()
{
   Field.floorNum = 1;
   SequenceManager.elapsedTurn = 0;
}

/*
* スタート時の注意書きにYESと答えた場合
*/
public void GameStartCautionYes()
{
   if (type == 0)
   {
       startCautionPanel.SetActive(false);
       saveDataManager.DeleteFlashData();
       type = 1;
   }
}

/*
* スタート時の注意書きにNOと答えた場合
*/
public void GameStartCautionNo()
{
   if (type == 0)
   {
       startCautionPanel.SetActive(false);
       startButton.Select();
   }
}

// メソッドを変更
public void GameStart()
{
   if (type == 0)
   {
       if (saveDataManager.ExistsFlashData())
       {
           startCautionPanel.SetActive(true);
           startCautionPanel.GetComponentsInChildren<Button>()[0].Select();
       }
       else type = 1;
   }
}

実行してみます。期待通り動けばOKです。

スクリーンショット 2020-11-02 23.04.30

ゲームオーバー処理

今回の本題として、ゲームオーバー画面(仮)を作っていきたいと思います。イメージとしては真っ暗な背景に白文字で「GAME OVER」と表示され、スペースキーを押すとスタート画面に遷移するといった感じです。
まずはゲームオーバーのシーンを作成します。新しいシーンを作成し、名前を「GameOverScene」にして下さい。
画面をこんな感じにします。

スクリーンショット 2020-11-02 23.46.39

作成したUIのパネルに画像を設定することで表示しています。
「GameOverAction」スクリプトを作成し、以下を記述して下さい。

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameOverAction : MonoBehaviour
{
   public FadeInOut fade;

   private bool isPushKey = false;

   // Update is called once per frame
   void Update()
   {
       if (isPushKey)
       {
           if (fade.Fade(true)) SceneManager.LoadScene("StartScene");
       }
       else if (fade.Fade(false) && Input.anyKeyDown && Input.GetKeyDown(KeyCode.Space))
       {
           isPushKey = true;
       }
   }
}

Fadeパネルを別途用意しておきましょう。このスクリプトはGAME OVERを表示しているパネルにアタッチします。ビルド設定からのシーン登録も忘れずに!
次にActorParamsControllerクラスを変更します。

// スクリプトの最初に追加
using UnityEngine.SceneManagement;

// メソッドを追加
/**
* プレイヤーの死亡処理
*/
private void PlayerDeath()
{
   SceneManager.LoadScene("GameOverScene");
}

/**
* 敵の死亡処理
*/
private void EnemyDeath()
{
   Field field = GetComponentInParent<Field>();
   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;
       }
   }
   field.AddEnemy();
   field.GetComponent<EffectManager>().Play(EffectManager.EType.Death, field.gameObject, transform.position);
   Destroy(gameObject);
}

/**
* 宝箱開封処理
*/
private void ChestOpen()
{
   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;
       }
   }
   field.GetComponent<EffectManager>().Play(EffectManager.EType.Death, field.gameObject, transform.position);
   Destroy(gameObject);
}

// メソッドを変更
private bool DeathJudgment()
{
   if (parameter.hp <= 0)
   {
       if (parameter.id >= EActor.CHEST) ChestOpen();
       else if (parameter.id > EActor.PLAYER) EnemyDeath();
       else PlayerDeath();
       return true;
   }
   return false;
}

DeathJudgementメソッドの中身を切り分けました。
ビルドしたら、あえて倒されるように操作します。倒された時にGameOver画面に遷移すればOKです。

HPゲージの減り方が遅い

足踏みした時などにゲージの減少が間に合っていないので修正しておきましょう。
PlayerGaugesControllerクラスのメソッドを変更して下さい。

private float FillGauge(Image image, float per)
{
   if (image.fillAmount > per)
   {
       image.fillAmount -= decrease * ActorAction.runSpeedRate;
       if (image.fillAmount < per) image.fillAmount = per;
   }
   if (image.fillAmount < per)
   {
       image.fillAmount += decrease * ActorAction.runSpeedRate;
       if (image.fillAmount > per) image.fillAmount = per;
   }
   return image.fillAmount;
}

実行して、足踏みした時などの処理を確認しておいて下さい。

タイトル画面に戻った時などにメッセージを消去する

足踏みをしてゲームオーバーになった後ゲームを再開すると、前回のゲームのメッセージが表示されてしまうことがあります。これを改善します。
Messageクラスに以下のメソッドを追記します。

/**
* キューの中身を全て削除する
*/
public static void AllCrear()
{
   texts.Clear();
}

StartButtonクラスのStartメソッドに以下を追記します。

private void Start()
{
   Field.floorNum = 1;
   SequenceManager.elapsedTurn = 0;
   // 以下を追記
   Message.AllCrear();
}

実行してみて、メッセージが出てこなくなればOKです。

画面サイズによってインベントリやメッセージの位置などを調節する

これは大まかにはCanvasの設定を変えることで可能です。
全てのCanvasを以下のように変えて下さい。

スクリーンショット 2020-11-03 2.23.24

参照解像度は適宜変えるようにしましょう。
ただ、これを行なってもやっぱりこの二つはうまく表示されないと思います。スクリプト側から変える必要がありそうですね。早速行なっていきましょう。
ScrollItemクラスのメソッドを変更します。

public Vector3 GetMoveDistance()
{
   RectTransform transform = GetComponent<RectTransform>();
   float per = Screen.currentResolution.width / GetComponentInParent<CanvasScaler>().referenceResolution.x;
   float x1 = transform.position.x + transform.rect.x;
   float x2 = x1 + transform.sizeDelta.x;
   float vx1 = 0;
   float vx2 = Screen.currentResolution.width;
   if (x1 < vx1) return new Vector3((vx1 - x1 + paddingX) * per, 0);
   if (x2 > vx2) return new Vector3((vx2 - x2 - paddingX) * per, 0);
   return new Vector3(0, 0);
}

Viewportから値を参照するのではなく画面サイズから計算して反映するようにしました。
エラーが出ると思うのでScrollViewクラスも変更しておきます。

// パラメーターを削除
viewport

// メソッドを変更
public bool MoveSelect(EDir d)
{
   /*   省略   */
   // 以下を変更
   Vector3 moveDistance = contents[selectItemIndex].GetMoveDistance();
   nextPosX = content.transform.position.x + moveDistance.x;
   contents[selectItemIndex].SetSelected(true);
   return Move(nextPosX, maxPerFrameScroll);
}

次はメッセージです。これがうまく表示されないのは固定の値にしているからなので、画面サイズなどを参照した値に直します。
MessageWindowクラスを以下のように変更して下さい。

void Update()
{
   if (isAdding)
   {
       MessageAnimation anim;
       if (!isFalling)
       {
           /*   省略   */
       }
           
       for (int i = 0; i < transform.childCount - 1; i++)
       {
           anim = transform.GetChild(i).GetComponent<MessageAnimation>();
           float height = anim.GetComponent<RectTransform>().sizeDelta.y;
           if (anim.IsDeleting()) continue;
           isFalling =
               !anim.MoveMessage(transform.position + new Vector3(0, -height * (transform.childCount - i - 1), 0), maxPerFrameV);
       }
   }
   else ShowMessage();
}

private void ShowMessage()
{
   if (Message.GetCount() > 0)
   {
       isAdding = true;
       isFalling = transform.childCount > 0;
       Message.Data m = Message.Get();
       Text msg = Instantiate(text, transform);
       msg.transform.position = transform.position + new Vector3(-Screen.currentResolution.width, 0, 0);
       msg.color = m.GetColor();
       msg.text = m.str;
   }
}

次にMessageAnimationクラスも変更します。

private void DeleteMessage()
{
   if (isMoving) return;
   isDeleting = true;
   float height = GetComponent<RectTransform>().sizeDelta.y;
   MoveMessage(prevPos + new Vector3(0, -height * 2, 0), maxPerFrameD);
   if (transform.position.y < -height)
   {
       Destroy(gameObject);
   }
}

これでどの画面サイズでも同じように表示されるようになったはずです。
(なお、低解像度アスペクト比にチェックを入れた場合はやはり表示がおかしくなりますが、まあいいでしょう)

キー設定をまとめる

今はキー入力受け取りをバラバラにスクリプトから受け取っていますが、そうすると後から変更しようと思った時、変更箇所がわかりにくくなってしまいます。ということで、新たに「KeyConfig」スクリプトを用意して実際のキー受け取りはここで行うようにします。

using UnityEngine;

public static class KeyConfig
{
   public enum EType
   {
       // セーブ・ロード操作
       EDITORSAVE = KeyCode.S,
       EDITORLOAD = KeyCode.L,
       // シーン移動操作
       SCENEMOVE = KeyCode.Space,
       // プレイヤー操作
       PLAYERMOVELEFT = KeyCode.LeftArrow,
       PLAYERMOVERIGHT = KeyCode.RightArrow,
       PLAYERMOVEUP = KeyCode.UpArrow,
       PLAYERMOVEDOWN = KeyCode.DownArrow,
       PLAYERATTACK = KeyCode.Space,
       PLAYERCHANGEDIR = KeyCode.C,
       PLAYERWAITTURN = KeyCode.A,
       PLAYERRUN = KeyCode.RightShift,
       // ダンジョンメニュー操作
       DUNGEONMENUUP = KeyCode.LeftArrow,
       DUNGEONMENUDOWN = KeyCode.RightArrow,
       DUNGEONMENUACTION = KeyCode.Space,
       DUNGEONMENUOPEN = KeyCode.Escape,
       DUNGEONMENUCLOSE = KeyCode.Escape,
       // ステータス画面操作
       STATUSWINDOWCLOSE = KeyCode.Escape,
       // インベントリ操作
       INVENTORYMOVELEFT = KeyCode.LeftArrow,
       INVENTORYMOVERIGHT = KeyCode.RightArrow,
       INVENTORYSUBMENUUP = KeyCode.UpArrow,
       INVENTORYSUBMENUDOWN = KeyCode.DownArrow,
       INVENTORYACTION = KeyCode.Space,
       INVENTORYCANCEL = KeyCode.E,
       INVENTORYSORT = KeyCode.R,
       INVENTORYITEMOPEN = KeyCode.E,
       INVENTORYSTONEOPEN = KeyCode.R,
       INVENTORYCLOSE = KeyCode.Escape,
       // 中断セーブ画面操作
       FLASHSAVEMENUUP = KeyCode.LeftArrow,
       FLASHSAVEMENUDOWN = KeyCode.RightArrow,
       FLASHSAVEACTION = KeyCode.Space,
       FLASHSAVECLOSE = KeyCode.Escape,
       // 階段メニュー操作
       STAIRSMENUUP = KeyCode.UpArrow,
       STAIRSMENUDOWN = KeyCode.DownArrow,
       STAIRSMENUACTION = KeyCode.Space,
       // タイトルへ確認画面操作
       BACKTITLEMENUUP = KeyCode.LeftArrow,
       BACKTITLEMENUDOWN = KeyCode.RightArrow,
       BACKTITLEACTION = KeyCode.Space,
       BACKTITLECLOSE = KeyCode.Escape
   }

   /**
   * タイプに応じたキー入力受け取り
   */
   public static bool GetKey(EType type) => Input.anyKey && Input.GetKey((KeyCode)type);
   public static bool GetKeyDown(EType type) => Input.anyKeyDown && Input.GetKeyDown((KeyCode)type);
   public static bool GetKeyUp(EType type) => Input.GetKeyUp((KeyCode)type);
}

そして以下のスクリプトの該当箇所を変更していきます。
(詳細は長くなってしまうので省略します)

・BackTitleAction
・DirUtil
・DungeonMenuAction
・FlashSaveAction
・GameOverAction
・InventoryAction
・PlayerOperation
・SaveDataManager
・StairsMenuAction
・StatusWindowAction

基本的にはVisual Studio内の検索で「KeyCode」を調べると変更箇所が分かります。
わからなければ都度聞いて下さい。

+値のつく確率

今は+値のつく確率が一定ですが、階数や+値の大きさなどによってその確率を変動できたらいいですね。ということで実装します。
まずExcelAppearDataに「FloorPlusRate」というシートを作成します。
そこに以下のように記述して下さい。

スクリーンショット 2020-11-03 22.19.00

「floor」は階数、「plus0〜」はその階数で対応する+値が出る確率を表しています。筆者は便宜上全て同じようにしましたが、この値は適宜調節するようにして下さい。
このデータを取り込む前に、ExcelAppearDataクラスに追記します。

// 以下を追記
[System.Serializable]
public class FloorPlusRateData
{
   public int floor;
   public int plus0;
   public int plus1;
   public int plus2;
   public int plus3;
   public int plus4;
   public int plus5;
   public int plus6;
   public int plus7;
   public int plus8;
   public int plus9;
   public int plus10;
   private const int maxPlus = 10;

   /**
   * データを返す
   */
   private int Get(int plus)
   {
       System.Type type = GetType();
       System.Reflection.FieldInfo field = type.GetField("plus" + plus);
       return (int)field.GetValue(this);
   }

   /**
   * 確率に応じた+値を返す
   */
   public int GetByRate()
   {
       int maxRate = 0;
       for (int i = 0; i <= maxPlus; i++) maxRate += Get(i);
       int r = Random.Range(0, maxRate);
       for (int i = maxPlus; i > 0; i--)
       {
           r -= Get(i);
           if (r < 0) return i;
       }
       return 0;
   }
}

public List<FloorPlusRateData> FloorPlusRate;

このデータを使って確率を変えていきたいと思います。
ItemクラスのSetExtParamsメソッドを変更します。

public void SetExtParams(ExcelAppearData.ItemPlusMaxData d, ExcelAppearData.FloorPlusRateData p)
{
   int plus = p.GetByRate();
   int[] s = { 0, 1, 2, 3 };
   for (int i = 0; i < plus; i++)
   {
       for (int j = 0; j < s.Length; j++)
       {
           int tmp = s[j];
           int randomIndex = UnityEngine.Random.Range(j, s.Length);
           s[j] = s[randomIndex];
           s[randomIndex] = tmp;
       }
       for (int j = 0; j < s.Length; j++)
       {
           if (s[j] == 0 && d.atk > extData.atk)
           {
               extData.atk++;
               break;
           }
           if (s[j] == 1 && d.def > extData.def)
           {
               extData.def++;
               break;
           }
           if (s[j] == 2 && d.hp > extData.hp)
           {
               extData.hp++;
               break;
           }
           if (s[j] == 3 && d.food > extData.food)
           {
               extData.food++;
               break;
           }
       }
   }
}

Fieldクラスにエラーが出ていると思うのでそれも変更します。

private Item GetItem(string name)
{
   Item itemData;
   EItem itemId = EItem.NONE;
   if (name.Equals("Random"))
   {
       /*   省略   */
   }
   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;
   // 以下1(2)行を変更
   itemData.SetExtParams(appearDatabase.ItemPlusMax.Find(n => n.type == itemData.type),
       appearDatabase.FloorPlusRate.Find(n => n.floor == floorNum));
   return itemData;
}

ItemInventoryEditorクラスを使用されている方はこちらも変更します。

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-11-04 1.40.09

ちゃんと指定された+値が振られているかも確認しておきましょう。

敵の追尾能力をもう少し上げる

たまにプレイヤーが近くにいても追尾してこないことがあるので修正したいと思います。
EnemyOperationクラスのDetectTargetメソッドの中身を変更します。

private bool DetectTarget(ActorMovement actorMovement)
{
   Field field = GetComponentInParent<Field>();
   Pos2D agrid = actorMovement.grid;
   Pos2D tgrid = target.newGrid;
   int minX = Mathf.Min(agrid.x, tgrid.x);
   int maxX = Mathf.Max(agrid.x, tgrid.x);
   int minZ = Mathf.Min(agrid.z, tgrid.z);
   int maxZ = Mathf.Max(agrid.z, tgrid.z);
   ObjectPosition room = field.GetInRoom(agrid.x, agrid.z);
   if (room == null)
   {
       for (int x = minX; x <= maxX; x++)
       {
           for (int z = minZ; z <= maxZ; z++)
           {
               if (field.IsCollide(x, z))
               {
                   return false;
               }
           }
       }
       return true;
   }
   if (field.IsInRoomOf(room, tgrid.x, tgrid.z)) return true;
   else
   {
       for (int x = minX; x <= maxX; x++)
       {
           for (int z = minZ; z <= maxZ; z++)
           {
               if (field.IsCollide(x, z))
               {
                   return false;
               }
           }
       }
       return true;
   }
}

プレイヤーと敵の間に壁がなければ追いかけてくるようにしました。

ということで、Step11で当初予定していた実装はほぼほぼ完成したので、本ステップはこれにて終了致します。
ここまでお付き合い下さり、ありがとうございました。

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