
前回の装備の画面で、買った装備を付け替えて強くなれるようになりました。ところで、仲間の素材は4人分あるのに、ロビーにもバトルにも3人しかいませんでした・・・。今回は4人目のメドゥーサを仲間に入れて、ロビーの「なかま」で、バトルに出る3人を入れ替えられるようにしたいと思います!
- シリーズの目次:ターン制RPGの作り方
- 前回:#19 装備の画面
今回作るもの

- 仲間を4人にする。並び順の上の3人がバトルに出て、4人目は控え
- 控えの仲間は、ロビーのすみで本を読んで待っている
- ロビーのメニューの「なかま」で、2人を選ぶと、その2人の並び順を入れ替える
- 選んでいる人が白く光る。2人目を選んでいる間は、1人目も光ったまま
- 入れ替えると、2人が相手の場所へぴょんと跳ぶ
- バトルに出る3人どうしを入れ替えれば、並び順(コマンドを選ぶ順番)も変えられる
- 控えの仲間も、勝ったときの経験値を半分もらう(置いていかれないように)
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| GameSession | 仲間を4人に。上の3人(Party)と控え(Reserves)に分ける | - |
| PartyMember | 控えの仲間が、経験値をもらう | (付けない) |
| UnitView | 本を読んで待つ・相手の場所へ跳ぶ | - |
| PartyMenu | なかまの入れ替えの画面 | Lobby の PartyMenu |
| LobbyMenu | 「なかま」のボタン、控えの見た目 | - |
| BattleManager | 控えにも経験値の半分 | - |
仲間を4人にしよう
![左に「Members(並び順)」の4つの箱が縦に並ぶ。[0] スライム・[1] フェアリー・[2] キョンシーの3つは緑の「Party(上から3人):バトルに出る・ロビーの3人」へ、[3] メドゥーサは黄色の「Reserves(4人目から):控え」へ線でつながっている](https://anogame.net/wp-content/uploads/2026/10/unity-turn-based-rpg-20-party-swap-members-diagram.png)
この節では、GameSessionに4人目を入れて、「バトルに出る3人」と「控え」に分けます。
#17 ロビーと休憩室で作ったGameSessionのPartyには、3人だけが入っていました。これを「4人全員の並び順(Members)」に変えて、Partyは「Membersの上から3人」を返すようにします。こうしておくと、入れ替えはMembersの順番を変えるだけで済みます。
GameSession

GameSessionのstartPartyとPartyのところを、次のように書き換えます。
[SerializeField] private CharacterData[] startMembers; // 最初の仲間(4人。上の3人がバトルに出る)
public const int PartySize = 3; // バトルに出る人数
public List<PartyMember> Members { get; } = new List<PartyMember>(); // 仲間全員(この並び順の、上の3人がバトルに出る)
public List<PartyMember> Party => Members.GetRange(0, Mathf.Min(PartySize, Members.Count)); // バトルに出る3人
public List<PartyMember> Reserves => Members.GetRange(Party.Count, Members.Count - Party.Count); // 控え(4人目から)
Awakeで仲間を作るところも、Membersに入れるように直します。
foreach (CharacterData data in startMembers)
{
Members.Add(new PartyMember(data, 0));
}
- Members:4人全員です。この並び順の上の3人がバトルに出ます
- Party:
GetRange(始めの番号, 数)は、Listの一部を取り出した新しいListを返します(Listは6-13 数が変わるデータを扱おう:List)。Mathf.Minは、仲間が3人より少ないときのためです - Reserves:上の3人より後ろ、4人目からです
Partyは「取り出したList」なので、Party.Addのように書き換えてもMembersには入りません。仲間を増やしたり並びを変えたりするのは、Membersのほうです
バトルの側(#17で作ったBattleLineupのLoadParty)は、今までどおりGameSession.Instance.Partyを使っているので、何も変えなくてOKです。並び順の上の3人が、そのままバトルに出ます。
- Hierarchyの GameSession を選び、Start Membersを 4 にして、Slime・Fairy・Jiangshi・Medusa を順に入れる(Data>Charactersから)
スライム名前を変えた欄(startParty → startMembers)は、Inspectorで入れ直しになります・・・
控えは、ロビーのすみで本を読んで待つ


控えの仲間も、ロビーに出しておきます。走ったり弾んだりはせずに、正面を向いて本を読んでいる絵にします。#14で、まほうを覚えたときに使った book のポーズのうち、正面を向いている2枚目(book_02)を止め絵に使います。
UnitViewのPlayFramesAsyncの上に、次のメソッドを足します。
// 控え:正面を向いて、本を読んで待っている(book の2枚目。正面を向いていて、止め絵に向いている)
public void ShowReading()
{
idleBounce.enabled = false;
image.sprite = bookFrames[1];
}
- 待機の弾み(IdleBounce)を止めてから絵を変えます。止めないと、次のコマで待機の絵に戻ってしまいます
- 立ち上がるときは、今までの
ResumeIdleで待機に戻せます


- Hierarchyで、Stage>PartyAreaの Slime を選んでCtrl+Dで複製し、名前を「ReserveView」にする
- Pos X -590、Pos Y -10(左の窓ぎわ)



右の本棚の前は、一覧のウインドが開くと隠れちゃうから、左の窓ぎわにしたのよ!
なかまを入れ替える画面を作ろう


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


- LobbyWindowの中の PartyButton(なかま)を選んで、Buttonの Interactable にチェックを入れる
- #17で、まだ押せないようにしておいたボタンです


- ShopWindowを選んでCtrl+Dで複製し、名前を「PartyWindow」にする(隠したまま。CompareWindowの下に移す)
#18 売店・#19 装備の画面と同じ一覧なので、左に名前、右はしにレベルが出せます。控えの人は、右はしに「ひかえ」も出します。
跳んで入れ替わる


入れ替えたときに、ただ絵が変わるだけだと、何が起きたか分かりにくいですよね。2人が相手の場所へ山なりに跳ぶ演出を付けます。
UnitViewのUnitの下に、もとの位置を読めるプロパティを足します。
public Vector2 HomePosition => homePosition; // もとの位置(なかまの入れ替えで、相手の場所へ跳ぶのに使う)
ShowReadingの下に、跳ぶメソッドと、もとに戻すメソッドを足します。
// なかまの入れ替え:target の場所へ、山なりに ぴょんと跳ぶ
public async Awaitable HopToAsync(Vector2 target)
{
const float seconds = 0.45f;
const float height = 120f; // 跳ぶ高さ
Vector2 from = rectTransform.anchoredPosition;
if (target.x < from.x)
{
transform.localScale = new Vector3(-1f, 1f, 1f); // 左へ跳ぶときは、左を向く
}
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
float t = Mathf.Clamp01(time / seconds);
// 横はまっすぐ、高さは Sin で 0 → 1 → 0
rectTransform.anchoredPosition = Vector2.Lerp(from, target, t) + new Vector2(0f, Mathf.Sin(t * Mathf.PI) * height);
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
rectTransform.anchoredPosition = target;
}
// もとの位置・向きに戻す(なかまの入れ替えで、跳んだあと)
public void ReturnHome()
{
rectTransform.anchoredPosition = homePosition;
transform.localScale = Vector3.one;
}
- HopToAsync:横は
Lerpでまっすぐ進み、高さにSinを足します。tが0→1のとき、Sin(t × π)は0→1→0なので、山なりになります - 仲間の絵は右を向いているので、左へ跳ぶときは
localScaleのXを-1にして左を向かせます(#11の、にげるときと同じやり方です) - ReturnHome:跳んだあと、場所と向きをもとに戻します。このあとで絵を入れ替えるので、見た目は「相手の場所に着いた」ままです
PartyMenu


なかまの画面のスクリプトを作ります。
- 「PartyMenu」スクリプトを作る
using TMPro;
using UnityEngine;
// なかまの入れ替え:2人を選ぶと、その2人の並び順を入れ替える(上の3人がバトルに出る)
public class PartyMenu : MonoBehaviour
{
[SerializeField] private CommandMenu listMenu; // 一覧(PartyWindow)
[SerializeField] private MessageWindow messageWindow;
[SerializeField] private TMP_Text messageText; // 上のメッセージ(「だれを いれかえる?」をすぐ出す)
[SerializeField] private UnitView[] memberViews; // 並び順の場所(PartyArea の3人と、控えの ReserveView)
[SerializeField] private LobbyMenu lobbyMenu; // 入れ替えたら、ロビーの仲間とステータスを出し直す
// なかまの画面を開いて、キャンセルされるまで入れ替えをする
public async Awaitable OpenAsync()
{
GameSession session = GameSession.Instance;
listMenu.gameObject.SetActive(true);
int firstIndex = 0;
while (true)
{
// 一覧:名前と、右はしにレベル(入れ替えると変わるので、毎回作り直す)
string[] names = MemberNames(session);
string[] values = MemberValues(session);
bool[] usables = Usables(session);
// 1人目:選んでいる人を光らせる
messageText.text = "だれを いれかえる?";
listMenu.OnSelect = index => Highlight(index, -1);
firstIndex = await listMenu.WaitForChoiceAsync(names, values, usables, firstIndex);
if (firstIndex < 0)
{
break; // キャンセル:なかまの画面を閉じる
}
// 2人目:1人目は光らせたまま、選んでいる人も光らせる
int chosenIndex = firstIndex; // ラムダ式の中で使うので、別の変数にしておく
messageText.text = $"{session.Members[firstIndex].Data.CharacterName}と だれを いれかえる?";
listMenu.OnSelect = index => Highlight(chosenIndex, index);
int secondIndex = await listMenu.WaitForChoiceAsync(names, values, usables, firstIndex);
listMenu.OnSelect = null;
Highlight(-1, -1);
if (secondIndex < 0 || secondIndex == firstIndex)
{
continue; // キャンセル・同じ人:1人目を選び直す
}
await SwapAsync(firstIndex, secondIndex);
}
listMenu.OnSelect = null;
Highlight(-1, -1);
listMenu.gameObject.SetActive(false);
}
// 2人が相手の場所へ跳んで、並び順を入れ替える
private async Awaitable SwapAsync(int indexA, int indexB)
{
GameSession session = GameSession.Instance;
PartyMember memberA = session.Members[indexA];
PartyMember memberB = session.Members[indexB];
UnitView viewA = memberViews[indexA];
UnitView viewB = memberViews[indexB];
// 本を読んでいた控えも、立ち上がって跳ぶ。2人いっしょに跳ばしてから、両方を待つ
viewA.ResumeIdle();
viewB.ResumeIdle();
Awaitable hopA = viewA.HopToAsync(viewB.HomePosition);
Awaitable hopB = viewB.HopToAsync(viewA.HomePosition);
await hopA;
await hopB;
// 並び順を入れ替える。見た目は、場所をもとに戻して、絵を入れ替える(同じように見える)
session.Members[indexA] = memberB;
session.Members[indexB] = memberA;
viewA.ReturnHome();
viewB.ReturnHome();
lobbyMenu.ShowParty();
await messageWindow.ShowAsync($"{memberA.Data.CharacterName}と {memberB.Data.CharacterName}が いれかわった!");
}
// 一覧の左:名前
private string[] MemberNames(GameSession session)
{
string[] names = new string[session.Members.Count];
for (int i = 0; i < session.Members.Count; i++)
{
names[i] = session.Members[i].Data.CharacterName;
}
return names;
}
// 一覧の右はし:レベル(控えには「ひかえ」も)
private string[] MemberValues(GameSession session)
{
string[] values = new string[session.Members.Count];
for (int i = 0; i < session.Members.Count; i++)
{
string level = $"Lv{session.Members[i].Level}";
values[i] = i < GameSession.PartySize ? level : $"ひかえ {level}";
}
return values;
}
// だれでも選べる
private bool[] Usables(GameSession session)
{
bool[] usables = new bool[session.Members.Count];
for (int i = 0; i < usables.Length; i++)
{
usables[i] = true;
}
return usables;
}
// indexA と indexB の人だけ光らせる(-1 なら、光らせない)
private void Highlight(int indexA, int indexB)
{
for (int i = 0; i < memberViews.Length; i++)
{
memberViews[i].SetHighlight(i == indexA || i == indexB);
}
}
}
- memberViews:並び順の番号と、ロビーの場所を合わせた配列です。0〜2がPartyAreaの3人、3が控えのReserveViewです。場所は動かさずに、そこに立つ人を入れ替えるので、番号がそのまま使えます
- 1人目・2人目:#19で作った
OnSelectで、一覧で選んでいる人を光らせます。2人目を選んでいる間は、1人目も光らせたままにします。同じ人を選んだり、Escを押したりしたら、1人目を選び直します chosenIndex:ラムダ式(index => ...)の中で使う変数は、あとで呼ばれたときの値が使われます。firstIndexはループで書き換わるので、別の変数に入れておきます- SwapAsync:2人を同時に跳ばしたいので、先に
HopToAsyncを2つとも始めてから、順にawaitします。着いたらMembersの2人を入れ替えて、ShowPartyで、ロビーの絵・名前・HP・MPを出し直します
- Hierarchyで、Canvasの外に空のオブジェクトを作り、名前を「PartyMenu」にして、「PartyMenu」スクリプトを付ける
- List Menuに PartyWindow、Message Windowに MessageWindow、Message Textに MessageText
- Member Viewsを 4 にして、Stage>PartyAreaの Slime・Fairy・Jiangshi(Lobby MenuのParty Viewsと同じ順番)、最後に ReserveView
- Lobby Menuに Lobby
LobbyMenu


LobbyMenuに、「なかま」のボタンと控えの見た目をつなぎます。
partyViewsの下と、equipMenuの下に、欄を足します。
[SerializeField] private UnitView reserveView; // 控え(ロビーのすみで本を読んでいる)
[SerializeField] private Button partyButton; // なかま
[SerializeField] private PartyMenu partyMenu;
ShowPartyを、次のように書き換えます。入れ替えると、その場所に立つ人が変わるので、絵と名前も毎回入れ直します。PartyMenuから呼べるようにpublicにします。
// 仲間の絵・名前・レベル・HP・MP と、お金を表示し直す(倒れている人は、戦闘不能の絵)
// HP・MP のゲージは、減る演出をせずにそのまま(バトルから戻ってきたとき、満タンから減って見えないように)
// なかまを入れ替えたときにも呼ぶ
public void ShowParty()
{
GameSession session = GameSession.Instance;
List<PartyMember> party = session.Party;
for (int i = 0; i < party.Count; i++)
{
PartyMember member = party[i];
UnitView view = partyViews[i];
StatusRow row = rows[i];
view.SetupCharacter(member.Data);
row.SetName(member.Data.CharacterName);
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();
}
}
// 控え:本を読んで待っている(いなければ隠す)
List<PartyMember> reserves = session.Reserves;
reserveView.gameObject.SetActive(reserves.Count > 0);
if (reserves.Count > 0)
{
PartyMember reserve = reserves[0];
reserveView.SetupCharacter(reserve.Data);
if (reserve.IsDead)
{
reserveView.ShowDown();
}
else
{
reserveView.Revive();
reserveView.ShowReading();
}
}
goldCounter.ShowInstant(session.Gold);
}
- 一番上に
using System.Collections.Generic;を足します(Listを使うので) - 同じ番号の絵と行を何度も使うので、
view・rowに入れてから使います。控えもreserveに入れておきます ShowPartyで絵を入れるようになったので、Startのループは行を作るだけにします。memberは1回しか使わなくなるので、ループの中はrows[i] = statusWindow.AddRow(session.Party[i].Data);の1行にします- 倒れたまま控えにした人は、本を読まずに戦闘不能の絵にします
Startで、ボタンが押されたときのメソッドを登録します(equipButtonの下)。
partyButton.onClick.AddListener(OnPartyClicked);
OnEquipClickedの下に、次のメソッドを足します。#19でまとめたOpenSubMenuを使うので、これだけで済みます。
private void OnPartyClicked()
{
OpenSubMenu(partyMenu.OpenAsync, partyButton);
}
- Lobbyオブジェクトの Lobby Menu に、Reserve View(ReserveView)・Party Button(PartyButton)・Party Menu(PartyMenu)を入れる
控えにも経験値を半分


この節では、バトルに出ていない控えの仲間にも、経験値を分けます。
控えがずっとLv1のままだと、あとで入れ替えたくなっても、弱すぎて使えません・・・。そこで、勝ったときの経験値の半分を、控えももらえるようにします。
PartyMemberのStatsAtLevelの上に、次のメソッドを足します。
// 経験値をもらう(控えの仲間。バトルに出ている人は BattleUnit.GainExp)。レベルが上がったら true
public bool GainExp(int amount)
{
int oldLevel = Level;
Stats before = Stats;
TotalExp += amount;
if (Level == oldLevel)
{
return false;
}
Hp += Stats.maxHp - before.maxHp; // 最大HPが増えた分だけ、今のHPも増やす
Mp += Stats.maxMp - before.maxMp; // MP も同じ
return true;
}
- PartyMemberの
LevelとStatsは、経験値から計算しています。なのでTotalExpを足すだけで、レベルも能力も変わります - 最大HPが増えた分だけ、今のHPも増やします。バトルでのレベルアップ(#12のBattleUnitの
GainExp)と同じ考え方です - 覚えるまほうは、バトルの始めにレベルから調べているので、控えの間にレベルが上がっても、次にバトルに出たときに使えます
BattleManagerのShowVictoryAsyncで、立っている仲間が経験値をもらうループのあとに、1行足します。
await GiveReserveExpAsync(exp / 2);
ShowLevelUpAsyncの上に、次のメソッドを足します。
// 控えの仲間も、経験値を半分もらう(バトルに出ていなくても、置いていかれないように)
private async Awaitable GiveReserveExpAsync(int exp)
{
if (GameSession.Instance == null || exp <= 0)
{
return; // Battle シーンから再生したときは、控えがいない
}
foreach (PartyMember member in GameSession.Instance.Reserves)
{
if (member.IsDead)
{
continue; // 倒れている人は、もらえない(バトルに出ている人と同じ)
}
string name = member.Data.CharacterName;
bool isLevelUp = member.GainExp(exp);
await messageWindow.ShowAsync($"ひかえの {name}も {exp}ポイント もらった!");
if (isLevelUp)
{
await messageWindow.ShowAsync($"{name}は レベル{member.Level}に あがった!");
}
}
}
exp / 2はintどうしの割り算なので、小数は切り捨てです(7なら3)- 控えはバトルの画面にいないので、レベルアップの演出は出さずに、メッセージだけにしています
動かして確認!


- Lobbyシーンを開いて再生する。左の窓ぎわで、メドゥーサが本を読んでいる
- 「なかま」→ キョンシー → メドゥーサ。2人が跳んで入れ替わり、下のステータスの3行目がメドゥーサになる
- 「地下へ」で、メドゥーサがバトルに出ている。「まほう」に まほうの矢 がある
- 勝つと、控えのキョンシーにも経験値の半分が入る(GIFは、確かめやすいように敵を2体にしています。6ポイントの半分で3ポイント)
- 控えのまま何回か勝つと、控えのレベルも上がる(#12の表で、10ポイントたまるとLv2。「なかま」の一覧の右はしで確かめられる)


#20の動作確認
- ロビーの左の窓ぎわに、控えの仲間が本を読んで立っている
- 「なかま」で4人の一覧が開き、選んでいる人が白く光る。控えの右はしには「ひかえ」
- 2人目を選んでいる間は、1人目も光ったまま。同じ人を選ぶかEscで、1人目を選び直す
- 2人を選ぶと、相手の場所へ跳んで入れ替わる。下のステータスも入れ替わる
- バトルに出る3人どうしを入れ替えると、並び順(コマンドを選ぶ順番)が変わる
- 「地下へ」で、上の3人がバトルに出る
- 勝つと、控えの仲間も経験値の半分をもらう(倒れている控えはもらえない)
あとがき
今回は、4人目のメドゥーサを仲間に入れて、ロビーでバトルに出る3人を入れ替えられるようにしました。並び順の上の3人がバトルに出る、と決めておいたので、バトルの側は何も変えずに済みましたね。これで、ロビーのメニューは「セーブ」以外がそろいました。次回は、地下の階を作って、何回か戦いながら奥へ進めるようにしたいと思います!



メドゥーサの全体まほうは、レベルが上がってからのお楽しみです・・・



控えにも経験値が入るから、どんどん入れ替えて試してみてね!
コメント
コメント一覧 (1件)
[…] 前回:#20 なかまの入れ替え […]