ターン制RPGの作り方#21|地下へ進もう:階ごとの敵と、先へ進む・ロビーへ帰る

地下1階のバトルに勝つと「しょうり!」のウインドに「さきへ すすむ」と「ロビーへ」のボタン。「さきへ すすむ」を選ぶと、3人が右へ走って画面の外へ出ていき、暗くなってから、紙ひこうき3体のバトルが始まる。右上の表示が「ちか1かい 1/3」から「ちか1かい 2/3」に変わる
勝ったら、先へ進んで次のバトル

前回、4人目のメドゥーサが仲間に入って、ロビーでバトルに出る3人を選べるようになりました。でも「地下へ」を押すと、毎回同じ敵と1回戦って、ロビーに戻ってくるだけでした・・・。今回は、地下を「階」に分けて、1つの階で何回か戦いながら奥へ進み、クリアしたら次の階へ行けるようにしたいと思います!

目次

今回作るもの

3つの階の表。ちか1かい(いつもの書庫)は1回目がこぼれたインク2体、2回目が紙ひこうき3体、3回目が本の虫とこぼれたインク。ちか2かい(緑がかった書庫)は羽ペン2体、ぶあつい辞書と羽ペン、とびだす地球儀2体。ちか3かい(紫の暗い書庫)はランプのおばけ2体、まほうのページ3体、ランプのおばけとまほうのページ2体
地下1〜3階と、戦う順番の敵
  • 地下は3つの階。1つの階で3回戦うと、その階はクリア
  • 階ごとに、出てくる敵の組み合わせと背景がちがう
  • 勝つと「さきへ すすむ」か「ロビーへ」を選べる
    • 進むと、HPやMPはそのままで次のバトル。3人が右へ走っていく
    • ロビーへ帰ると、休憩室や売店が使える(その階は、次に来たとき1回目から)
  • 右上に、今いる階と何回目のバトルか(ちか1かい 2/3)を出す
  • クリアした階の次の階へ、ロビーの「地下へ」から行けるようになる
  • 地下2階へ行けるようになると、売店に めざましどけい・ぶんちん・司書のエプロン が並ぶ
スクリプト役割付ける場所
FloorData階のデータ(名前・背景・戦う順番の敵の組み合わせ)(付けない。データのアセット)
GameSession階のデータと、どこまで進んだか-
BattleLineup今いる階の敵と背景で並べる-
ResultWindow「さきへ すすむ」のボタン-
UnitView右へ走って、画面の外へ-
BattleManager右上の階の表示、進む・帰る、階のクリア-
LobbyMenu「地下へ」で階を選ぶ-
IShopGoods ほか売り物が並ぶようになる階-

階のデータを作ろう

地下2・3階の新しい敵5体を並べた画像。左から羽ペン(金色のペン先の白い羽)、ぶあつい辞書(ギザギザの歯の赤い本)、とびだす地球儀(ばねの先に目)、ランプのおばけ(オレンジのほのお)、まほうのページ(紫の文字が光る紙)
地下2・3階の新しい敵

この節では、地下2・3階の敵を足して、階ごとのデータ(FloorData)を作ります。

地下2・3階の敵と背景

素材のTurnBasedRpgAssets.zipに、新しい敵5体と、地下2・3階の背景を足しました。

  • zipをダウンロードし直して、TurnBasedRpg/SpritesのEnemiesとBackgroundsをプロジェクトに入れる
    • 敵:enemy_quill_pen・enemy_thick_dictionary・enemy_popup_globe・enemy_lamp_ghost・enemy_magic_page
    • 背景:bg_library_b2(緑がかった古い書庫)・bg_library_b3(紫の暗い書庫)
    • zipの中のunitypackageから入れると、Spriteの設定も入った状態で取り込まれます
QuillPenのInspector。Enemy Nameに羽ペン、Spriteにenemy_quill_pen、StatsはMax Hp 26・Attack 15・Defense 6・Speed 14、Exp 8・Gold 8、Commandsの Element 0 に Attack
羽ペンのデータ
  • Data/Enemiesに、Create>TurnBasedRpg>Enemy Data で5つ作り、下の表のとおりに入れる
    • Commandsは、どれも Attack の1つだけ
ファイル名Enemy NameSpriteHPこうげきぼうぎょすばやさExpGold
QuillPen羽ペンenemy_quill_pen261561488
ThickDictionaryぶあつい辞書enemy_thick_dictionary48161641012
PopupGlobeとびだす地球儀enemy_popup_globe34141091110
LampGhostランプのおばけenemy_lamp_ghost401810121615
MagicPageまほうのページenemy_magic_page30148131414
  • 深い階ほど、能力も、倒したときの経験値・お金も大きくしています。地下1階の敵(HP 12〜22)より、だいぶ強めです
スライム

「れんぞくひっかき」「のしかかり」みたいな、この敵たちだけの行動は、次の回で作ります。今回はみんな「たたく」だけ・・・

FloorData

Floor1のInspector。Floor Nameに ちか1かい、Backgroundにbg_library_b1、Battlesが3つで、Element 0のEnemiesにSpilledInkが2つ、Element 1にPaperPlaneが3つ、Element 2にBookwormとSpilledInk
地下1階のデータ

1つの階のデータを、ScriptableObjectで作ります。敵のデータ(#2のEnemyData)と同じように、Inspectorで中身を入れられる「アセット」にします。

  • 「FloorData」スクリプトを作る
using UnityEngine;

// 1回のバトルに出てくる敵の組み合わせ
[System.Serializable]
public class EnemyGroup
{
    public EnemyData[] enemies; // 出てくる敵(1〜4体)
}

// 地下の1つの階のデータ:名前・背景と、戦う順番に並べた敵の組み合わせ
[CreateAssetMenu(fileName = "NewFloor", menuName = "TurnBasedRpg/Floor Data")]
public class FloorData : ScriptableObject
{
    [SerializeField] private string floorName;     // 画面に出す名前(ちか1かい)
    [SerializeField] private Sprite background;    // バトルの背景
    [SerializeField] private EnemyGroup[] battles; // 戦う順番に、出てくる敵の組み合わせ(この数だけ戦う)

    public string FloorName => floorName;
    public Sprite Background => background;
    public int BattleCount => battles.Length;

    // battleIndex 番目(0 から)のバトルに出てくる敵
    public EnemyData[] GetEnemies(int battleIndex)
    {
        return battles[battleIndex].enemies;
    }
}
  • EnemyGroup:1回のバトルの「敵の組み合わせ」です。Unityは、配列の配列(EnemyData[][])をInspectorに出せないので、配列を1つ持つクラスで包んでから、その配列(EnemyGroup[])にしています
  • [System.Serializable]を付けると、MonoBehaviourでもScriptableObjectでもない普通のクラスも、Inspectorで中身を入れられるようになります(#16のItemStackと同じです)
  • battles:並べた順番に戦います。要素の数が、その階で戦う回数になります

「何回戦うか」を別の数で持たずに、battlesの数で決めています。こうしておくと、数と中身がずれることがありません。あとでボスを足すときも、要素を1つ増やすだけで済みます。

  • TurnBasedRpg/Dataに「Floors」フォルダを作り、Create>TurnBasedRpg>Floor Data で3つ作る(Floor1・Floor2・Floor3)
  • 上の「地下1〜3階と、戦う順番の敵」の図のとおりに入れる
ファイル名Floor NameBackground1回目2回目3回目
Floor1ちか1かいbg_library_b1SpilledInk・SpilledInkPaperPlane・PaperPlane・PaperPlaneBookworm・SpilledInk
Floor2ちか2かいbg_library_b2QuillPen・QuillPenThickDictionary・QuillPenPopupGlobe・PopupGlobe
Floor3ちか3かいbg_library_b3LampGhost・LampGhostMagicPage・MagicPage・MagicPageLampGhost・MagicPage・MagicPage

GameSession

LobbyのGameSessionのInspector。Start Membersの4人、Start Items、Start Gold 50の下に、Floorsが3つ(Floor1・Floor2・Floor3)
GameSessionに3つの階

階のデータと「どこまで進んだか」は、シーンをまたいで覚えておきたいので、#17で作ったGameSessionに持たせます。

startGoldの下に、欄を足します。

    [SerializeField] private FloorData[] floors;         // 地下の階(上から 地下1・地下2・地下3)

Goldの下に、プロパティを足します。

    public FloorData[] Floors => floors;
    public int ClearedFloorCount { get; set; } // クリアした階の数(地下1階をクリアしたら 1)
    public int ReachedFloorCount => Mathf.Min(ClearedFloorCount + 1, floors.Length); // 行ける階の数(クリアした階の、1つ下まで)
    public int FloorIndex { get; set; }  // 今いる階(0 が地下1階)
    public int BattleIndex { get; set; } // その階の、何回目のバトルか(0 から)
    public FloorData CurrentFloor => floors[FloorIndex];
  • ClearedFloorCount:クリアした階の数です。覚えておくのはこの数だけで、行ける階の数(ReachedFloorCount)は、ここから計算します
  • ReachedFloorCount:クリアした階の1つ下まで行けます。地下3階より下は無いので、Mathf.Minで階の数より大きくならないようにしています
  • FloorIndex・BattleIndex:今いる階と、その階の何回目か。ロビーで階を選んだときに決めて、勝つたびにBattleIndexを1つ進めます
  • Hierarchyの GameSession を選び、Floorsを 3 にして、Floor1・Floor2・Floor3 を順に入れる

地下を進もう

「しょうり!」のウインドで「さきへ すすむ」を選ぶと、ウインドが消えて、フェアリー・スライムちゃん・キョンシーが少しずつずれて右へ走り出し、画面の外へ出ていく。暗くなって、次のバトルが始まる
右へ走って、次のバトルへ

この節では、Battleシーンを「今いる階の、何回目のバトル」にして、勝ったら先へ進めるようにします。

次のバトルのために新しいシーンは作りません。同じBattleシーンを、もう一度読み込みます。読み込み直すたびに、GameSessionのBattleIndexを見て、出てくる敵を変えます。

今いる階の敵と背景で並べる

#17で、仲間と持ち物はGameSessionから読むようにしました。敵も同じように、GameSessionの今いる階から読みます。

BattleLineupの一番上にusing UnityEngine.UI;を足して(Imageを使うので)、enemiesの欄を次のように書き換えます。

    [SerializeField] private EnemyData[] enemies;    // 確認用:Battle シーンから再生したときの敵(1〜4体)
    [SerializeField] private Image background;       // バトルの背景(階ごとに変える)

Setupの、敵を並べるループを次のように書き換えます。

        EnemyData[] enemyGroup = LoadEnemies();
        for (int i = 0; i < enemyGroup.Length; i++)
        {
            EnemyData enemy = enemyGroup[i];
            UnitView view = Instantiate(enemyPrefab, enemyArea);
            // 真ん中をそろえて横に並べ、1体おきに上下へずらす
            float x = (i - (enemyGroup.Length - 1) / 2f) * enemySpacing;
            float y = (i % 2 == 0) ? 60f : -60f;
            ((RectTransform)view.transform).anchoredPosition = new Vector2(x, y);
            view.SetupEnemy(enemy);
            EnemyUnits.Add(new BattleUnit(enemy, view));
        }

LoadPartyの上に、次のメソッドを足します。

    // ロビーから来たら、今いる階の背景にして、その階の何回目かのバトルの敵を出す
    // Battle シーンから再生したとき(GameSession が無い)は、確認用に Inspector の敵を出す
    private EnemyData[] LoadEnemies()
    {
        GameSession session = GameSession.Instance;
        if (session == null)
        {
            return enemies;
        }
        FloorData floor = session.CurrentFloor;
        background.sprite = floor.Background;
        return floor.GetEnemies(session.BattleIndex);
    }
  • enemiesは、partyやstartItemsと同じ「Battleシーンから再生したときの確認用」になります
  • Hierarchyの Battle を選び、Battle LineupのBackgroundに、Canvasの Background を入れる

今いる階のウインド

バトルの画面の右上を大きく写したところ。メッセージのウインドの下に、紺のウインドで「ちか1かい 2/3」
右上に、今いる階と何回目か

今どの階の、何回目のバトルなのかが分かるように、右上に小さなウインドを出します。ロビーのお金のウインド(GoldWindow)と同じ形なので、コピーして使います。

BattleのHierarchyで、MessageWindowの下にFloorWindow。InspectorはRect TransformのPos X -80・Pos Y -180、Width 420・Height 90
GoldWindowをコピーしたFloorWindow
  • Lobbyシーンを開き、HierarchyのGoldWindowを選んでCtrl+C
  • Battleシーンを開き、Canvasを右クリック>Paste As Child
  • 名前を「FloorWindow」にして、MessageWindowのすぐ下に置く
  • Gold Counterのコンポーネントを外す(右上の︙>Remove Component)
  • Widthを 420 にする(「ちか1かい 1/3」が1行に収まるように)
  • 子のGoldTextの名前を「FloorText」にする

Widthがお金のウインドと同じ280のままだと、「ちか1かい」と「1/3」が2行に折り返して、枠からはみ出してしまいます。階の名前を長くしたときも、1行に収まるようにWidthを広げてください。

BattleManagerのsceneFaderの下に、欄を足します。

    [SerializeField] private GameObject floorWindow; // 右上の、今いる階のウインド
    [SerializeField] private TMP_Text floorText;     // 今いる階と、何回目のバトルか(ちか1かい  2/3)

StartBattleAsyncで、lineup.Setup();の下に1行足します。

        ShowFloor();

SelectActionsAsyncの上に、次のメソッドを足します。

    // 右上に、今いる階と、何回目のバトルか(Battle シーンから再生したときは、ウインドを隠す)
    private void ShowFloor()
    {
        GameSession session = GameSession.Instance;
        floorWindow.SetActive(session != null);
        if (session != null)
        {
            FloorData floor = session.CurrentFloor;
            floorText.text = $"{floor.FloorName}  {session.BattleIndex + 1}/{floor.BattleCount}";
        }
    }
  • BattleIndexは0から数えているので、画面には+ 1して「1/3」から出します
  • Battleオブジェクトの Battle Manager に、Floor Window(FloorWindow)・Floor Text(FloorText)を入れる

結果のウインドに「さきへ すすむ」

「しょうり! けいけんち 6ポイント 8ゴールドを てにいれた」のウインドの下に、黄色く選ばれた「さきへ すすむ」と、白い「ロビーへ」のボタンが横に並んでいる
さきへ すすむ と ロビーへ

勝ったときのウインド(#11のResultWindow)に、「さきへ すすむ」のボタンを足します。負けたときや、その階の最後のバトルでは、このボタンは出しません。

BattleのHierarchyで、ResultWindowの中にButtonsがあり、その子にNextButton(灰色)とRetryButton。InspectorはButtonsで、Rect TransformのPos Y 80・Width 640・Height 64、Horizontal Layout GroupのSpacing 40、Child Alignment:Middle Center、Control Child SizeとChild Force Expandのチェックは全部外れている
2つのボタンを、Horizontal Layout Groupで横に並べる
  • Hierarchyで ResultWindow を右クリック>Create Empty で空のオブジェクトを作り、名前を「Buttons」にする
    • Rect Transform:アンカーを下の真ん中(bottom-center)、Pos Y 80、Width 640、Height 64
    • Add Component>Horizontal Layout Group:Spacing 40、Child Alignment:Middle Center、Control Child SizeとChild Force Expandのチェックは全部外す
  • RetryButton(ロビーへ)を、Buttonsの中へドラッグする
  • RetryButtonを選んでCtrl+Dで複製し、名前を「NextButton」にして、Buttonsの一番上(RetryButtonの上)に置く
    • 子のText (TMP)の文字を「さきへ すすむ」にする
    • 一番上のチェックを外して、隠しておく

9-6 Scroll ViewとLayout Groupでリストを作ろうのHorizontal Layout Groupは、隠した子を飛ばして並べます。なので、NextButtonを隠すと、ロビーへのボタンだけが真ん中に来ます。位置を自分で計算しなくていいので楽ですね。

ResultWindowを、次のように書き換えます。

using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

// バトルが終わったときのウインド(勝った・負けた・にげた)。「ロビーへ」か「さきへ すすむ」が押されるまで待つ
public class ResultWindow : MonoBehaviour
{
    [SerializeField] private TMP_Text titleText;
    [SerializeField] private TMP_Text bodyText;
    [SerializeField] private Button retryButton; // ロビーへ
    [SerializeField] private Button nextButton;  // さきへ すすむ(まだ先のバトルがあるときだけ出す)

    // どちらかのボタンが押されたら、待っているところに知らせる(さきへ すすむ なら true)
    private readonly AwaitableCompletionSource<bool> choiceSource = new AwaitableCompletionSource<bool>();

    private void Awake()
    {
        retryButton.onClick.AddListener(() => choiceSource.TrySetResult(false));
        nextButton.onClick.AddListener(() => choiceSource.TrySetResult(true));
        gameObject.SetActive(false); // 最初は隠しておく
    }

    // ウインドを出して、ボタンが押されるまで待つ。canGoOn が true なら「さきへ すすむ」も出す
    // 「さきへ すすむ」が押されたら true
    public async Awaitable<bool> ShowAsync(string title, string body, bool canGoOn = false)
    {
        titleText.text = title;
        bodyText.text = body;
        nextButton.gameObject.SetActive(canGoOn);
        choiceSource.Reset();
        gameObject.SetActive(true);

        // 最後のメッセージを読んだときの決定が、このフレームに残っていることがあるので、1フレーム待ってから選ぶ
        await Awaitable.NextFrameAsync(destroyCancellationToken);
        Button firstButton = canGoOn ? nextButton : retryButton; // 先へ進めるなら、そちらを先に選んでおく
        EventSystem.current.SetSelectedGameObject(firstButton.gameObject);

        bool goesOn = await choiceSource.Awaitable;
        gameObject.SetActive(false);
        return goesOn;
    }
}
  • AwaitableCompletionSource<bool>:今までは「押された」ことだけを知らせていましたが、どちらのボタンが押されたかをboolで返すようにしました。#6で、選んだ相手を返したAwaitableCompletionSource<UnitView>と同じ形です
  • ボタンが押されたときの処理は1行だけなので、メソッドを作らずにラムダ式(() => …)で登録しています
  • canGoOn = falseのように、引数に= 値を書いておくと、呼ぶときに省略できます。負けた・にげたときのShowAsync("ぜんめつ…", …)は、今までのままでOKです
  • ResultWindowの Result Window に、Next Button(NextButton)を入れる

ResultWindowは「どちらが押されたか」を返すだけで、押されたあとに何をするか(次のバトルへ・ロビーへ)は、呼んだBattleManagerが決めます。ウインドは選ばせることだけに集中させておくと、あとでボタンを増やすときも直す場所が少なくて済みます。

走って次のバトルへ

「さきへ すすむ」を選んだら、3人が右へ走って画面の外へ出ていき、それから次のバトルにします。#11の、にげるときのRunAwayAsyncの反対向きです。

UnitViewのRunAwayAsyncの下に、次のメソッドを足します。

    // 先へ進む:右を向いたまま、画面の外へ走っていく
    public async Awaitable RunForwardAsync()
    {
        Vector2 from = rectTransform.anchoredPosition;
        Vector2 to = from + new Vector2(1800f, 0f);
        const float seconds = 0.7f;
        float time = 0f;
        while (time < seconds)
        {
            time += Time.deltaTime;
            rectTransform.anchoredPosition = Vector2.Lerp(from, to, time / seconds);
            await Awaitable.NextFrameAsync(destroyCancellationToken);
        }
    }
  • 仲間は画面の左にいるので、1800右へ動かすと、画面(横1920)の右の外まで出ます

BattleManagerのRunAsyncを直していきます。まず、phase = BattlePhase.Start;の下に変数を足します。

        bool goesOn = false; // 勝って「さきへ すすむ」を選んだら true

BattlePhase.Victoryのところで、ShowVictoryAsyncの結果を受け取ります。

                case BattlePhase.Victory:
                    goesOn = await ShowVictoryAsync();

RunAsyncの最後の、ロビーに戻るところを次のように書き換えます。

        if (goesOn)
        {
            // 「さきへ すすむ」:みんなで右へ走っていって、同じシーンで次のバトル
            await RunForwardAsync();
            await sceneFader.LoadSceneAsync("Battle");
        }
        else
        {
            // 「ロビーへ」が押された:暗くして、ロビーに戻る
            await sceneFader.LoadSceneAsync("Lobby");
        }
  • 先へ進むときも、その上の「バトルの結果を持ち越す」(SaveToMember)は通ります。なので、減ったHPやMPは、そのまま次のバトルに持ち越されます

ShowVictoryAsyncを、boolを返すように書き換えます(コメントを1行足して、AwaitableをAwaitable<bool>にします)。

    // 「さきへ すすむ」を選んだら true
    private async Awaitable<bool> ShowVictoryAsync()

ShowVictoryAsyncの最後のresultWindow.ShowAsync(…)の行を、次のように書き換えます。

        return await ShowVictoryResultAsync($"けいけんち {exp}ポイント\n{gold}ゴールドを てにいれた");

ShowVictoryAsyncの下に、2つのメソッドを足します。

    // 勝ったときのウインド。その階にまだバトルが残っていれば「さきへ すすむ」も出す(選んだら true)
    // 最後のバトルなら、その階をクリアして、次の階へ行けるようにする
    private async Awaitable<bool> ShowVictoryResultAsync(string body)
    {
        GameSession session = GameSession.Instance;
        if (session == null)
        {
            await resultWindow.ShowAsync("しょうり!", body); // Battle シーンから再生したときは、ロビーへ戻るだけ
            return false;
        }

        FloorData floor = session.CurrentFloor;
        session.BattleIndex++;
        if (session.BattleIndex < floor.BattleCount)
        {
            return await resultWindow.ShowAsync("しょうり!", body, true);
        }

        // その階の最後のバトルに勝った:クリア。はじめてなら、次の階へ行けるようになる
        await messageWindow.ShowAsync($"{floor.FloorName}を クリアした!");
        if (session.FloorIndex == session.ClearedFloorCount)
        {
            session.ClearedFloorCount++;
            int nextIndex = session.FloorIndex + 1;
            if (nextIndex < session.Floors.Length)
            {
                await messageWindow.ShowAsync($"{session.Floors[nextIndex].FloorName}へ いけるように なった!");
            }
        }
        await resultWindow.ShowAsync("クリア!", body);
        return false;
    }

    // 先へ進む:立っている仲間が、少しずつずれて右へ走っていく
    private async Awaitable RunForwardAsync()
    {
        List<Awaitable> runs = new List<Awaitable>();
        foreach (BattleUnit unit in lineup.PartyUnits)
        {
            if (unit.IsDead)
            {
                continue; // 倒れている人は、その場で眠ったまま(次のバトルにも、倒れたまま出る)
            }
            unit.View.ResumeIdle(); // 喜ぶポーズから、走る(弾む)すがたに戻す
            runs.Add(unit.View.RunForwardAsync());
            await Awaitable.WaitForSecondsAsync(0.08f, destroyCancellationToken);
        }
        foreach (Awaitable run in runs)
        {
            await run;
        }
    }
  • BattleIndex++:勝ったら、次のバトルの番号へ進めます。まだその階のバトルが残っていれば、「さきへ すすむ」も出します
  • 最後のバトルなら、その階はクリアです。クリアのところは、次の節で説明します
  • RunForwardAsync:3人いっしょに走り出すより、0.08秒ずつずらすと、ばらばらに駆け出していく感じになります。始めるのだけずらして、着くのはあとでまとめて待ちます(#20の「先に始めてから、順にawait」と同じです)

次の階へ

地下1階の3回目、本の虫とこぼれたインクを倒すと「ちか1かいを クリアした!」「ちか2かいへ いけるように なった!」と出て、「クリア!」のウインドには「ロビーへ」のボタンだけ
地下1階をクリア

この節では、クリアした階を覚えて、ロビーから次の階へ行けるようにします。

クリアした階を覚える

前の節のShowVictoryResultAsyncの後半が、階のクリアです。

  • その階の最後のバトルに勝ったら「〇〇を クリアした!」
  • session.FloorIndex == session.ClearedFloorCountは、「今いる階が、まだクリアしていない一番上の階か」です。地下1階(0)を初めてクリアしたとき、ClearedFloorCountは0なので、ここに入って1になります
  • もう一度地下1階をクリアしても、ClearedFloorCountはもう1なので、増えません(行ける階が勝手に増えないように)
  • 次の階があれば「〇〇へ いけるように なった!」と知らせます。地下3階には次の階が無いので、出しません

地下3階のいちばん奥には、ボスの「禁じられた本」がいます。ボスは、FloorDataのBattlesにもう1つ足すだけで「4回目のバトル」として出せるようにしてあります。

ロビーで階を選ぶ

ロビーで「地下へ」を押すと、右に「どの かいへ いく?」の一覧。ちか1かい の右はしに「クリア」、ちか2かい が選ばれていて、ちか3かい は灰色。決めると暗くなって、緑がかった書庫で羽ペン2体とのバトルが始まり、右上に「ちか2かい 1/3」
ロビーの「地下へ」で、行く階を選ぶ

ロビーの「地下へ」を押したら、行く階の一覧を出します。まだ行けない階は灰色にして、「この先にまだ階がある」ことが分かるようにしています。

LobbyのHierarchyで、PartyWindowの下にFloorWindow(灰色)。InspectorはFloorWindowで、Pos X 1450・Pos Y 560、Width 560・Height 448
ShopWindowを複製したFloorWindow
  • Lobbyシーンを開き、ShopWindowを選んでCtrl+Dで複製し、名前を「FloorWindow」にする(隠したまま。PartyWindowの下に移す)

LobbyMenuのdungeonButtonの下に、欄を足します。

    [SerializeField] private CommandMenu floorMenu;   // 行く階の一覧(FloorWindow)

OnDungeonClickedを次のように書き換えて、その下にChooseFloorAsyncを足します。

    // 地下へ:行く階を選んで、その階の1回目のバトルへ(キャンセルしたら、メニューに戻る)
    private async void OnDungeonClicked()
    {
        if (isBusy)
        {
            return;
        }
        isBusy = true;
        menuGroup.interactable = false;
        try
        {
            int floorIndex = await ChooseFloorAsync();
            if (floorIndex >= 0)
            {
                GameSession session = GameSession.Instance;
                session.FloorIndex = floorIndex;
                session.BattleIndex = 0;
                await sceneFader.LoadSceneAsync("Battle");
                return;
            }
        }
        catch (System.OperationCanceledException)
        {
            return; // シーンが切り替わったら、そこで終わり
        }
        BackToMenu(dungeonButton);
    }

    // 地下の階の一覧から、行く階を選ぶ(まだ行けない階は灰色)。キャンセルなら -1
    private async Awaitable<int> ChooseFloorAsync()
    {
        GameSession session = GameSession.Instance;
        FloorData[] floors = session.Floors;
        string[] names = new string[floors.Length];
        string[] values = new string[floors.Length];
        bool[] usables = new bool[floors.Length];
        for (int i = 0; i < floors.Length; i++)
        {
            FloorData floor = floors[i];
            names[i] = floor.FloorName;
            values[i] = i < session.ClearedFloorCount ? "クリア" : "";
            usables[i] = i < session.ReachedFloorCount;
        }

        messageText.text = "どの かいへ いく?";
        floorMenu.gameObject.SetActive(true);
        // 最初は、行ける中で一番深い階を選んでおく
        int floorIndex = await floorMenu.WaitForChoiceAsync(names, values, usables, session.ReachedFloorCount - 1);
        floorMenu.gameObject.SetActive(false);
        return floorIndex;
    }
  • 一覧は、#18で作ったWaitForChoiceAsyncです。右はしには、クリアした階に「クリア」を出します
  • 階を選んだら、FloorIndexをその階に、BattleIndexを0にしてから、Battleシーンを読み込みます。途中でロビーへ帰った階も、次は1回目からです
  • 一番深い階を最初に選んでおくと、Enterを押すだけで、新しい階へ進めます

キャンセルしたときに、メニューへ戻る処理は、#19のOpenSubMenuの後半と同じです。なので、そこをメソッドに分けて、両方から使います。OpenSubMenuの、menuGroup.interactable = true;からisBusy = false;までをBackToMenu(button);の1行にして、OpenSubMenuの下に次のメソッドを足します。

    // ロビーのメニューに戻る(button を選び直す)
    private void BackToMenu(Button button)
    {
        menuGroup.interactable = true;
        messageText.text = "ここは まほう図書館の ロビー。";
        ShowParty(); // 装備で最大HPなどが変わったときのために、出し直す
        EventSystem.current.SetSelectedGameObject(button.gameObject);
        isBusy = false;
    }
LobbyのLobbyオブジェクトのInspector。Lobby Menu (Script)のDungeon Buttonの下に、Floor Menu(FloorWindow (Command Menu))が増えている
LobbyMenuにFloorWindowをつなぐ
  • Lobbyオブジェクトの Lobby Menu の Floor Menu に、FloorWindow を入れる
フェアリー

灰色の「ちか3かい」が見えていると、早く行ってみたくなるでしょ?

売店の品ぞろえを増やす

売店の一覧を2つ並べた画像。左の「クリアする前」は ハーブティー・まほうのクッキー・どくけしキャンディ・ペーパーナイフ・ブックカバー の5つ。右の「ちか1かい をクリアしたあと」は、めざましどけい・ぶんちん・司書のエプロンが増えて、一番下の司書のエプロン 100G が選ばれている
地下2階へ行けるようになると、売り物が増える

#18 売店で作ったきり、まだ並べていなかった強い装備を、深い階へ行けるようになったら並べます。

売り物(IShopGoods)に、「この階まで行けるようになったら並ぶ」という数を足します。IShopGoodsのPriceの下に1行足します。

    int ShopFloor { get; }    // 売店に並ぶのは、この階まで行けるようになってから(1 なら最初から)

ItemDataとEquipmentDataのpriceの欄の下に、欄を足します(コメントの位置は、それぞれの欄に合わせてください)。

    [SerializeField] private int shopFloor = 1;           // 売店に並ぶようになる階(2 なら、地下2階へ行けるようになってから)

どちらも、Priceのプロパティの下に1行足します。

    public int ShopFloor => shopFloor;

ShopのOpenAsyncで、売り物のListを作ったあとに、まだ並べないものを取りのぞきます。

        List<IShopGoods> goodsList = new List<IShopGoods>();
        goodsList.AddRange(items);
        goodsList.AddRange(equipments);
        // まだ行けない階の売り物は、並べない
        goodsList.RemoveAll(product => product.ShopFloor > session.ReachedFloorCount);
  • RemoveAll:Listから、条件に合うものを全部取りのぞきます。条件はラムダ式で書きます(productは、Listの1つずつ)
  • Shopの欄には、売り物を全部入れておくだけで、並ぶかどうかは、それぞれのデータのshopFloorで決まります
PaperweightのInspector。Equipment Nameに ぶんちん、Slot:Weapon、Price 120、BonusのAttack 7、一番下にShop Floor 2
ぶんちんは、地下2階へ行けるようになってから
  • 次の5つの Shop Floor を変える(ほかは1のまま)
    • 2:AlarmClock(めざましどけい)・Paperweight(ぶんちん)・LibrarianApron(司書のエプロン)
    • 3:MagicPen(まほうのペン)・MagicCape(まほうのマント)
LobbyのShopオブジェクトのInspector。ItemsにHerbTea・MagicCookie・AntidoteCandy・AlarmClockの4つ、EquipmentsにPaperKnife・BookCover・Paperweight・LibrarianApron・MagicPen・MagicCapeの6つ
Shopの欄には、売り物を全部入れる
  • Lobbyの Shop を選び、Itemsに AlarmClock、Equipmentsに Paperweight・LibrarianApron・MagicPen・MagicCape を足す

動かして確認!

地下2階のバトル。緑がかった書庫で、右上に「ちか2かい 1/3」。羽ペン2体を、スライムちゃん・フェアリー・キョンシーが たたかう で攻撃する。羽ペンの こうげき で、フェアリーのHPが大きく減る
地下2階は、敵がぐっと強くなる
  • Lobbyシーンを開いて再生し、「地下へ」→ ちか1かい
  • 右上に「ちか1かい 1/3」。こぼれたインク2体とのバトルになる
  • 勝つと「さきへ すすむ」と「ロビーへ」。進むと3人が右へ走っていき、紙ひこうき3体の「2/3」になる
  • 3回目に勝つと「ちか1かいを クリアした!」「ちか2かいへ いけるように なった!」で、ロビーへ
  • 「地下へ」の一覧で、ちか1かい に「クリア」、ちか2かい が選べる。ちか3かい は灰色
  • 売店に、めざましどけい・ぶんちん・司書のエプロンが並ぶ
  • ちか2かい は、背景が緑がかった書庫になり、羽ペンなどの強い敵が出る

GIFは、確かめやすいように、仲間を最初からLv4にして撮っています。Lv1のままだと、地下1階の3回でもHPがけっこう減るので、途中で一度ロビーへ帰って休憩室に寄るのもアリです。

#21の動作確認

  • ロビーの「地下へ」で、階の一覧が出る(まだ行けない階は灰色)
  • 右上に、今いる階と何回目のバトルか(ちか1かい 1/3)が1行で出る
  • 勝つと「さきへ すすむ」「ロビーへ」を選べる。最後のバトルでは「ロビーへ」だけ
  • 「さきへ すすむ」で、立っている仲間が右へ走っていき、次のバトルになる(HP・MPはそのまま)
  • 階の3回目に勝つと、その階をクリアして、次の階へ行けるようになる
  • 地下2階へ行けるようになると、売店の品物が増える

あとがき

今回は、地下を3つの階に分けて、何回か戦いながら奥へ進めるようにしました。階の中身をFloorDataにまとめたので、敵の組み合わせを変えたり、戦う回数を増やしたりするのは、Inspectorだけで出来ます。次回は、地下2・3階の敵に、それぞれの特別な行動を付けたいと思います!

スライム

地下2階の羽ペン、「たたく」だけなのに、もうけっこう痛いです・・・

フェアリー

ぶんちんと司書のエプロンを買ってから行くといいわよ!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次