ターン制RPGの作り方#13|まほうとMP:まほうの一覧と、攻撃・回復のまほう

スライムちゃんが「まほう」から ちょこっとヒール、フェアリーが ヒール、キョンシーが「わざ」から れんげき を選ぶ。キョンシーが前に出て紙ひこうきに2回攻撃し、フェアリーとスライムちゃんが力をためるポーズで回復すると、緑のキラキラと「+5」「+4」が上がる

ここまでで、たたかう・ぼうぎょ・にげる が使えるようになりました。今回は、RPGの華「まほう」です。「まほう」を選ぶとまほうの一覧が開いて、MPを使って、回復したり攻撃したりできるようにします。キョンシーの「わざ」も、同じしくみで作ってみたいと思います!

目次

今回作るもの

  • まほう・わざ も、#4 コマンドをデータにして、メニューに並べようで作ったBattleCommandを受け継いだSkillCommandにする
  • 「まほう」「わざ」を選ぶと、その人のまほうの一覧が開く。MPが足りないまほうは灰色で選べない
  • まほうを使うとMPが減り、ステータスのウインドのMPも減る
  • Lv1で使える4つのまほう・わざ
使う人名前MP相手効果ポーズ
スライムちゃんちょこっとヒール3味方1人回復 威力12力をためる(magic)
フェアリーヒール3味方1人回復 威力20力をためる(magic)
キョンシーれんげき3敵1体こうげきの0.7倍で2回攻撃(attack)
メドゥーサまほうの矢3敵1体まほう 威力14杖をふる(orb)
スクリプト役割付ける場所
BattleCommand・AttackCommand相手を選ぶときに使う人を渡す、ボタンの文字と使えるか、共通の処理-
SkillCommandまほう・わざの共通の形(MP)(付けない)
AttackSpellCommand・HealSpellCommand・MultiHitSkillCommand攻撃のまほう・回復のまほう・れんげき(付けない)
MagicCommandまほうの一覧を開く-
BattleUnit・DamageCalculatorMP・回復と、まほうの計算-
CommandMenu・TargetSelector使えないボタンを灰色に、味方も選べるように-
CharacterData・UnitView・BattleEffectsまほうのポーズと、回復の数字-
BattleContext・BattleManagerまほうの一覧を渡す-

ちょっと長めの回なので、少しずつ進めましょう。

まほうもコマンドにしよう

Data/Skills/Healを選択したInspector。Command Nameにヒール、Mp Costに3、Effect PrefabにHealSparkles、Powerに20

この節では、まほう1つ1つを、たたかう と同じBattleCommandの仲間にします。

まほうも「選んで、実行する」もの

#6 たたかう相手を選ぼうと#7 たたかう:ダメージを計算してメッセージを出そうで、コマンドには「選ぶ(SelectAsync)」と「実行する(ExecuteAsync)」があることを見てきました。まほうも同じで、ヒールなら「だれに使うか選ぶ」→「回復する」です。

そこで、まほう1つ1つもBattleCommandを受け継いだScriptableObjectにします。こうしておくと、

  • まほうの一覧は、コマンドのボタンと同じCommandMenuで並べられる
  • 選んだまほうは、たたかう と同じように、すばやさの順に実行される

と、今までのしくみがそのまま使えます。

BattleCommandを直す

まほうでは、相手を選ぶときに「だれが使うか」が必要になります(ヒールは使う人の まりょく で回復量が変わり、一覧も人によって違うので)。BattleCommandを、次のように書き換えます。

using UnityEngine;

// コマンド1つ分の共通の形(たたかう・まほう・ぼうぎょ・にげる、まほうの1つ1つ も、これを受け継いで作る)
public abstract class BattleCommand : ScriptableObject
{
    [SerializeField] private string commandName; // ボタンに出す名前

    public string CommandName => commandName;

    // ボタンに出す文字(まほうは MP も出す)
    public virtual string MenuLabel => commandName;

    // true なら、すばやさに関係なく、ターンの最初に動く(ぼうぎょ など)
    public virtual bool ActsFirst => false;

    // true なら、パーティみんなの行動。選んだら、ほかの人のコマンドは取り消す(にげる など)
    public virtual bool IsForWholeParty => false;

    // user が今使えるか(MP が足りない まほう は false にして、ボタンを灰色にする)
    public virtual bool CanUse(BattleUnit user)
    {
        return true;
    }

    // user がコマンドを選んだあとに、相手などを選ぶ。キャンセルされたら null を返す
    // 何も選ばないコマンドは、このまま決まる
    public virtual async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        // ほかのコマンドと同じ「待って答えを返す」形にそろえるため、1フレームだけ待つ
        await Awaitable.NextFrameAsync();
        return new BattleAction(this, null);
    }

    // 選んだコマンドを実行する(まだ中身を作っていないコマンドは、このメッセージだけ)
    public virtual async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
    {
        await context.Message.ShowAsync($"{user.Name}は ようすを みている。");
    }

    // ねらっていた相手が先に倒れていたら、まだ立っている相手に変える
    protected BattleUnit FindTarget(BattleUnit user, BattleAction action, BattleContext context)
    {
        UnitView targetView = action.Target;
        if (targetView.Unit.IsDead)
        {
            targetView = context.FindLivingOpponent(user);
        }
        return targetView.Unit;
    }

    // HPが0になった相手を、敵なら消して、仲間なら戦闘不能にする
    protected async Awaitable ShowFallAsync(BattleUnit target, BattleContext context)
    {
        if (target.IsEnemy)
        {
            await target.View.DisappearAsync();
            await context.Message.ShowAsync($"{target.Name}を たおした!");
        }
        else
        {
            target.View.ShowDown();
            await context.Message.ShowAsync($"{target.Name}は たおれてしまった!");
        }
    }
}
  • SelectAsync(BattleUnit user, …):使う人を受け取るようにしました
  • MenuLabel・CanUse:ボタンに出す文字と、今使えるか。ふつうのコマンドは名前のまま・いつでも使える。まほうでは、このあとoverrideして書き換えます
  • FindTarget・ShowFallAsync:#10 敵の行動と、受けるときの演出でAttackCommandに書いた「ねらいを変える」「倒れたら消す」を、まほうでも使うので、親のクラスに移しました。protectedは「このクラスと、受け継いだクラスからだけ使える」という意味です

AttackCommandを、次のように書き換えます。

using UnityEngine;

// たたかう:敵を1体選んで、ダメージを与える
[CreateAssetMenu(fileName = "Attack", menuName = "TurnBasedRpg/Commands/Attack")]
public class AttackCommand : BattleCommand
{
    // 敵を1体選ぶ。キャンセルされたら null を返す
    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        context.MessageText.text = "だれを ねらう?";
        UnitView target = await context.TargetSelector.SelectTargetAsync(context.Enemies, context.Party);
        if (target == null)
        {
            return null;
        }
        return new BattleAction(this, target);
    }

    // 前に出て、振って、当てて、戻る。HPが0になったら、敵は消えて、仲間は戦闘不能に
    public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
    {
        BattleUnit target = FindTarget(user, action, context);

        await context.Message.ShowAsync($"{user.Name}の こうげき!");
        await user.View.StepAsync(true);       // 前に出る
        await user.View.PlayAttackPoseAsync(); // 振りかぶって、振る(ポーズの絵が無い敵は、何もしない)

        int damage = DamageCalculator.Physical(user, target);
        target.TakeDamage(damage);
        await context.Effects.PlayHitAsync(target.View, damage); // 当たった!

        // もとの位置に戻りながら、メッセージを出す
        Awaitable stepBack = user.View.StepAsync(false);
        await context.Message.ShowAsync($"{target.Name}に {damage}の ダメージ!");
        await stepBack;

        if (target.IsDead)
        {
            await ShowFallAsync(target, context);
        }
    }
}
  • 中身は今までと同じです。最初と最後を、親のクラスに移したFindTarget・ShowFallAsyncに置きかえて短くなりました

MPを持たせる

BattleUnitに、MPと、使えるまほうを足します。Hpの下に足して、

    public int Mp { get; private set; }
    public SkillCommand[] Skills { get; } // 使えるまほう・わざ(仲間だけ)

仲間のコンストラクタのHp = Stats.maxHp;の下に2行足します。

        Mp = Stats.maxMp;
        Skills = data.Skills;

GainExpで、最大MPが増えた分だけ、今のMPも増やします。int oldMaxHp = Stats.maxHp;とHp += …の行を、次のように書き換えます。

        int oldMaxHp = Stats.maxHp;
        int oldMaxMp = Stats.maxMp;
        Level = newLevel;
        Stats = Stats.AtLevel(characterData.BaseStats, characterData.Growth, Level);
        Hp += Stats.maxHp - oldMaxHp; // 最大HPが増えた分だけ、今のHPも増やす
        Mp += Stats.maxMp - oldMaxMp; // MP も同じ

TakeDamageの下に、MPを使うメソッドと、回復のメソッドを足します。

    // MP を使う(0より下にはしない)
    public void SpendMp(int amount)
    {
        Mp = Mathf.Max(0, Mp - amount);
    }

    // HP を回復する(最大HPより上にはしない)。実際に増えた量を返す
    public int Heal(int amount)
    {
        int before = Hp;
        Hp = Mathf.Min(Stats.maxHp, Hp + amount);
        return Hp - before;
    }

#12のBattleManagerのShowLevelUpAsyncで、MPを満タンのまま出していた行を、今のMPを出すように書き換えます。

        unit.Row.SetMp(unit.Mp, after.maxMp);

CharacterDataに、使えるまほうを足します(変数はcommandsの下、プロパティはCommandsの下)。

    [SerializeField] private SkillCommand[] skills; // 使えるまほう・わざ(覚えるレベルは #14)

    public SkillCommand[] Skills => skills;

まほうの計算

DamageCalculatorを、次のように書き換えます。

using UnityEngine;

// ダメージ・回復の計算(式はここだけに書く)
public static class DamageCalculator
{
    // たたかう:(こうげき − ぼうぎょ ÷ 2)× 倍率 × 0.9〜1.1。ぼうぎょ 中は半分。最低でも1
    public static int Physical(BattleUnit attacker, BattleUnit defender, float rate = 1f)
    {
        float baseDamage = (attacker.Stats.attack - defender.Stats.defense / 2f) * rate;
        float damage = baseDamage * GameRandom.Range(0.9f, 1.1f);
        if (defender.IsGuarding)
        {
            damage /= 2f;
        }
        return Mathf.Max(1, Mathf.RoundToInt(damage));
    }

    // 攻撃のまほう:(威力 + まりょく)× 0.9〜1.1 − ぼうぎょ ÷ 4。ぼうぎょ 中は半分。最低でも1
    public static int Magic(int power, BattleUnit caster, BattleUnit defender)
    {
        float damage = (power + caster.Stats.magic) * GameRandom.Range(0.9f, 1.1f) - defender.Stats.defense / 4f;
        if (defender.IsGuarding)
        {
            damage /= 2f;
        }
        return Mathf.Max(1, Mathf.RoundToInt(damage));
    }

    // 回復のまほう:(威力 + まりょく ÷ 2)× 0.9〜1.1
    public static int Heal(int power, BattleUnit caster)
    {
        float amount = (power + caster.Stats.magic / 2f) * GameRandom.Range(0.9f, 1.1f);
        return Mathf.Max(1, Mathf.RoundToInt(amount));
    }
}
  • rate = 1f:引数に= 1fと書くと、省略したときは1になります(省略できる引数)。たたかう は今までどおりPhysical(user, target)、れんげき はPhysical(user, target, 0.7f)と書けます
  • まほうは、ぼうぎょ が効きにくい:たたかう は ぼうぎょ の半分を引きますが、まほうは4分の1しか引きません。かたい敵には、まほうが効きます
  • 回復は まりょく の半分:まりょく が高いフェアリーのヒールは、威力20+まりょく12÷2で、だいたい26回復します

SkillCommand

  • Scripts/Commandsに「SkillCommand」スクリプトを作る
using UnityEngine;

// まほう・わざ 1つ分の共通の形。MP を使う
public abstract class SkillCommand : BattleCommand
{
    [SerializeField] private int mpCost;                  // 使う MP
    [SerializeField] private ParticleSystem effectPrefab; // 当たった・回復したところに出す(無ければ空のまま)

    public int MpCost => mpCost;

    // ボタンには「ヒール  3」のように、使う MP も出す
    public override string MenuLabel => $"{CommandName}  {mpCost}";

    // MP が足りなければ使えない
    public override bool CanUse(BattleUnit user)
    {
        return user.Mp >= mpCost;
    }

    // MP を使って、ステータスのウインドの MP を書き換える
    protected void SpendMp(BattleUnit user)
    {
        user.SpendMp(mpCost);
        if (user.Row != null)
        {
            user.Row.SetMp(user.Mp, user.Stats.maxMp);
        }
    }

    // 敵を1体選ぶ(たたかう と同じ)
    protected async Awaitable<BattleAction> SelectEnemyAsync(BattleContext context)
    {
        context.MessageText.text = "だれを ねらう?";
        UnitView target = await context.TargetSelector.SelectTargetAsync(context.Enemies, context.Party);
        if (target == null)
        {
            return null;
        }
        return new BattleAction(this, target);
    }

    // 味方を1人選ぶ(倒れている人は選べない)
    protected async Awaitable<BattleAction> SelectAllyAsync(BattleContext context)
    {
        context.MessageText.text = "だれに つかう?";
        UnitView target = await context.TargetSelector.SelectTargetAsync(context.LivingParty(), context.Enemies);
        if (target == null)
        {
            return null;
        }
        return new BattleAction(this, target);
    }

    // 当たった・回復したところに、パーティクルを出す
    protected void PlayEffect(UnitView target)
    {
        if (effectPrefab != null)
        {
            Instantiate(effectPrefab, target.transform.position, Quaternion.identity);
        }
    }
}
  • abstractなので、SkillCommandそのものは作れません。このあと作る3つのまほうが、これを受け継ぎます(6-14 クラスを受け継ごう:継承とabstract)
  • MenuLabelとCanUseをoverrideして、ボタンにMPを出し、MPが足りないときは使えないようにしています

まほうの一覧を開こう

スライムちゃんが「まほう」を選ぶと、コマンドのウインドの右上に「ちょこっとヒール 3」の一覧が開く。選ぶと敵が暗くなり、回復する味方を選ぶ

この節では、「まほう」を選んだら、その人のまほうの一覧を開くようにします。9-8 キーボード・ゲームパッドでUIを操作しようで作った「サブメニュー」と同じ形です。

一覧のウインド

一覧のボタンを並べる部品は、コマンドのボタンを並べているCommandMenuを、もう1つ使います。

  • Canvasの子にSkillWindow(UI (Canvas)>Image)
    • StatusWindowのすぐ下に置く
    • アンカー:bottom-left、Pos X:690、Pos Y:430、Width:440、Height:260
      • コマンドのウインドの右上、ステータスのウインドの上に出します。ウインドの絵は少しだけ透けているので、ステータスの文字と重ならない場所に置いています
    • Source Image:ui_window、Image Type:Sliced
    • Add Component>Grid Layout Group:Padding 24、Cell Size:X 392・Y 60、Spacing:Y 8、Constraint:Fixed Column Count、Constraint Count:1
      • CommandWindowと同じGrid Layout Groupで、1列に縦に並べます。CommandButtonのプレハブは大きさが0なので、並べる側(Grid)が大きさを決める必要があります
    • Add Component>CommandMenu、Button PrefabにPrefabs/CommandButton
  • SkillWindowのチェックを外して、隠しておく
Canvas>SkillWindow(StatusWindowのすぐ下)を選択したInspector。Pos X 690・Pos Y 430、Width 440・Height 260、Grid Layout GroupのCell Size 392×60

使えないボタンを灰色に

CommandMenuのWaitForCommandAsyncに、使う人を渡すようにして、使えないボタンを灰色にします。WaitForCommandAsyncとCreateButtonsを、次のように書き換えます。

    // user のコマンドでボタンを作り、どれかが押されるまで待つ(キャンセルなら null)
    public async Awaitable<BattleCommand> WaitForCommandAsync(BattleCommand[] commands, BattleUnit user)
    {
        // 前の人の決定(Enter など)が、まだこのフレームに残っていることがあるので、1フレーム待ってから始める
        await Awaitable.NextFrameAsync(destroyCancellationToken);
        CreateButtons(commands, user);
        commandSource.Reset();
        isWaiting = true;

        // 最初は、使えるボタンの一番上を選ぶ(全部使えなければ一番上)
        lastSelected = buttons[0].gameObject;
        foreach (Button button in buttons)
        {
            if (button.interactable)
            {
                lastSelected = button.gameObject;
                break;
            }
        }
        EventSystem.current.SetSelectedGameObject(lastSelected);

        BattleCommand command = await commandSource.Awaitable;

        isWaiting = false;
        return command;
    }

    // コマンドの数だけボタンを作る(前のボタンは消す)。user が使えないコマンドは灰色にする
    private void CreateButtons(BattleCommand[] commands, BattleUnit user)
    {
        foreach (Button oldButton in buttons)
        {
            // Destroy はフレームの最後に消えるので、先に隠して、並びから外しておく
            oldButton.gameObject.SetActive(false);
            Destroy(oldButton.gameObject);
        }
        buttons.Clear();

        foreach (BattleCommand command in commands)
        {
            Button button = Instantiate(buttonPrefab, transform);
            button.GetComponentInChildren<TMP_Text>().text = command.MenuLabel;
            button.interactable = command.CanUse(user);
            button.onClick.AddListener(() => Choose(command));
            buttons.Add(button);
        }
    }
  • button.interactable:9-8で見たとおり、Interactableがオフのボタンは灰色になって、キーボードでも飛ばされます
  • ボタンの文字をCommandNameからMenuLabelに変えたので、まほうは「ヒール 3」のようにMPも出ます

MagicCommand

MagicCommandを、次のように書き換えます。

using UnityEngine;

// まほう・わざ:その人のまほうの一覧を開いて、選んだまほうの相手を選ぶ
[CreateAssetMenu(fileName = "Magic", menuName = "TurnBasedRpg/Commands/Magic")]
public class MagicCommand : BattleCommand
{
    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        while (true)
        {
            context.MessageText.text = $"どの {CommandName}を つかう?";
            context.SkillMenu.gameObject.SetActive(true);
            BattleCommand skill = await context.SkillMenu.WaitForCommandAsync(user.Skills, user);
            context.SkillMenu.gameObject.SetActive(false);

            if (skill == null)
            {
                return null; // 一覧でキャンセル:コマンドを選び直す
            }

            BattleAction action = await skill.SelectAsync(user, context);
            if (action != null)
            {
                return action; // まほうと相手が決まった
            }
            // 相手を選ぶところでキャンセルしたら、もう一度まほうの一覧を開く
        }
    }
}
  • 一覧で選んだまほうのSelectAsyncを呼んで、相手を選ばせます。返ってきた行動(BattleAction)のコマンドは「まほう」ではなく「ヒール」などのまほう本体なので、実行するときは、まほう本体のExecuteAsyncが呼ばれます
  • while (true):相手を選ぶところでキャンセルしたら、ループの頭に戻って、もう一度一覧を開きます
  • キョンシーの「わざ」も、同じMagicCommand(名前だけ「わざ」)です。何を並べるかは、その人のSkillsで決まります

BattleContextとBattleManager

BattleContextに、一覧の部品と、倒れていない仲間を返すメソッドを足します。TargetSelectorの下に足して、

    public CommandMenu SkillMenu;         // まほうの一覧

FindLivingOpponentの下に足します。

    // まだ倒れていない仲間の見た目
    public UnitView[] LivingParty()
    {
        List<UnitView> living = new List<UnitView>();
        foreach (UnitView view in Party)
        {
            if (!view.Unit.IsDead)
            {
                living.Add(view);
            }
        }
        return living.ToArray();
    }
  • 1行目にusing System.Collections.Generic;を足す(Listを使うため)

BattleManagerに、一覧の部品を足して(commandMenuの下)、

    [SerializeField] private CommandMenu skillMenu; // まほうの一覧(SkillWindow)

StartBattleAsyncで、BattleContextに入れます(context.TargetSelector = targetSelector;の下)。

        context.SkillMenu = skillMenu;

SelectActionsAsyncで、コマンドを待つところと、相手を選ぶところに、使う人を渡します。

            BattleCommand command = await commandMenu.WaitForCommandAsync(unit.Commands, unit);
            BattleAction action = null;
            if (command != null)
            {
                action = await command.SelectAsync(unit, context);
            }
  • BattleのBattle ManagerのSkill Menuに、SkillWindowをドラッグ

味方も選べるようにする

ヒールは、味方を選びます。#6のTargetSelectorは、敵を選ぶ間は仲間を暗くしていました。味方を選ぶときは、反対に敵を暗くして、選べる味方を明るくしたいので、SelectTargetAsyncを次のように書き換えます。

    // candidates から1人選ぶまで待って、選んだ相手を返す(キャンセルなら null)
    // others は、選んでいる間だけ暗くしておく人たち(敵を選ぶときは仲間、仲間を選ぶときは敵)
    public async Awaitable<UnitView> SelectTargetAsync(UnitView[] candidates, UnitView[] others)
    {
        // コマンドを決めた Enter が、まだこのフレームに残っていることがあるので、1フレーム待ってから始める
        await Awaitable.NextFrameAsync(destroyCancellationToken);

        // 選べる人は明るく、ほかの人は暗くして、点滅を止める(選ぶ方に目が行くように)
        foreach (UnitView view in others)
        {
            view.SetHighlight(false);
            view.SetDimmed(true);
        }
        foreach (UnitView view in candidates)
        {
            view.SetHighlight(false);
            view.SetDimmed(false);
        }

        targets = candidates;
        index = 0;
        targetSource.Reset();
        isWaiting = true;
        // コマンドのボタンの選択を外して、矢印キーで▶が動かないようにする
        EventSystem.current.SetSelectedGameObject(null);
        targets[index].SetHighlight(true);

        UnitView target = await targetSource.Awaitable;

        targets[index].SetHighlight(false);
        foreach (UnitView view in others)
        {
            view.SetDimmed(false);
        }
        isWaiting = false;
        return target;
    }
  • 引数の名前をpartyからothersに変えて、「暗くする人たち」を外から渡すようにしました。たたかう は今までどおり(context.Enemies, context.Party)、ヒールは(context.LivingParty(), context.Enemies)
  • 選び終わったら、暗くした人を元に戻します(敵を暗くしたままにしないように)
  • 味方を選んでいる間は、使う人の点滅も止まって、選んでいる人だけが光ります

3つのまほう・わざ

フェアリーがヒールを となえると、正面を向いて力をためるポーズになり、自分のまわりに緑のキラキラと「+5」。続いてスライムちゃんが ちょこっとヒール で自分を回復して「+4」。ステータスのHPのゲージが増える

この節では、攻撃のまほう・回復のまほう・れんげき の3つを作ります。それぞれの「選ぶ」「実行する」を書くだけです。

まほうのポーズ

素材には、まほうのためのポーズが入っています。

  • <名前>_orb_61・62:右を向いて杖をふる(攻撃のまほう)
  • <名前>_magic_01・02:正面を向いて力をためる(回復のまほう)

CharacterDataに足します(変数はwinFramesの下、プロパティはWinFramesの下)。

    [SerializeField] private Sprite[] orbFrames;  // 杖をふる(orb_61・62)
    [SerializeField] private Sprite[] castFrames; // 力をためる(magic_01・02)

    public Sprite[] OrbFrames => orbFrames;
    public Sprite[] CastFrames => castFrames;
  • 4人のデータのOrb Framesに_orb_61・_orb_62、Cast Framesに_magic_01・_magic_02をドラッグ
  • 4人のデータのSkillsに、このあと作るまほうを入れる(表のとおり)
Data/Characters/FairyのInspector。Orb Framesにfairy_orb_61・62、Cast Framesにfairy_magic_01・02、SkillsのElement 0にHeal (Heal Spell Command)

UnitViewに、変数を足して(winFramesの下)、

    private Sprite[] orbFrames;  // 杖をふる(仲間だけ)
    private Sprite[] castFrames; // 力をためる(仲間だけ)

SetupCharacterの最後に2行足します。

        orbFrames = data.OrbFrames;
        castFrames = data.CastFrames;

一番下に、ポーズのメソッドを足します。

    // 攻撃のまほう:杖をふる
    public async Awaitable PlayOrbAsync()
    {
        await PlayFramesAsync(orbFrames);
    }

    // 回復のまほう:正面を向いて、力をためる
    public async Awaitable PlayCastAsync()
    {
        await PlayFramesAsync(castFrames);
    }

    // 待機の弾みを止めて、ポーズの絵を順に出す(最後の絵のまま止まる)
    private async Awaitable PlayFramesAsync(Sprite[] frames)
    {
        idleBounce.enabled = false;
        foreach (Sprite frame in frames)
        {
            image.sprite = frame;
            await Awaitable.WaitForSecondsAsync(0.2f, destroyCancellationToken);
        }
    }

    // 待機の弾みに戻す(戦闘不能の人は戻さない)
    public void ResumeIdle()
    {
        if (idleBounce != null && !isDown)
        {
            idleBounce.enabled = true;
        }
    }

パーティクル

#8 攻撃の手応えを作ろうのHitSparksを複製して、2つ作ります。

  • Prefabs/HitSparksを Ctrl+D で複製して、名前をHealSparkles(回復のキラキラ)
    • Start Lifetime:0.6〜1、Start Speed:1〜2.5、Start Size:0.25〜0.5
    • Start Color:#7CFC9A・#FFFFFF
    • Gravity Modifier:-0.5(マイナスにすると、上へ上がっていきます)
    • Duration:1、EmissionのBurstsのCount:14、ShapeのRadius:0.8
  • もう1つ複製して、名前をMagicBurst(攻撃のまほうの光)
    • Start Color:#B48CFF・#FFFFFF、Start Speed:4〜9、Gravity Modifier:0
    • EmissionのBurstsのCount:24、ShapeのRadius:0.3
Prefabs/HealSparklesを開いたInspectorのMainモジュール。Start Lifetime 0.6〜1、Start Speed 1〜2.5、Start Size 0.25〜0.5、Start Colorが緑と白、Gravity Modifierが-0.5

回復の数字

BattleEffectsに、回復したときの演出を足します。まず、回復の色を足して(maxShakeの下)、

    [SerializeField] private Color healColor = new Color(0.45f, 0.95f, 0.55f); // 回復の数字の色(緑)

ShowTextの下に、メソッドを足します。

    // 回復した:ステータスのゲージを増やして、緑の数字を出す
    public async Awaitable PlayHealAsync(UnitView target, int amount)
    {
        BattleUnit unit = target.Unit;
        if (unit.Row != null)
        {
            unit.Row.SetHp(unit.Hp, unit.Stats.maxHp);
        }
        ShowText(target, $"+{amount}", healColor);
        await Awaitable.WaitForSecondsAsync(0.5f, destroyCancellationToken);
    }
  • #12で作ったShowTextで、緑の「+26」を飛ばします。ダメージは白、回復は緑と色を分けると、ひと目で区別できます

回復のまほう(HealSpellCommand)

  • Scripts/Commandsに「HealSpellCommand」スクリプトを作る
using UnityEngine;

// 回復のまほう:味方1人の HP を回復する
[CreateAssetMenu(fileName = "NewHealSpell", menuName = "TurnBasedRpg/Skills/Heal Spell")]
public class HealSpellCommand : SkillCommand
{
    [SerializeField] private int power = 12; // 回復の威力

    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        return await SelectAllyAsync(context);
    }

    public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
    {
        BattleUnit target = action.Target.Unit;
        if (target.IsDead)
        {
            target = user; // 選んだ人が先に倒れていたら、自分を回復する
        }

        SpendMp(user);
        await context.Message.ShowAsync($"{user.Name}は {CommandName}を となえた!");
        await user.View.PlayCastAsync(); // 力をためる

        PlayEffect(target.View);
        int amount = DamageCalculator.Heal(power, user);
        int healed = target.Heal(amount); // 最大HPを超えた分は増えないので、実際に増えた量
        await context.Effects.PlayHealAsync(target.View, healed);
        user.View.ResumeIdle();

        await context.Message.ShowAsync($"{target.Name}の HPが {healed} かいふくした!");
    }
}
  • Healは、実際に増えた量を返します。HPが残り少ししか減っていなければ、まほうの威力が大きくても、増えるのはその分だけです

攻撃のまほう(AttackSpellCommand)

  • Scripts/Commandsに「AttackSpellCommand」スクリプトを作る
using UnityEngine;

// 攻撃のまほう:敵1体に、まりょく で ダメージを与える
[CreateAssetMenu(fileName = "NewAttackSpell", menuName = "TurnBasedRpg/Skills/Attack Spell")]
public class AttackSpellCommand : SkillCommand
{
    [SerializeField] private int power = 14; // まほうの威力

    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        return await SelectEnemyAsync(context);
    }

    public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
    {
        BattleUnit target = FindTarget(user, action, context);

        SpendMp(user);
        await context.Message.ShowAsync($"{user.Name}は {CommandName}を となえた!");
        await user.View.PlayOrbAsync(); // 杖をふる

        PlayEffect(target.View);
        int damage = DamageCalculator.Magic(power, user, target);
        target.TakeDamage(damage);
        await context.Effects.PlayHitAsync(target.View, damage);
        user.View.ResumeIdle();

        await context.Message.ShowAsync($"{target.Name}に {damage}の ダメージ!");
        if (target.IsDead)
        {
            await ShowFallAsync(target, context);
        }
    }
}
  • 前に出ずに、その場で杖をふります。当たったときの白い光・のけぞり・数字・揺れは、たたかう と同じPlayHitAsyncです

れんげき(MultiHitSkillCommand)

  • Scripts/Commandsに「MultiHitSkillCommand」スクリプトを作る
using UnityEngine;

// れんげき:前に出て、こうげき の何割かで何回も攻撃する
[CreateAssetMenu(fileName = "NewMultiHit", menuName = "TurnBasedRpg/Skills/Multi Hit")]
public class MultiHitSkillCommand : SkillCommand
{
    [SerializeField] private float damageRate = 0.7f; // 1回のダメージの倍率
    [SerializeField] private int hitCount = 2;        // 攻撃する回数

    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        return await SelectEnemyAsync(context);
    }

    public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
    {
        BattleUnit target = FindTarget(user, action, context);

        SpendMp(user);
        await context.Message.ShowAsync($"{user.Name}の {CommandName}!");
        await user.View.StepAsync(true);

        int total = 0;
        for (int i = 0; i < hitCount; i++)
        {
            await user.View.PlayAttackPoseAsync();
            int damage = DamageCalculator.Physical(user, target, damageRate);
            target.TakeDamage(damage);
            total += damage;
            await context.Effects.PlayHitAsync(target.View, damage);
            if (target.IsDead)
            {
                break; // 途中で倒れたら、残りの攻撃はしない
            }
        }

        Awaitable stepBack = user.View.StepAsync(false);
        await context.Message.ShowAsync($"{target.Name}に あわせて {total}の ダメージ!");
        await stepBack;
        if (target.IsDead)
        {
            await ShowFallAsync(target, context);
        }
    }
}
  • 0.7倍で2回なので、合わせると1.4倍。たたかう より強いぶん、MPを3使います
  • 1回ずつ数字が飛び出して、最後に合計をメッセージで出します

まほうのデータを作る

  • TurnBasedRpg/Dataに「Skills」フォルダを作り、Create>TurnBasedRpg>Skills から4つ作る
ファイル作り方Command NameMp CostEffect Prefabほか
SmallHealHeal Spellちょこっとヒール3HealSparklesPower 12
HealHeal Spellヒール3HealSparklesPower 20
DoubleStrikeMulti Hitれんげき3なしDamage Rate 0.7、Hit Count 2
MagicArrowAttack Spellまほうの矢3MagicBurstPower 14
  • 4人のデータのSkillsに、スライムちゃん=SmallHeal、フェアリー=Heal、キョンシー=DoubleStrike、メドゥーサ=MagicArrow を入れる

動かして確認!

まほうとわざを選んだターン。キョンシーの れんげき で紙ひこうきに2回攻撃し、フェアリーと スライムちゃんが回復のまほうを使う。ステータスのMPが減っていく

再生して、まほうを使ってみましょう。

  • 1ターン目:スライムちゃんとフェアリーは たたかう、キョンシーは わざ > れんげき(敵の攻撃でHPを減らしておく)
  • 2ターン目:スライムちゃんは まほう > ちょこっとヒール > 自分、フェアリーは まほう > ヒール > 自分、キョンシーはもう一度 れんげき
  • 3ターン目:キョンシーのMPが0になって、れんげき が灰色になる

メドゥーサの まほうの矢 を見るときは、BattleのBattle LineupのPartyとParty Viewsの3人目をメドゥーサにします(確かめたら戻しましょう。なかまの入れ替えは#20で作ります)。

メドゥーサが まほうの矢 を となえて杖をふると、こぼれたインクに紫の光が散って「26」のダメージ
キョンシーのMPが0になったあと、わざの一覧の「れんげき 3」が灰色になっていて、選べない

#13の動作確認

  • 「まほう」「わざ」を選ぶと一覧が開き、Escで閉じてコマンドに戻る
  • 相手を選ぶところでEscを押すと、一覧に戻る
  • 回復のまほうは、味方を選び、緑のキラキラと「+数字」でHPが増える
  • まほうを使うと、ステータスのMPが減る。MPが足りないまほうは灰色で選べない
  • れんげき は2回続けて攻撃し、合計のダメージが出る

あとがき

今回は、まほうもBattleCommandの仲間にして、一覧から選んで使えるようにしました。コマンドを1つずつのクラスにしておいたおかげで、まほうが増えても、選び方と実行のしかたを書くだけで済みますね。次回は、全体をねらうまほうと、レベルが上がって新しいまほうを覚えるところを作りたいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次