
前回で、敵も攻撃してくるようになりました。でも、敵を全員倒しても、仲間が全員倒れても、メッセージが出て止まるだけです。今回は、バトルの終わりを作ります。勝ったら喜んで「しょうり!」、負けたら画面が暗くなって「ぜんめつ…」、そして「にげる」で戦わずに逃げることもできるようにします。これで、1回のバトルが最初から最後まで遊べるようになりますよ!
- シリーズの目次:ターン制RPGの作り方
- 前回:#10 敵の行動と、受けるときの演出
今回作るもの
- 勝ったとき:生きている仲間が喜ぶポーズ(win_01〜03)になり、手に入れた経験値とお金を出す
ResultWindow - 負けたとき:画面がゆっくり暗くなる
ScreenFadeと、「ぜんめつ…」 - にげる:すばやさで決まる確率で逃げる。逃げられたら、3人が画面の外へ走っていく。失敗したら、敵だけが動く
- 「もういちど」で、バトルを最初からやり直す
| スクリプト | 役割 | 付ける場所 |
|---|---|---|
| ResultWindow | 勝ち・負け・にげたのウインド。「もういちど」を待つ | ResultWindow(新しく作る) |
| ScreenFade | 画面を暗くする | ScreenFade(新しく作る) |
| CharacterData・UnitView | 喜ぶポーズと、逃げる動きを足す | - |
| BattleCommand・RunCommand・BattleContext | にげる を作る | - |
| BattlePhase・BattleManager | 勝ち・負け・にげたの場面と、やり直し | - |
勝ったとき

この節では、敵を全員倒したときに、仲間が喜ぶポーズになって「しょうり!」のウインドを出すところまで作ります。
喜ぶポーズ
CharacterDataに、喜ぶ絵を足します(変数はdownSpriteの下、プロパティはDownSpriteの下)。
[SerializeField] private Sprite[] winFrames; // 喜ぶポーズ(win_01・02・03)
public Sprite[] WinFrames => winFrames;
- 4人のデータのWin Framesに、それぞれの
_win_01・_win_02・_win_03をドラッグする

UnitViewに、変数を足します(downSpriteの下)。
private Sprite[] winFrames; // 喜ぶポーズ(仲間だけ)
SetupCharacterの最後に1行足します。
winFrames = data.WinFrames;
UnitViewの一番下に、喜ぶメソッドを足します。
// 勝った:喜ぶ絵を 01 → 02 → 03 と切りかえて、03 のまま止める(仲間だけ)
public async Awaitable PlayWinAsync()
{
idleBounce.enabled = false; // 待機の弾みが絵を上書きしないように止める
foreach (Sprite frame in winFrames)
{
image.sprite = frame;
await Awaitable.WaitForSecondsAsync(0.15f, destroyCancellationToken);
}
}
- 正面を向いて喜ぶ3枚です。最後の03(手を上げたところ)で止めておくと、静止画でも「勝った!」と分かります
結果のウインド(ResultWindow)

- Canvasの子にResultWindow(UI (Canvas)>Image)
- Canvasの子の一番下(一番手前)に置く
- アンカー:middle-center、Pos X・Y:0、Width:720、Height:420
- Source Image:ui_window、Image Type:Sliced(ほかのウインドと同じ)
- ResultWindowの子にTitleText(Text – TextMeshPro)
- アンカー:top-stretch、Left・Right:40、Pos Y:-70、Height:100
- 「しょうり!」、Font Size:64、太字、Alignment:中央
- ResultWindowの子にBodyText(Text – TextMeshPro)
- アンカー:stretch-stretch、Left・Right:40、Top:130、Bottom:130
- 「けいけんち 11ポイント」、Font Size:36、Alignment:中央
- Prefabs/CommandButtonを、ResultWindowの子にドラッグして、名前をRetryButtonにする
- アンカー:bottom-center、Pos Y:80、Width:280、Height:64
- 中の文字を「もういちど」にする
- #4 コマンドをデータにして、メニューに並べようで作ったボタンなので、選ばれると▶が出ます

- 「ResultWindow」スクリプトを作って、ResultWindowに付ける
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
// バトルが終わったときのウインド(勝った・負けた・にげた)。「もういちど」が押されるまで待つ
public class ResultWindow : MonoBehaviour
{
[SerializeField] private TMP_Text titleText;
[SerializeField] private TMP_Text bodyText;
[SerializeField] private Button retryButton;
// 「もういちど」が押されたら、待っているところに知らせる
private readonly AwaitableCompletionSource retrySource = new AwaitableCompletionSource();
private void Awake()
{
retryButton.onClick.AddListener(OnRetryClicked);
gameObject.SetActive(false); // 最初は隠しておく
}
// ウインドを出して、「もういちど」が押されるまで待つ
public async Awaitable ShowAsync(string title, string body)
{
titleText.text = title;
bodyText.text = body;
retrySource.Reset();
gameObject.SetActive(true);
// 最後のメッセージを読んだときの決定が、このフレームに残っていることがあるので、1フレーム待ってから選ぶ
await Awaitable.NextFrameAsync(destroyCancellationToken);
EventSystem.current.SetSelectedGameObject(retryButton.gameObject);
await retrySource.Awaitable;
}
private void OnRetryClicked()
{
retrySource.TrySetResult();
}
}
- #4のCommandMenuと同じ形です。ボタンが押されたら
TrySetResultで知らせ、await retrySource.Awaitableで待っていたところが先へ進みます- 返す値が無いので、
<>の付かないAwaitableCompletionSourceを使います
- 返す値が無いので、
Awakeで隠すので、シーンではResultWindowを表示したまま(チェックを入れたまま)にしておきます。チェックを外しておくとAwakeが呼ばれず、ボタンの登録もされません- Title Text・Body Text・Retry Buttonに、それぞれをドラッグ
負けたとき

この節では、仲間が全員倒れたときに、画面をゆっくり暗くして「ぜんめつ…」を出すところまで作ります。
画面を暗くするImageを置きます。
- Canvasの子にScreenFade(UI (Canvas)>Image)
- ResultWindowのすぐ上(ResultWindowより1つ前)に置く。結果のウインドは、暗くした画面より手前に出ます
- アンカー:stretch-stretch、Left・Top・Right・Bottom:0
- Source Image:なし、Color:黒、Raycast Targetのチェックを外す
- 「ScreenFade」スクリプトを作って、ScreenFadeに付ける
using UnityEngine;
using UnityEngine.UI;
// 画面を、黒い板の濃さでゆっくり暗くする
[RequireComponent(typeof(Image))]
public class ScreenFade : MonoBehaviour
{
private Image image;
private void Awake()
{
image = GetComponent<Image>();
SetAlpha(0f); // 最初は透明
}
// seconds 秒かけて、濃さを targetAlpha(0〜1)にする
public async Awaitable FadeAsync(float targetAlpha, float seconds)
{
float startAlpha = image.color.a;
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
SetAlpha(Mathf.Lerp(startAlpha, targetAlpha, time / seconds));
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
SetAlpha(targetAlpha);
}
private void SetAlpha(float alpha)
{
Color color = image.color;
color.a = alpha;
image.color = color;
}
}
- 黒い板の濃さ(Alpha)を、
Lerpで少しずつ変えるだけです。真っ黒(1)にせず0.7ぐらいで止めると、倒れた3人がうっすら見えて、負けたことが伝わります - エディタでは画面が真っ黒になりますが、再生すると
Awakeで透明になります
にげる

この節では、すばやさで決まる確率で逃げるコマンドを作ります。失敗すると敵だけが動き、成功すると3人が走り去ります。
逃げる確率
にげるが成功する確率は、すばやさで決めます。
- 確率= 仲間の平均すばやさ ÷(仲間の平均すばやさ + 敵の平均すばやさ)
| 例 | 仲間の平均 | 敵の平均 | 確率 |
|---|---|---|---|
| 地下1階の3体 | (9+12+15)÷3=12 | (5+16+7)÷3=約9.3 | 12÷21.3=約56% |
| 足の速い紙ひこうき1体 | 12 | 16 | 12÷28=約43% |
| 足の遅いこぼれたインク1体 | 12 | 5 | 12÷17=約71% |
- 敵が速いほど逃げにくく、遅いほど逃げやすい。どちらも同じ速さなら50%です
- 0%や100%にならないので、「絶対に逃げられる」ことも「絶対に逃げられない」こともありません(ボスから逃げられないようにするのは#23)
パーティみんなで逃げる
にげるは、1人だけが逃げるのではなく、パーティみんなで逃げるコマンドです。だれかが にげる を選んだら、ほかの人のコマンドは取り消して、すぐに逃げることにします。失敗したら、そのターンは仲間は何もせず、敵だけが動きます。
BattleCommandに、プロパティを足します(ActsFirstの下)。
// true なら、パーティみんなの行動。選んだら、ほかの人のコマンドは取り消す(にげる など)
public virtual bool IsForWholeParty => false;
BattleContextに、にげきれたかを足します(Enemiesの下)。
public bool IsEscaped; // にげきれたら true
UnitViewの一番下に、逃げる動きを足します。
// にげる:左を向いて、画面の外へ走っていく
public async Awaitable RunAwayAsync()
{
transform.localScale = new Vector3(-1f, 1f, 1f); // 左右を反転して、左を向く
Vector2 from = rectTransform.anchoredPosition;
Vector2 to = from + new Vector2(-900f, 0f);
const float seconds = 0.5f;
float time = 0f;
while (time < seconds)
{
time += Time.deltaTime;
rectTransform.anchoredPosition = Vector2.Lerp(from, to, time / seconds);
await Awaitable.NextFrameAsync(destroyCancellationToken);
}
}
localScaleのXを-1にすると、絵が左右反対になります。右を向いた絵しかなくても、左へ走っていくように見えます
RunCommandを、次のように書き換えます。
using System.Collections.Generic;
using UnityEngine;
// にげる:パーティみんなで逃げる。確率は すばやさで決まる
[CreateAssetMenu(fileName = "Run", menuName = "TurnBasedRpg/Commands/Run")]
public class RunCommand : BattleCommand
{
public override bool ActsFirst => true; // 敵より先に逃げる
public override bool IsForWholeParty => true; // 選んだら、ほかの人のコマンドは取り消す
public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
{
await context.Message.ShowAsync($"{user.Name}たちは にげだした!");
// 仲間の平均すばやさ ÷(仲間の平均すばやさ + 敵の平均すばやさ)
float partySpeed = AverageSpeed(context.Party);
float enemySpeed = AverageSpeed(context.Enemies);
float chance = partySpeed / (partySpeed + enemySpeed);
if (GameRandom.Range(0f, 1f) >= chance)
{
await context.Message.ShowAsync("しかし まわりこまれてしまった!");
return;
}
context.IsEscaped = true;
// 立っている仲間が、いっしょに走り出す(始めておいて、あとで待つ)
List<Awaitable> runs = new List<Awaitable>();
foreach (UnitView view in context.Party)
{
if (!view.Unit.IsDead)
{
runs.Add(view.RunAwayAsync());
}
}
foreach (Awaitable run in runs)
{
await run;
}
await context.Message.ShowAsync("うまく にげきれた!");
}
// まだ倒れていない人の、すばやさの平均
private float AverageSpeed(UnitView[] views)
{
int total = 0;
int count = 0;
foreach (UnitView view in views)
{
if (!view.Unit.IsDead)
{
total += view.Unit.Stats.speed;
count++;
}
}
return (float)total / count;
}
}
- GameRandom.Range(0f, 1f) >= chance:0〜1の乱数が確率より大きかったら失敗。確率0.56なら、56%の割合で成功します
- 逃げられるかどうかは結果に関わるので、#7 たたかう:ダメージを計算してメッセージを出そうで作った
GameRandomを使う(シードを決めれば、毎回同じ結果になる)
- 逃げられるかどうかは結果に関わるので、#7 たたかう:ダメージを計算してメッセージを出そうで作った
- (float)total / count:
intどうしの割り算は小数点以下が切り捨てられるので、floatにしてから割る - 3人いっしょに走る:#8 攻撃の手応えを作ろうと同じ「始めておいて、あとで待つ」。人数が決まっていないので、
Listに入れて、あとで1つずつawaitします
BattleManagerで、バトルを終わらせる
BattlePhaseに、にげたを足します(Defeatの下)。
Escaped, // にげた
BattleManagerに、2つの欄を足します(battleEffectsの下)。
[SerializeField] private ResultWindow resultWindow;
[SerializeField] private ScreenFade screenFade;
一番上のusingの並びに、シーンを読み込み直すためのusingを足します。
using UnityEngine.SceneManagement;
RunAsyncのActionから下を、次のように書き換えます。
case BattlePhase.Action:
await ExecuteActionsAsync();
if (context.IsEscaped)
{
phase = BattlePhase.Escaped;
}
else if (LivingEnemyViews().Length == 0)
{
phase = BattlePhase.Victory;
}
else if (IsPartyDown())
{
phase = BattlePhase.Defeat;
}
else
{
phase = BattlePhase.Command; // 次のターン
}
break;
case BattlePhase.Victory:
await ShowVictoryAsync();
phase = BattlePhase.End;
break;
case BattlePhase.Defeat:
await messageWindow.ShowAsync("みんな たおれてしまった…");
await screenFade.FadeAsync(0.7f, 1f);
await resultWindow.ShowAsync("ぜんめつ…", "もういちど ちょうせんしよう!");
phase = BattlePhase.End;
break;
case BattlePhase.Escaped:
await resultWindow.ShowAsync("にげだした", "こんどは かてるかな?");
phase = BattlePhase.End;
break;
}
}
// 「もういちど」が押された:今のシーンを読み込み直して、バトルを最初から
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
- にげきれたかは、敵や仲間が全員倒れたかより先に調べる
whileを抜けるのは、結果のウインドで「もういちど」が押されたあと。今のシーンを読み込み直すと、全部が最初の状態に戻ります- ロビーへ帰るようにするのは、#17で作ります
RunAsyncの下に、勝ったときのメソッドを足します。
// 勝った:立っている仲間が喜んで、手に入れた経験値とお金を出す
private async Awaitable ShowVictoryAsync()
{
foreach (BattleUnit unit in lineup.PartyUnits)
{
if (!unit.IsDead)
{
_ = unit.View.PlayWinAsync(); // 喜ぶのは待たずに、メッセージと一緒に進める
}
}
await messageWindow.ShowAsync("てきを ぜんぶ たおした!");
int exp = 0;
int gold = 0;
foreach (BattleUnit enemy in lineup.EnemyUnits)
{
exp += enemy.Exp;
gold += enemy.Gold;
}
await resultWindow.ShowAsync("しょうり!", $"けいけんち {exp}ポイント\n{gold}ゴールドを てにいれた");
}
- _ = unit.View.PlayWinAsync();:
_ =は「戻ってきたAwaitableは使わない」という書き方。喜ぶポーズは0.45秒で終わるので、待たずにメッセージを出しています - 経験値とお金は、倒した敵の合計。まだ表示するだけで、仲間に入れるのは次の#12です
BattleUnitに、敵の経験値とお金を足します。IsEnemyの下に足して、
public int Exp { get; } // 倒されたときの経験値(敵だけ)
public int Gold { get; } // 倒されたときのお金(敵だけ)
敵のコンストラクタの最後に2行足します。
Exp = data.Exp;
Gold = data.Gold;
最後に、SelectActionsAsyncで にげる を選んだときの処理を足します。action.User = unit;の下です。
action.User = unit;
if (command.IsForWholeParty)
{
// にげる などは、パーティみんなの行動。ほかの人のコマンドは取り消す
BrightenParty();
return new BattleAction[] { action };
}
- 2人目や3人目で にげる を選んでも、それまでに選んだ人のコマンドは取り消して、にげる だけを返す
- 失敗したときは、仲間の行動が1つも残っていないので、そのターンは敵だけが動きます
ExecuteActionsAsyncの、残りの行動をやめる条件に、にげきれたときを足します。
if (context.IsEscaped || LivingEnemyViews().Length == 0 || IsPartyDown())
{
// にげきれたり、途中で勝ち負けが決まったりしたら、残りの行動はしない
break;
}
- にげる は
ActsFirstなので、敵より先に動きます。逃げきれたら、そのあとの敵の行動はしません
Battleに入れる
- BattleのBattle ManagerのResult WindowにResultWindow、Screen FadeにScreenFadeをドラッグ

「もういちど」を押してもバトルが始まらないときは、File>Build Profiles>Scene ListにBattleのシーンが入っているか確かめましょう。SceneManager.LoadSceneで読み込めるのは、Scene Listに入っているシーンだけです。
動かして確認!
勝ち・負け・にげるを、それぞれ条件を決めて確かめます。
勝ったとき
- BattleのBattle LineupのEnemiesを1体(SpilledInk)にする
- スライムちゃんとキョンシーで、こぼれたインクを攻撃する

負けたとき
- #10と同じように、3人のMax Hpを一時的に5にする

にげる
Battle ManagerのRandom Seedを決めて、1ターン目に にげる を選びます。
- Random Seed 2:「しかし まわりこまれてしまった!」→ 敵だけが攻撃してくる
- Random Seed 1:「うまく にげきれた!」→ 3人が左へ走っていって、「にげだした」のウインド

#11の動作確認
- 敵を全員倒すと、立っている仲間が喜ぶポーズになって、「しょうり!」と経験値・お金が出る
- 仲間が全員倒れると、画面が暗くなって「ぜんめつ…」
- にげる は、成功すると3人が走り去って「にげだした」、失敗すると敵だけが動く
- どの終わり方でも、「もういちど」でバトルが最初から始まる
- 確かめたら、Enemiesを3体、Max Hpを元の値、Random Seedを0に戻す
あとがき
今回は、勝ち・負け・にげるで、バトルを終わらせられるようにしました。これで1回のバトルが、最初から最後まで遊べるようになりましたね。次回は、勝ったときの経験値とお金を仲間に入れて、レベルアップを作りたいと思います!
コメント
コメント一覧 (1件)
[…] 前回:#11 勝ち負けと、にげる […]