ターン制RPGの作り方#27|エンディングとビルド:ボスを倒したらスタッフロール、Windowsで遊べる形に

地下3階の奥の図書館で、4人が喜ぶポーズで弾んでいる。上では「まほう図書館の地下」の下に、なかま・本から にげだした ものたちの名前が、下から上へ流れていく
ボスを倒したあとの、スタッフロール

前回、効果音とBGMを付けて、ボタンや攻撃に音が鳴るようにしました。でも、まだボスを倒しても、いつものように「クリア!」が出て、ロビーへ戻るだけです・・・。今回は、いよいよ最終回です。ボスを倒したらエンディングでスタッフロールを流し、最後にWindowsで遊べる形にビルドして、ゲームを完成させたいと思います!

目次

今回作るもの

スタッフロールが流れ終わったところ。「あそんでくれて ありがとう!」「おわり」の下で4人が喜んでいて、いちばん下に「タイトルへ」のボタン
流れ終わると「タイトルへ」
  • Player Settingsで、ゲームの名前を「まほう図書館の地下」にする
  • 地下3階をクリアしたら、新しい Endingシーン へ。4人が喜んで、スタッフロールが下から上へ流れる
  • スタッフロールの名前は、仲間と敵のデータから作る。最後に「タイトルへ」で、タイトル画面に戻る
  • Windowsで遊べるように、ビルドする
スクリプト変えること
BattleManager地下3階をクリアしたら、Endingシーンへ
GameSessionタイトルへ戻るときに、持ち越していたものを消す
EndingRoll(新しく作る)クリアを記録して、スタッフロールを流す
スライム

ついに、最終回なんですね・・・!

ゲームの名前を付けよう

Project SettingsのPlayer。Company Nameが anogame、Product Nameが まほう図書館の地下
Company NameとProduct Name

まず、ゲームの名前を決めます。Edit>Project Settings>Player を開いて、上の2つを入れます。

項目値
Company Name自分の名前など(ここでは anogame)
Product Nameまほう図書館の地下
  • Product Name は、ビルドしたゲームのウインドウの上に出る名前です。今回のスタッフロールの1行目にも使います
  • #25で、セーブのファイルはApplication.persistentDataPathに置きました。この場所は、Company NameとProduct Nameで決まります
C:\Users\<ユーザー名>\AppData\LocalLow\anogame\まほう図書館の地下\save.json

名前を変えると、セーブの場所も変わります。今までのセーブは前の場所に残ったままなので、タイトルの「つづきから」が灰色になります。もう一度ロビーでセーブすれば、新しい場所に書かれます。

ボスを倒したらエンディングへ

地下3階のボスに勝ったところ。「ちか3かいを クリアした!」「にげだした ものたちを ぜんぶ 本に もどした!」のあと、画面が暗くなって、4人が喜ぶエンディングに切りかわる
地下3階をクリアすると、エンディングへ

今までは、どの階でも、最後のバトルに勝つと「クリア!」のウインドが出て、ロビーへ戻っていました。いちばん下の地下3階だけは、ウインドを出さずに、Endingシーンへ行くようにします。

BattleManager

actionsの下に、エンディングへ行くかどうかを足します。

    private bool isGameCleared;     // いちばん下の階をクリアした(エンディングへ)

ShowVictoryResultAsyncの、最後のawait resultWindow.ShowAsync("クリア!", body);の上に、地下3階のときの処理を足します。

        // いちばん下の階をクリアした:ウインドは出さずに、エンディングへ(バランスを確かめている間は、ロビーへ戻ってくり返す)
        if (session.FloorIndex == session.Floors.Length - 1 && !BalanceTester.IsRunning)
        {
            await messageWindow.ShowAsync("にげだした ものたちを ぜんぶ 本に もどした!");
            isGameCleared = true;
            return false;
        }
        await resultWindow.ShowAsync("クリア!", body);
  • session.Floors.Length - 1は、いちばん下の階の番号です。階を増やしても、いちばん下をクリアしたときにエンディングになります
  • #24のBalanceTesterで確かめている間は、今までどおりロビーへ戻ります。エンディングへ行ってしまうと、次の回が始まらないためです

RunAsyncの最後の、シーンを読み込むところに、Endingへ行く分かれ道を足します。

        if (goesOn)
        {
            // 「さきへ すすむ」:みんなで右へ走っていって、同じシーンで次のバトル
            await RunForwardAsync();
            await sceneFader.LoadSceneAsync("Battle");
        }
        else if (isGameCleared)
        {
            // いちばん下の階をクリアした:エンディングへ
            await sceneFader.LoadSceneAsync("Ending");
        }
        else
        {
            // 「ロビーへ」が押された:暗くして、ロビーに戻る
            await sceneFader.LoadSceneAsync("Lobby");
        }

GameSessionを、タイトルへ戻るときに消す

エンディングで「タイトルへ」を押すと、画面が暗くなってタイトル画面へ。「つづきから」が押せるようになっていて、選ぶとロビーから始まる
タイトルに戻ると、つづきからが押せる

#17で作ったGameSessionは、DontDestroyOnLoadで、シーンを切りかえても残ります。今までは、タイトルへ戻ることが無かったので、それで困りませんでした。

でも、エンディングからタイトルへ戻って「はじめから」を選ぶと、前の仲間やお金が残ったままのロビーが始まってしまいます。そこで、タイトルへ戻るときに、GameSessionを消すメソッドを足します。CreateSaveDataの上に書きます。

    // タイトルへ戻るときに呼ぶ。持ち越していたものを消して、次にロビーを読み込んだときに作り直す
    public static void Discard()
    {
        if (Instance == null)
        {
            return;
        }
        Destroy(Instance.gameObject);
        Instance = null;
    }
  • Instanceをnullにしておくと、次にロビーのシーンを読み込んだとき、ロビーに置いてあるGameSessionが新しいInstanceになります。そこで「はじめから」なら最初の値、「つづきから」ならセーブの中身で作り直されます

SoundPlayerは消しません。エンディングとタイトルは同じ曲にするので、#26で作った「同じ曲なら、そのまま続ける」がそのまま効きます。

エンディングのシーンを作ろう

EndingシーンのHierarchyと、EndingオブジェクトのInspector。Ending Roll (Script)のViewportにCredits、Credits TextにCreditsText、Membersに4人、Floorsに3つの階、Maker Nameに あなた、Scroll Speed 90、Fast Rate 4、End GroupにEndWindow、Title ButtonにTitleButton、Scene FaderにSceneFade
Endingシーンの中身

Endingシーンは、#25のTitleシーンを複製して作ります。背景・4人・ボタンのウインド・SceneFade・SoundPlayerが、そのまま使えます。

  • Projectウインドで Title シーンを選んで、Ctrl+D で複製し、名前を Ending にする
  • Endingシーンを開いて、Logo・Titleのオブジェクトと、ContinueButtonを消す
  • Backgroundの色を、少し暗くする(R・G 115、B 133)

4人を、喜ぶポーズで弾ませる

SlimeのIdle BounceのInspector。Framesに slime_win_01・slime_win_02・slime_win_03 が入っている
待機のコマを、喜ぶコマに

4人は、#1のIdleBounceで弾ませています。IdleBounceは、入れたコマを1→2→3→2の順にくり返すだけなので、コマを喜ぶポーズ(win)に替えれば、そのまま喜び続けてくれます。

  • PartyAreaの Pos Y を -230 にして、4人を下へ下げる
  • 4人それぞれのIdle Bounceの Frames に、そのキャラクターの win_01・win_02・win_03 を入れる
  • Imageの Source Image も、win_01にしておく

コードを1行も書かずに、喜ぶエンディングになりました。

スタッフロールの見える範囲

CreditsのInspector。Pos Y 240・Width 1400・Height 520、Rect Mask 2DのSoftnessがX 0・Y 80
見える範囲と、端をふわっと消すSoftness

スタッフロールは、上の真ん中の決まった範囲だけで見えるようにします。範囲の外の文字は、Rect Mask 2D で隠します。

  • Canvasを右クリック>Create Empty で、名前を Credits にする(Stageのすぐ下)
  • Rect Transformを、Pos X 0・Pos Y 240・Width 1400・Height 520 にする
  • Add Component で Rect Mask 2D を足して、Softness の Y を 80 にする
    • Softnessを入れると、範囲の上と下の端で、文字がふわっと消えていきます
  • Creditsを右クリック>UI>Text – TextMeshPro で、名前を CreditsText にする
    • アンカーは、Shift+Altを押しながら top-center を選ぶ(上の真ん中が基準になる)
    • Width 1400、Font Size 56、Bold、Alignmentは Center と Top、Line Spacing 10

文字の高さは、コードで、中身に合わせて変えます。

Rect Mask 2Dは、9-6 Scroll ViewとLayout Groupでリストを作ろうのScroll Viewの中でも使われている、子どもを四角い範囲だけ見せるコンポーネントです。

タイトルへのボタン

EndWindowのInspector。Pos Y 40・Width 308・Height 112、中にTitleButtonが1つ
ボタンは1つだけ
  • TitleWindowの名前を EndWindow にして、Pos Y 40・Height 112 にする(ボタン1つ分)
  • NewGameButtonの名前を TitleButton にして、文字を「タイトルへ」にする

EndWindowには、TitleシーンでつけたCanvas Groupが残っています。流れ終わるまでは、これで見えないようにしておきます。

EndingRoll

スタッフロールが下から上へ流れていく。「まほう図書館の地下」、なかま(スライム・フェアリー・キョンシー・メドゥーサ)、本から にげだした ものたち(こぼれたインク・紙ひこうき など)、地下の おくで まっていたもの(禁じられた本)、つくった人(あなた)のあと、「あそんでくれて ありがとう!」「おわり」が真ん中で止まり、「タイトルへ」のボタンが出る
データから作ったスタッフロール(2倍の速さ)

Scriptsフォルダに、EndingRollを作ります。

using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

// エンディング:クリアを記録して、スタッフロールを下から上へ流し、最後に「タイトルへ」のボタンを出す
public class EndingRoll : MonoBehaviour
{
    [SerializeField] private RectTransform viewport; // スタッフロールが見える範囲(RectMask2D を付ける)
    [SerializeField] private TMP_Text creditsText;   // 流す文字(viewport の子。上の真ん中を基準にする)
    [SerializeField] private CharacterData[] members; // なかま(名前を出す)
    [SerializeField] private FloorData[] floors;      // 地下の階(出てきた敵の名前を出す)
    [SerializeField] private string makerName = "あなた"; // つくった人
    [SerializeField] private float scrollSpeed = 90f; // 1秒に流れる長さ
    [SerializeField] private float fastRate = 4f;     // 決定キーを押している間は、この倍の速さで流す
    [SerializeField] private CanvasGroup endGroup;    // 「タイトルへ」のボタンのウインド(流れ終わってから出す)
    [SerializeField] private Button titleButton;
    [SerializeField] private SceneFader sceneFader;

    private InputSystem_Actions inputActions;
    private bool isBusy; // タイトルへ切り替えている間は、ボタンを受け付けない

    private void Awake()
    {
        inputActions = new InputSystem_Actions();
    }

    private void OnEnable()
    {
        inputActions.Enable();
    }

    private void OnDisable()
    {
        inputActions.Disable();
    }

    private async void Start()
    {
        SoundPlayer.PlayBgm(Bgm.Title);
        SaveClear();
        titleButton.onClick.AddListener(OnTitleClicked);
        endGroup.alpha = 0f;
        endGroup.interactable = false;
        creditsText.text = BuildCredits();
        try
        {
            await ScrollCreditsAsync();
            await ShowEndAsync();
        }
        catch (System.OperationCanceledException)
        {
            return;
        }
        EventSystem.current.SetSelectedGameObject(titleButton.gameObject);
    }

    // クリアした記録を残す(みんな元気にしてから)。タイトルの「つづきから」で、ロビーから遊べる
    private void SaveClear()
    {
        GameSession session = GameSession.Instance;
        if (session == null)
        {
            return; // Ending シーンから再生したときは、残すものがない
        }
        foreach (PartyMember member in session.Members)
        {
            member.RestoreAll();
        }
        SaveSystem.Save(session.CreateSaveData());
    }

    // スタッフロールの文字を、データから作る(名前を変えても、そのまま流れる)
    private string BuildCredits()
    {
        List<string> memberNames = new List<string>();
        foreach (CharacterData member in members)
        {
            memberNames.Add(member.CharacterName);
        }

        // 出てきた順に、同じ敵は1回だけ。ボスは別に分ける
        List<string> enemyNames = new List<string>();
        List<string> bossNames = new List<string>();
        foreach (FloorData floor in floors)
        {
            for (int i = 0; i < floor.BattleCount; i++)
            {
                foreach (EnemyData enemy in floor.GetEnemies(i))
                {
                    List<string> names = enemy.IsBoss ? bossNames : enemyNames;
                    if (!names.Contains(enemy.EnemyName))
                    {
                        names.Add(enemy.EnemyName);
                    }
                }
            }
        }

        StringBuilder builder = new StringBuilder();
        builder.AppendLine($"<size=150%>{Application.productName}</size>"); // Player Settings の Product Name
        AppendSection(builder, "なかま", memberNames);
        AppendSection(builder, "本から にげだした ものたち", enemyNames);
        AppendSection(builder, "地下の おくで まっていたもの", bossNames);
        AppendSection(builder, "つくった人", new List<string> { makerName });
        builder.AppendLine();
        builder.AppendLine();
        builder.AppendLine("あそんでくれて ありがとう!");
        builder.AppendLine();
        builder.Append("<size=150%>おわり</size>");
        return builder.ToString();
    }

    // 間を2行あけて、見出し(小さめの色つき)と、名前を1行ずつ
    private static void AppendSection(StringBuilder builder, string heading, List<string> names)
    {
        builder.AppendLine();
        builder.AppendLine();
        builder.AppendLine($"<size=70%><color=#FFE08A>{heading}</color></size>");
        foreach (string name in names)
        {
            builder.AppendLine(name);
        }
    }

    // 下から上へ流す。最後の行(おわり)が、見える範囲のまん中に来たら止める
    private async Awaitable ScrollCreditsAsync()
    {
        RectTransform credits = creditsText.rectTransform;
        float textHeight = creditsText.preferredHeight;
        credits.sizeDelta = new Vector2(credits.sizeDelta.x, textHeight);

        float viewHeight = viewport.rect.height;
        float y = -viewHeight;                   // 文字の上の端が、見える範囲の下の端にある
        float endY = textHeight - viewHeight / 2f; // 文字の下の端が、見える範囲のまん中にある
        while (y < endY)
        {
            float speed = inputActions.UI.Submit.IsPressed() ? scrollSpeed * fastRate : scrollSpeed;
            y = Mathf.Min(endY, y + speed * Time.deltaTime);
            credits.anchoredPosition = new Vector2(0f, y);
            await Awaitable.NextFrameAsync(destroyCancellationToken);
        }
    }

    // 「タイトルへ」のボタンを、ふわっと出す
    private async Awaitable ShowEndAsync()
    {
        const float seconds = 0.5f;
        float time = 0f;
        while (time < seconds)
        {
            time += Time.deltaTime;
            endGroup.alpha = Mathf.Clamp01(time / seconds);
            await Awaitable.NextFrameAsync(destroyCancellationToken);
        }
        endGroup.alpha = 1f;
        endGroup.interactable = true;
    }

    private async void OnTitleClicked()
    {
        if (isBusy)
        {
            return;
        }
        isBusy = true;
        GameSession.Discard(); // 持ち越していたものを消す(タイトルの はじめから・つづきから で作り直す)
        try
        {
            await sceneFader.LoadSceneAsync("Title");
        }
        catch (System.OperationCanceledException)
        {
            // シーンが切り替わったら、そこで終わり
        }
    }
}

コードのポイントです。

  • クリアの記録:エンディングに入ったら、みんなのHP・MPを満タンにしてからセーブします。セーブはロビーでしかできないので、ここで残さないと、「つづきから」でボスの前に戻ってしまいます
  • データから作るスタッフロール:仲間の名前はCharacterData、敵の名前はFloorDataから集めます。#22のように敵を足したり、名前を変えたりしても、スタッフロールを書き直さずに済みます
    • 同じ敵は何回も出てくるので、Containsで、まだ入っていない名前だけ足します
    • ボス(IsBoss)は、別の見出しに分けます
  • StringBuilder:文字を何行もつなげるときに使うクラスです。AppendLineで1行足して改行、Appendで改行なしで足します
  • リッチテキスト:<size=150%>・<color=#FFE08A>のように書くと、文字の一部だけ大きさや色を変えられます(9-3 TextMeshProで美しいテキストを表示しよう)
  • 流し方:CreditsTextは、上の真ん中が基準です。anchoredPosition.yが-viewHeightのとき、文字の上の端が見える範囲の下の端にあり、yを増やすと上へ動きます。文字の下の端が、見える範囲のまん中に来たら止めます。最後の行の「おわり」が、まん中に残ります
  • 決定キー(EnterやAボタン)を押している間は、4倍の速さで流れます。WasPressedThisFrameは押した瞬間だけですが、IsPressedは押している間ずっとtrueです
  • タイトルへ:ボタンが押されたら、GameSession.Discard()で持ち越していたものを消してから、Titleシーンへ切りかえます

Hierarchyで Create Empty を作り、名前を Ending にして、EndingRollを付けます。

欄入れるもの
ViewportCredits
Credits TextCreditsText
MembersSlime・Fairy・Jiangshi・Medusa(Charactersフォルダ)
FloorsFloor1・Floor2・Floor3(Floorsフォルダ)
Maker Name自分の名前など
End GroupEndWindow
Title ButtonTitleButton
Scene FaderSceneFade

Scene Listに、Endingを足す

Build ProfilesのScene List。Title(0)・Lobby(1)・Battle(2)・Ending(3)の4つに、チェックが入っている
Endingを4番目に

File>Build Profiles を開いて、Scene List の一番下に、Endingシーンをドラッグして足します。シーンを読み込むには、Scene Listに入っている必要があります(8-3 スクリプトでシーンを切り替えよう)。

Windowsでビルドしよう

ビルドしたMagicLibrary.exeを起動したところ。タイトルバーに「まほう図書館の地下」、1280×720のウインドウにタイトル画面が出ている
ビルドしたゲームを起動

最後に、Unityが無くても遊べる形にします。ビルドの基本は、10-1 ビルドの基本:ゲームを実行可能ファイルにすると10-2 初めてのビルド:Windows/Mac向け実行ファイルを作ろうにまとめてあるので、ここでは、このゲームで設定するところだけ書きます。

ウインドウの大きさ

Project SettingsのPlayerのResolution and Presentation。Fullscreen ModeがWindowed、Default Screen Widthが1280、Default Screen Heightが720、Resizable Windowにチェック
1280×720のウインドウで起動

Edit>Project Settings>Player の Resolution and Presentation を開いて、次のようにします。

項目値
Fullscreen ModeWindowed
Default Screen Width / Height1280 / 720
Resizable Windowチェックを入れる
  • 全画面ではなく、1280×720のウインドウで起動します。#1でCanvas Scalerを Scale With Screen Size(1920×1080)にしてあるので、ウインドウの大きさを変えても、画面の割合はそのままです

ビルドする

  • File>Build Profiles で Windows を選ぶ(Activeになっていなければ、Switch Platform)
  • Build を押して、空のフォルダ を選ぶ(例:プロジェクトのフォルダのBuild/MagicLibrary)。ファイルの名前はMagicLibraryにする
    • Assetsの中は選ばないでください。ビルドしたファイルがプロジェクトに混ざってしまいます

ビルドが終わると、選んだフォルダに、こうしたファイルが出来ます。

MagicLibrary/
├── MagicLibrary.exe       ← これを起動する
├── UnityPlayer.dll
├── UnityCrashHandler64.exe
├── D3D12/
├── MonoBleedingEdge/
├── MagicLibrary_Data/     ← ゲームの中身が全部入っている
└── まほう図書館の地下_BurstDebugInformation_DoNotShip/  ← 配らない
  • 最後の…_DoNotShipのフォルダは、エラーを調べるときの情報です。名前のとおり、遊ぶ人には渡しません
  • ファイルの名前は、英語にしておくと安心です。ウインドウの上に出る名前は、Product Nameの「まほう図書館の地下」になります
  • ビルドしたゲームのセーブも、AppData\LocalLow\anogame\まほう図書館の地下に置かれます。エディタで遊んだセーブと同じ場所なので、続きから遊べます
友達に配るとき
  • .exeだけをコピーしても動きません。…_DoNotShipのフォルダを除いて、フォルダごとZIPに圧縮して渡します
  • 個人が作った.exeは署名が無いので、初めて起動するときに「WindowsによってPCが保護されました」と出ることがあります。「詳細情報」>「実行」で起動できます

動かして確認!

エンディングのあと、タイトルの「つづきから」で始めたロビー。「地下へ」で、ちか1かい・ちか2かい・ちか3かいが全部「クリア」になっていて、フェアリーのHPは53/53に戻っている
つづきからは、クリアしたあとのロビーから
  • Titleシーンから再生して、ロビーから地下3階へ行き、ボスを倒す
  • 「ちか3かいを クリアした!」のあと、クリアのウインドは出ずに、Endingシーンに切りかわる
  • 4人が喜ぶポーズで弾み、スタッフロールが下から上へ流れる。決定キーを押している間は速くなる
  • 「おわり」がまん中で止まって、「タイトルへ」のボタンが出る。押すとタイトルに戻り、曲は続いたまま
  • タイトルの「つづきから」で、HP・MPが満タンのロビーから始まる。地下3階まで選べる
  • ビルドしたMagicLibrary.exeでも、同じように遊べる

#27の動作確認

  • 地下3階をクリアすると、Endingシーンへ
  • スタッフロールに、仲間と敵の名前が出る
  • エンディングに入ると、クリアがセーブされる
  • タイトルに戻って「はじめから」を選ぶと、Lv1の3人から始まる
  • ビルドしたゲームが、1280×720のウインドウで起動する

シリーズのまとめ

これまでに作った画面を並べたもの。タイトル、ロビー、メドゥーサの全体のいなずま、禁じられた本の登場、スタッフロール、「おわり」と「タイトルへ」
全27回で作った画面

全27回で作ってきたものを、振り返ります。

回作ったもの覚えた機能
#1〜#3バトル画面・キャラクターのデータ・ステータスCanvas・ScriptableObject・ゲージ
#4〜#6コマンドのメニュー・だれの番か・ねらう相手継承・UIのナビゲーション・await で選択を待つ
#7〜#10ダメージ・攻撃の演出・バトルの流れ・敵の行動async/await・ヒットストップ・enum のステートマシン
#11・#12勝ち負けとにげる・レベルアップ成長の表
#13〜#16まほう・全体のまほう・状態異常・どうぐパーティクル・Scroll View
#17〜#20ロビー・売店・装備・なかまの入れ替えDontDestroyOnLoad・インターフェース
#21〜#24地下3階・ボス・自動プレイでバランスデータで階を組む・同じ条件で何度も確かめる
#25〜#27セーブ・タイトル・音・エンディング・ビルドJsonUtility・AudioSource・ビルド

次にやってみること

  • 仲間やまほうを増やす。まほうのアセットを作って、CharacterDataの覚えるまほうに足すだけで、まほうの一覧に並びます
  • 地下4階を足す。FloorDataを1つ作って、GameSessionとEndingRollのFloorsに足せば、エンディングはいちばん下の階のあとに移ります
  • フィールドを歩く。ロビーと地下の間に、歩いて敵に出会うマップを入れると、もっとRPGらしくなります
  • 絵と音を差し替える。配った素材は仮の絵と音なので、自分の絵や音にすると、一気に自分のゲームになります
スライム

27回、長い冒険でした・・・。最初は立っているだけだったのに、ちゃんとボスまで倒せるゲームになりましたね!

フェアリー

作ったゲームは、ぜひ友達に遊んでもらってね。「ここがむずかしい」「このまほうが好き」っていう感想が、次の改良のヒントになるわ!

あとがき

全27回、おつかれさまでした!バトル画面に4人を並べるところから始めて、コマンド・演出・まほう・どうぐ・ロビー・地下3階・ボスと積み重ね、最後にセーブと音とエンディングを付けて、Windowsで遊べるゲームになりました。#24で作った自動プレイは、敵や装備を足したときのバランスの確かめにも、そのまま使えます。ぜひ、自分だけの「まほう図書館の地下」に育ててみてください!

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

コメント

コメントする

目次