
前回の売店で、ペーパーナイフやブックカバーを買えるようになりました。でも、買っただけでは装備のふくろに入っているだけで、まだ強くなりません・・・。今回は「装備の画面」を作って、だれに何を付けるかを選べるようにしたいと思います。付けたら能力がどれだけ変わるかも、ひと目で分かるようにしますよ!
- シリーズの目次:ターン制RPGの作り方
- 前回:#18 売店
今回作るもの

- ロビーのメニューに「そうび」を足す
- 装備の画面は、だれの → どこ(ぶき・ぼうぐ)→ どれの順に選ぶ
- だれの:選んでいる仲間が白く光る
- どこ:右はしに、今付けている装備の名前が出る
- どれ:装備のふくろの中から選ぶ。付けたときの能力の変化を「こうげき 12 → 15」のように出す(上がると緑、下がると赤)。「はずす」も選べる
- 付けると、前に付けていた装備はふくろに戻る。付けた仲間は喜ぶポーズ
- 装備の分は、バトルの能力にも足される(レベルが上がっても残る)
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| Stats | 2つの能力を足し算する | (付けない) |
| PartyMember | ぶき・ぼうぐを持ち、能力に装備の分を足す | (付けない) |
| BattleUnit | バトルでレベルが上がっても、装備の分を残す | - |
| CommandMenu | 選んでいるボタンが変わったら知らせる | - |
| EquipMenu | 装備の画面 | Lobby の EquipMenu |
| LobbyMenu | 「そうび」のボタン | - |
能力に装備を足そう

この節では、仲間の能力に装備の分を足します。
能力は、#12 経験値とレベルアップで作ったStats.AtLevelで、レベルから計算しています。ここに、ぶきとぼうぐのBonus(#18のEquipmentDataで作りました)を足せば、装備した能力になります。ダメージの計算(DamageCalculator)は何も変えません。計算に使うStatsが強くなるだけです。
Statsの足し算とPartyMember

まず、Statsどうしを+で足せるようにします。StatsのAtLevelの下に、次のメソッドを足します。
// 2つの能力を足す(今のレベルの能力に、装備の分を足すときに使う)
public static Stats operator +(Stats a, Stats b)
{
Stats stats = new Stats();
stats.maxHp = a.maxHp + b.maxHp;
stats.maxMp = a.maxMp + b.maxMp;
stats.attack = a.attack + b.attack;
stats.defense = a.defense + b.defense;
stats.speed = a.speed + b.speed;
stats.magic = a.magic + b.magic;
return stats;
}
operator +と書くと、stats + weapon.Bonusのように、数と同じ書き方で足し算できるようになります- HP・MP・4つの能力を、それぞれ足した新しいStatsを返します。足す前のStatsは書き換えません
次に、PartyMemberに、ぶきとぼうぐを持たせます。PartyMemberを、次のように書き換えます。
using UnityEngine;
// 仲間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 EquipmentData Weapon { get; private set; } // ぶき(無ければ null)
public EquipmentData Armor { get; private set; } // ぼうぐ(無ければ null)
public int Level => LevelTable.LevelFor(TotalExp);
public Stats Stats => StatsAtLevel(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;
}
// level のときの能力(装備の分も足す)。バトルでレベルが上がったときにも使う
public Stats StatsAtLevel(int level)
{
return Calculate(level, Weapon, Armor);
}
// slot に付けている装備(無ければ null)
public EquipmentData GetEquipment(EquipSlot slot)
{
if (slot == EquipSlot.Weapon)
{
return Weapon;
}
return Armor;
}
// slot を equipment に付け替えたら、能力がどうなるか(equipment が null なら、はずしたとき)
public Stats StatsIfEquipped(EquipSlot slot, EquipmentData equipment)
{
if (slot == EquipSlot.Weapon)
{
return Calculate(Level, equipment, Armor);
}
return Calculate(Level, Weapon, equipment);
}
// slot に equipment を付ける(null ならはずす)。前に付けていた装備を返す
public EquipmentData Equip(EquipSlot slot, EquipmentData equipment)
{
EquipmentData oldEquipment = GetEquipment(slot);
if (slot == EquipSlot.Weapon)
{
Weapon = equipment;
}
else
{
Armor = equipment;
}
// 最大HP・MP が下がったら、今の HP・MP が最大をこえないように
Hp = Mathf.Min(Hp, Stats.maxHp);
Mp = Mathf.Min(Mp, Stats.maxMp);
return oldEquipment;
}
// level の能力に、weapon と armor の分を足す
private Stats Calculate(int level, EquipmentData weapon, EquipmentData armor)
{
Stats stats = Stats.AtLevel(Data.BaseStats, Data.Growth, level);
if (weapon != null)
{
stats = stats + weapon.Bonus;
}
if (armor != null)
{
stats = stats + armor.Bonus;
}
return stats;
}
}
- Weapon・Armor:付けている装備です。付け替えるのは
Equipだけにしたいので、private setにしています - Stats:今のレベルの能力に、ぶき・ぼうぐの分を足したものになりました。ロビーでもバトルでも、このStatsを使います
- StatsIfEquipped:付け替えたら能力がどうなるか、を計算します。実際には付け替えないので、装備の画面で「12 → 15」を出すのに使えます
- Equip:付けて、前に付けていたものを返します。返したものは、装備の画面でふくろに戻します。最大HPが下がる装備をはずしたときのために、今のHPが最大をこえないようにしています
最後に、バトルの中でレベルが上がったときです。BattleUnitのGainExpでは、レベルの能力を計算し直していました。ここでStats.AtLevelを使うと、装備の分が消えてしまうので、PartyMemberに計算してもらいます。
Stats = Member.StatsAtLevel(Level); // 装備の分も足す
characterDataのコメントも「覚えるまほうを調べる」に直しておきます(能力の計算には、もう使いません)
選んでいる仲間を光らせよう

この節では、一覧で選んでいるボタンが変わったことを、CommandMenuから知らせてもらえるようにします。これがあると、選んでいる仲間を光らせたり、選んでいる装備の「12 → 15」を出したりできます。
CommandMenuに、知らせる先を入れるプロパティを足します(lastSelectedの下)。
// 選んでいるボタンが変わったときに呼ばれる(ボタンの番号を渡す)。装備の画面で、能力の変化を出すのに使う
public System.Action<int> OnSelect { get; set; }
Updateの最後のelseを、次のように書き換えます。今までは「スクロールする一覧のときだけ」調べていましたが、どの一覧でも、選ぶボタンが変わったら知らせます。
else
{
if (current != lastSelected)
{
if (scrollRect != null)
{
ScrollTo((RectTransform)current.transform); // 選ぶボタンが変わったら、見えるところまでスクロール
}
NotifySelect(current);
}
lastSelected = current;
}
WaitForChoiceAsyncで、最初のボタンを選んだところ(SetSelectedGameObjectの下)でも知らせます。
NotifySelect(lastSelected);
Chooseの上に、知らせるメソッドを足します。
// button が何番目のボタンかを調べて、OnSelect に知らせる
private void NotifySelect(GameObject button)
{
if (OnSelect == null)
{
return;
}
for (int i = 0; i < buttons.Count; i++)
{
if (buttons[i].gameObject == button)
{
OnSelect(i);
return;
}
}
}
System.Action<int>は「intを1つ受け取るメソッド」を入れておける型です。装備の画面からlistMenu.OnSelect = Highlight;のように、呼んでほしいメソッドを入れます- 何も入っていない(
null)ときは、何もしません。バトルの一覧は今までどおりです
装備の画面を作ろう

この節では、ロビーに装備の画面を作ります。
ボタンと一覧

- PrefabsのCommandButtonを、LobbyWindowの中にドラッグして、名前を「EquipButton」、文字を「そうび」にする
- ShopButtonのすぐ下に置く(売店の右に並びます。2列なので、メニューは3段になります)
- ShopWindowを選んでCtrl+Dで複製し、名前を「EquipWindow」にする(隠したまま。場所も同じ右の本棚の前)

- 売店と同じ一覧を使うので、ボタン(ListButton)の右はしに、レベルや今の装備の名前が出せます
能力の変化のウインド

左に、能力の変化を出すウインドを作ります。
- Canvasを右クリック>UI>Imageで作り、名前を「CompareWindow」にする(EquipWindowのすぐ下)
- Anchor Presetsは左下(bottom-left)、Pos X 270、Pos Y 560、Width 420、Height 260
- Source Imageに ui_window、Image Typeは Sliced
- CompareWindowを右クリック>UI>Text – TextMeshProで作り、名前を「CompareText」にする
- Anchor PresetsでAltを押しながらstretch、Left 40・Right 32・Top 24・Bottom 24
- Font Size 40、色は白、左寄せ、Line Spacing 10
- CompareWindowの一番上のチェックを外して、隠しておく

- 文字は、遊んでいる間はスクリプトで書き換えます。見た目を確かめるために、仮の文字を入れておくと便利です
EquipMenu

装備の画面のスクリプトを作ります。
- 「EquipMenu」スクリプトを作る
using System.Collections.Generic;
using TMPro;
using UnityEngine;
// 装備の画面:だれの → どこ(ぶき・ぼうぐ)→ どれ、の順に選んで、装備を付け替える
public class EquipMenu : MonoBehaviour
{
[SerializeField] private CommandMenu listMenu; // 一覧(EquipWindow)
[SerializeField] private GameObject compareWindow; // 能力の変化を出すウインド
[SerializeField] private TMP_Text compareText; // 「こうげき 12 → 15」
[SerializeField] private MessageWindow messageWindow;
[SerializeField] private TMP_Text messageText; // 上のメッセージ(「だれの そうびを かえる?」をすぐ出す)
[SerializeField] private UnitView[] partyViews; // PartyArea の3人(選んでいる人を光らせる)
[SerializeField] private Color upColor = new Color(0.5f, 1f, 0.5f); // 上がる数字の色
[SerializeField] private Color downColor = new Color(1f, 0.45f, 0.45f); // 下がる数字の色
private static readonly string[] SlotNames = { "ぶき", "ぼうぐ" }; // EquipSlot の順
// 装備の画面を開いて、キャンセルされるまで付け替えをする
public async Awaitable OpenAsync()
{
GameSession session = GameSession.Instance;
listMenu.gameObject.SetActive(true);
int memberIndex = 0;
while (true)
{
// だれの:選んでいる人を光らせる
string[] names = new string[session.Party.Count];
string[] levels = new string[session.Party.Count];
bool[] usables = new bool[session.Party.Count];
for (int i = 0; i < session.Party.Count; i++)
{
PartyMember member = session.Party[i];
names[i] = member.Data.CharacterName;
levels[i] = $"Lv{member.Level}";
usables[i] = true;
}
messageText.text = "だれの そうびを かえる?";
listMenu.OnSelect = Highlight;
memberIndex = await listMenu.WaitForChoiceAsync(names, levels, usables, memberIndex);
listMenu.OnSelect = null;
if (memberIndex < 0)
{
break; // キャンセル:装備の画面を閉じる
}
await ChooseSlotAsync(session.Party[memberIndex], partyViews[memberIndex]);
}
Highlight(-1);
listMenu.gameObject.SetActive(false);
}
// どこ:ぶき・ぼうぐ(右はしに、今付けているもの)
private async Awaitable ChooseSlotAsync(PartyMember member, UnitView view)
{
int slotIndex = 0;
while (true)
{
string[] equipped = new string[SlotNames.Length];
bool[] usables = new bool[SlotNames.Length];
for (int i = 0; i < SlotNames.Length; i++)
{
EquipmentData equipment = member.GetEquipment((EquipSlot)i);
equipped[i] = equipment != null ? equipment.GoodsName : "なし";
usables[i] = true;
}
messageText.text = $"{member.Data.CharacterName}の どこを かえる?";
slotIndex = await listMenu.WaitForChoiceAsync(SlotNames, equipped, usables, slotIndex);
if (slotIndex < 0)
{
return; // キャンセル:だれの、に戻る
}
await ChooseEquipmentAsync(member, view, (EquipSlot)slotIndex);
}
}
// どれ:ふくろの中の、その場所の装備と「はずす」。選んでいるものを付けたときの能力の変化を出す
private async Awaitable ChooseEquipmentAsync(PartyMember member, UnitView view, EquipSlot slot)
{
GameSession session = GameSession.Instance;
List<EquipmentData> candidates = new List<EquipmentData>();
foreach (EquipmentData equipment in session.Equipments)
{
if (equipment.Slot == slot)
{
candidates.Add(equipment);
}
}
candidates.Add(null); // 一番下は「はずす」
string[] labels = new string[candidates.Count];
bool[] usables = new bool[candidates.Count];
for (int i = 0; i < candidates.Count; i++)
{
EquipmentData candidate = candidates[i];
if (candidate != null)
{
labels[i] = candidate.GoodsName;
usables[i] = true;
}
else
{
labels[i] = "はずす";
usables[i] = member.GetEquipment(slot) != null; // 何も付けていなければ、はずせない
}
}
messageText.text = $"どの {SlotNames[(int)slot]}を そうびする?";
compareWindow.SetActive(true);
listMenu.OnSelect = index => ShowCompare(member, slot, candidates[index]);
int chosenIndex = await listMenu.WaitForChoiceAsync(labels, null, usables);
listMenu.OnSelect = null;
compareWindow.SetActive(false);
if (chosenIndex < 0)
{
return; // キャンセル:どこ、に戻る
}
// 付け替える:選んだものはふくろから出して、前に付けていたものはふくろに戻す
EquipmentData chosen = candidates[chosenIndex];
EquipmentData oldEquipment = member.Equip(slot, chosen);
if (chosen != null)
{
session.Equipments.Remove(chosen);
}
if (oldEquipment != null)
{
session.Equipments.Add(oldEquipment);
}
if (chosen != null)
{
_ = view.PlayWinAsync(); // 喜ぶのは待たずに、メッセージと一緒に進める
await messageWindow.ShowAsync($"{member.Data.CharacterName}は {chosen.GoodsName}を そうびした!");
view.ResumeIdle();
}
else
{
await messageWindow.ShowAsync($"{member.Data.CharacterName}は {oldEquipment.GoodsName}を はずした。");
}
}
// equipment に付け替えたら能力がどう変わるかを、「こうげき 12 → 15」のように出す(上がると緑、下がると赤)
private void ShowCompare(PartyMember member, EquipSlot slot, EquipmentData equipment)
{
Stats before = member.Stats;
Stats after = member.StatsIfEquipped(slot, equipment);
compareText.text = CompareLine("こうげき", before.attack, after.attack) + "\n"
+ CompareLine("ぼうぎょ", before.defense, after.defense) + "\n"
+ CompareLine("すばやさ", before.speed, after.speed) + "\n"
+ CompareLine("まりょく", before.magic, after.magic);
}
// 1行分。数字は2けたの幅にそろえる
private string CompareLine(string label, int before, int after)
{
string afterText = $"<mspace=0.6em>{after,2}</mspace>";
if (after > before)
{
afterText = $"<color=#{ColorUtility.ToHtmlStringRGB(upColor)}>{afterText}</color>";
}
else if (after < before)
{
afterText = $"<color=#{ColorUtility.ToHtmlStringRGB(downColor)}>{afterText}</color>";
}
return $"{label} <mspace=0.6em>{before,2}</mspace> → {afterText}";
}
// index の人だけ光らせる(-1 なら、だれも光らせない)
private void Highlight(int index)
{
for (int i = 0; i < partyViews.Length; i++)
{
partyViews[i].SetHighlight(i == index);
}
}
}
- OpenAsync・ChooseSlotAsync・ChooseEquipmentAsync:だれの → どこ → どれ、と1段ずつ深くなります。どの段でもEscで1つ前に戻れるように、
whileでくり返しています。前に選んでいた番号(memberIndex・slotIndex)を覚えておくので、戻ったときも同じところが選ばれています - だれの:
listMenu.OnSelect = Highlight;で、一覧で選んでいる仲間を光らせます。光らせるのは#5 だれの番か、ひと目で分かるようにしようで作ったSetHighlightです - どれ:ふくろの中から、その場所(ぶき・ぼうぐ)の装備だけを集めて、一番下に「はずす」を足します。何も付けていないときは、「はずす」は灰色です
- ShowCompare:選んでいる装備に付け替えたら、を
StatsIfEquippedで計算して、今の能力と並べます。<color=#...>は、TextMeshProで文字の一部だけ色を変える書き方です - 付け替える:選んだ装備はふくろから出し、前に付けていた装備はふくろに戻します。付けた仲間は、#11の喜ぶポーズをします
- Hierarchyで、Canvasの外に空のオブジェクトを作り、名前を「EquipMenu」にして、「EquipMenu」スクリプトを付ける
- List Menuに EquipWindow、Compare Windowに CompareWindow、Compare Textに CompareText
- Message Windowに MessageWindow、Message Textに MessageText
- Party Viewsに、Stage>PartyAreaの Slime・Fairy・Jiangshi(Lobby MenuのParty Viewsと同じ順番)
スライムどこにいても、Escを押せば1つ前に戻れるようにしておくと、迷子になりませんね・・・



戻ったときに、さっき選んでいたところが選ばれたままなのもうれしいわ。
LobbyMenu


LobbyMenuに、「そうび」のボタンをつなぎます。売店のときと同じ流れなので、#18で作ったOnShopClickedの中身を、売店と装備の画面の両方で使えるメソッドにまとめます。
shopの下に、欄を足します。
[SerializeField] private Button equipButton; // そうび
[SerializeField] private EquipMenu equipMenu;
Startで、ボタンが押されたときのメソッドを登録します(shopButtonの下)。
equipButton.onClick.AddListener(OnEquipClicked);
OnShopClickedを、次のように書き換えます。
private void OnShopClicked()
{
OpenSubMenu(shop.OpenAsync, shopButton);
}
private void OnEquipClicked()
{
OpenSubMenu(equipMenu.OpenAsync, equipButton);
}
// 売店・装備の画面を開いて、閉じたらロビーのメニューに戻る(button を選び直す)
private async void OpenSubMenu(System.Func<Awaitable> openAsync, Button button)
{
if (isBusy)
{
return;
}
isBusy = true;
menuGroup.interactable = false; // 一覧から、メニューのボタンへ移れないように
try
{
await openAsync();
}
catch (System.OperationCanceledException)
{
return;
}
menuGroup.interactable = true;
messageText.text = "ここは まほう図書館の ロビー。";
ShowParty(); // 装備で最大HPなどが変わったときのために、出し直す
EventSystem.current.SetSelectedGameObject(button.gameObject);
isBusy = false;
}
- OpenSubMenu:「開いて、閉じるまで待つ」メソッド(
shop.OpenAsync・equipMenu.OpenAsync)と、閉じたあとに選び直すボタンを受け取ります。System.Func<Awaitable>は「Awaitableを返すメソッド」を入れておける型です - 閉じたら
ShowPartyで、レベル・HP・MPを出し直します。今の装備はHPやMPを上げませんが、HPが上がる装備を作ったときにも、ちゃんと表示が変わります
- Lobbyオブジェクトの Lobby Menu に、Equip Button(EquipButton)・Equip Menu(EquipMenu)を入れる
動かして確認!


- Lobbyシーンを開いて再生し、「売店」でペーパーナイフを買う
- 「そうび」→ スライムちゃん →「ぶき」→ ペーパーナイフ。左のウインドに「こうげき 12 → 15」が緑で出る
- 決めると、スライムちゃんが喜んで「スライムは ペーパーナイフを そうびした!」
- もう一度「ぶき」を開くと、右はしに「ペーパーナイフ」と出ている。「はずす」を選ぶと「こうげき 15 → 12」が赤で出る(ここではEscで戻る)
- 「地下へ」で、スライムちゃんのダメージが増えていることを確かめる。こぼれたインク(ぼうぎょ4)へのダメージは、こうげき12だと最大でも
(12 − 4 ÷ 2) × 1.1 = 11ですが、こうげき15なら12〜14になります
#19の動作確認
- ロビーのメニューに「そうび」が増えて、3段に並ぶ
- 「そうび」で仲間の一覧が開き、選んでいる人が白く光る。右はしにレベルが出る
- 仲間を選ぶと、ぶき・ぼうぐと、今付けている装備の名前(無ければ「なし」)が出る
- ぶき・ぼうぐを選ぶと、ふくろの中のその場所の装備と「はずす」が出る。何も付けていなければ「はずす」は灰色
- 選んでいる装備を付けたときの能力の変化が、左のウインドに出る(上がると緑、下がると赤)
- 付けると喜ぶポーズになり、前に付けていた装備はふくろに戻る(ほかの仲間に付けられる)
- どの段でもEscで1つ前に戻れて、最後のEscでロビーのメニューに戻る
- バトルでも、装備の分だけ強くなっている。レベルが上がっても装備の分は残る
あとがき
今回は、装備の画面を作って、買った装備を付け替えられるようにしました。能力の計算は「レベルの能力+装備」に変えただけで、ダメージの計算は何も変えずに強くなりましたね。次回は、ロビーの「なかま」を作って、4人の中からバトルに出る3人を選べるようにしたいと思います!
コメント
コメント一覧 (2件)
[…] 前回:#19 装備の画面 […]
[…] キャンセルしたときに、メニューへ戻る処理は、#19のOpenSubMenuの後半と同じです。なので、そこをメソッドに分けて、両方から使います。OpenSubMenuの、menuGroup.interactable = true;からisBusy = false;までをBackToMenu(button);の1行にして、OpenSubMenuの下に次のメソッドを足します。 […]