
ここまでで、3人のコマンドを選んで「たたかう」ができるようになりました。でも、今は上に並んだ順(スライムちゃん → フェアリー → キョンシー)に動いていて、足の速いキョンシーも最後です。また、先に倒れた敵をねらっていた人は「もう たおれている!」で何もできずに終わってしまいます。今回は、すばやさの順に動いて、次のターンへ進むというバトルの流れを、1つのスクリプトにまとめてみたいと思います!
- シリーズの目次:ターン制RPGの作り方
- 前回:#8 攻撃の手応えを作ろう
今回作るもの
- バトルの場面(始まり・コマンドを選ぶ・行動する・勝った)に名前を付けて、場面ごとに進める
BattleManager - すばやさの順に行動を並べる
ActionOrder - ねらっていた敵が先に倒れていたら、ほかの敵にねらいを変える
- 「ぼうぎょ」のコマンド(そのターンに受けるダメージが半分)
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| BattlePhase | バトルの場面の名前(enum) | (付けない) |
| BattleManager | バトルの流れ。場面ごとに進める | Battle |
| BattleLineup | 並べるだけにする(流れは BattleManager へ) | Battle |
| ActionOrder | すばやさ順に並べる | (付けない) |
| BattleUnit | 選べるコマンドと、ぼうぎょ中かを足す | - |
| BattleAction | 行動する人を足す | - |
| BattleContext | まだ倒れていない敵を探すメソッドを足す | - |
| BattleCommand・GuardCommand | ぼうぎょ を作る | - |
| AttackCommand | 倒れた敵をねらっていたら、ねらいを変える | - |
| DamageCalculator | ぼうぎょ 中はダメージを半分に | - |
バトルの場面に名前を付けよう
バトルは「場面」のくり返し
ターン制のバトルは、次の場面をくり返しています。
- 始まり:敵があらわれる
- コマンドを選ぶ:3人のコマンドを順に選ぶ
- 行動する:選んだ行動を、すばやさの順に実行する
- 敵が全員倒れていたら勝ち。まだいれば、2に戻る
この「今どの場面か」に名前を付けておくと、流れが読みやすくなります。名前を付けるのに使うのが、6-15 選択肢に名前を付けよう:enum(列挙型)とswitch文で紹介したenumです。
- 「BattlePhase」スクリプトを作る
// バトルの場面
public enum BattlePhase
{
Start, // 始まり(敵があらわれる)
Command, // 3人のコマンドを選ぶ
Action, // すばやさの順に行動する
Victory, // 勝った
End, // バトルが終わった
}
- 負け(#10)と、にげた(#11)は、その回で足します
並べるだけのBattleLineup
#2 キャラクターと敵のデータから使ってきたBattleLineupは、「並べる」と「バトルを進める」の両方をしていました。バトルの流れはこれから大きくなっていくので、流れは新しいBattleManagerに移して、BattleLineupは並べるだけにします。
まず、BattleUnitに、選べるコマンドと「ぼうぎょ中か」を足します。
public BattleCommand[] Commands { get; } // 選べるコマンド(仲間だけ。敵は null)
public bool IsGuarding { get; set; } // ぼうぎょ しているか(ターンの始めに戻す)
Viewの下に足す- 仲間のデータから作るコンストラクタ(
CharacterDataを受け取る方)の最後に、次の1行を足す
Commands = data.Commands;
BattleLineupを、次のように書き換えます。
using System.Collections.Generic;
using UnityEngine;
// データから仲間と敵を並べて、バトル中の1人ずつ(BattleUnit)を作る
public class BattleLineup : MonoBehaviour
{
[SerializeField] private CharacterData[] party; // バトルに出る3人
[SerializeField] private UnitView[] partyViews; // PartyArea の3人
[SerializeField] private EnemyData[] enemies; // 出てくる敵(1〜4体)
[SerializeField] private UnitView enemyPrefab;
[SerializeField] private RectTransform enemyArea;
[SerializeField] private float enemySpacing = 300f; // 敵の横の間隔
[SerializeField] private StatusWindow statusWindow;
public BattleUnit[] PartyUnits { get; private set; } // 仲間(上から順に)
public StatusRow[] StatusRows { get; private set; } // 仲間のステータスの行(上から順に)
public List<BattleUnit> EnemyUnits { get; } = new List<BattleUnit>(); // 敵(左から順に)
public UnitView[] PartyViews => partyViews;
// 並べる(BattleManager が、バトルの始めに呼ぶ)
public void Setup()
{
PartyUnits = new BattleUnit[party.Length];
StatusRows = new StatusRow[party.Length];
for (int i = 0; i < party.Length; i++)
{
CharacterData character = party[i];
UnitView view = partyViews[i];
view.SetupCharacter(character);
StatusRows[i] = statusWindow.AddRow(character);
PartyUnits[i] = new BattleUnit(character, view);
}
for (int i = 0; i < enemies.Length; i++)
{
EnemyData enemy = enemies[i];
UnitView view = Instantiate(enemyPrefab, enemyArea);
// 真ん中をそろえて横に並べ、1体おきに上下へずらす
float x = (i - (enemies.Length - 1) / 2f) * enemySpacing;
float y = (i % 2 == 0) ? 60f : -60f;
((RectTransform)view.transform).anchoredPosition = new Vector2(x, y);
view.SetupEnemy(enemy);
EnemyUnits.Add(new BattleUnit(enemy, view));
}
}
}
Startをやめて、SetupをBattleManagerから呼ぶ。どちらのStartが先に動くかは決まっていないので、「並べてから、バトルを始める」順番を確実にするため- 作ったBattleUnitやステータスの行は、BattleManagerから読めるようにプロパティで出す。
{ get; private set; }は「外からは読むだけ、書き換えるのはこのクラスの中だけ」
場面ごとに進めるBattleManager
- 「BattleManager」スクリプトを作る
using System.Collections.Generic;
using TMPro;
using UnityEngine;
// バトルの流れ:コマンドを選ぶ → すばやさ順に動く → 勝ったかを調べる、をくり返す
public class BattleManager : MonoBehaviour
{
[SerializeField] private BattleLineup lineup;
[SerializeField] private CommandMenu commandMenu;
[SerializeField] private TMP_Text messageText;
[SerializeField] private MessageWindow messageWindow;
[SerializeField] private TargetSelector targetSelector;
[SerializeField] private BattleEffects battleEffects;
[SerializeField] private int randomSeed = 0; // 0以外にすると、毎回同じ結果になる(確かめるとき用)
[SerializeField] private BattlePhase phase; // 今の場面(再生中に Inspector で見られるように)
private BattleContext context;
private BattleAction[] actions; // このターンに選んだ、3人の行動
private async void Start()
{
try
{
await RunAsync();
}
catch (System.OperationCanceledException)
{
// オブジェクトが消えたら、そこで終わり
}
}
// バトルの流れ。今の場面のことをして、次の場面を決める
private async Awaitable RunAsync()
{
phase = BattlePhase.Start;
while (phase != BattlePhase.End)
{
switch (phase)
{
case BattlePhase.Start:
await StartBattleAsync();
phase = BattlePhase.Command;
break;
case BattlePhase.Command:
actions = await SelectActionsAsync();
phase = BattlePhase.Action;
break;
case BattlePhase.Action:
await ExecuteActionsAsync();
if (LivingEnemyViews().Length == 0)
{
phase = BattlePhase.Victory;
}
else
{
phase = BattlePhase.Command; // 次のターン
}
break;
case BattlePhase.Victory:
await messageWindow.ShowAsync("てきを ぜんぶ たおした!");
phase = BattlePhase.End;
break;
}
}
}
// 始まり:並べて、コマンドに渡す道具をまとめる
private async Awaitable StartBattleAsync()
{
if (randomSeed != 0)
{
GameRandom.SetSeed(randomSeed);
}
lineup.Setup();
context = new BattleContext();
context.MessageText = messageText;
context.Message = messageWindow;
context.TargetSelector = targetSelector;
context.Effects = battleEffects;
context.Party = lineup.PartyViews;
await messageWindow.ShowAsync($"{lineup.EnemyUnits[0].Name}たちが あらわれた!");
}
// 3人のコマンドを順に選ぶ(キャンセルで前の人に戻る)
private async Awaitable<BattleAction[]> SelectActionsAsync()
{
// 新しいターン:ぼうぎょ を解いて、ねらえる敵を調べ直す
foreach (BattleUnit unit in lineup.PartyUnits)
{
unit.IsGuarding = false;
}
context.Enemies = LivingEnemyViews();
BattleUnit[] party = lineup.PartyUnits;
BattleAction[] selected = new BattleAction[party.Length];
int index = 0;
while (index < party.Length)
{
BattleUnit unit = party[index];
StatusRow row = lineup.StatusRows[index];
messageText.text = $"{unit.Name}は どうする?";
unit.View.SetHighlight(true);
row.SetHighlight(true);
DimPartyExcept(index);
BattleCommand command = await commandMenu.WaitForCommandAsync(unit.Commands);
BattleAction action = null;
if (command != null)
{
action = await command.SelectAsync(context);
}
unit.View.SetHighlight(false);
row.SetHighlight(false);
if (command == null)
{
// コマンドでキャンセルしたら、前の人に戻る(1人目なら、そのまま1人目)
if (index > 0)
{
index--;
}
continue;
}
if (action == null)
{
// 相手選びなどでキャンセルしたら、同じ人のコマンドを選び直す
continue;
}
action.User = unit;
selected[index] = action;
index++;
}
BrightenParty();
return selected;
}
// 選んだ行動を、すばやさの順に実行する
private async Awaitable ExecuteActionsAsync()
{
ActionOrder.Sort(actions);
foreach (BattleAction action in actions)
{
if (LivingEnemyViews().Length == 0)
{
// 途中で敵がいなくなったら、残りの行動はしない
break;
}
await action.Command.ExecuteAsync(action.User, action, context);
}
}
// まだ倒れていない敵の見た目
private UnitView[] LivingEnemyViews()
{
List<UnitView> living = new List<UnitView>();
foreach (BattleUnit unit in lineup.EnemyUnits)
{
if (!unit.IsDead)
{
living.Add(unit.View);
}
}
return living.ToArray();
}
// index の人以外の仲間を暗くする
private void DimPartyExcept(int index)
{
for (int i = 0; i < lineup.PartyViews.Length; i++)
{
lineup.PartyViews[i].SetDimmed(i != index);
}
}
// 仲間を全員、もとの明るさに戻す
private void BrightenParty()
{
foreach (UnitView view in lineup.PartyViews)
{
view.SetDimmed(false);
}
}
}
SelectActionsAsyncやLivingEnemyViewsなどは、BattleLineupにあったものを移しただけです。新しいところを見ていきます。
- RunAsync
phaseが今の場面。switchで場面ごとにやることをして、最後に次の場面をphaseに入れる。Endになったら、whileを抜けて終わり- 1ターンは「Command → Action → Command → …」のくり返し。Actionのあとに敵が残っていなければVictoryへ進む
- 負けやにげるが入っても、
caseと「次の場面」を足すだけ。バトルの流れが、RunAsyncを上から読めば分かる形になっています
- phase に [SerializeField]
- 再生中にBattleを選ぶと、InspectorのPhaseで今の場面が見られる。うまく進まないときに、どこで止まっているかが分かります
- StartBattleAsync:BattleLineupの
Startでしていた準備。最後に「〇〇たちが あらわれた!」を出す - SelectActionsAsync:ターンの始めに ぼうぎょ を解き、ねらえる敵を調べ直す。決まった行動には、
action.User = unit;で「だれの行動か」を入れておく - ExecuteActionsAsync:
ActionOrder.Sortで並べ替えてから、順に実行する。全員倒したら、残りの人は動かない
Battleに付ける
- BattleにAdd Component>BattleManager
- Lineup:Battle(Battle Lineupが付いている、同じオブジェクト)
- Command Menu:CommandWindow、Message Text:MessageText、Message Window:MessageWindow
- Target Selector:TargetSelector、Battle Effects:Effects
- Battle Lineupの欄は、並べる分(Party・Party Views・Enemies・Enemy Prefab・Enemy Area・Enemy Spacing・Status Window)だけになる

すばやさの順に並べよう
行動する人を覚える
並べ替えるには、それぞれの行動が「だれの行動か」が必要です。BattleActionに、行動する人を足します。
// 1人分の「どのコマンドを・だれに」使うか(バトルの流れで、すばやさ順に実行する)
public class BattleAction
{
public BattleCommand Command { get; }
public UnitView Target { get; } // 相手がいないコマンド(ぼうぎょ など)は null
public BattleUnit User { get; set; } // 行動する人(選び終わったあとに BattleManager が入れる)
public BattleAction(BattleCommand command, UnitView target)
{
Command = command;
Target = target;
}
}
- コマンドの
SelectAsyncは「だれが使うか」を知らないので、Userはあとから入れられるようにsetにしている
ActionOrder
- 「ActionOrder」スクリプトを作る
using System;
// 行動の順番を決める:すばやさ × 0.8〜1.2 の大きい順
public static class ActionOrder
{
public static void Sort(BattleAction[] actions)
{
// 1人ずつ、この順番を決めるための速さを出す
float[] speeds = new float[actions.Length];
for (int i = 0; i < actions.Length; i++)
{
BattleAction action = actions[i];
float speed = action.User.Stats.speed * GameRandom.Range(0.8f, 1.2f);
if (action.Command.ActsFirst)
{
speed += 1000f; // ぼうぎょ などは、だれよりも先に動く
}
speeds[i] = speed;
}
// speeds を小さい順に並べると、actions も同じように並びかわる。それを逆にして大きい順に
Array.Sort(speeds, actions);
Array.Reverse(actions);
}
}
- すばやさ × 0.8〜1.2
- すばやさだけで決めると、毎ターン同じ順番になってしまう。少しばらつかせて、すばやさが近い人どうしは、順番が入れかわることがあるようにしている
- キョンシー(15)はだいたい一番、フェアリー(12)とスライムちゃん(9)は、たまに入れかわります
- ばらつきは結果に関わるので、#7 たたかう:ダメージを計算してメッセージを出そうで作った
GameRandomを使う(シードを決めれば、毎回同じ順番になる)
- Array.Sort(speeds, actions)
- 2つの配列を渡すと、1つ目(speeds)を小さい順に並べながら、2つ目(actions)も同じように並べかえてくれる
- 欲しいのは大きい順なので、
Array.Reverseで逆にする Arrayを使うので、1行目にusing System;を書いている
ActsFirstは、次の ぼうぎょ で作ります。
ぼうぎょのコマンド
ぼうぎょは、「そのターンに受けるダメージを半分にする」コマンドです。敵より先に構えないと意味がないので、すばやさに関係なく、ターンの最初に動くようにします。
BattleCommandに、次のプロパティを足します(CommandNameの下)。
// true なら、すばやさに関係なく、ターンの最初に動く(ぼうぎょ など)
public virtual bool ActsFirst => false;
GuardCommandを、次のように書き換えます。
using UnityEngine;
// ぼうぎょ:そのターンに受けるダメージを半分にする
[CreateAssetMenu(fileName = "Guard", menuName = "TurnBasedRpg/Commands/Guard")]
public class GuardCommand : BattleCommand
{
// 敵より先に構えるので、ターンの最初に動く
public override bool ActsFirst => true;
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
user.IsGuarding = true;
await context.Message.ShowAsync($"{user.Name}は みを まもっている。");
}
}
virtualのプロパティも、メソッドと同じようにoverrideで中身を変えられる。ほかのコマンドは何も書かなければfalseのまま- ぼうぎょ中かどうかは、BattleUnitの
IsGuardingに入れておく。BattleManagerがターンの始めにfalseに戻すので、効くのはそのターンだけ
DamageCalculatorで、ぼうぎょ中ならダメージを半分にします。
using UnityEngine;
// ダメージの計算(式はここだけに書く)
public static class DamageCalculator
{
// たたかう:(こうげき − ぼうぎょ ÷ 2)× 0.9〜1.1。ぼうぎょ 中は半分。最低でも1
public static int Physical(BattleUnit attacker, BattleUnit defender)
{
float baseDamage = attacker.Stats.attack - defender.Stats.defense / 2f;
float damage = baseDamage * GameRandom.Range(0.9f, 1.1f);
if (defender.IsGuarding)
{
damage /= 2f;
}
return Mathf.Max(1, Mathf.RoundToInt(damage));
}
}
フェアリーまだ敵が攻撃してこないから、ぼうぎょ の効き目は見えないね



次の回で敵が攻撃してくるようになったら、ダメージが半分になるのを確かめましょう!
倒れた敵をねらっていたら、ねらいを変えよう
すばやさの順に動くと、「キョンシーが先に倒した敵を、あとのスライムちゃんもねらっていた」ということがよく起きます。そのときは、まだ立っている敵に、ねらいを変えることにします。
BattleContextに、まだ倒れていない敵を探すメソッドを足します。
using TMPro;
// コマンドが相手を選んだり、メッセージを出したりするときに使う、バトルの道具をまとめたもの
public class BattleContext
{
public TMP_Text MessageText; // 上のメッセージ(すぐに書き換えるとき)
public MessageWindow Message; // 上のメッセージ(1文字ずつ出して待つとき)
public TargetSelector TargetSelector; // 相手を選ぶ道具
public BattleEffects Effects; // 当たったときの演出
public UnitView[] Party; // 仲間の見た目
public UnitView[] Enemies; // 敵の見た目(ターンの始めに、倒れていない敵)
// まだ倒れていない敵を、左から探して1体返す(いなければ null)
public UnitView FindLivingEnemy()
{
foreach (UnitView view in Enemies)
{
if (!view.Unit.IsDead)
{
return view;
}
}
return null;
}
}
AttackCommandのExecuteAsyncの最初を、次のように書き換えます。
// 前に出て、振って、当てて、戻る。HPが0になったら、しぼんで消える
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
UnitView targetView = action.Target;
if (targetView.Unit.IsDead)
{
// ねらっていた敵が、先に倒れていた。まだ立っている敵に、ねらいを変える
targetView = context.FindLivingEnemy();
}
BattleUnit target = targetView.Unit;
await context.Message.ShowAsync($"{user.Name}の こうげき!");
if (target.IsDead)で「もう たおれている!」を出していたところを、ねらいを変えるように書き換えるawait context.Message.ShowAsync($"{user.Name}の こうげき!");から下は、#8 攻撃の手応えを作ろうのまま- 敵が全員倒れていたら、BattleManagerがその前に止めるので、
FindLivingEnemyがnullを返すことはない
動かして確認!
再生して、2ターン戦ってみましょう。
- 1ターン目:スライムちゃんとキョンシーは「たたかう」でこぼれたインク、フェアリーは「ぼうぎょ」
- 2ターン目:3人とも「たたかう」で紙ひこうき






#9の動作確認
- 3人のコマンドを選ぶと、ぼうぎょ → すばやさの順(だいたいキョンシー → フェアリー → スライムちゃん)に動く
- 動き終わったら、次のターンのコマンドを選ぶ
- 先に倒れた敵をねらっていた人は、ほかの敵を攻撃する
- 敵を全員倒したら「てきを ぜんぶ たおした!」で止まる
- 再生中にBattleを選ぶと、InspectorのPhaseで今の場面が分かる
コマンドを選んだあと何も起きないときは、BattleのBattle Managerの欄が空になっていないか確かめましょう。BattleLineupから欄を消したので、前にBattle Lineupに入れていたもの(Command Menu など)は、Battle Managerに入れ直します。
あとがき
今回は、バトルの流れをBattleManagerにまとめて、すばやさの順に動くようにしました。場面に名前を付けておくと、「今どこで、次にどこへ進むか」がはっきりしますね。次回は、いよいよ敵が攻撃してきます。受けたときの演出と、仲間が倒れたときの扱いを作りたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#9 バトルの流れを作ろう […]