ターン制RPGの作り方#24|自動プレイでバランスを決めよう:同じシードで何回も戦って数える

地下3階のバトルが、速回しで勝手に進む。スライム・フェアリー・キョンシーがランプのおばけ2体を倒し、右上の「ちか3かい 1/4」が「2/4」になって、まほうのページ3枚と戦う。フェアリーがヒール、スライムがちょこっとヒールを自分たちで選んでいる
自動プレイで、地下3階を速く進む

前回、地下3階の奥にボスの「禁じられた本」を出しました。でも、目安のレベルで、ちょうどよく勝てるのかは、まだ確かめていません。どく が効きすぎるのも、そのままでした・・・。手で何回も遊んで確かめるのは大変なので、今回はゲームに自分で遊んでもらって、結果を数えたいと思います!

目次

今回作るもの

Consoleに「シード7:クリア(ターン 3, 6, 4, 14)のこりHP 138/204」から「シード10」までの行と、最後に「【ちか3かい Lv7】10回中 10回 クリア」
10回通した結果がConsoleに出る
  • 仲間のコマンドを、決まった考え方で自動で選ぶ(AutoPlayer)
  • 決まったレベル・装備の3人で、1つの階を何回も通して、クリアした回数・ターン数・残ったHPを Console に出す(BalanceTester)
  • 結果を見て、どく の強さなどを直す
スクリプト変えること
AutoPlayer(新しく作る)仲間の行動を決める
BattleManager自動プレイのときは、メニューを出さずに AutoPlayer に決めてもらう
StatusSpellCommandかける状態を、外から見られるように
BattleEffectsヒットストップのあと、速さを元に戻す
LevelTableそのレベルになる経験値を返す
BalanceTester(新しく作る)何回も通して、結果を数える
ResultWindow確かめている間は、ウインドを出さずに進む

デッキ構築の番外編では、画面を使わずに、ルールだけを何千回も回すシミュレーターを作りました。今回は、ゲームそのものを速く動かします。

  • 良いところ:今まで作ったコマンドのクラスを、そのまま使える(ルールを2か所に書かなくていい。演出の途中で止まるような不具合も見つかる)
  • 気をつけるところ:画面を動かすので、1回通すのに数十秒かかる。何千回ではなく、10回くらいずつ確かめる
スライム

ゲームが、わたしたちの代わりに戦ってくれるんですか!?

自動でコマンドを選ぼう

Battleシーンで、ランプのおばけとまほうのページ2枚と戦う。左下のコマンドの欄は空のまま、フェアリー・スライム・キョンシーが順に こうげきし、まほうのページを たおす
コマンドを選ばなくても、3人が戦う

この節では、仲間のコマンドを自動で選ぶAutoPlayerを作って、バトルが勝手に進むようにします。

考え方を決める

禁じられた本が「ちからを ためている!」。次のターン、フェアリー・メドゥーサ・スライムが「みを まもっている。」と ぼうぎょ し、禁じられた本の おおきな かみつき は、スライムに14のダメージで済む
ためているボスを見て、3人とも ぼうぎょ

自動プレイは、うまいプレイヤーである必要はありません。いつも同じ考え方で選ぶことが大事です。同じ考え方で遊んでもらうと、数字を変えたときに、結果がどう変わったかを比べられます。

上から順に調べて、当てはまったものを選びます。

  1. ちからを ためている敵がいたら、ぼうぎょ(#22のぶあつい辞書と、ボス)
  2. 倒れた仲間がいたら、起こす(めざめの歌)
  3. HPが半分より少ない仲間がいたら、回復する(2人以上なら、全体の回復を先に)
  4. 敵が2体以上なら、全体の攻撃まほう(いなずま)
  5. ボスがいたら、どく(まだかかっていなければ)と、1体に使う わざ・まほう(まほうの矢・れんげき・ぷるんアタック)
  6. それ以外は、たたかう
  • ねらうのは、HPがいちばん少ない敵(早く数を減らす)
  • どうぐ・補助のまほう(まもりの風・おふだ・せきかのまなざし)は使わない。使わずに勝てるなら、使う人はもっと楽に勝てる、と考えます
  • ぼうぎょ は、ためている敵を見たときだけ使う。「ためる」の次は大きな攻撃、というのは、画面のメッセージを見れば分かることなので、人も同じようにするはずです
  • MPが足りないまほうは選ばない(CanUse)。MPが無くなったら、たたかう だけになる
  • かんたんにするため、ほかの人が何を選んだかは見ません。フェアリーとスライムが同じ人を回復して、2回目は「0 かいふくした」になることもあります。少しへたなくらいのほうが、人が遊ぶときより きびしめに確かめられます

AutoPlayer

Scriptsフォルダに、AutoPlayerを作ります。#10のEnemyBrainと同じように、MonoBehaviourを受け継がないstaticのクラスです。

using System.Collections.Generic;

// 自動プレイ:仲間の行動を、決まった考え方で選ぶ(バランスを確かめるとき用)
// どうぐ・補助のまほう は使わない(使わずに勝てるなら、使う人はもっと楽に勝てる)
public static class AutoPlayer
{
    public static BattleAction Decide(BattleUnit unit, BattleContext context)
    {
        // 1. ちからを ためている敵がいたら、ぼうぎょ(次のターンに、大きな攻撃が来る)
        List<BattleUnit> enemies = context.LivingOpponents(unit);
        if (IsAnyoneCharging(enemies))
        {
            return new BattleAction(FindCommand<GuardCommand>(unit), null);
        }

        // 2. 倒れた仲間がいたら、起こす
        BattleUnit down = FindDownAlly(context);
        ReviveSpellCommand revive = FindSkill<ReviveSpellCommand>(unit, false);
        if (down != null && revive != null)
        {
            return new BattleAction(revive, down.View);
        }

        // 3. HP が半分より少ない仲間がいたら、回復する(2人以上なら、全体の回復を先に探す)
        List<BattleUnit> hurt = FindHurtAllies(context);
        HealSpellCommand allHeal = FindSkill<HealSpellCommand>(unit, true);
        if (hurt.Count >= 2 && allHeal != null)
        {
            return new BattleAction(allHeal, null);
        }
        HealSpellCommand heal = FindSkill<HealSpellCommand>(unit, false);
        if (hurt.Count >= 1 && heal != null)
        {
            return new BattleAction(heal, hurt[0].View);
        }

        // 4. 敵が2体以上なら、全体の攻撃まほう
        AttackSpellCommand allSpell = FindSkill<AttackSpellCommand>(unit, true);
        if (enemies.Count >= 2 && allSpell != null)
        {
            return new BattleAction(allSpell, null);
        }

        // 5. ボスがいたら、どく(まだかかっていなければ)と、1体に使う わざ・まほう
        BattleUnit weakest = FindWeakest(enemies); // HP がいちばん少ない敵をねらう
        BattleUnit boss = context.Lineup.FindBoss();
        if (boss != null)
        {
            StatusSpellCommand poison = FindSkill<StatusSpellCommand>(unit, false);
            if (poison != null && poison.Effect.PoisonRate > 0f && !HasEffect(boss, poison.Effect))
            {
                return new BattleAction(poison, boss.View);
            }
            SkillCommand skill = FindSkill<AttackSpellCommand>(unit, false);
            if (skill == null)
            {
                skill = FindSkill<MultiHitSkillCommand>(unit, false);
            }
            if (skill != null)
            {
                return new BattleAction(skill, weakest.View);
            }
        }

        // 6. それ以外は、たたかう
        return new BattleAction(FindCommand<AttackCommand>(unit), weakest.View);
    }

    // 順番に行動する敵のうち、前の行動が ためる だった敵がいるか(ボス・ぶあつい辞書)
    private static bool IsAnyoneCharging(List<BattleUnit> enemies)
    {
        foreach (BattleUnit enemy in enemies)
        {
            EnemyMove[] moves = enemy.EnemyData.Moves;
            if (!enemy.EnemyData.UsesMovesInOrder || enemy.MoveCount == 0)
            {
                continue;
            }
            EnemyMove lastMove = moves[(enemy.MoveCount - 1) % moves.Length];
            if (lastMove.command is ChargeCommand)
            {
                return true;
            }
        }
        return false;
    }

    // unit が覚えていて、今使える(MP が足りる)T のまほうを探す。targetsAll は、全体のものか
    // あとに覚えたものほど強いので、後ろから探す。無ければ null
    private static T FindSkill<T>(BattleUnit unit, bool targetsAll) where T : SkillCommand
    {
        for (int i = unit.Skills.Count - 1; i >= 0; i--)
        {
            if (unit.Skills[i] is T skill && skill.TargetsAll == targetsAll && skill.CanUse(unit))
            {
                return skill;
            }
        }
        return null;
    }

    // コマンドのボタンから、T のコマンドを探す(たたかう)
    private static T FindCommand<T>(BattleUnit unit) where T : BattleCommand
    {
        foreach (BattleCommand command in unit.Commands)
        {
            if (command is T found)
            {
                return found;
            }
        }
        return null;
    }

    // 倒れている仲間(いなければ null)
    private static BattleUnit FindDownAlly(BattleContext context)
    {
        foreach (UnitView view in context.Party)
        {
            if (view.Unit.IsDead)
            {
                return view.Unit;
            }
        }
        return null;
    }

    // HP が半分より少ない仲間。減っている割合が大きい順に並べる
    private static List<BattleUnit> FindHurtAllies(BattleContext context)
    {
        List<BattleUnit> hurt = new List<BattleUnit>();
        foreach (UnitView view in context.LivingParty())
        {
            BattleUnit ally = view.Unit;
            if (ally.Hp < ally.Stats.maxHp / 2f)
            {
                hurt.Add(ally);
            }
        }
        hurt.Sort((a, b) => HpRate(a).CompareTo(HpRate(b)));
        return hurt;
    }

    // HP の残りの割合(0〜1)
    private static float HpRate(BattleUnit unit)
    {
        return (float)unit.Hp / unit.Stats.maxHp;
    }

    // HP がいちばん少ない人
    private static BattleUnit FindWeakest(List<BattleUnit> units)
    {
        BattleUnit weakest = units[0];
        foreach (BattleUnit unit in units)
        {
            if (unit.Hp < weakest.Hp)
            {
                weakest = unit;
            }
        }
        return weakest;
    }

    // unit が effect にかかっているか
    private static bool HasEffect(BattleUnit unit, StatusEffectData effect)
    {
        foreach (ActiveEffect active in unit.Effects)
        {
            if (active.Data == effect)
            {
                return true;
            }
        }
        return false;
    }
}
  • FindSkill<T>は、覚えたまほうから、T のクラスのものを探します。<T>の部分にHealSpellCommandと書けば回復のまほう、AttackSpellCommandと書けば攻撃のまほうを探せます。where T : SkillCommandは「T には SkillCommand を受け継いだクラスしか入れられない」という決まりです
  • unit.Skills[i] is T skillは、「T のクラス(か、それを受け継いだクラス)なら true にして、そのまま skill という名前で使えるようにする」という書き方です
  • 覚えたまほうは、覚えた順に並んでいます。あとに覚えたものほど強いので、後ろから探します
  • IsAnyoneChargingは、順番に行動する敵の、前の行動を調べます。#22で作ったMoveCountは、行動を決めた回数です。仲間がコマンドを選ぶときは、敵はまだこのターンの行動を決めていないので、MoveCount - 1番目が前のターンの行動です
  • 全体のまほうは、ねらう相手を実行するときに決めるので、BattleActionの相手はnullです(#14の全体のまほうと同じ)

どくのまほうかどうかを調べるために、StatusSpellCommandに、かける状態を返すプロパティを足します。chanceの欄の下に書きます。

    public StatusEffectData Effect => effect;

BattleManagerにつなぐ

BattleManagerのrandomSeedの下に、チェックの欄を足します。

    [SerializeField] private bool autoPlay;      // チェックすると、仲間のコマンドを自動で選ぶ(確かめるとき用)

actionsの下に、自動プレイかどうかを返すプロパティを足します。今はチェックの欄を返すだけです(次の節で、もう1つ足します)。

    // 自動プレイか
    private bool IsAutoPlay => autoPlay;

SelectActionsAsyncの、動けない人を飛ばすifの下に、次のifを足します。

            if (IsAutoPlay)
            {
                // 自動プレイ:メニューを出さずに、AutoPlayer に決めてもらう
                BattleAction autoAction = AutoPlayer.Decide(unit, context);
                autoAction.User = unit;
                selected[index] = autoAction;
                index++;
                continue;
            }
  • コマンドのメニューも、相手を選ぶ矢印も出さずに、すぐ次の人へ進みます
  • 実行するところ(ExecuteActionsAsync)は、何も変えません。自分で選んだときと同じクラスが、同じように動きます

Battleシーンを開いて、Battleオブジェクトの Auto Play にチェックを入れて再生すると、3人が勝手に戦います。

フェアリー

ちゃんと、減った人を回復してるわね!

何回も通して、結果を数えよう

BalanceTesterのInspector。Run On Playのチェックは外れていて、Floor Index 2、Level 7、Partyは3、Weapon PaperKnife、Armor BookCover、Run Count 10、Speed 20
BalanceTesterの設定

この節では、決まったレベル・装備の3人で、1つの階を何回も通して、結果をConsoleに出すBalanceTesterを作ります。

速く動かす

1回のバトルを、ふつうの速さで見ていると時間がかかります。Time.timeScaleを大きくすると、ゲームの時間が速く進みます(10なら10倍)。

  • Awaitable.WaitForSecondsAsyncやTime.deltaTimeで数えている演出・メッセージは、みんな速くなる
  • 1つだけ困るところがあります。#8のヒットストップは、止めたあとにTime.timeScale = 1fに戻していました。これだと、1回当たるたびに、ふつうの速さに戻ってしまいます

BattleEffectsのHitStopAsyncを、止める前の速さを覚えておいて、そこへ戻すように直します。

    // 一瞬だけ時間を止める
    private async Awaitable HitStopAsync()
    {
        float timeScale = Time.timeScale; // 止める前の速さ(自動プレイで速くしているときは 1 ではない)
        Time.timeScale = 0f;
        try
        {
            // timeScale が 0 の間も進む unscaledDeltaTime で数える
            float time = 0f;
            while (time < hitStopSeconds)
            {
                await Awaitable.NextFrameAsync(destroyCancellationToken);
                time += Time.unscaledDeltaTime;
            }
        }
        finally
        {
            // 途中で止まっても、必ず時間を戻す
            Time.timeScale = timeScale;
        }
    }
  • ヒットストップと揺れはunscaledDeltaTimeで数えているので、速くしても、ふつうの速さのままです。そのぶん20倍にしても20倍の速さにはならず、地下3階を1回通すのに30秒くらいかかります

そのレベルになる経験値

決まったレベルから始めるために、LevelTableに、そのレベルになるのに必要な経験値の合計を返すメソッドを足します。LevelForの上に書きます。

    // level になるのに必要な、経験値の合計
    public static int TotalExpFor(int level)
    {
        return totalExp[level - 1];
    }

BalanceTester

Scriptsフォルダに、BalanceTesterを作ります。

using UnityEngine;
using UnityEngine.SceneManagement;

// バランスを確かめる:決まったレベル・装備の3人で、1つの階を自動プレイで何回も通して、結果を Console に出す
// ロビーのシーンに置く。Run On Play をチェックして再生すると始まる
public class BalanceTester : MonoBehaviour
{
    [SerializeField] private bool runOnPlay;          // チェックして再生すると、確かめ始める
    [SerializeField] private int floorIndex = 2;      // 確かめる階(0 が地下1階)
    [SerializeField] private int level = 7;           // 仲間のレベル
    [SerializeField] private CharacterData[] party;   // バトルに出る3人
    [SerializeField] private EquipmentData weapon;    // 3人に付ける ぶき(空なら無し)
    [SerializeField] private EquipmentData armor;     // 3人に付ける ぼうぐ(空なら無し)
    [SerializeField] private int runCount = 10;       // 何回通すか(シードを 1, 2, 3 … と変える)
    [SerializeField, Range(1f, 20f)] private float speed = 10f; // 何倍の速さで進めるか

    public static BalanceTester Instance { get; private set; }
    public static bool IsRunning => Instance != null && Instance.isRunning;

    private bool isRunning;
    private int seed;        // 今の回のシード
    private int[] turns;     // 今の回の、バトルごとのターン数
    private int clearCount;  // クリアした回数

    private void Awake()
    {
        // ロビーに戻ってくると、もう1つ作られる。先にあるほうを使い、新しいほうは消す(GameSession と同じ)
        if (Instance != null)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }

    private void Start()
    {
        if (Instance != this || !runOnPlay)
        {
            return;
        }
        isRunning = true;
        Time.timeScale = speed;
        SceneManager.sceneLoaded += OnSceneLoaded;
        seed = 1;
        StartRun();
    }

    private void OnDestroy()
    {
        // 途中で再生を止めたときも、イベントの登録を外して、速さを戻す(外さないと、次の再生でも呼ばれてしまう)
        if (Instance == this)
        {
            SceneManager.sceneLoaded -= OnSceneLoaded;
            Time.timeScale = 1f;
        }
    }

    // 1回分を始める:仲間のレベル・装備・並びをそろえて、シードを決め、その階の1回目のバトルへ
    private void StartRun()
    {
        GameSession session = GameSession.Instance;
        for (int i = 0; i < party.Length; i++)
        {
            CharacterData data = party[i];
            PartyMember member = session.Members.Find(m => m.Data == data);
            session.Members.Remove(member);
            session.Members.Insert(i, member); // 上から3人がバトルに出る
        }
        foreach (PartyMember member in session.Members)
        {
            member.TotalExp = LevelTable.TotalExpFor(level);
            member.Equip(EquipSlot.Weapon, weapon);
            member.Equip(EquipSlot.Armor, armor);
            member.RestoreAll();
        }
        session.FloorIndex = floorIndex;
        session.BattleIndex = 0;
        session.ClearedFloorCount = floorIndex;

        turns = new int[session.CurrentFloor.BattleCount];
        GameRandom.SetSeed(seed); // 同じシードなら、毎回同じ結果になる
        SceneManager.LoadScene("Battle");
    }

    // バトルのターンが始まるたびに、BattleManager から呼ぶ
    public static void CountTurn()
    {
        if (IsRunning)
        {
            Instance.turns[GameSession.Instance.BattleIndex]++;
        }
    }

    // ロビーに戻ってきた=1回分が終わった(クリアしたか、ぜんめつした)。結果を出して、次の回へ
    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        if (scene.name != "Lobby")
        {
            return;
        }
        LogRun();
        seed++;
        if (seed <= runCount)
        {
            StartRun();
            return;
        }

        // 全部終わった
        string floorName = GameSession.Instance.CurrentFloor.FloorName;
        Debug.Log($"【{floorName} Lv{level}】{runCount}回中 {clearCount}回 クリア");
        SceneManager.sceneLoaded -= OnSceneLoaded;
        Time.timeScale = 1f;
        isRunning = false;
    }

    // 1回分の結果を Console に出す(ターン数は、バトルごとにカンマで区切る)
    private void LogRun()
    {
        GameSession session = GameSession.Instance;
        int battleIndex = session.BattleIndex; // クリアしたら、バトルの数と同じになっている
        bool isCleared = battleIndex >= session.CurrentFloor.BattleCount;
        int foughtCount = isCleared ? battleIndex : battleIndex + 1;
        string turnText = TurnText(foughtCount);
        if (!isCleared)
        {
            Debug.Log($"シード{seed}:{battleIndex + 1}回目で ぜんめつ(ターン {turnText})");
            return;
        }

        clearCount++;
        int hp = 0;
        int maxHp = 0;
        foreach (PartyMember member in session.Party)
        {
            hp += member.Hp;
            maxHp += member.Stats.maxHp;
        }
        Debug.Log($"シード{seed}:クリア(ターン {turnText})のこりHP {hp}/{maxHp}");
    }

    // 戦ったバトルのターン数を「3, 4, 9」のように並べる
    private string TurnText(int foughtCount)
    {
        string text = turns[0].ToString();
        for (int i = 1; i < foughtCount; i++)
        {
            text += $", {turns[i]}";
        }
        return text;
    }
}

流れは次のとおりです。

  1. ロビーで再生すると、Startで速くして、1回目を始める
  2. StartRun:4人のレベル・装備をそろえて、HP・MPを満タンにし、バトルに出る3人を上に並べる。シードを決めて、その階の1回目のバトルへ
  3. バトルは、今までの流れのまま進む。勝てば「さきへ すすむ」で次のバトルへ、最後のバトルに勝つか、ぜんめつすると、ロビーへ戻る
  4. OnSceneLoaded:ロビーに戻ってきたら、1回分が終わったということ。結果を出して、シードを1つ増やして、次の回へ
  5. 決めた回数が終わったら、クリアした回数を出して、速さを元に戻す
  • SceneManager.sceneLoadedは、シーンを読み込み終わるたびに呼ばれるイベントです。+=でメソッドを登録しておくと、ロビーに戻ってきたのが分かります。終わったら-=で外します
  • OnDestroyで、登録を外します。途中で再生を止めたときに外し忘れると、次に再生したときも、前のBalanceTesterのメソッドが呼ばれてしまいます(Enter Play Mode Optionsで、再生のたびのリロードを切っているとき。staticのイベントは、再生を止めても残ります)。自分も、途中で止めたあとに、結果が2つずつ混ざって出てきて気づきました
  • ロビーに戻るたびに、シーンのBalanceTesterがもう1つ作られます。#17のGameSessionと同じように、Instanceがあれば新しいほうを消します。消すのは次のフレームなので、Startは新しいほうでも呼ばれます。Instance != thisで、新しいほうは何もしないようにします
  • シードを1, 2, 3 … と変えて何回も通します。同じシードなら、何回やっても同じ結果になるので、数字を直したあとに同じ10回で比べられます
  • session.Members.Find(m => m.Data == data)は、仲間の中から、データがdataの人を探します(#21のRemoveAllと同じ、=>の書き方)

結果のウインドを出さずに進む

勝ったときの結果のウインドは、ボタンを押すまで待ちます。確かめている間は、ウインドを出さずに進むようにします。ResultWindowのShowAsyncの最初に、次のifを足します。

        if (BalanceTester.IsRunning)
        {
            return canGoOn; // バランスを確かめている間は、出さずに進む(先へ進めなければ、ロビーへ)
        }

ターンを数える

BattleManagerのIsAutoPlayに、確かめているときも足します。

    // 自動プレイか(チェックしたとき・バランスを確かめているとき)
    private bool IsAutoPlay => autoPlay || BalanceTester.IsRunning;

RunAsyncのcase BattlePhase.Command:の最初に、1行足します。ターンが始まるたびに数えます。

                case BattlePhase.Command:
                    BalanceTester.CountTurn();
  • 確かめていないときは、CountTurnは何もしません

ロビーに置く

  • Lobbyシーンを開いて、Hierarchyの何もないところを右クリック>Create Empty。名前を BalanceTester にして、Add Component>BalanceTester
  • Floor Index 2(地下3階)・Level 7・Partyは 3 つにして、Slime・Fairy・Jiangshi
  • Weapon PaperKnife・Armor BookCover・Run Count 10・Speed 20
  • 確かめるときだけ、Run On Play にチェックを入れて再生する。ふつうに遊ぶときは外しておく

再生すると、すぐに地下3階のバトルが始まり、3人が勝手に、速く戦います。10回通すと、「今回作るもの」の画像のように、Consoleに1回ずつの結果と、クリアした回数が並びます。

  • 「シード3:クリア(ターン 4, 4, 4, 14)のこりHP 147/204」は、4回のバトルが4・4・4・14ターンで、最後にHPが3人あわせて147残った、という意味です
  • ぜんめつした回は「シード1:4回目で ぜんめつ(ターン 3, 4, 4, 12)」のように、何回目のバトルで負けたかが出ます

結果を見て、数字を決めよう

禁じられた本がまほうのページを2枚よびだしたあと、ターンのおわりに、禁じられた本の上に緑の「20」が出て「禁じられた本は どくで 20の ダメージ!」
どくのダメージは、ボスにも20まで

この節では、BalanceTesterの結果を見て、どく の強さと、地下3階の敵・ボスの数字を直します。

目安を決める

数字を直す前に、どうなっていたら ちょうどいいかを決めておきます。決めておかないと、いつまでも直し続けることになります。

  • 確かめる条件は、それぞれの階の目安のレベルのいちばん下(地下1階 Lv1・地下2階 Lv4・地下3階 Lv7)。休まずに、その階を最後まで通す
  • 地下1・2階:ほとんど勝てる(はじめての人が、つまずかないように)
  • 地下3階:ふつうのバトルは3ターンくらい。ボスは10ターン以上かかって、どうぐを使わなくても、半分以上は勝てる
  • ボスに どく をかけると少し楽になるけれど、どく だけでは勝てない

最初の結果

まずは、何も直さずに、それぞれの階を10回ずつ通しました。地下3階は、3人の組み合わせを2つためしています(キョンシーの代わりにメドゥーサ)。

階レベル・装備3人クリアふつうのバトルボス戦のこりHP
地下1Lv1・なしスライム・フェアリー・キョンシー10回2ターン-69〜95%
地下2Lv4・なしスライム・フェアリー・キョンシー10回2〜7ターン-72〜84%
地下3Lv7・ペーパーナイフとブックカバースライム・フェアリー・キョンシー10回2〜3ターン9〜14ターン67〜90%
地下3Lv7・ペーパーナイフとブックカバースライム・フェアリー・メドゥーサ10回1ターン3ターン45〜69%
  • のこりHPは、その階を通し終わったときの、3人のHPの合計の割合です
  • 地下1・2階は、目安どおり、ほとんど減らずに勝てています
  • 地下3階は、ふつうのバトルが2〜3ターンで終わり、HPもあまり減りません。最後の階にしては、やさしすぎます
  • メドゥーサがいると、いなずま で、ふつうのバトルが1ターンで終わります。そして、ボスが3ターンで倒れています。前回心配していた、どく です

どく に上限を付ける

PoisonのInspector。Turns 5、Can Actにチェック、Damage Rate 1、Poison Rate 0.25、その下にMax Poison Damage 20
どくに、ダメージの上限を足した

どくは、ターンのおわりに最大HPの1/4のダメージでした。続くのは5ターンです。

  • ボス(HP 420)がかかると、1ターンに105。5ターンで525なので、どくだけでボスが倒れてしまいます
  • 仲間や、ふつうの敵がかかったときは、1/4で ちょうどいい強さです(Lv1のスライムで1ターンに10)

そこで、割合はそのままにして、1回のダメージに上限を付けます。

StatusEffectDataのpoisonRateの下に、上限の欄を足します。

    [SerializeField] private int maxPoisonDamage = 0;   // そのダメージの上限(0 なら上限なし。HP の多いボスに効きすぎないように)

PoisonRateの下に、ダメージを計算するメソッドを足します。

    // 最大HP が maxHp の人が、ターンのおわりに受けるダメージ(最低でも1、上限があればそこまで)
    public int PoisonDamage(int maxHp)
    {
        int damage = Mathf.Max(1, Mathf.RoundToInt(maxHp * poisonRate));
        if (maxPoisonDamage > 0)
        {
            damage = Mathf.Min(damage, maxPoisonDamage);
        }
        return damage;
    }

BattleManagerのEndTurnAsyncで、どくのダメージを計算していた行を、このメソッドを使うように直します。

                    damages.Add(effect.Data.PoisonDamage(unit.Stats.maxHp));
  • Data/Effectsの Poison を選び、Max Poison Damage を 20 にする
  • 20にしたのは、仲間がかかったときに変わらないようにするためです。いちばんHPが多いスライムでも、Lv10でHP 83(1/4で21)なので、ほとんど今までどおりです
  • ボスには、1ターンに20、5ターンで100。HPの1/4くらいなので、まほうの矢(ボスに1回40〜50)を何回か使うのと同じくらいです。かけておくと、少し楽になります

地下3階の敵を強くする

どくを直しても、地下3階のふつうのバトルは、すぐに終わってしまいます。地下3階の2種類を、HPとこうげきを上げて、もう少し粘るようにします。

敵HPこうげき
ランプのおばけ40 → 7018 → 24
まほうのページ30 → 4514 → 20
  • Data/Enemiesの LampGhost と MagicPage を選び、StatsのMax HpとAttackを、上の表のとおりにする
  • ランプのおばけを70にしたのは、Lv7のメドゥーサの いなずま(50くらい)で、1回では倒れないようにするためです
  • まほうのページは、ボスが よびだす敵でもあります。最初は、ページを55にして、ボスのHPとこうげきも上げてみました。すると、ボス戦のたびにページが何回も よばれて、ボス戦が長いと25ターンをこえるようになってしまいました
  • ページを45に下げて、ボスは#23の数字のままにすると、ボス戦は10〜20ターンくらいに収まりました

決めた数字で、もう一度

直したあとで、地下3階をもう一度10回ずつ通しました。シードは同じ1〜10なので、数字を変えたことだけで、結果がどう変わったかを比べられます。

3人クリアふつうのバトルボス戦のこりHP
スライム・フェアリー・キョンシー10回3〜6ターン11〜14ターン60〜96%
スライム・フェアリー・メドゥーサ7回1〜3ターン12〜24ターン8〜88%
  • どちらも、決めた目安(半分以上は勝てる、ボス戦は10ターン以上)に入りました
  • メドゥーサの3人は、ふつうのバトルで毎回 いなずま(MP 9)を使うので、ボスの前にはMPがあまり残りません(Lv7のメドゥーサのMPは36)。負けた3回は、どれもボス戦です。人が遊ぶなら、まほうのクッキーでMPを戻したり、いなずま を使いすぎないようにしたりできるので、もっと勝てるはずです
  • 地下3階の3回のバトルで、仲間はLv7からLv9になります。ボスの200ポイントで、Lv10に届きます(#12で決めたとおり)

そのままにしたもの

結果を見て、直さなかったものもあります。

  • 地下1・2階の敵:やさしめですが、はじめの階なので、このままにします。どうぐを使わなくても進めるので、はじめての人でも つまずきません
  • ダメージの式(#7):敵の数字を変えるだけで、ちょうどいいところに入ったので、式はそのままです。式を変えると、全部の階が一度に変わってしまうので、まずは数字から直すのがおすすめです
  • 仲間がかかる どく(#16の本の虫):地下1階は10回とも、どのバトルも2ターンで勝てていて、のこりHPも7割より上でした
  • ぜんめつしたときの決まり(#17):お金が半分になって、ロビーへ戻ります。レベルは残るので、ボスに負けても、もう一度 挑めます。このままにします

動かして確認!

メドゥーサの いなずま でまほうのページを たおし、禁じられた本も50のダメージで たおれる。「てきを ぜんぶ たおした!」で3人が喜び、レベル10に上がる
自動プレイで、ボスに勝つ
  • Lobbyシーンを開いて、BalanceTesterの Run On Play にチェックを入れて再生する
  • すぐに地下3階のバトルが始まり、3人が自動で、速く戦う。ためている敵がいると、3人とも ぼうぎょ する
  • 1回分が終わると、次のシードで最初からやり直す。10回終わると、Consoleに「【ちか3かい Lv7】10回中 10回 クリア」と出て、ロビーで止まる
  • もう一度再生すると、同じシードなので、まったく同じ結果が出る
  • Floor Index・Level・Partyを変えると、ほかの階や組み合わせも確かめられる
  • 確かめ終わったら、Run On Play のチェックを外しておく

#24の動作確認

  • Battleシーンで Auto Play にチェックを入れると、仲間のコマンドが自動で決まる
  • BalanceTesterで、決まったレベル・装備の3人が、1つの階を何回も通す
  • 結果(クリアしたか・バトルごとのターン数・のこりHP)がConsoleに出る
  • 同じシードなら、何回やっても同じ結果になる
  • どくのダメージは、ボスにも1ターン20まで

あとがき

今回は、ゲームに自分で遊んでもらって、バランスを数字で確かめました。思っていたより、地下3階がやさしかったり、メドゥーサの いなずま と どく が強すぎたりと、遊んでいるだけでは気づきにくいことが、表にすると一目で分かりました。次回は、遊んだ続きから始められるように、セーブ・ロードと、タイトル画面を作りたいと思います!

スライム

自分たちが戦っているのを見るの、ふしぎな気分でした・・・!

フェアリー

ためているのを見たら、ちゃんと ぼうぎょ してたわね!

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

コメント

コメント一覧 (1件)

コメントする

目次