
前回、自動プレイで、地下3階とボスのバランスを決めました。これで最後まで遊べるようになりましたが、今はゲームを終わると、レベルもお金も最初に戻ってしまいます・・・。今回は、ロビーでセーブして、タイトル画面の「つづきから」で続きから遊べるようにしたいと思います!
- シリーズの目次:ターン制RPGの作り方
- 前回:#24 自動プレイでバランスを決めよう
今回作るもの

- ロビーの セーブ のボタンで、仲間・持ち物・お金・進んだ階を、JSONのファイルに保存する
- 新しい Titleシーン を作る。タイトルの文字が落ちてきて、「はじめから」「つづきから」を選べる
- セーブが無いときは、「つづきから」を押せないようにする
| スクリプト | 変えること |
|---|---|
| SaveData(新しく作る) | セーブするものを、1つのクラスにまとめる |
| SaveSystem(新しく作る) | JSONにして、ファイルに書く・読む |
| Inventory | 個数を調べる・全部なくす |
| GameSession | セーブデータを作る・セーブの中身に置きかえる |
| LobbyMenu | セーブのボタン |
| TitleMenu(新しく作る) | タイトル画面のボタン |
JSONで保存するやり方は、12-6 データを保存しよう:PlayerPrefsとJSONで紹介しています。今回は、そのやり方をRPGのデータに使います。
スライムこれで、ボスの前でセーブして、ひと休みできますね!
セーブするものを決めよう


まず、何を残せば「続きから」になるかを決めます。#17で、シーンをまたいで持ち越すものをGameSessionにまとめました。セーブするのも、その中身です。
| 残すもの | GameSessionの中 |
|---|---|
| 仲間4人(並び順・経験値・今のHPとMP・装備) | Members |
| 持ち物(どうぐと個数) | Inventory |
| 装備のふくろ(買って、まだ付けていない装備) | Equipments |
| お金 | Gold |
| クリアした階の数 | ClearedFloorCount |
レベルと能力は、#17で経験値から計算するようにしたので、経験値だけ残せば戻せます。セーブはロビーでだけ出来るようにするので、今いる階や、バトルの途中のことは残さなくて大丈夫です。
アセットは名前で残す
仲間のData(CharacterData)や、どうぐ(ItemData)、装備(EquipmentData)は、ScriptableObjectのアセットです。JsonUtilityでアセットをそのまま書き出すと、{"instanceID":12345}のような番号になります。この番号は、ゲームを起動するたびに変わるので、次に遊ぶときには使えません。
そこで、アセットは名前(SlimeやHerbTeaのような、ファイルの名前)で残し、読むときに、その名前のアセットを探して戻します。
名前で探すので、ゲームを配ったあとでアセットのファイルの名前を変えると、前のセーブが読めなくなります。名前を変えたいときは、画面に出す名前(characterNameなど)のほうを変えます。
SaveData
Scriptsフォルダに、SaveDataを作ります。セーブするものを、1つのクラスにまとめます。
using System.Collections.Generic;
// セーブするもの。キャラクター・どうぐ・装備のアセットは、そのままでは保存できないので、アセットの名前で持つ
[System.Serializable]
public class SaveData
{
public List<MemberSaveData> members = new List<MemberSaveData>(); // 仲間全員(並び順のまま。上の3人がバトルに出る)
public List<ItemSaveData> items = new List<ItemSaveData>(); // 持ち物
public List<string> equipments = new List<string>(); // 装備のふくろ
public int gold;
public int clearedFloorCount; // クリアした階の数
}
// 仲間1人分
[System.Serializable]
public class MemberSaveData
{
public string character; // CharacterData の名前(Slime など)
public int totalExp;
public int hp;
public int mp;
public string weapon; // ぶきの名前(無ければ空)
public string armor; // ぼうぐの名前(無ければ空)
}
// どうぐ1種類と、その個数
[System.Serializable]
public class ItemSaveData
{
public string item;
public int count;
}
[System.Serializable]を付けたクラスは、JsonUtilityでJSONにできます(12-6)- JsonUtilityがJSONにするのは、publicの変数(フィールド)です。
{ get; set; }のプロパティは書き出されないので、ここでは変数で書いています Listや、クラスの中のクラス(MemberSaveData)も、[System.Serializable]が付いていれば、そのまま書き出せます
ファイルに書いて、読もう


この節では、SaveDataをファイルに書く・読むSaveSystemを作り、ロビーのセーブのボタンから使えるようにします。
SaveSystem
Scriptsフォルダに、SaveSystemを作ります。どのシーンからでも使えるように、staticのクラスにします。
using System.IO;
using UnityEngine;
// セーブデータを JSON にして、ファイルに書く・読む
public static class SaveSystem
{
// 保存する場所(Windows なら C:/Users/<ユーザー名>/AppData/LocalLow/<会社名>/<プロダクト名>)
private static string SavePath => Path.Combine(Application.persistentDataPath, "save.json");
// セーブのファイルがあるか(タイトルの「つづきから」を押せるか)
public static bool HasSave => File.Exists(SavePath);
public static void Save(SaveData data)
{
string json = JsonUtility.ToJson(data, true);
File.WriteAllText(SavePath, json);
Debug.Log($"セーブしました:{SavePath}");
}
// セーブを読む(無ければ null)
public static SaveData Load()
{
if (!HasSave)
{
return null;
}
string json = File.ReadAllText(SavePath);
return JsonUtility.FromJson<SaveData>(json);
}
}
Application.persistentDataPathは、アプリがデータを保存してよい場所です。会社名とプロダクト名は、Edit>Project Settings>Player の Company Name と Product Name ですToJsonの2つ目をtrueにすると、改行の入った読みやすいJSONになります- セーブしたら、保存した場所をConsoleに出します。あとでファイルを見たり、消したりするときに使います
持ち物の個数を調べる
セーブデータを作るときに、どうぐの個数が要ります。読むときには、最初の持ち物を一度消してから、入れ直します。InventoryのItemsの上に、2つのメソッドを足します。
// item を何個持っているか(持っていなければ 0)
public int CountOf(ItemData item)
{
foreach (ItemStack stack in stacks)
{
if (stack.item == item)
{
return stack.count;
}
}
return 0;
}
// 全部なくす(セーブから読むとき、最初の持ち物を消してから入れ直す)
public void Clear()
{
stacks.Clear();
}
GameSessionで、セーブデータを作る・戻す


GameSessionに、今の中身からSaveDataを作るメソッドと、SaveDataの中身に置きかえるメソッドを足します。
まず、名前からアセットを探すための一覧を、floorsの下に足します。仲間は、startMembersに4人全員が入っているので、そこから探します。
[SerializeField] private ItemData[] allItems; // セーブから読むときに、名前で探す どうぐ(全部入れておく)
[SerializeField] private EquipmentData[] allEquipments; // 同じく、装備(全部入れておく)
Instanceの下に、「つづきから始めるか」を足します。タイトル画面でtrueにしてから、ロビーを読み込みます。
// true にしてからロビーを読み込むと、セーブの続きから始める(タイトルの「つづきから」)
public static bool LoadsSave { get; set; }
Awakeの最後(Gold = startGold;の下)に、つづきからのときに、セーブの中身で置きかえる処理を足します。
// つづきから:最初の値を、セーブの中身で置きかえる
if (LoadsSave)
{
LoadsSave = false; // 次に再生したときに、残らないように
ApplySaveData(SaveSystem.Load());
}
Awakeの下に、4つのメソッドを足します。
// 今の仲間・持ち物・お金・進んだ階を、セーブデータにする
public SaveData CreateSaveData()
{
SaveData data = new SaveData();
foreach (PartyMember member in Members)
{
MemberSaveData memberData = new MemberSaveData();
memberData.character = member.Data.name;
memberData.totalExp = member.TotalExp;
memberData.hp = member.Hp;
memberData.mp = member.Mp;
memberData.weapon = NameOf(member.Weapon);
memberData.armor = NameOf(member.Armor);
data.members.Add(memberData);
}
foreach (ItemData item in Inventory.Items())
{
ItemSaveData itemData = new ItemSaveData();
itemData.item = item.name;
itemData.count = Inventory.CountOf(item);
data.items.Add(itemData);
}
foreach (EquipmentData equipment in Equipments)
{
data.equipments.Add(equipment.name);
}
data.gold = Gold;
data.clearedFloorCount = ClearedFloorCount;
return data;
}
// セーブデータの中身に置きかえる(名前から、アセットを探して戻す)
private void ApplySaveData(SaveData data)
{
Members.Clear();
foreach (MemberSaveData memberData in data.members)
{
CharacterData character = FindByName(startMembers, memberData.character);
PartyMember member = new PartyMember(character, memberData.totalExp);
member.Equip(EquipSlot.Weapon, FindByName(allEquipments, memberData.weapon));
member.Equip(EquipSlot.Armor, FindByName(allEquipments, memberData.armor));
// HP・MP は、装備を付けてから入れる(装備の分だけ、最大HP・MPが増えるので)
member.Hp = memberData.hp;
member.Mp = memberData.mp;
Members.Add(member);
}
Inventory.Clear();
foreach (ItemSaveData itemData in data.items)
{
Inventory.Add(FindByName(allItems, itemData.item), itemData.count);
}
Equipments.Clear();
foreach (string equipmentName in data.equipments)
{
Equipments.Add(FindByName(allEquipments, equipmentName));
}
Gold = data.gold;
ClearedFloorCount = data.clearedFloorCount;
}
// アセットの名前(無ければ空の文字)
private static string NameOf(Object asset)
{
if (asset == null)
{
return "";
}
return asset.name;
}
// assets の中から、名前が assetName のものを探す(無ければ null)
private static T FindByName<T>(T[] assets, string assetName) where T : Object
{
foreach (T asset in assets)
{
if (asset.name == assetName)
{
return asset;
}
}
return null;
}
nameは、アセットのファイルの名前です(Slime.assetならSlime)FindByNameは、#24のFindSkill<T>と同じように、型をTにしたメソッドです。where T : Objectと書くと、TはUnityのアセットだけになるので、nameが使えます。どうぐ・装備・キャラクターの3つを、1つのメソッドで探せます- 装備を付けてから、HPとMPを入れます。
Equipは、HPが最大をこえないように削るので、先にHPを入れると、装備で増えた分が消えてしまいます LoadsSaveは、読んだらすぐfalseに戻します。static の値は、シーンが変わっても残るので、戻さないと、あとで別の場面からロビーへ来たときにも、また読み込んでしまいます
GameSessionのオブジェクトを選び、All Items にどうぐの4つ(ハーブティー・まほうのクッキー・どくけしキャンディ・めざましどけい)、All Equipments に装備の6つを入れます。Itemsフォルダ・Equipmentsフォルダのアセットを、まとめて欄の名前にドラッグすると、一度に入ります。
新しいどうぐや装備を作ったら、ここにも入れるのを忘れないようにします。入れ忘れると、セーブから読んだときに、そのどうぐが消えてしまいます。
ロビーのセーブのボタン


#17で、ロビーにセーブのボタンだけ置いて、押せないようにしていました。これを使えるようにします。
LobbyMenuのpartyMenuの下に、ボタンの欄を足します。
[SerializeField] private Button saveButton; // セーブ
Startで、ほかのボタンと同じように、押されたときのメソッドを登録します。
partyButton.onClick.AddListener(OnPartyClicked);
saveButton.onClick.AddListener(OnSaveClicked);
OnShopClickedの上に、押されたときのメソッドを足します。休憩室のOnRestClickedと同じ形です。
private async void OnSaveClicked()
{
if (isBusy)
{
return;
}
isBusy = true;
try
{
await SaveAsync();
}
catch (System.OperationCanceledException)
{
return;
}
isBusy = false;
}
RestAsyncの上に、セーブするメソッドを足します。ただ保存するだけだと、押したのかどうか分かりにくいので、3人が本を開いて、記録を書きこむようにします。
// セーブ:3人が本に、ぼうけんの記録を書きこむ。そのあいだに、ファイルに保存する
private async Awaitable SaveAsync()
{
GameSession session = GameSession.Instance;
List<PartyMember> party = session.Party;
List<Awaitable> writings = new List<Awaitable>();
for (int i = 0; i < party.Count; i++)
{
if (!party[i].IsDead) // 倒れている人は、書かない
{
writings.Add(partyViews[i].PlayBookAsync());
}
}
SaveSystem.Save(session.CreateSaveData());
foreach (Awaitable writing in writings)
{
await writing;
}
await messageWindow.ShowAsync("ぼうけんの きろくを かきこんだ!");
foreach (UnitView view in partyViews)
{
view.ResumeIdle();
}
}
- 本を読むポーズ(
PlayBookAsync)は、#14でまほうを覚えたときに作ったものです - 3人のポーズを先に全部始めてから、順に
awaitします(同時に動かす形) - 最後の絵(book_03)のまま、メッセージを出します。読み終わったら、
ResumeIdleで待機の弾みに戻します
Lobbyシーンで、次のように設定します。
- LobbyWindowの中の SaveButton を選び、Buttonの Interactable にチェックを入れる
- Lobbyのオブジェクトを選び、LobbyMenuの Save Button に、SaveButtonをドラッグする
保存したファイルを見てみる
再生して、ロビーの セーブ を押すと、Consoleに「セーブしました:」と、保存した場所が出ます。エクスプローラーでその場所を開き、save.jsonをメモ帳で開くと、次のように書かれています(レベルを上げて、少し買い物をしたあとのセーブです)。
{
"members": [
{
"character": "Slime",
"totalExp": 330,
"hp": 83,
"mp": 17,
"weapon": "PaperKnife",
"armor": ""
},
{
"character": "Fairy",
"totalExp": 300,
"hp": 50,
"mp": 44,
"weapon": "",
"armor": "BookCover"
},
{
"character": "Jiangshi",
"totalExp": 310,
"hp": 61,
"mp": 14,
"weapon": "",
"armor": ""
},
{
"character": "Medusa",
"totalExp": 280,
"hp": 52,
"mp": 42,
"weapon": "",
"armor": ""
}
],
"items": [
{
"item": "HerbTea",
"count": 3
},
{
"item": "MagicCookie",
"count": 2
},
{
"item": "AlarmClock",
"count": 1
}
],
"equipments": [
"MagicPen"
],
"gold": 480,
"clearedFloorCount": 2
}
- アセットは、
"Slime"や"PaperKnife"のように、名前で入っています - 装備を付けていないところは、空の文字(
"")です。読むときは、FindByNameが見つけられずにnullを返すので、装備なしに戻ります - キョンシーのHPは、61/64のまま残っています。セーブしたときのHPから続きます



ちゃんと、わたしの ブックカバー も書いてあるわね!
タイトル画面を作ろう


この節では、ゲームを起動したときに最初に出る、タイトル画面を作ります。
Titleシーンを作る


ロビーには、背景・4人・ボタンのウインド・暗くする板(SceneFade)と、タイトル画面に要るものがそろっています。Lobbyシーンを複製して作ります。
- ProjectのScenesフォルダで、Lobbyを選んで Ctrl+D で複製し、名前を Title にする。ダブルクリックで開く
- いらないものを消す。Canvasの中の StatusWindow・GoldWindow・MessageWindow・ScreenFade・ShopWindow・EquipWindow・CompareWindow・FloorWindow・PartyWindow と、Canvasの外の GameSession・Lobby・Shop・EquipMenu・PartyMenu・BalanceTester
- Background の Image の Color を、少し暗い色(R 140・G 140・B 158)にする。上に出す文字が読みやすくなります
- PartyArea(Stageの中)の Anchor Presets を真ん中にして、Pos X 0・Pos Y -40 にする。中の4人を横に並べる
| オブジェクト | 名前を変える | Pos X | Pos Y | Width・Height | Idle Bounceの Frames(Imageの Source Imageは1枚目) |
|---|---|---|---|---|---|
| Slime | そのまま | 450 | 0 | 300 | slime_walk_61・62・63 |
| Fairy | そのまま | 150 | 0 | 300 | fairy_walk_61・62・63 |
| Jiangshi | そのまま | -150 | 0 | 300 | jiangshi_walk_61・62・63 |
| ReserveView | Medusa | -450 | 0 | 300 | medusa_walk_61・62・63 |
ロビーでは、LobbyMenuが仲間の絵を入れていました。タイトルにはLobbyMenuが無いので、Idle Bounceの Frames に、待機の3コマを直接入れておきます。4人とも右を向いて、地下へ向かっていくように並べます。
タイトルの文字を作ります。
- Canvasを右クリック>Create Empty で、名前を Logo にする。Hierarchyで、Stageのすぐ下に動かす
- Anchor Presets を上の真ん中にして、Pivot を X 0.5・Y 0(下の真ん中)、Pos X 0・Pos Y -330、Width 1600・Height 200 にする。Pivotを下にしておくと、つぶれるときに、床に着いたように下を基準にしてつぶれます
- Logoを右クリック>UI>Text – TextMeshPro で、名前を Shadow にする。Anchor Presets を Alt+右下(全体に広げる)にして、Left 8・Right -8・Top 8・Bottom -8(右下に8ずらす)。Textに「まほう図書館の地下」、Font Asset は NotoSansJP SDF、Font Size 130、Bold、真ん中ぞろえ、Color は黒に近い紺(R 26・G 20・B 51・A 217)
- Shadowを Ctrl+D で複製して、名前を Text にする。Left・Right・Top・Bottom を0、Color を薄い黄色(R 255・G 237・B 153)にする
影の文字を先に置いて、同じ文字を少し右下にずらして後ろに重ねると、明るい背景の上でも読みやすくなります。
ボタンのウインドを作ります。
- LobbyWindow の名前を TitleWindow にする。Anchor Presets を下の真ん中にして、Pivot X 0.5・Y 0、Pos X 0・Pos Y 70、Width 308・Height 188 にする
- Grid Layout Groupの Cell Size を X 260・Y 64、Constraint Count を 1 にする(1列に並べる)
- 中の ShopButton・EquipButton・PartyButton・SaveButton を消す
- DungeonButton の名前を NewGameButton、中の文字を「はじめから」にする。RestButton の名前を ContinueButton、中の文字を「つづきから」にする
TitleWindowには、ロビーで使っていた Canvas Group が、そのまま付いています。これを、タイトルの文字が落ちてくるまで、ボタンを隠しておくのに使います。
TitleMenu


Scriptsフォルダに、TitleMenuを作ります。
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
// タイトル:はじめから・つづきから。セーブが無ければ、つづきからは押せない(灰色)
public class TitleMenu : MonoBehaviour
{
[SerializeField] private RectTransform logo; // タイトルの文字(上から落ちてきて、ぽよんと弾む)
[SerializeField] private CanvasGroup menuGroup; // ボタンのウインド(文字が落ちてから出す)
[SerializeField] private Button newGameButton; // はじめから
[SerializeField] private Button continueButton; // つづきから
[SerializeField] private SceneFader sceneFader;
[SerializeField] private float dropSeconds = 0.5f; // 落ちてくる時間
[SerializeField] private float squashSeconds = 0.3f; // つぶれて戻る時間
private bool isBusy; // ロビーへ切り替えている間は、ボタンを受け付けない
private async void Start()
{
bool hasSave = SaveSystem.HasSave;
continueButton.interactable = hasSave;
newGameButton.onClick.AddListener(OnNewGameClicked);
continueButton.onClick.AddListener(OnContinueClicked);
menuGroup.alpha = 0f;
menuGroup.interactable = false;
try
{
await DropLogoAsync();
await ShowMenuAsync();
}
catch (System.OperationCanceledException)
{
return;
}
// セーブがあれば「つづきから」、無ければ「はじめから」を選んでおく
Button firstButton = hasSave ? continueButton : newGameButton;
EventSystem.current.SetSelectedGameObject(firstButton.gameObject);
}
private void OnNewGameClicked()
{
StartGame(false);
}
private void OnContinueClicked()
{
StartGame(true);
}
// ロビーへ。loadsSave が true なら、セーブの続きから
private async void StartGame(bool loadsSave)
{
if (isBusy)
{
return;
}
isBusy = true;
GameSession.LoadsSave = loadsSave;
try
{
await sceneFader.LoadSceneAsync("Lobby");
}
catch (System.OperationCanceledException)
{
// シーンが切り替わったら、そこで終わり
}
}
// タイトルの文字が、上から落ちてきて、着地でつぶれてから戻る
private async Awaitable DropLogoAsync()
{
Vector2 to = logo.anchoredPosition;
Vector2 from = to + new Vector2(0f, 600f);
float time = 0f;
while (time < dropSeconds)
{
time += Time.deltaTime;
float rate = Mathf.Clamp01(time / dropSeconds);
logo.anchoredPosition = Vector2.Lerp(from, to, rate * rate); // だんだん速く落ちる
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
time = 0f;
while (time < squashSeconds)
{
time += Time.deltaTime;
// Sin で 0 → 1 → 0。いちばんつぶれたときに、横 1.15 倍・縦 0.8 倍
float wave = Mathf.Sin(Mathf.Clamp01(time / squashSeconds) * Mathf.PI);
logo.localScale = new Vector3(1f + 0.15f * wave, 1f - 0.2f * wave, 1f);
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
logo.localScale = Vector3.one;
}
// ボタンのウインドを、ふわっと出す
private async Awaitable ShowMenuAsync()
{
const float seconds = 0.3f;
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
menuGroup.alpha = Mathf.Clamp01(time / seconds);
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
menuGroup.alpha = 1f;
menuGroup.interactable = true;
}
}
- セーブのファイルが無ければ、「つづきから」の
interactableを外します。押せないボタンは灰色になり、キーボードやゲームパッドで動かしても、選ばれなくなります - 落ちてくるところは、
rate * rate(2乗)にして、だんだん速くしています。そのままrateを使うと、同じ速さでスーッと下りてくるだけで、落ちた感じが出ません - 着地したら、横に広く・縦に低くつぶれてから戻ります。#22の ふくらむ と同じく、
Sinで 0 → 1 → 0 と変えています - 「はじめから」でも「つづきから」でも、ロビーを読み込むのは同じです。違うのは
GameSession.LoadsSaveだけで、ロビーのGameSessionがAwakeで見て、セーブを読むかどうかを決めます
Canvasの外に、Create Emptyで Title を作り、TitleMenuを付けます。Logo にLogo、Menu Group にTitleWindow、New Game Button にNewGameButton、Continue Button にContinueButton、Scene Fader にSceneFadeをドラッグします。
GameSessionはロビーに置いたままです。Lobbyシーンから再生すると、今までどおり最初の値で始まるので、ロビーやバトルを確かめるときは、そのままLobbyから再生できます。
最初に開くシーンをTitleにする


ゲームを起動したときは、Scene Listの0番のシーンから始まります(8-3 スクリプトでシーンを切り替えよう)。
- File>Build Profiles を開き、左上の Scene List を選ぶ
- Open Scene List の上の一覧に、ProjectのTitleをドラッグして入れる
- Titleをドラッグして、一番上(0番)に動かす



起動したら、まずタイトルですね!
動かして確認!


- Titleシーンを開いて再生する。初めてなら「つづきから」は灰色で、「はじめから」が選ばれている
- 「はじめから」で、Lv1の3人と50ゴールドのロビーから始まる
- 地下へ行ってレベルを上げたり、売店で買い物をしたりしてから、ロビーの セーブ を押す。3人が本を開いて「ぼうけんの きろくを かきこんだ!」と出る
- 再生を止めて、もう一度Titleから再生する。今度は「つづきから」が選ばれている
- 「つづきから」で、セーブしたときのレベル・HP・装備・持ち物・お金のロビーから始まる。地下へ行くと、クリアした階の、1つ下まで選べる
- 最初から確かめ直したいときは、Consoleに出た場所のsave.jsonを消す
#25の動作確認
- ロビーのセーブで、仲間・持ち物・装備のふくろ・お金・クリアした階が、save.jsonに書かれる
- アセットは、名前でJSONに入っている
- タイトルの文字が落ちてきて、弾んでから、ボタンが出る
- セーブが無いときは、「つづきから」が灰色で押せない
- 「つづきから」で、セーブしたときの続きから始まる
あとがき
今回は、JSONでセーブ・ロードして、タイトル画面から続きを遊べるようにしました。アセットをそのまま保存せずに、名前で残すところがポイントです。名前で残しておけば、あとで数字を直しても(#24のように敵を強くしても)、前のセーブがそのまま読めます。次回は、ボタンや攻撃に効果音を付けて、場面ごとにBGMを切りかえたいと思います!



タイトルの文字、ぽよんってなるのが、わたしみたいで好きです・・・!



ちゃんとセーブしてから、ボスに挑むのよ!
コメント
コメント一覧 (1件)
[…] 前回:#25 セーブ・ロードとタイトル画面 […]