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

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


まず、ゲームの名前を決めます。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階だけは、ウインドを出さずに、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シーンは、#25のTitleシーンを複製して作ります。背景・4人・ボタンのウインド・SceneFade・SoundPlayerが、そのまま使えます。
- Projectウインドで Title シーンを選んで、Ctrl+D で複製し、名前を Ending にする
- Endingシーンを開いて、Logo・Titleのオブジェクトと、ContinueButtonを消す
- Backgroundの色を、少し暗くする(R・G 115、B 133)
4人を、喜ぶポーズで弾ませる


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行も書かずに、喜ぶエンディングになりました。
スタッフロールの見える範囲


スタッフロールは、上の真ん中の決まった範囲だけで見えるようにします。範囲の外の文字は、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の中でも使われている、子どもを四角い範囲だけ見せるコンポーネントです。
タイトルへのボタン


- TitleWindowの名前を EndWindow にして、Pos Y 40・Height 112 にする(ボタン1つ分)
- NewGameButtonの名前を TitleButton にして、文字を「タイトルへ」にする
EndWindowには、TitleシーンでつけたCanvas Groupが残っています。流れ終わるまでは、これで見えないようにしておきます。
EndingRoll


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を付けます。
| 欄 | 入れるもの |
|---|---|
| Viewport | Credits |
| Credits Text | CreditsText |
| Members | Slime・Fairy・Jiangshi・Medusa(Charactersフォルダ) |
| Floors | Floor1・Floor2・Floor3(Floorsフォルダ) |
| Maker Name | 自分の名前など |
| End Group | EndWindow |
| Title Button | TitleButton |
| Scene Fader | SceneFade |
Scene Listに、Endingを足す


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


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


Edit>Project Settings>Player の Resolution and Presentation を開いて、次のようにします。
| 項目 | 値 |
|---|---|
| Fullscreen Mode | Windowed |
| Default Screen Width / Height | 1280 / 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が保護されました」と出ることがあります。「詳細情報」>「実行」で起動できます
動かして確認!


- Titleシーンから再生して、ロビーから地下3階へ行き、ボスを倒す
- 「ちか3かいを クリアした!」のあと、クリアのウインドは出ずに、Endingシーンに切りかわる
- 4人が喜ぶポーズで弾み、スタッフロールが下から上へ流れる。決定キーを押している間は速くなる
- 「おわり」がまん中で止まって、「タイトルへ」のボタンが出る。押すとタイトルに戻り、曲は続いたまま
- タイトルの「つづきから」で、HP・MPが満タンのロビーから始まる。地下3階まで選べる
- ビルドした
MagicLibrary.exeでも、同じように遊べる
#27の動作確認
- 地下3階をクリアすると、Endingシーンへ
- スタッフロールに、仲間と敵の名前が出る
- エンディングに入ると、クリアがセーブされる
- タイトルに戻って「はじめから」を選ぶと、Lv1の3人から始まる
- ビルドしたゲームが、1280×720のウインドウで起動する
シリーズのまとめ


全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で作った自動プレイは、敵や装備を足したときのバランスの確かめにも、そのまま使えます。ぜひ、自分だけの「まほう図書館の地下」に育ててみてください!
コメント