ターン制RPGの作り方#11|勝ち負けと、にげる:喜ぶポーズ・暗くなる画面・逃げる確率

キョンシーとスライムちゃんの攻撃でこぼれたインクを倒すと、3人が正面を向いて喜び、「しょうり!」のウインドが出る。「もういちど」を押すと、バトルが最初から始まる

前回で、敵も攻撃してくるようになりました。でも、敵を全員倒しても、仲間が全員倒れても、メッセージが出て止まるだけです。今回は、バトルの終わりを作ります。勝ったら喜んで「しょうり!」、負けたら画面が暗くなって「ぜんめつ…」、そして「にげる」で戦わずに逃げることもできるようにします。これで、1回のバトルが最初から最後まで遊べるようになりますよ!

目次

今回作るもの

  • 勝ったとき:生きている仲間が喜ぶポーズ(win_01〜03)になり、手に入れた経験値とお金を出すResultWindow
  • 負けたとき:画面がゆっくり暗くなるScreenFadeと、「ぜんめつ…」
  • にげる:すばやさで決まる確率で逃げる。逃げられたら、3人が画面の外へ走っていく。失敗したら、敵だけが動く
  • 「もういちど」で、バトルを最初からやり直す
スクリプト役割付ける場所
ResultWindow勝ち・負け・にげたのウインド。「もういちど」を待つResultWindow(新しく作る)
ScreenFade画面を暗くするScreenFade(新しく作る)
CharacterData・UnitView喜ぶポーズと、逃げる動きを足す-
BattleCommand・RunCommand・BattleContextにげる を作る-
BattlePhase・BattleManager勝ち・負け・にげたの場面と、やり直し-

勝ったとき

こぼれたインクを倒すと3人が喜び、「しょうり!」「けいけんち 3ポイント」「4ゴールドを てにいれた」。Enterで「もういちど」を押すと、バトルが最初から始まる

この節では、敵を全員倒したときに、仲間が喜ぶポーズになって「しょうり!」のウインドを出すところまで作ります。

喜ぶポーズ

CharacterDataに、喜ぶ絵を足します(変数はdownSpriteの下、プロパティはDownSpriteの下)。

    [SerializeField] private Sprite[] winFrames; // 喜ぶポーズ(win_01・02・03)

    public Sprite[] WinFrames => winFrames;
  • 4人のデータのWin Framesに、それぞれの_win_01・_win_02・_win_03をドラッグする
SlimeのWin Framesにslime_win_01・02・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)

3人が喜ぶポーズの前に、「しょうり!」「けいけんち 3ポイント」「4ゴールドを てにいれた」と、▶の付いた「もういちど」のボタンが並んだウインド
  • 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にする
Canvasの一番下にResultWindow、その子にTitleText・BodyText・RetryButton。InspectorのResult WindowにTitle Text・Body Text・Retry Button
  • 「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で透明になります

にげる

Random Seed 2では「しかし まわりこまれてしまった!」で敵だけが攻撃してくる。Random Seed 1では「うまく にげきれた!」で3人が左を向いて走り去り、「にげだした」のウインド。「もういちど」で最初から

この節では、すばやさで決まる確率で逃げるコマンドを作ります。失敗すると敵だけが動き、成功すると3人が走り去ります。

逃げる確率

にげるが成功する確率は、すばやさで決めます。

  • 確率= 仲間の平均すばやさ ÷(仲間の平均すばやさ + 敵の平均すばやさ)
例仲間の平均敵の平均確率
地下1階の3体(9+12+15)÷3=12(5+16+7)÷3=約9.312÷21.3=約56%
足の速い紙ひこうき1体121612÷28=約43%
足の遅いこぼれたインク1体12512÷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%の割合で成功します
  • (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をドラッグ
Battle ManagerのResult WindowにResultWindow、Screen FadeにScreenFade

「もういちど」を押してもバトルが始まらないときは、File>Build Profiles>Scene ListにBattleのシーンが入っているか確かめましょう。SceneManager.LoadSceneで読み込めるのは、Scene Listに入っているシーンだけです。

動かして確認!

勝ち・負け・にげるを、それぞれ条件を決めて確かめます。

勝ったとき

  • BattleのBattle LineupのEnemiesを1体(SpilledInk)にする
  • スライムちゃんとキョンシーで、こぼれたインクを攻撃する
こぼれたインクを倒すと3人が喜び、「しょうり!」「けいけんち 3ポイント」「4ゴールドを てにいれた」。Enterで「もういちど」を押すと、バトルが最初から始まる

負けたとき

  • #10と同じように、3人のMax Hpを一時的に5にする
最後のキョンシーが倒れると「みんな たおれてしまった…」のあと、画面がゆっくり暗くなり、「ぜんめつ…」のウインドが出る

にげる

Battle ManagerのRandom Seedを決めて、1ターン目に にげる を選びます。

  • Random Seed 2:「しかし まわりこまれてしまった!」→ 敵だけが攻撃してくる
  • Random Seed 1:「うまく にげきれた!」→ 3人が左へ走っていって、「にげだした」のウインド
Random Seed 2では「しかし まわりこまれてしまった!」で敵だけが攻撃してくる。Random Seed 1では「うまく にげきれた!」で3人が左を向いて走り去り、「にげだした」のウインド。「もういちど」で最初から

#11の動作確認

  • 敵を全員倒すと、立っている仲間が喜ぶポーズになって、「しょうり!」と経験値・お金が出る
  • 仲間が全員倒れると、画面が暗くなって「ぜんめつ…」
  • にげる は、成功すると3人が走り去って「にげだした」、失敗すると敵だけが動く
  • どの終わり方でも、「もういちど」でバトルが最初から始まる
  • 確かめたら、Enemiesを3体、Max Hpを元の値、Random Seedを0に戻す

あとがき

今回は、勝ち・負け・にげるで、バトルを終わらせられるようにしました。これで1回のバトルが、最初から最後まで遊べるようになりましたね。次回は、勝ったときの経験値とお金を仲間に入れて、レベルアップを作りたいと思います!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次