見出し画像

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

前回の記事はこちら
前回はステータス画面を作成しました。

一回ゲーム本体をビルドしてみる

何となくアプリケーションを出力してみたらおかしなことになったので、直し方について書いておこうと思いました。
おかしなこと、というのはこういうことです。

スクリーンショット 2020-11-01 21.50.55

マップが読み込まれていません。これは相対パスでマップデータファイルを指定し、ファイルを直接読みこむ処理にしているから起こっています。
(ちなみにビルドしたアプリケーションと同じ位置にAssets/Mapsフォルダを作成し、その中にマップデータファイルを格納すれば普通に読み込まれます。テストプレイ時であれば差し替えが容易なこの方がいいかもしれませんね)
ということは、このファイルをExcelActorDataなどと同じようなScriptableObjectに変換してあげればいいのではないでしょうか。
早速行っていきましょう。MapDataスクリプトを作成して下さい。以下のように記述します。

using System.Collections.Generic;
using UnityEngine;
using System.Xml.Linq;

[CreateAssetMenu(fileName = "MapData", menuName = "ScriptableObjects/MapData", order = 1)]
public class MapData : ScriptableObject
{
   [System.Serializable]
   public class ObjectData
   {
       public string name;
       public string type;
       public int px;
       public int pz;
       public int width;
       public int height;

       public void SetObject(string n, string t, int x, int z, int w, int h)
       {
           name = n;
           type = t;
           px = x;
           pz = z;
           width = w;
           height = h;
       }
   }

   [System.Serializable]
   public class ObjectDatas
   {
       public List<ObjectData> objectDatas;
   }

   [System.Serializable]
   public class MapInfo
   {
       public int width = 30;
       public int height = 30;
       public int pw = 32;
       public int ph = 32;

       public void SetInfo(int w1, int h1, int w2, int h2)
       {
           width = w1;
           height = h1;
           pw = w2;
           ph = h2;
       }
   }
   public string mapName;
   public int floorNumMax;
   public List<Array2D> mapDatas = new List<Array2D>();
   public List<LoadFieldMap.DungeonInfo> dungeonInfos = new List<LoadFieldMap.DungeonInfo>();
   public MapInfo mapInfo;
   public List<ObjectDatas> objectDatas = new List<ObjectDatas>();

   /**
   * Z軸に対して反対の値を返す
   */
   private int ToMirrorX(int xgrid, int mapWidth)
   {
       return mapWidth - xgrid - 1;
   }

   private void OnValidate()
   {
       ReadMapFile(mapName);
   }

   /**
   * TMXファイルからマップデータを取得する
   */
   private void ReadMapFile(string path)
   {
       mapDatas = new List<Array2D>();
       dungeonInfos = new List<LoadFieldMap.DungeonInfo>();
       objectDatas = new List<ObjectDatas>();
       try
       {
           for (int i = 0; i < floorNumMax; i++)
           {
               XDocument xml = XDocument.Load(path);
               XElement map = xml.Element("map");
               XElement group = null;
               foreach (var gp in map.Elements("group"))
               {
                   if (gp.Attribute("name").Value.Equals((i + 1) + "F"))
                   {
                       group = gp;
                       break;
                   }
               }
               if (group == null)
               {
                   mapDatas.Add(null);
                   objectDatas.Add(new ObjectDatas());
                   dungeonInfos.Add(new LoadFieldMap.DungeonInfo());
                   mapInfo = new MapInfo();
                   return;
               }
               int w = int.Parse(map.Attribute("width").Value);
               int h = int.Parse(map.Attribute("height").Value);
               int pw = int.Parse(map.Attribute("tilewidth").Value);
               int ph = int.Parse(map.Attribute("tileheight").Value);
               Array2D data = null;
               foreach (var layer in group.Elements("layer"))
               {
                   switch (layer.Attribute("name").Value)
                   {
                       case "Tile Layer":
                           string[] sdata = (layer.Element("data").Value).Split(',');
                           w = int.Parse(layer.Attribute("width").Value);
                           h = int.Parse(layer.Attribute("height").Value);
                           data = new Array2D(w, h);
                           for (int z = 0; z < h; z++)
                           {
                               for (int x = 0; x < w; x++)
                               {
                                   data.Set(x, z, int.Parse(sdata[ToMirrorX(x, w) + z * w]) - 1);
                               }
                           }

                           break;
                   }
               }
               mapDatas.Add(data);
               MapInfo mi = new MapInfo();
               mi.SetInfo(w, h, pw, ph);
               mapInfo = mi;
               List<ObjectData> objectData = new List<ObjectData>();
               foreach (var objgp in group.Elements("objectgroup"))
               {
                   switch (objgp.Attribute("name").Value)
                   {
                       case "Object Layer":
                           foreach (var obj in objgp.Elements("object"))
                           {
                               int x = int.Parse(obj.Attribute("x").Value);
                               int z = int.Parse(obj.Attribute("y").Value);
                               int rw = int.Parse(obj.Attribute("width").Value);
                               int rh = int.Parse(obj.Attribute("height").Value);
                               string name = obj.Attribute("name").Value;
                               string type = "";
                               foreach (var prop in obj.Element("properties").Elements("property"))
                               {
                                   switch (prop.Attribute("name").Value)
                                   {
                                       case "Type":
                                           type = prop.Attribute("value").Value;
                                           break;
                                   }
                               }
                               ObjectData objdata = new ObjectData();
                               objdata.SetObject(name, type, ToMirrorX(x / pw, w) - (rw / pw - 1), z / ph, rw / pw, rh / ph);
                               objectData.Add(objdata);
                           }
                           break;
                   }
               }
               ObjectDatas objs = new ObjectDatas();
               objs.objectDatas = objectData;
               objectDatas.Add(objs);
               LoadFieldMap.DungeonInfo info = new LoadFieldMap.DungeonInfo();
               if (data == null)
               {
                   if (group.Element("properties") != null)
                   {
                       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;
                               case "ChestNumMax":
                                   info.chestNumMax = num;
                                   break;
                               case "IsStartEnd":
                                   info.isStartEnd = num;
                                   break;
                           }
                       }
                   }
                   
               }
               dungeonInfos.Add(info);
           }
           Debug.Log("Read Map File");
       }
       catch (System.Exception i_exception)
       {
           Debug.LogErrorFormat("{0}", i_exception);
       }
       return;
   }
}

LoadFieldMapスクリプトも変更します。

using System.Collections.Generic;
using UnityEngine;
using System.Xml.Linq;

public class LoadFieldMap : MonoBehaviour
{
   public string mapName;
   public MapData mapData;
   public DungeonInfo dungeonInfo;
   public Field field;

   private RandomDungeon dungeon;

   // Start is called before the first frame update
   void Start()
   {
       dungeon = new RandomDungeon();
       Load();
   }

   /**
   * マップを読み込む
   */
   public void Load(bool isPlayerPositionRandom = false)
   {
       field.Reset();
       Array2D mapdata = ReadMapData(mapData);
       // 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");
       }
   }

   /**
   * Z軸に対して反対の値を返す
   */
   private int ToMirrorX(int xgrid, int mapWidth)
   {
       return mapWidth - xgrid - 1;
   }

   /**
   * MapDataからマップデータを取得する
   */
   private Array2D ReadMapData(MapData mapData)
   {
       int idx = Field.floorNum - 1;
       if (mapData.floorNumMax < Field.floorNum)
       {
           Array2D data =
               dungeon.Create(30, 30, field, dungeonInfo);
           return data;
       }
       if (mapData.mapDatas[idx].width < 1 || mapData.mapDatas[idx].height < 1)
       {
           DungeonInfo info = mapData.dungeonInfos[idx].Get();
           if (info.roomSizeMin < 0) info.roomSizeMin = dungeonInfo.roomSizeMin;
           if (info.roomSizeMax < 0) info.roomSizeMax = dungeonInfo.roomSizeMax;
           if (info.outerMargin < 0) info.outerMargin = dungeonInfo.outerMargin;
           if (info.posMargin < 0) info.posMargin = dungeonInfo.posMargin;
           if (info.connectRate < 0) info.connectRate = dungeonInfo.connectRate;
           if (info.enemyNumMin < 0) info.enemyNumMin = dungeonInfo.enemyNumMin;
           if (info.enemyNumMax < 0) info.enemyNumMax = dungeonInfo.enemyNumMax;
           if (info.itemNumMin < 0) info.itemNumMin = dungeonInfo.itemNumMin;
           if (info.itemNumMax < 0) info.itemNumMax = dungeonInfo.itemNumMax;
           if (info.chestNumMax < 0) info.chestNumMax = dungeonInfo.chestNumMax;
           if (info.isStartEnd < -1) info.isStartEnd = dungeonInfo.isStartEnd;
           field.enemyNumMin = info.enemyNumMin;
           Array2D data = dungeon.Create(mapData.mapInfo.width, mapData.mapInfo.height, field, info);
           return data;
       }
       for (int i = 0; i < mapData.objectDatas[idx].objectDatas.Count; i++)
       {
           MapData.ObjectData obj = mapData.objectDatas[idx].objectDatas[i];
           field.SetObject(obj.name, obj.type, obj.px, obj.pz, obj.width, obj.height);
       }
       return mapData.mapDatas[Field.floorNum - 1];
   }

   /**
   * TMXファイルからマップデータを取得する
   */
   private Array2D ReadMapFile(string path)
   {
       try
       {
           XDocument xml = XDocument.Load(path);
           XElement map = xml.Element("map");
           XElement group = null;
           foreach (var gp in map.Elements("group"))
           {
               if (gp.Attribute("name").Value.Equals(Field.floorNum + "F"))
               {
                   group = gp;
                   break;
               }
           }
           if (group == null) return null;
           int w = int.Parse(map.Attribute("width").Value);
           int h = int.Parse(map.Attribute("height").Value);
           int pw = int.Parse(map.Attribute("tilewidth").Value);
           int ph = int.Parse(map.Attribute("tileheight").Value);
           Array2D data = null;
           foreach (var layer in group.Elements("layer"))
           {
               switch (layer.Attribute("name").Value)
               {
                   case "Tile Layer":
                       string[] sdata = (layer.Element("data").Value).Split(',');
                       w = int.Parse(layer.Attribute("width").Value);
                       h = int.Parse(layer.Attribute("height").Value);
                       data = new Array2D(w, h);
                       for (int z = 0; z < h; z++)
                       {
                           for (int x = 0; x < w; x++)
                           {
                               data.Set(x, z, int.Parse(sdata[ToMirrorX(x, w) + z * w]) - 1);
                           }
                       }

                       break;
               }
           }
           foreach (var objgp in group.Elements("objectgroup"))
           {
               switch (objgp.Attribute("name").Value)
               {
                   case "Object Layer":
                       foreach (var obj in objgp.Elements("object"))
                       {
                           int x = int.Parse(obj.Attribute("x").Value);
                           int z = int.Parse(obj.Attribute("y").Value);
                           int rw = int.Parse(obj.Attribute("width").Value);
                           int rh = int.Parse(obj.Attribute("height").Value);
                           string name = obj.Attribute("name").Value;
                           string type = "";
                           foreach (var prop in obj.Element("properties").Elements("property"))
                           {
                               switch (prop.Attribute("name").Value)
                               {
                                   case "Type":
                                       type = prop.Attribute("value").Value;
                                       break;
                               }
                           }
                           field.SetObject(name, type, ToMirrorX(x / pw, w) - (rw / pw - 1), z / ph, rw / pw, rh / ph);
                       }
                       break;
               }
           }
           if (data == null)
           {
               DungeonInfo info = dungeonInfo.Get();
               if (group.Element("properties") != null)
               {
                   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;
                           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;
   }

   private class RandomDungeon
   {
       /*   省略   */
   }
   [System.Serializable]
   public class DungeonInfo
   {
       public int roomSizeMax = -1;
       public int roomSizeMin = -1;
       public int outerMargin = -1;
       public int posMargin = -1;
       public int connectRate = -1;
       public int enemyNumMin = -1;
       public int itemNumMin = -1;
       public int enemyNumMax = -1;
       public int itemNumMax = -1;
       public int chestNumMax = -1;
       public int isStartEnd = -2;

       /**
       * コピーを返す
       */
       public DungeonInfo Get()
       {
           DungeonInfo info = new DungeonInfo();
           info.roomSizeMin = roomSizeMin;
           info.roomSizeMax = roomSizeMax;
           info.outerMargin = outerMargin;
           info.posMargin = posMargin;
           info.connectRate = connectRate;
           info.enemyNumMin = enemyNumMin;
           info.enemyNumMax = enemyNumMax;
           info.itemNumMin = itemNumMin;
           info.itemNumMax = itemNumMax;
           info.chestNumMax = chestNumMax;
           info.isStartEnd = isStartEnd;
           return info;
       }
   }
}

一応ファイル名で読み込む方も残しておくことにしました。必要に応じて使い分けて下さい。
MapDataの作成の仕方は以下の通りです。

・プロジェクトタブ内の任意のファイルを開いた状態で、作成ボタンを押す
・すると「ScriptableObjects」という項目が増えているのでクリック
・開いたメニューの中に「MapData」があるのでクリック
・作成されたMapDataに任意の名前をつける
・Map Nameに「Assets/Maps/~.tmx」などファイルの読み込み場所を入力
・Floor Num Maxに階数の最大数を入力(1以上)
・他の項目にデータが入力され、デバッグログに「Read Map File」と表示されたらOK!

後はFieldのLoad Field MapコンポーネントのMap Dataに作成したMapDataを設定するだけです。
実行してみて、エラーなどが起きないことを確認したら、ゲーム本体をビルドします。
「ビルド設定」を開き、右下にある「ビルドして実行」を選択します。
後は手順に従って作成して下さい。場合によってはプレイヤー設定も変える必要があるかもしれません。

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

無事実行されました。なお、画面サイズや解像度によってはメッセージやインベントリの表示がおかしくなることがありますが、それはまた今度直そうと思います。

セーブ処理の実装

前置きが随分と長くなってしまいましたが、本題はこちらです。
ちゃんとしたセーブ処理の実装をしていきたいと思います。
今のPlayerPrefsに保存する形式をやめて、バイナリファイルに保存するようにしましょう。
ただ、デバッグ時(のSキーを押した時)にはPlayerPrefsに保存したいと思いますので、エディタの時と実際の実行時を切り替えできるようにします。
SaveDataManagerクラスを以下のように変更します。

// スクリプトの最初に記述
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

// パラメーターを追記
private const string continueFlag = "ContinueFlag";
private const string saveDir = "/Save";
private const string flashSaveFilePath = saveDir + "/fsave.bin";

// メソッドを追加
/**
* データを保存する
*/
public void Save(bool isFlash)
{
   saveData.playerData = MakePlayerData();
   saveData.enemyDatas = MakeEnemyDatas();
   saveData.itemDatas = MakeItemDatas();
   saveData.mapData = MakeMapData();
   if (!Directory.Exists(Application.dataPath + saveDir))
   {
       Directory.CreateDirectory(Application.dataPath + saveDir);
   }
   if (isFlash)
   {
       BinaryFormatter bf = new BinaryFormatter();
       FileStream file = File.Create(Application.dataPath + flashSaveFilePath);
       try
       {
           bf.Serialize(file, saveData);
       }
       finally
       {
           if (file != null)
           {
               file.Close();
               File.Delete(path);
           }
                   
       }
   }
}

/**
* データを読みこむ
*/
public void Load(bool isFlash)
{
   if (isFlash)
   {
       string path = Application.dataPath + flashSaveFilePath;
       if (File.Exists(path))
       {
           BinaryFormatter bf = new BinaryFormatter();
           FileStream file = File.Open(path, FileMode.Open);
           try
           {
               saveData = (SaveData)bf.Deserialize(file);
               LoadMapData(saveData);
               LoadItemData(saveData);
               LoadEnemyDatas(saveData);
               LoadPlayerData(saveData);
           }
           finally
           {
               if (file != null)
               {
                   file.Close();
                   File.Delete(path);
               }
                       
           }
       }
       else
       {
           Debug.Log("no load file");
       }
   }
}

/**
* 一時保存データがあるか
*/
public bool ExistsFlashData() => File.Exists(Application.dataPath + flashSaveFilePath);

/**
* コンティニューフラグを立てる
*/
public void SetContineFlag(int flag) => PlayerPrefs.SetInt(continueFlag, flag);

/**
* 続きからスタートするかどうか調べ
* スタートする場合はtrueを返す
*/
public bool ContinueStart()
{
   if (PlayerPrefs.HasKey(continueFlag))
   {
       int flag = PlayerPrefs.GetInt(continueFlag);
       if (flag < 0)
       {
           Load(true);
           PlayerPrefs.SetInt(continueFlag, 0);
           return true;
       }
   }
   return false;
}

// メソッドを変更
void Update()
{
   #if UNITY_EDITOR
       if (Input.anyKeyDown)
       {
           if (Input.GetKeyDown(KeyCode.S))
           {
               Save();
               Message.Add("セーブしました!");
           }
           if (Input.GetKeyDown(KeyCode.L))
           {
               Load();
               Message.Add("ロードしました!");
           }
       }
   #endif
}

Saveメソッドでバイナリ形式にシリアライズしてファイルに書き込み、Loadメソッドでデシリアライズし読み込んでいます。なお、一時的なデータなので読み込んだ後ファイルを削除しています。
これでセーブ処理の基礎はできました。

ゲーム中断画面の作成

FLASH SAVEが選ばれた時の画面も作成していきたいと思います。
イメージはこんな感じです。

一時保存サンプル

FLASH SAVEを選択するとこの画面が表示される

ゲーム中断サンプル2

上の画面でYESを選択すると表示される

これをもとに外形を作成します。筆者は以下のようになりました。

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

ヒエラルキータブではこんな感じになっています。

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

FlashSaveCheckにあたるオブジェクトにはSubMenuスクリプトをアタッチしておき、以下のように設定して下さい。

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

Choice Namesには任意で入力して下さい。

FlashSaveActionスクリプト

「FlashSaveAction」スクリプトを作成して下さい。コードを書いていきます。

using UnityEngine;
using UnityEngine.SceneManagement;

public class FlashSaveAction : MonoBehaviour
{
   public SaveDataManager saveDataManager;
   public SubMenu subMenu;
   public FadeInOut fade;
   public float inputHoldDelay = 0.3f;

   private EAct action = EAct.KeyInput;
   private const string choiceOrder = "Yes,No";
   private float inputTime = 0;

   // 独自の更新メソッド
   public void Proc()
   {
       switch (action)
       {
           case EAct.KeyInput: KeyInput(); break;
           case EAct.MoveBegin: MoveBegin(); break;
           case EAct.Move: Move(); break;
           case EAct.MoveEnd: MoveEnd(); break;
           case EAct.ActBegin: ActBegin(); break;
           case EAct.Act: Act(); break;
           case EAct.ActEnd: ActEnd(); break;
           case EAct.TurnEnd: TurnEnd(); break;
       }
   }

   /**
   * 現在の行動状態を返す
   */
   public EAct GetAction() => action;

   /**
   * 選択する項目を変更する
   */
   private bool MoveSelectSubMenuItem()
   {
       if (Input.anyKeyDown)
       {
           inputTime = 0;
           if (Input.GetKeyDown(KeyCode.LeftArrow)) return subMenu.MoveSelect(EDir.Up);
           if (Input.GetKeyDown(KeyCode.RightArrow)) return subMenu.MoveSelect(EDir.Down);
       }
       inputTime += Time.deltaTime;
       if (inputTime < inputHoldDelay) return true;
       if (!Input.anyKey) return true;
       inputTime = 0;
       if (Input.GetKey(KeyCode.LeftArrow)) return subMenu.MoveSelect(EDir.Up);
       if (Input.GetKey(KeyCode.RightArrow)) return subMenu.MoveSelect(EDir.Down);
       return true;
   }

   /**
   * 項目を選択
   */
   private void SelectSubMenuItem()
   {
       if (Input.anyKeyDown)
       {
           if (Input.GetKeyDown(KeyCode.Escape))
           {
               while (subMenu.GetSelectItemMethod() != "No")
                   subMenu.MoveSelect(EDir.Up);
               action = EAct.Act;
               Hide();
               return;
           }
           if (Input.GetKeyDown(KeyCode.Space))
           {
               action = EAct.Act;
               Hide();
           }
       }
   }

   /**
   * 画面を開く
   */
   private void Show(bool isAfter)
   {
       transform.GetChild(0).gameObject.SetActive(!isAfter);
       transform.GetChild(1).gameObject.SetActive(isAfter);
       if (!isAfter)
       {
           subMenu.SetChoices(choiceOrder);
           subMenu.Show();
       }
       
       gameObject.SetActive(true);
   }

   /**
   * 画面を閉じる
   */
   private void Hide()
   {
       gameObject.SetActive(false);
   }

   /**
   * 待機中
   */
   private void KeyInput()
   {
       Show(false);
       action = EAct.ActBegin;
   }

   /**
   * アクションを始める
   */
   private void ActBegin()
   {
       MoveSelectSubMenuItem();
       SelectSubMenuItem();
   }

   /**
   * アクション中
   */
   private void Act()
   {
       string method = subMenu.GetSelectItemMethod();
       switch (method)
       {
           case "Yes":
               saveDataManager.Save(true);
               Show(true);
               action = EAct.ActEnd;
               break;
           default:
               if (Input.GetKeyUp(KeyCode.Space) || Input.GetKeyUp(KeyCode.Escape))
                   action = EAct.TurnEnd;
               break;
       }
   }

   /**
   * アクションが終わった
   */
   private void ActEnd()
   {
       if (Input.anyKeyDown && Input.GetKeyDown(KeyCode.Space))
           action = EAct.MoveBegin;
   }

   /**
   * 移動開始
   */
   private void MoveBegin()
   {
       fade.Fade(true);
       action = EAct.Move;
   }

   /**
   * 移動中
   */
   private void Move()
   {
       if (fade.Fade(true))
       {
           action = EAct.MoveEnd;
           SceneManager.LoadScene("StartScene");
       }
   }

   /**
   * 移動が終わった
   */
   private void MoveEnd()
   {
       action = EAct.TurnEnd;
   }

   /**
   * ターンが終わった
   */
   private void TurnEnd()
   {
       action = EAct.KeyInput;
   }
}

「YES」を選択するとフェードしてシーン遷移するようにしました。
DungeonMenuActionクラスを変更します。

// パラメーターを追加
public FlashSaveAction flashSaveAction;

// メソッドを追加
private bool ShowFlashSaveWindow()
{
   flashSaveAction.Proc();
   EAct sAct = flashSaveAction.GetAction();
   return sAct != EAct.KeyInput;
}

// メソッドを変更
private void SelectSubMenuItem()
{
   if (Input.anyKeyDown)
   {
       if (Input.GetKeyDown(KeyCode.Escape))
       {
           /*   省略   */
       }
       if (Input.GetKeyDown(KeyCode.Space))
       {
           string method = subMenu.GetSelectItemMethod();
           switch (method)
           {
               case "Status":
                   if (ShowStatusWindow())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               case "Inventory":
                   Hide();
                   action = EAct.KeyInput;
                   inventoryAction.OpenInventory(Input.GetKey(KeyCode.R));
                   break;
               case "Save":
                   if (ShowFlashSaveWindow())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               case "Stairs":
                   if (ShowStairsMenu())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               default:
                   Hide();
                   action = EAct.Act;
                   break;
           }
       }
   }
}

private void Act()
{
   string method = subMenu.GetSelectItemMethod();
   switch (method)
   {
       case "Status":
           if (!ShowStatusWindow()) Show();
           break;
       case "Save":
           if (!ShowFlashSaveWindow()) Show();
           break;
       case "Stairs":
           if (!ShowStairsMenu()) action = EAct.ActEnd;
           break;
       default:
           if (Input.GetKeyUp(KeyCode.Space) || Input.GetKeyUp(KeyCode.Escape)) action = EAct.ActEnd;
           break;
   }
}

FlashSaveActionスクリプトをFlashSaveWindowにアタッチします。
ビルドして、実行してみます。

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

表示されました。「YES」を選択すると、Assetsフォルダ内にSaveフォルダが作成され、中に「fsave.bin」というファイルができるはずです。

タイトル画面からロードする

fsave.binを読み込んでみましょう。
LoadFieldMapクラスを変更します。

// パラメーターを追加
public SaveDataManager saveDataManager;

// メソッドを変更
void Start()
{
   dungeon = new RandomDungeon();
   if (!saveDataManager.ContinueStart()) Load();
}

Load Field MapコンポーネントにSaveDataManagerを設定するのを忘れないようにしましょう。
StartSceneに移り、「SaveDataManager」オブジェクトを作成して下さい。そのSaveDataManagerオブジェクトにSaveDataManagerスクリプトをアタッチします。
そしてStartButtonを以下のように変更します。

// staticにする
private static int type = 0;

// パラメーターを追加
public SaveDataManager saveDataManager;

// メソッドを変更
private void Update()
{
   if (type > 0)
   {
       if (fade.Fade(true))
       {
           switch (type)
           {
               case 1:
                   type = 0;
                   SceneManager.LoadScene("MainScene");
                   break;
               case 2:
                   type = 0;
                   SceneManager.LoadScene("MainScene");
                   break;
               case 3:
                   type = 0;
                   Exit();
                   break;
           }
       }
   }
}

public void GameContinue()
{
   if (type == 0)
   {
       type = 2;
       if (saveDataManager.ExistsFlashData())
           saveDataManager.SetContineFlag(-1);
       else type = 0;
   }
}

実行してみます。
CONTINUEボタンを押すと読み込むことができます。
なお、読み込むとデータが削除されますが、仕様です。

スタート画面に戻る

最後にスタート画面に戻る機能をつけたいと思います。
戻る前にワンクッション置いた方がいいですね。という訳で注意書きを用意します。
基本はFLASH SAVEの時と同じなので外形については省略します。
参考までに、筆者はこのようにしました。

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

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

外形が完成したら、BackTitleActionスクリプトを作成します。
そして以下を記述します。

using UnityEngine;
using UnityEngine.SceneManagement;

public class BackTitleAction : MonoBehaviour
{
   public SubMenu subMenu;
   public FadeInOut fade;
   public float inputHoldDelay = 0.3f;

   private EAct action = EAct.KeyInput;
   private const string choiceOrder = "Yes,No";
   private float inputTime = 0;

   // 独自の更新メソッド
   public void Proc()
   {
       switch (action)
       {
           case EAct.KeyInput: KeyInput(); break;
           case EAct.MoveBegin: MoveBegin(); break;
           case EAct.Move: Move(); break;
           case EAct.MoveEnd: MoveEnd(); break;
           case EAct.ActBegin: ActBegin(); break;
           case EAct.Act: Act(); break;
           case EAct.ActEnd: ActEnd(); break;
           case EAct.TurnEnd: TurnEnd(); break;
       }
   }

   /**
   * 現在の行動状態を返す
   */
   public EAct GetAction() => action;

   /**
   * 選択する項目を変更する
   */
   private bool MoveSelectSubMenuItem()
   {
       if (Input.anyKeyDown)
       {
           inputTime = 0;
           if (Input.GetKeyDown(KeyCode.LeftArrow)) return subMenu.MoveSelect(EDir.Up);
           if (Input.GetKeyDown(KeyCode.RightArrow)) return subMenu.MoveSelect(EDir.Down);
       }
       inputTime += Time.deltaTime;
       if (inputTime < inputHoldDelay) return true;
       if (!Input.anyKey) return true;
       inputTime = 0;
       if (Input.GetKey(KeyCode.LeftArrow)) return subMenu.MoveSelect(EDir.Up);
       if (Input.GetKey(KeyCode.RightArrow)) return subMenu.MoveSelect(EDir.Down);
       return true;
   }

   /**
   * 項目を選択
   */
   private void SelectSubMenuItem()
   {
       if (Input.anyKeyDown)
       {
           if (Input.GetKeyDown(KeyCode.Escape))
           {
               while (subMenu.GetSelectItemMethod() != "No")
                   subMenu.MoveSelect(EDir.Up);
               action = EAct.Act;
               Hide();
               return;
           }
           if (Input.GetKeyDown(KeyCode.Space))
           {
               action = EAct.Act;
               Hide();
           }
       }
   }

   /**
   * 画面を開く
   */
   private void Show()
   {
       subMenu.SetChoices(choiceOrder);
       subMenu.Show();
       gameObject.SetActive(true);
   }

   /**
   * 画面を閉じる
   */
   private void Hide()
   {
       gameObject.SetActive(false);
   }

   /**
   * 待機中
   */
   private void KeyInput()
   {
       Show();
       action = EAct.ActBegin;
   }

   /**
   * アクションを始める
   */
   private void ActBegin()
   {
       MoveSelectSubMenuItem();
       SelectSubMenuItem();
   }

   /**
   * アクション中
   */
   private void Act()
   {
       string method = subMenu.GetSelectItemMethod();
       switch (method)
       {
           case "Yes":
               action = EAct.ActEnd;
               break;
           default:
               if (Input.GetKeyUp(KeyCode.Space) || Input.GetKeyUp(KeyCode.Escape))
                   action = EAct.TurnEnd;
               break;
       }
   }

   /**
   * アクションが終わった
   */
   private void ActEnd()
   {
       action = EAct.MoveBegin;
   }

   /**
   * 移動開始
   */
   private void MoveBegin()
   {
       fade.Fade(true);
       action = EAct.Move;
   }

   /**
   * 移動中
   */
   private void Move()
   {
       if (fade.Fade(true))
       {
           action = EAct.MoveEnd;
           Field.floorNum = 1;
           SequenceManager.elapsedTurn = 0;
           SceneManager.LoadScene("StartScene");
       }
   }

   /**
   * 移動が終わった
   */
   private void MoveEnd()
   {
       action = EAct.TurnEnd;
   }

   /**
   * ターンが終わった
   */
   private void TurnEnd()
   {
       action = EAct.KeyInput;
   }
}

DungeonMenuActionクラスを変更します。

// 順番が間違っていたので修正
private const string choiceOrder =
   "Status,Inventory,Save,Stairs,Achieve,Setting,Title,Exit";

// パラメーターを追記
public BackTitleAction backTitleAction;

// メソッドを追加
/**
* タイトルに戻る確認の画面を開く
*/
private bool ShowBackTitleWindow()
{
   backTitleAction.Proc();
   EAct sAct = backTitleAction.GetAction();
   return sAct != EAct.KeyInput;
}

// メソッドを変更
private void SelectSubMenuItem()
{
   if (Input.anyKeyDown)
   {
       if (Input.GetKeyDown(KeyCode.Escape))
       {
           while (subMenu.GetSelectItemMethod() != "Exit")
               subMenu.MoveSelect(EDir.Up);
           action = EAct.Act;
           Hide();
           return;
       }
       if (Input.GetKeyDown(KeyCode.Space))
       {
           string method = subMenu.GetSelectItemMethod();
           switch (method)
           {
               case "Status":
                   if (ShowStatusWindow())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               case "Inventory":
                   Hide();
                   action = EAct.KeyInput;
                   inventoryAction.OpenInventory(Input.GetKey(KeyCode.R));
                   break;
               case "Save":
                   if (ShowFlashSaveWindow())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               case "Stairs":
                   if (ShowStairsMenu())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               case "Title":
                   if (ShowBackTitleWindow())
                   {
                       Hide();
                       action = EAct.Act;
                   }
                   break;
               default:
                   Hide();
                   action = EAct.Act;
                   break;
           }
       }
   }
}

private void Act()
{
   string method = subMenu.GetSelectItemMethod();
   switch (method)
   {
       case "Status":
           if (!ShowStatusWindow()) Show();
           break;
       case "Save":
           if (!ShowFlashSaveWindow()) Show();
           break;
       case "Stairs":
           if (!ShowStairsMenu()) action = EAct.ActEnd;
           break;
       case "Title":
           if (!ShowBackTitleWindow()) Show();
           break;
       default:
           if (Input.GetKeyUp(KeyCode.Space) || Input.GetKeyUp(KeyCode.Escape)) action = EAct.ActEnd;
           break;
   }
}

BackTitleActionスクリプトはBackTitleWindowにアタッチしておいて下さい。
実行してみます。タイトル画面に戻れればOKです。

これにてメニュー編は一旦終了致します。ですが設定画面と実績画面も後にちゃんと実装するので安心して下さい。

ということで、今回はこの前さぼってしまった分長めに記事を書いてみましたがいかがでしたでしょうか。
次回はゲームオーバー処理から作っていこうかなと思っています。

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