ターン制RPGの作り方#17|ロビーと休憩室:シーンをまたいで、HPとお金を持ち越そう

こぼれたインクを倒して「しょうり!」。「ロビーへ」を押すと画面が暗くなり、図書館のロビーに戻る。HPはバトルで減ったまま(スライム 12/38)で、右上のお金は54G。「休憩室」を選ぶと10G払って、暗くなった部屋で3人がうとうとし、明るくなるとHPが満タンになって喜ぶ
バトルからロビーに戻り、休憩室で元気になる

ここまでは、バトルが終わると同じバトルを最初からやり直していました。せっかくレベルが上がっても、次のバトルではLv1に戻ってしまいます・・・。今回は、まほう図書館の「ロビー」を作って、バトルとロビーを行き来できるようにしたいと思います。減ったHPや、もらったお金はそのまま持ち越し、休憩室でお金を払うと元気になりますよ!

目次

今回作るもの

まほう図書館のロビー。窓と本棚のある部屋に、フェアリー・スライムちゃん・キョンシーが立っている。上に「ここは まほう図書館の ロビー。」、右上に「50 G」、左下のメニューに 地下へ・休憩室・売店・なかま・セーブ(売店から下は灰色)、右下に3人のHPとMP
まほう図書館のロビー
  • まほう図書館の「ロビー」のシーン(Lobby)を作る。メニューは「地下へ」「休憩室」「売店」「なかま」「セーブ」
    • 今回動くのは「地下へ」と「休憩室」です。売店は#18、なかまは#20、セーブは#25で作ります(それまでは灰色で押せないボタン)
  • 仲間(経験値・今のHPとMP)、持ち物、お金を、シーンをまたいで持ち越す(GameSession)
  • シーンを切り替えるときは、画面を暗くしてから読み込み、新しいシーンを明るくして始める(SceneFader)
  • バトルが終わったら「ロビーへ」で戻る。ぜんめつしたら、お金が半分になってロビーで目をさます
  • 休憩室:10ゴールドで、みんなのHPとMPが満タンになる(倒れた仲間も起きる)
スクリプト役割付ける場所
PartyMember仲間1人分の、バトルが終わっても残るもの(付けない)
GameSessionシーンをまたいで持ち越すもの(仲間・持ち物・お金)Lobby の GameSession
SceneFader暗くしてから、シーンを切り替えるLobby・Battle の SceneFade
BattleUnit・BattleLineup・BattleManager持ち越したものでバトルを始めて、結果を書き戻す-
LobbyMenuロビーのメニューと休憩室Lobby の Lobby
CharacterData・UnitView休憩室で うとうとするポーズ-

持ち越すものをまとめよう

上に「Lobby シーン」と「Battle シーン」の箱があり、「地下へ →」「← ロビーへ」で行き来する。下の緑の箱は「GameSession(DontDestroyOnLoad:シーンを切り替えても消えない)」で、中に Party:仲間3人(PartyMember)、Inventory:持ち物、Gold:お金
シーンを切り替えても、GameSessionだけは残る

この節では、シーンを切り替えても消えずに残る入れ物を作ります。

シーンを切り替えると、前のシーンのオブジェクトは全部消えます。BattleLineupの中の仲間(BattleUnit)も、持ち物も一緒に消えてしまいますね。そこで、持ち越したいものをGameSessionという1つのオブジェクトにまとめて、シーンを切り替えても消えないようにします。

PartyMember

2つのステータスの行。上の「バトルの終わり」も、下の「ロビーに戻ったところ」も、スライム HP 12/38・MP 8/8、フェアリー 9/26・4/20、キョンシー 20/32・6/6 で同じ
バトルで減ったHPが、ロビーでもそのまま

バトル中の1人(BattleUnit)は、絵(UnitView)やステータスの行とつながっているので、バトルのシーンと一緒に消えてしまいます。そこで、バトルが終わっても残したいものだけを、別のクラスにします。

  • 「PartyMember」スクリプトを作る
// 仲間1人分の、バトルが終わっても残るもの(経験値・今の HP と MP)
public class PartyMember
{
    public CharacterData Data { get; }
    public int TotalExp { get; set; } // もらった経験値の合計
    public int Hp { get; set; }
    public int Mp { get; set; }

    public int Level => LevelTable.LevelFor(TotalExp);
    public Stats Stats => Stats.AtLevel(Data.BaseStats, Data.Growth, Level); // 今のレベルの能力
    public bool IsDead => Hp <= 0;

    // 最初は HP・MP を満タンにしておく
    public PartyMember(CharacterData data, int totalExp)
    {
        Data = data;
        TotalExp = totalExp;
        RestoreAll();
    }

    // HP・MP を満タンにする(休憩室・ぜんめつしたとき)
    public void RestoreAll()
    {
        Hp = Stats.maxHp;
        Mp = Stats.maxMp;
    }
}
  • 覚えておくのは、経験値の合計・今のHP・今のMPの3つだけです
  • レベルと能力は、経験値から計算できるので覚えません。#12 経験値とレベルアップのLevelTableとStats.AtLevelをそのまま使います
  • 覚えたまほうも、レベルから決まるので覚えません(BattleUnitを作るときにLearnSkillsで調べ直します)
  • MonoBehaviourではない、ふつうのクラスです。持ち物のInventoryと同じですね

CharacterData(ScriptableObject)に今のHPを書き込まないの?

ScriptableObjectのアセットに値を書き込んでも、シーンを切り替えて残ります。ただ、エディタで再生しているときは、書き込んだ値が再生を止めても戻りません。遊ぶたびにアセットのHPが減っていき、気づかないうちにデータが壊れてしまいます。

ScriptableObjectは「最初の能力」「成長」のような、遊んでも変わらないデータだけにして、遊んで変わるものはPartyMemberのような別のクラスに持たせましょう。ScriptableObjectについては12-8 ScriptableObjectでデータを作ろうを見てください。

GameSession

ロビーの右上のお金のウインド。左の「最初」は 50 G、右の「1回勝って戻った」は 54 G
もらったお金も持ち越す

仲間3人・持ち物・お金をまとめて持つ、GameSessionを作ります。

  • 「GameSession」スクリプトを作る
using System.Collections.Generic;
using UnityEngine;

// シーンをまたいで持ち越すもの(仲間・持ち物・お金)。ロビーのシーンに1つ置く
public class GameSession : MonoBehaviour
{
    [SerializeField] private CharacterData[] startParty; // 最初の仲間(3人)
    [SerializeField] private ItemStack[] startItems;     // 最初の持ち物
    [SerializeField] private int startGold = 50;         // 最初のお金

    // どのシーンからでも、GameSession.Instance で使えるようにする
    public static GameSession Instance { get; private set; }

    public List<PartyMember> Party { get; } = new List<PartyMember>();
    public Inventory Inventory { get; } = new Inventory();
    public int Gold { get; set; }

    private void Awake()
    {
        // ロビーに戻ってくると、シーンの GameSession がもう1つ作られる。先にあるほうを使い、新しいほうは消す
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject); // シーンを切り替えても消さない

        foreach (CharacterData data in startParty)
        {
            Party.Add(new PartyMember(data, 0));
        }
        foreach (ItemStack stack in startItems)
        {
            Inventory.Add(stack.item, stack.count);
        }
        Gold = startGold;
    }
}
  • Instance:staticなので、どのシーンのスクリプトからでもGameSession.Instance.Goldのように使えます
  • 2つ目を消す:GameSessionはLobbyシーンに置くので、ロビーに戻ってくるたびに新しいGameSessionが作られます。先にあるほう(遊んできた中身が入っているほう)を残して、新しいほうは消します
  • 最初の仲間・持ち物・お金はInspectorで決めます。今まで#16でBattleLineupに書いていた最初の持ち物は、ここに引っ越します

暗くしてシーンを切り替えよう

ロビーで「地下へ」を押すと、画面が真っ黒になり、図書館の地下のバトル画面がだんだん明るくなって「こぼれたインクたちが あらわれた!」
暗くなってから、次のシーンが明るくなる

この節では、画面を黒くしてからシーンを切り替える仕組みを作ります。いきなりパッと切り替わるより、ずっと「場所を移った」感じが出ます!

SceneFader

BattleシーンのHierarchyで、Canvasの一番下にSceneFadeがある。InspectorはRect Transformが画面いっぱい(stretch、Left・Top・Right・Bottomが0)、ImageのColorが黒、Scene FaderのFade Secondsが0.4
Canvasの一番下に、画面いっぱいの黒い板

画面いっぱいの黒い板(Image)を置いて、その濃さを変えます。

  • 「SceneFader」スクリプトを作る
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

// シーンの切り替え:黒い板で暗くしてから読み込む。新しいシーンは、真っ黒から明るくして始める
[RequireComponent(typeof(Image))]
public class SceneFader : MonoBehaviour
{
    [SerializeField] private float fadeSeconds = 0.4f; // 暗くする・明るくするのにかける時間

    private Image image;

    private void Awake()
    {
        image = GetComponent<Image>();
        SetAlpha(1f); // シーンの始めは真っ黒
    }

    private async void Start()
    {
        try
        {
            await FadeAsync(0f);
        }
        catch (System.OperationCanceledException)
        {
            // 明るくしている途中でシーンが切り替わったら、そこで終わり
        }
    }

    // 暗くしてから、sceneName のシーンを読み込む
    public async Awaitable LoadSceneAsync(string sceneName)
    {
        await FadeAsync(1f);
        await SceneManager.LoadSceneAsync(sceneName);
    }

    // fadeSeconds 秒かけて、黒い板の濃さを targetAlpha(0〜1)にする
    private async Awaitable FadeAsync(float targetAlpha)
    {
        float startAlpha = image.color.a;
        float time = 0f;
        while (time < fadeSeconds)
        {
            time += Time.deltaTime;
            SetAlpha(Mathf.Lerp(startAlpha, targetAlpha, time / fadeSeconds));
            await Awaitable.NextFrameAsync(destroyCancellationToken);
        }
        SetAlpha(targetAlpha);
    }

    // 濃さを変える。透明なときは Image を切って、ボタンのクリックをじゃましないようにする
    private void SetAlpha(float alpha)
    {
        Color color = image.color;
        color.a = alpha;
        image.color = color;
        image.enabled = alpha > 0f;
    }
}
  • LoadSceneAsync:暗くし終わってから、SceneManager.LoadSceneAsyncで次のシーンを読み込みます。LoadSceneAsyncはawaitで待てるので、読み込みが終わるまで待ってから先に進みます(6-16 待つ処理をすっきり書こう:async/awaitとAwaitable)
  • Awake・Start:新しいシーンは真っ黒で始めて、Startでだんだん明るくします。前のシーンの最後(真っ黒)とつながるので、切り替わる瞬間が見えません
  • SetAlpha:透明なときはimage.enabled = falseにしています。黒い板は画面いっぱいなので、透明でも残っているとボタンのクリックを横取りしてしまうからです
  • シーンが切り替わると、SceneFaderも消えます。そのときdestroyCancellationTokenで待つのが止まってOperationCanceledExceptionが出るので、catchで受け止めています

BattleシーンにSceneFadeを置きます。

  • HierarchyのCanvasを右クリック>UI>Imageで作り、名前を「SceneFade」にする
    • Canvasの一番下に置く(ほかのウインドよりも上に描かれるように)
    • Rect TransformのAnchor Presetsで、Altを押しながら右下のstretchを選ぶ(画面いっぱい)
    • ImageのColorを黒、A(アルファ)は0にする
  • 「SceneFader」スクリプトを付ける
スライム

Colorのアルファを0にしておくのは、編集しているときにGame・Sceneビューが真っ黒にならないようにするためです・・・

フェアリー

再生すると、Awakeで真っ黒にしてから明るくなるのね。

バトルからロビーへ帰ろう

勝って「しょうり! けいけんち 3ポイント 4ゴールドを てにいれた」。「ロビーへ」を押すと暗くなり、ロビーが明るくなって、右上のお金が 54 G
勝ったら「ロビーへ」で戻る

この節では、バトルをGameSessionの仲間で始めて、終わったら結果を書き戻してロビーへ戻るようにします。

BattleUnit

Battle LineupのInspector。Party・Party Views・Enemiesが3、Start Expが0、Start ItemsにHerbTea 3。下のBattle ManagerのScene FaderにSceneFade(Scene Fader)
Battle LineupとBattle Manager

BattleUnitを、PartyMemberから作るように変えます。

まず、持ち越す仲間のデータを覚えておくプロパティを足します(Rowの下)。

    public PartyMember Member { get; }       // 持ち越す仲間のデータ(仲間だけ。敵は null)

仲間のデータから作るコンストラクタ(BattleUnit(CharacterData data, UnitView view, int totalExp))を、次のように書き換えます。

    // 仲間から作る。経験値と今の HP・MP は、持ち越してきたもの
    public BattleUnit(PartyMember member, UnitView view)
    {
        Name = member.Data.CharacterName;
        characterData = member.Data;
        Member = member;
        TotalExp = member.TotalExp;
        Level = member.Level;
        Stats = member.Stats;
        Hp = member.Hp;
        Mp = member.Mp;
        LearnSkills();
        View = view;
        view.Unit = this;
        Commands = member.Data.Commands;
    }

    // バトルの結果(経験値・今の HP と MP)を、持ち越す仲間のデータに書き戻す
    public void SaveToMember()
    {
        Member.TotalExp = TotalExp;
        Member.Hp = Hp;
        Member.Mp = Mp;
    }
  • HPとMPは、満タンではなく持ち越してきた値で始めます
  • バトル中は、今までどおりBattleUnitのHPを減らします。PartyMemberに書き戻すのは、バトルが終わったときの1回だけです(SaveToMember)
  • BattleUnitの中のことは、PartyMemberに書き戻さなければバトルの外に出ません。どく・せきかなどの状態や、ぼうぎょアップは、バトルが終わったら消えます

ゲージを、減る演出なしで出す

ロビーに戻ったところのステータスの行が2つ。上の「直す前」は、HPのゲージの後ろに赤い部分が残っていて、満タンから減っていくところ。下の「直したあと」は、はじめから 12/38 の長さで、赤い部分がない
シーンの始めは、減る演出をしない

ステータスの行は、作ったときに満タン(StatusWindow.AddRow)で出して、そのあとで今のHPを入れています。今までは、バトルの始めはいつも満タンだったので問題ありませんでした。

ところが、HPを持ち越すと「満タン→12」と減ったことになり、#10で作った、遅れて縮む赤いゲージが動いてしまいます。シーンが始まったときに、ダメージを受けたように見えるのはおかしいですね。そこで、減る演出をせずに、すぐその長さにできるようにします。

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

    // isInstant を true にすると、減る演出をせずに、すぐその長さにする(シーンの始めに表示するとき)
    public void SetValue(int newCurrent, int newMax, bool isInstant = false)
    {
        current = newCurrent;
        max = newMax;
        float newFill = max > 0 ? (float)current / max : 0f;

        if (lag != null)
        {
            if (isInstant)
            {
                lag.fillAmount = newFill;
                lagTimer = 0f;
            }
            else if (newFill < fill.fillAmount)
            {
                lagTimer = lagDelay; // 減ったら、白いゲージは少し待ってから縮む
            }
            else
            {
                lag.fillAmount = newFill; // 増えたときは、すぐにそろえる
            }
        }

        fill.fillAmount = newFill;
        valueText.text = $"{current}/{max}";
    }

StatusRowのSetHp・SetMpにも、同じ引数を足して、Gaugeに渡します。

    public void SetHp(int hp, int maxHp, bool isInstant = false)
    {
        hpGauge.SetValue(hp, maxHp, isInstant);
    }

    public void SetMp(int mp, int maxMp, bool isInstant = false)
    {
        mpGauge.SetValue(mp, maxMp, isInstant);
    }
  • bool isInstant = falseのように、引数に= 値を書くと、呼ぶときに省略できます(省略したときはfalse)。バトル中のダメージや回復で呼んでいるところは、今までどおり演出つきのままで、直さなくて大丈夫です
  • trueを渡すのは、シーンの始めに行を出すところ(このあとのBattleLineupのSetupと、ロビーのLobbyMenu)です

BattleLineup

BattleLineupは、GameSessionがあれば、その仲間と持ち物を使うようにします。

party・startItemsのコメントを「確認用」に直し、Inventoryを、あとから入れられるように変えます。

    [SerializeField] private CharacterData[] party;  // 確認用:Battle シーンから再生したときの3人
    [SerializeField] private ItemStack[] startItems; // 確認用:Battle シーンから再生したときの持ち物
    public Inventory Inventory { get; private set; } // 持ち物

Setupの、仲間を並べるところ(敵を並べるforより上)を、次のように書き換えます。#16で足した、最初の持ち物を入れるforeachは消します。

    // 並べる(BattleManager が、バトルの始めに呼ぶ)
    public void Setup()
    {
        List<PartyMember> members = LoadParty();
        PartyUnits = new BattleUnit[members.Count];
        StatusRows = new StatusRow[members.Count];
        for (int i = 0; i < members.Count; i++)
        {
            PartyMember member = members[i];
            UnitView view = partyViews[i];
            view.SetupCharacter(member.Data);
            StatusRow row = statusWindow.AddRow(member.Data);
            BattleUnit unit = new BattleUnit(member, view);
            unit.Row = row;
            StatusRows[i] = row;
            PartyUnits[i] = unit;
            // レベルと能力を、ステータスの行に出す(持ち越した HP・MP は、減る演出をせずにそのまま)
            row.SetLevel(unit.Level);
            row.SetHp(unit.Hp, unit.Stats.maxHp, true);
            row.SetMp(unit.Mp, unit.Stats.maxMp, true);
            if (unit.IsDead)
            {
                view.ShowDown(); // ロビーから、倒れたまま来た
            }
        }

Setupの下に、仲間と持ち物を用意するメソッドを足します。

    // ロビーから来たら、GameSession の仲間と持ち物を使う
    // Battle シーンから再生したとき(GameSession が無い)は、確認用に Inspector の値で作る
    private List<PartyMember> LoadParty()
    {
        if (GameSession.Instance != null)
        {
            Inventory = GameSession.Instance.Inventory;
            return GameSession.Instance.Party;
        }

        List<PartyMember> members = new List<PartyMember>();
        foreach (CharacterData data in party)
        {
            members.Add(new PartyMember(data, startExp));
        }
        Inventory = new Inventory();
        foreach (ItemStack stack in startItems)
        {
            Inventory.Add(stack.item, stack.count);
        }
        return members;
    }
  • ロビーから来たときは、GameSessionの持ち物をそのまま使います。バトルでハーブティーを飲むと、GameSessionの持ち物が減るので、書き戻さなくても持ち越されます
  • Battleシーンを開いて再生したときは、GameSessionがありません。そのときは今までどおり、Inspectorの3人・Start Exp・Start Itemsで始めます。バトルだけを確かめたいときに便利なので、残しておきます
  • 倒れたままの仲間(にげたときなど)は、始めから倒れた絵にしておきます

BattleManager

勝ったときのウインド。「しょうり!」「けいけんち 3ポイント」「4ゴールドを てにいれた」の下に、黄色い「▶ ロビーへ」のボタン
ボタンを「ロビーへ」に

BattleManagerに、SceneFaderの欄を足します(screenFadeの下)。using UnityEngine.SceneManagement;は、もう使わないので消します。

    [SerializeField] private SceneFader sceneFader;

ShowVictoryAsyncで、もらったお金をGameSessionに足します(経験値のメッセージの下)。

        await messageWindow.ShowAsync($"{exp}ポイントの けいけんちを かくとく!");
        if (GameSession.Instance != null)
        {
            GameSession.Instance.Gold += gold; // お金は、持ち越すものに足す
        }

ぜんめつしたときのウインドの文字を変えます。

                    await resultWindow.ShowAsync("ぜんめつ…", "おかねを はんぶん なくして\nロビーに もどされた");

最後に、RunAsyncのwhileを抜けたあと(#11で、同じシーンを読み込み直していたところ)を、次のように書き換えます。

        // バトルの結果(経験値・HP・MP)を持ち越す
        foreach (BattleUnit unit in lineup.PartyUnits)
        {
            unit.SaveToMember();
        }
        if (IsPartyDown())
        {
            // ぜんめつしたら、お金が半分になって、ロビーで目をさます(HP・MP は満タン)
            foreach (BattleUnit unit in lineup.PartyUnits)
            {
                unit.Member.RestoreAll();
            }
            if (GameSession.Instance != null)
            {
                GameSession.Instance.Gold /= 2;
            }
        }

        // 「ロビーへ」が押された:暗くして、ロビーに戻る
        await sceneFader.LoadSceneAsync("Lobby");
  • 勝ったとき・にげたとき・ぜんめつしたとき、どれも最後はここに来ます。まず3人の結果をPartyMemberに書き戻します
  • ぜんめつしたら、ゲームオーバーにはせず、お金を半分なくして、満タンでロビーからやり直します。せっかく上げたレベル(経験値)は残るので、何度か挑戦すれば先に進めます
  • 「お金が半分」は、休憩室の10ゴールドや、#18の売店の値段とのかね合いで決めた仮の値です。#24のバランス調整で見直します

Battleシーンを直します。

  • BattleのBattle ManagerのScene Faderに、SceneFadeをドラッグする
  • ResultWindowのRetryButtonの文字(Text (TMP))を「ロビーへ」にする

ロビーを作ろう

LobbyシーンのHierarchy:Canvasの下にBackground、Stage>PartyArea(Fairy・Slime・Jiangshi)、MessageWindow、LobbyWindow(DungeonButton・RestButton・ShopButton・PartyButton・SaveButton)、StatusWindow、GoldWindow>GoldText、ScreenFade、SceneFade。Canvasの外にEventSystem・GameSession・Lobby。InspectorはLobbyWindowで、Grid Layout GroupのCell Sizeが180×60
Lobbyシーンの中身

この節では、まほう図書館のロビーのシーンを作ります。

Battleを複製して、Lobbyにする

背景・仲間・メッセージ・ステータスのウインドなど、ロビーにもバトルと同じものがたくさんあります。そこで、Battleシーンを複製して、いらないものを消して作ります。

  • ProjectのScenesフォルダでBattleを選び、Ctrl+Dで複製して、名前を「Lobby」にする
  • Lobbyを開いて、Hierarchyから次のものを消す
    • Canvasの外:Battle・Effects・TargetSelector
    • Canvasの中:EnemyArea(Stageの中)・ItemWindow・SkillWindow・DamageVignette・PopupLayer・ResultWindow
  • Backgroundの画像を bg_lobby にする(素材のzipに入っています)
  • MessageWindowのMessageTextを「ここは まほう図書館の ロビー。」にする
  • Stage>PartyAreaのPos Xを 800、Pos Yを 60 にする(部屋の真ん中あたりに立たせる)
  • SceneFadeは、Battleから複製したものがそのまま使えます

メニューとお金のウインド

LobbyのGoldWindow>GoldTextのInspector。Rect Transformはstretch(Left・Right 32)、TextMeshProの文字は「50 G」、Font Sizeが40
右上のお金

コマンドのウインドを、ロビーのメニューにします。

  • CommandWindowの名前を「LobbyWindow」にして、Command Menuのコンポーネントを外す(Remove Component)
  • PrefabsのCommandButtonを、LobbyWindowの中に5つドラッグして、名前と文字(Text (TMP))を次のようにする
    • DungeonButton:地下へ
    • RestButton:休憩室
    • ShopButton:売店
    • PartyButton:なかま
    • SaveButton:セーブ
  • ShopButton・PartyButton・SaveButtonは、ButtonのInteractableのチェックを外す(灰色になって押せない)
  • LobbyWindowにはGrid Layout Groupが付いているので、2列に並びます

右上に、お金のウインドを作ります。

  • Canvasを右クリック>UI>Imageで作り、名前を「GoldWindow」にする(StatusWindowのすぐ下に置く)
    • Anchor Presetsで右上(top-right)を選び、Alt+Shiftを押しながらもう一度選ぶ(基準点も右上に)
    • Pos X -60、Pos Y -180、Width 280、Height 90
    • Source Imageに ui_window、Image Typeは Sliced
  • GoldWindowを右クリック>UI>Text – TextMeshProで作り、名前を「GoldText」にする
    • Anchor PresetsでAltを押しながらstretchを選び、Left・Rightを 32 にする
    • 文字は「50 G」、Font Size 40、色は白、Alignmentは右寄せと上下の真ん中

GameSessionとLobbyMenu

LobbyのLobbyオブジェクトのInspector。Lobby Menu (Script)のParty Viewsが3、Status WindowにStatusWindow、Gold TextにGoldText、Message WindowにMessageWindow、Screen FadeにScreenFade、Scene FaderにSceneFade、Dungeon ButtonにDungeonButton、Rest ButtonにRestButton、Rest Priceが10、Sleep Secondsが2
LobbyMenuのInspector

ロビーのメニューを動かす、LobbyMenuを作ります。休憩室もここに書きます(次の節)。

  • 「LobbyMenu」スクリプトを作る
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

// ロビー:仲間とお金を見せて、メニューのボタンで「地下へ」「休憩室」に行く
public class LobbyMenu : MonoBehaviour
{
    [SerializeField] private UnitView[] partyViews;   // PartyArea の3人
    [SerializeField] private StatusWindow statusWindow;
    [SerializeField] private TMP_Text goldText;       // 右上のお金
    [SerializeField] private MessageWindow messageWindow;
    [SerializeField] private ScreenFade screenFade;   // 休憩室で、画面を暗くする
    [SerializeField] private SceneFader sceneFader;
    [SerializeField] private Button dungeonButton;    // 地下へ
    [SerializeField] private Button restButton;       // 休憩室
    [SerializeField] private int restPrice = 10;      // 休憩室のお金
    [SerializeField] private float sleepSeconds = 2f; // 眠っている時間

    private StatusRow[] rows;
    private bool isBusy; // 休憩中・シーンの切り替え中は、ボタンを受け付けない

    private void Start()
    {
        GameSession session = GameSession.Instance;
        rows = new StatusRow[session.Party.Count];
        for (int i = 0; i < session.Party.Count; i++)
        {
            PartyMember member = session.Party[i];
            partyViews[i].SetupCharacter(member.Data);
            rows[i] = statusWindow.AddRow(member.Data);
        }
        ShowParty();

        dungeonButton.onClick.AddListener(OnDungeonClicked);
        restButton.onClick.AddListener(OnRestClicked);
        EventSystem.current.SetSelectedGameObject(dungeonButton.gameObject);
    }

    // 仲間のレベル・HP・MP と、お金を表示し直す(倒れている人は、戦闘不能の絵)
    // HP・MP のゲージは、減る演出をせずにそのまま(バトルから戻ってきたとき、満タンから減って見えないように)
    private void ShowParty()
    {
        GameSession session = GameSession.Instance;
        for (int i = 0; i < session.Party.Count; i++)
        {
            PartyMember member = session.Party[i];
            UnitView view = partyViews[i];
            StatusRow row = rows[i];
            row.SetLevel(member.Level);
            row.SetHp(member.Hp, member.Stats.maxHp, true);
            row.SetMp(member.Mp, member.Stats.maxMp, true);
            if (member.IsDead)
            {
                view.ShowDown();
            }
            else
            {
                view.Revive();
            }
        }
        goldText.text = $"{session.Gold} G";
    }

    private async void OnDungeonClicked()
    {
        if (isBusy)
        {
            return;
        }
        isBusy = true;
        try
        {
            await sceneFader.LoadSceneAsync("Battle");
        }
        catch (System.OperationCanceledException)
        {
            // シーンが切り替わったら、そこで終わり
        }
    }

    private async void OnRestClicked()
    {
        if (isBusy)
        {
            return;
        }
        isBusy = true;
        try
        {
            await RestAsync();
        }
        catch (System.OperationCanceledException)
        {
            return;
        }
        isBusy = false;
    }
}
  • Start:GameSessionの仲間で、絵とステータスの行を作ります。ステータスのウインドは、バトルと同じStatusWindowです
  • ShowParty:レベル・HP・MPとお金を出します。休憩室で満タンになったときも、これで出し直します。ゲージはtrueを渡して、減る演出なしで出します
  • isBusy:暗くなっている途中や、休憩している途中に、もう一度ボタンを押されないようにします
  • OnDungeonClickedは、シーンが切り替わったらそのまま終わるので、isBusyを戻しません
  • RestAsyncは、次の節で足します(ここではまだエラーになります)

GameSessionとLobbyを置きます。

LobbyのGameSessionオブジェクトのInspector。Game Session (Script)のStart PartyにSlime・Fairy・Jiangshi、Start ItemsにHerbTea 3、Start Goldが50
最初の仲間・持ち物・お金
  • Hierarchyで、Canvasの外に空のオブジェクトを作り、名前を「GameSession」にして、「GameSession」スクリプトを付ける
    • Start Partyに Slime・Fairy・Jiangshi、Start Itemsに HerbTea・3、Start Goldは 50
    • DontDestroyOnLoadは、親のいないオブジェクトにしか効きません。Canvasの中に入れないように気をつけましょう
  • Canvasの外に空のオブジェクトをもう1つ作り、名前を「Lobby」にして、「LobbyMenu」スクリプトを付ける
    • Party Viewsに、Stage>PartyAreaのSlime・Fairy・Jiangshi(Start Partyと同じ順番)
    • ほかの欄に、上の図のようにドラッグする(Screen FadeはScreenFade、Scene FaderはSceneFadeです。名前が似ているので気をつけてください)

シーンの一覧に入れる

Build ProfilesのScene List。0がTurnBasedRpg/Scenes/Lobby、1がTurnBasedRpg/Scenes/Battle
Lobbyを0番に

スクリプトからシーンを読み込むには、Build ProfilesのScene Listにシーンを入れておく必要があります(8-1 シーンの基本とゲーム構成の考え方)。

  • File>Build Profilesを開き、左上のScene Listを選ぶ
  • ProjectからLobbyとBattleをドラッグして、Lobbyを0番(一番上)にする

0番のシーンが、ビルドしたゲームで最初に開くシーンになります。

休憩室

休憩室を選ぶと「10ゴールドで ひとやすみ。」右上のお金が 54 G から 44 G に。部屋が暗くなって3人が目を閉じて うとうとし、明るくなるとHPとMPのゲージが満タンになって、3人が喜ぶポーズ。「みんな げんきに なった!」
10ゴールドで、みんな元気に

この節では、お金を払って休むと、HPとMPが満タンになる休憩室を作ります。

うとうとするポーズ

スライムちゃん・フェアリー・キョンシーの sleep_01 と sleep_02 が2枚ずつ。3人とも目を閉じて、少しだけ姿勢が違う
sleep_01と02を交互に出す

素材のsleep_01・sleep_02を交互に出して、うとうとさせます。

CharacterDataに、うとうとする絵の欄を足します(eatFramesの下)。

    [SerializeField] private Sprite[] sleepFrames; // うとうとする(sleep_01・02)
    public Sprite[] SleepFrames => sleepFrames;

UnitViewにも、絵を覚えておく変数を足して(eatFramesの下)、SetupCharacterで入れます(eatFrames = data.EatFrames;の下)。

    private Sprite[] sleepFrames; // うとうとする(仲間だけ)
        sleepFrames = data.SleepFrames;

UnitViewに、うとうとするメソッドを足します(PlayEatAsyncの下)。

    // 休憩室:正面を向いて、seconds 秒のあいだ うとうとする(sleep_01 と 02 を交互に)
    public async Awaitable PlaySleepAsync(float seconds)
    {
        idleBounce.enabled = false;
        float time = 0f;
        int index = 0;
        while (time < seconds)
        {
            image.sprite = sleepFrames[index % sleepFrames.Length];
            index++;
            await Awaitable.WaitForSecondsAsync(0.4f, destroyCancellationToken);
            time += 0.4f;
        }
    }
  • 待機の弾み(IdleBounce)を止めてから、0.4秒ごとに絵を変えます
  • 終わったあとは、休憩のあとで喜ぶポーズ→ResumeIdleで待機に戻ります
Data/Characters/SlimeのInspector。Sleep Framesが2つ:slime_sleep_01、slime_sleep_02
Sleep Framesに2枚
  • Data/Charactersの4人(Slime・Fairy・Jiangshi・Medusa)のSleep Framesに、それぞれの sleep_01・sleep_02 を入れる
    • Down Spriteで使っている sleep_03(倒れている絵)とは別です

RestAsync

2つのステータスの行。上の「休憩の前」はスライム 12/38、フェアリー 9/26・MP 4/20、キョンシー 20/32。下の「休憩のあと」はスライム 38/38、フェアリー 26/26・MP 20/20、キョンシー 32/32 で、みんな満タン
休憩の前とあと

LobbyMenuに、休憩室のメソッドを足します(OnRestClickedの下)。

    // 休憩室:お金を払って眠ると、みんなの HP・MP が満タンになる(倒れた人も起きる)
    private async Awaitable RestAsync()
    {
        GameSession session = GameSession.Instance;
        if (session.Gold < restPrice)
        {
            await messageWindow.ShowAsync("おかねが たりない…");
            return;
        }
        session.Gold -= restPrice;
        goldText.text = $"{session.Gold} G";
        await messageWindow.ShowAsync($"{restPrice}ゴールドで ひとやすみ。");

        // 暗くしながら、みんなで うとうとする
        Awaitable dark = screenFade.FadeAsync(0.6f, 0.5f);
        Awaitable[] sleeps = new Awaitable[partyViews.Length];
        for (int i = 0; i < partyViews.Length; i++)
        {
            sleeps[i] = partyViews[i].PlaySleepAsync(sleepSeconds);
        }
        await dark;
        foreach (Awaitable sleep in sleeps)
        {
            await sleep;
        }
        await screenFade.FadeAsync(0f, 0.5f);

        // 目がさめたら満タン。起きて喜ぶ
        foreach (PartyMember member in session.Party)
        {
            member.RestoreAll();
        }
        ShowParty();
        foreach (UnitView view in partyViews)
        {
            _ = view.PlayWinAsync(); // 喜ぶのは待たずに、メッセージと一緒に進める
        }
        await messageWindow.ShowAsync("みんな げんきに なった!");
        foreach (UnitView view in partyViews)
        {
            view.ResumeIdle();
        }
    }
  • お金が足りなければ、メッセージだけ出して終わります
  • 部屋を暗くするのは、#11でぜんめつのときに使ったScreenFadeです。0.6まで暗くして、3人がうとうとし終わったら明るく戻します
  • 暗くするのと、3人のうとうとは、先に全部始めてから待ちます。1つずつawaitすると、1人ずつ順番に眠ってしまいます
  • 満タンにするのはPartyMember.RestoreAllです。倒れていた仲間もHPが満タンになるので、ShowPartyで起き上がった絵に戻ります
スライム

暗くなって、また明るくなると「ひと晩たった」感じがしますね・・・

フェアリー

起きたら3人で喜ぶのがいいわね!

動かして確認!

HPが減ったままのロビー(スライム 12/38、右上 54 G)で「地下へ」を押すと暗くなり、バトルが始まる。ステータスのゲージは、はじめから 12/38 のまま
減ったHPのまま、2回目の「地下へ」
  • Lobbyシーンを開いて再生する(GameSessionはLobbyにあるので、Lobbyから始めます)
  • 「地下へ」でバトルへ。勝ったら「ロビーへ」で戻る
  • もう一度「地下へ」を押して、減ったHPのまま始まることを確かめる。ロビーに戻ったときも、バトルが始まったときも、ゲージが満タンから減っていかない
  • 「休憩室」で満タンにする。お金が10ゴールドより少ないと、休めません
  • ぜんめつを試すときは、Data/Charactersの3人のBase StatsのMax Hpを5くらいにする(確かめたら戻す)

#17の動作確認

  • Lobbyから再生すると、ロビーに3人が並び、右上に「50 G」、左下にメニューが出る(売店・なかま・セーブは灰色)
  • 「地下へ」で画面が暗くなり、バトルが明るくなって始まる
  • 勝つと「ロビーへ」のボタンが出て、押すと暗くなってロビーに戻る。もらったお金が右上に足される
  • バトルで減ったHP・MP、上がったレベル、使ったどうぐは、ロビーでもそのまま。次のバトルにも持ち越される
  • ロビーに戻ったとき・バトルが始まったときに、HPのゲージが満タンから減っていく演出は出ない(はじめから今のHPの長さ)
  • 「休憩室」で10ゴールド減り、暗くなって3人がうとうとし、明るくなるとHP・MPが満タンになって喜ぶ
  • ぜんめつすると「おかねを はんぶん なくして ロビーに もどされた」。ロビーではお金が半分、HP・MPは満タン
  • Battleシーンを開いて再生したときは、今までどおりInspectorの3人・Start Exp・Start Itemsでバトルが始まる

あとがき

今回は、まほう図書館のロビーを作って、バトルとロビーを行き来できるようにしました。持ち越したいものだけをPartyMemberとGameSessionにまとめたので、バトルの中のしくみは、ほとんど変えずにすみましたね。暗くなってから切り替わるだけで、ぐっとゲームらしくなったと思います。次回は、ロビーの「売店」を作って、ためたお金でどうぐや装備を買えるようにしたいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次