ターン制RPGの作り方#16|どうぐとインベントリ:ハーブティー・まほうのクッキー・めざましどけい

どくで緑っぽくなったスライムちゃんが どくけしキャンディ を食べて元の色に戻り、倒れていたキョンシーが めざましどけい で起き上がる。次のターンは、フェアリーが まほうのクッキー でMPを、スライムちゃんが ハーブティー でHPを回復する

バトルが長くなってくると、HPやMPが足りなくなったり、仲間が倒れたままになったりします。そんなときに頼りになるのが「どうぐ」!今回は、持ち物の一覧を開いて、ハーブティーやめざましどけいを使えるようにしたいと思います。使った仲間が飲んだり食べたりするポーズも付けますよ!

目次

今回作るもの

左上:どくけしキャンディを食べたスライムちゃんに「なおった!」。右上:めざましどけいで、倒れていたキョンシーが +16 で起き上がる。左下:まほうのクッキーを食べたフェアリーに水色の +10。右下:ハーブティーを飲んだスライムちゃんに緑の +30
どうぐ効果使われた人のポーズ
ハーブティーHPを30回復飲む
まほうのクッキーMPを10回復食べる
どくけしキャンディどくを治す食べる
めざましどけい倒れた仲間を、HPを半分にして起こす-
  • どうぐを、まほうと同じようにコマンドのデータにする(ItemData)
  • 持ち物は、同じどうぐを個数でまとめる(Inventory)
  • 「どうぐ」を選ぶと、持ち物の一覧が開く。名前は左、個数は右にそろえて出し、入りきらないときはスクロールする
  • 前回お話しした「めざめの歌」(フェアリー Lv8)も、めざましどけいと一緒に作る
  • どくけしキャンディを試せるように、本の虫の攻撃を「かみつく」(30%で どく)にする
スクリプト役割付ける場所
ItemDataどうぐ1つ分のデータ(回復する量・治す状態・ポーズ)(付けない)
BattleCommand仲間を1人選ぶ(倒れている仲間からも選べる)-
BattleUnit・UnitView・CharacterDataMPの回復、状態を治す、起き上がる、飲む・食べるポーズ-
ItemStack・Inventory持ち物(どうぐと個数)(付けない)
CommandMenu一覧をスクロールする、ボタンの右はしに個数やMPを出すItemWindow・SkillWindow
ItemCommand「どうぐ」のコマンド。持ち物の一覧を開く(付けない)
SkillCommand・MagicCommandまほうの一覧の右はしに、使うMPを出す(付けない)
ReviveSpellCommandめざめの歌(付けない)
AttackCommand当たったときに、ときどき状態をかける(付けない)

どうぐのデータ

Data/Items/HerbTeaのInspector。Command Nameにハーブティー、Hp Amount 30、Mp Amount 0、Cure EffectはNone、Revivesのチェックなし、PoseはDrink、Effect PrefabにHealSparkles

この節では、どうぐ1つ分のデータを作ります。

ItemData

スライムちゃんのポーズの絵が5枚。drink_01〜03はびんを持って飲むところ、eat_01・02はケーキを食べるところ

どうぐも、まほうと同じく「選んで、相手を決めて、すばやさの順に使う」ものです。そこで、どうぐのデータはBattleCommandを受け継いで作ります。こうしておくと、#13 まほうとMPで作ったまほうの一覧(CommandMenu)に、そのまま並べられます。

  • 「ItemData」スクリプトを作る
using UnityEngine;

// どうぐを使われた人のポーズ
public enum ItemPose
{
    None,  // ポーズなし
    Drink, // 飲む(drink_01〜03)
    Eat,   // 食べる(eat_01・02)
}

// どうぐ1つ分のデータ。コマンドと同じように、相手を選んで使う
[CreateAssetMenu(fileName = "NewItem", menuName = "TurnBasedRpg/Item")]
public class ItemData : BattleCommand
{
    [SerializeField] private int hpAmount;                // 回復する HP
    [SerializeField] private int mpAmount;                // 回復する MP
    [SerializeField] private StatusEffectData cureEffect; // 治す状態(どくけしキャンディは どく)
    [SerializeField] private bool revives;                // チェックすると、倒れた仲間を起こす(めざましどけい)
    [SerializeField] private ItemPose pose;               // 使われた人のポーズ
    [SerializeField] private ParticleSystem effectPrefab; // 使われた人のところに出す

    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        // めざましどけい は倒れている仲間から、ほかは立っている仲間から選ぶ
        return await SelectPartyMemberAsync(context, revives);
    }

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

        // 同じターンに、ほかの仲間が最後の1つを使っていたら、もう無い
        if (!context.Inventory.Remove(this))
        {
            await context.Message.ShowAsync($"{user.Name}は {CommandName}を さがしたが、もう なかった!");
            return;
        }
        await context.Message.ShowAsync($"{user.Name}は {CommandName}を つかった!");

        if (revives)
        {
            if (!target.IsDead)
            {
                await context.Message.ShowAsync("しかし なにも おこらなかった。");
                return;
            }
            target.Revive();
            PlayEffect(target.View);
            await context.Effects.PlayHealAsync(target.View, target.Hp);
            await context.Message.ShowAsync($"{target.Name}は めを さました!");
            return;
        }

        // 使われた人が、飲む・食べる
        if (pose == ItemPose.Drink)
        {
            await target.View.PlayDrinkAsync();
        }
        else if (pose == ItemPose.Eat)
        {
            await target.View.PlayEatAsync();
        }
        PlayEffect(target.View);

        string message = "しかし なにも おこらなかった。";
        if (hpAmount > 0)
        {
            int amount = target.Heal(hpAmount);
            await context.Effects.PlayHealAsync(target.View, amount);
            message = $"{target.Name}の HPが {amount} かいふくした!";
        }
        if (mpAmount > 0)
        {
            int amount = target.RestoreMp(mpAmount);
            await context.Effects.PlayMpHealAsync(target.View, amount);
            message = $"{target.Name}の MPが {amount} かいふくした!";
        }
        if (cureEffect != null && target.RemoveEffect(cureEffect))
        {
            context.Effects.ShowText(target.View, "なおった!", Color.white);
            await Awaitable.WaitForSecondsAsync(0.5f);
            message = $"{target.Name}の {cureEffect.EffectName}が なおった!";
        }
        target.View.ResumeIdle();
        await context.Message.ShowAsync(message);
    }

    // 使われた人のところに、パーティクルを出す
    private void PlayEffect(UnitView target)
    {
        if (effectPrefab != null)
        {
            Instantiate(effectPrefab, target.transform.position, Quaternion.identity);
        }
    }
}
  • ItemPose:ポーズの種類に名前を付けたenumです(6-15 選択肢に名前を付けよう:enum(列挙型)とswitch文)。Inspectorでは、None・Drink・Eatから選べるリストになります
  • どうぐの種類ごとにクラスを作らない:4つのどうぐは「回復する量」「治す状態」「起こすか」がちがうだけなので、#15の状態と同じく、1つのデータの形にまとめました
  • もう無いとき:スライムちゃんとフェアリーが、同じターンに最後の1つを選ぶことがあります。使うときにRemoveして、もう無ければメッセージだけ出します
  • ポーズは使われた人:フェアリーがスライムちゃんにハーブティーを使ったら、飲むのはスライムちゃんです
  • メッセージ:最後に起きたことを1つ出します。何も起きなかったとき(どくではない人に、どくけしキャンディを使ったとき)は「しかし なにも おこらなかった。」のままです

ItemDataが使うものを足す

メッセージに「だれに つかう?」。倒れているキョンシーだけが白く光り、ほかは暗くなっている

ItemDataの中で使っている、仲間を選ぶ・起き上がる・飲む・食べるなどを足していきます。

BattleCommandに、仲間を1人選ぶメソッドを足します(FindTargetの上)。1行目にはusing System.Collections.Generic;を足してください。

    // 仲間を1人選ぶ。isDown が true なら倒れている仲間から、false なら立っている仲間から
    // 選べる人がいなければ、メッセージを出して null を返す
    protected async Awaitable<BattleAction> SelectPartyMemberAsync(BattleContext context, bool isDown)
    {
        List<UnitView> candidates = new List<UnitView>();
        foreach (UnitView view in context.Party)
        {
            if (view.Unit.IsDead == isDown)
            {
                candidates.Add(view);
            }
        }
        if (candidates.Count == 0)
        {
            await context.Message.ShowAsync("つかえる なかまが いない!");
            return null;
        }

        context.MessageText.text = "だれに つかう?";
        UnitView target = await context.TargetSelector.SelectTargetAsync(candidates.ToArray(), context.Enemies);
        if (target == null)
        {
            return null;
        }
        return new BattleAction(this, target);
    }
  • view.Unit.IsDead == isDownで、倒れている人だけ・立っている人だけを集めています
  • だれも倒れていないのにめざましどけいを選んだときは、「つかえる なかまが いない!」で選び直しになります

BattleUnitに、MPの回復・状態を治す・起き上がるを足します(GainExpの上)。

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

    // 状態を治す。かかっていたら true
    public bool RemoveEffect(StatusEffectData data)
    {
        foreach (ActiveEffect effect in Effects)
        {
            if (effect.Data == data)
            {
                Effects.Remove(effect);
                ShowEffects();
                return true; // 消したらすぐ抜けるので、foreach の途中で Remove してもエラーにならない
            }
        }
        return false;
    }

    // 倒れた人を起こす:HP を最大の半分にして、見た目を待機に戻す
    public void Revive()
    {
        Hp = Mathf.Max(1, Stats.maxHp / 2);
        View.Revive();
    }

CharacterDataに、飲む・食べるポーズの欄を足します(bookFramesの下と、BookFramesの下)。

    [SerializeField] private Sprite[] drinkFrames; // 飲む(drink_01・02・03)
    [SerializeField] private Sprite[] eatFrames;   // 食べる(eat_01・02)
    public Sprite[] DrinkFrames => drinkFrames;
    public Sprite[] EatFrames => eatFrames;

UnitViewに、変数を足して(bookFramesの下)、SetupCharacterの最後で受け取ります。

    private Sprite[] drinkFrames; // 飲む(仲間だけ)
    private Sprite[] eatFrames;   // 食べる(仲間だけ)
        drinkFrames = data.DrinkFrames;
        eatFrames = data.EatFrames;

UnitViewに、起き上がる(ShowDownの下)と、飲む・食べる(PlayBookAsyncの下)を足します。

    // 起き上がる:戦闘不能の絵をやめて、待機の弾みに戻す
    public void Revive()
    {
        isDown = false;
        idleBounce.enabled = true;
        SetDimmed(isDimmed);
    }
    // どうぐ:正面を向いて、飲む
    public async Awaitable PlayDrinkAsync()
    {
        await PlayFramesAsync(drinkFrames);
    }

    // どうぐ:正面を向いて、食べる
    public async Awaitable PlayEatAsync()
    {
        await PlayFramesAsync(eatFrames);
    }
  • 待機の弾み(IdleBounce)を動かすと、次のコマから待機の絵に戻ります。SetDimmedで、暗い色も元に戻しています

BattleEffectsに、MPの回復の数字を足します。欄はhealColorの下、メソッドはPlayHealAsyncの下です。

    [SerializeField] private Color mpColor = new Color(0.45f, 0.75f, 1f);      // MP の回復の数字の色(水色)
    // MP を回復した:ステータスのゲージを増やして、水色の数字を出す
    public async Awaitable PlayMpHealAsync(UnitView target, int amount)
    {
        BattleUnit unit = target.Unit;
        if (unit.Row != null)
        {
            unit.Row.SetMp(unit.Mp, unit.Stats.maxMp);
        }
        ShowText(target, $"+{amount}", mpColor);
        await Awaitable.WaitForSecondsAsync(0.5f, destroyCancellationToken);
    }
  • HPの回復は緑、MPの回復は水色にして、どちらが増えたか分かるようにしています(MPのゲージと同じ青系)

データを作ろう

Data/Characters/SlimeのInspector。Drink Framesにslime_drink_01・02・03、Eat Framesにslime_eat_01・02
  • 4人のCharacterData(Data/Characters)に、ポーズの絵を入れる
    • Drink Frames:〇〇_drink_01・02・03(〇〇は slime・fairy・jiangshi・medusa)
    • Eat Frames:〇〇_eat_01・02
  • TurnBasedRpg/Dataに「Items」フォルダを作り、Create>TurnBasedRpg>Item で4つ作る
ファイルCommand NameHp AmountMp AmountCure EffectRevivesPoseEffect Prefab
HerbTeaハーブティー300None-DrinkHealSparkles
MagicCookieまほうのクッキー010None-EatHealSparkles
AntidoteCandyどくけしキャンディ00Poison-EatHealSparkles
AlarmClockめざましどけい00None✓NoneHealSparkles
Data/Items/AlarmClockのInspector。Command Nameにめざましどけい、Revivesにチェック、PoseはNone

持ち物を作ろう

どうぐの一覧が2つ。左の1ターン目は、ハーブティー ×3・まほうのクッキー ×2・どくけしキャンディ ×1・めざましどけい ×1・ためしの どうぐ1 ×1…。右の2ターン目は、使い切ったどくけしキャンディとめざましどけいが消えている

この節では、持っているどうぐと、その個数を覚えておく「持ち物」を作ります。

ItemStackとInventory

ハーブティーを3つ持っていたら、一覧に3行並べるのではなく、「ハーブティー × 3」の1行にまとめたいですね。そこで、どうぐと個数をセットにして持ちます。

  • 「ItemStack」スクリプトを作る
// どうぐ1種類と、その個数(Inspector で最初の持ち物を書くのにも使う)
[System.Serializable]
public class ItemStack
{
    public ItemData item;
    public int count;
}
  • [System.Serializable]を付けると、Inspectorで中身を書けるようになります。#14のSkillLearningと同じです
  • 「Inventory」スクリプトを作る
using System.Collections.Generic;

// 持ち物:どうぐと個数のリスト。同じどうぐは1つにまとめて、個数で数える
public class Inventory
{
    private readonly List<ItemStack> stacks = new List<ItemStack>();

    // 持っているどうぐの種類の数
    public int Count => stacks.Count;

    // どうぐを count 個ふやす(もう持っていたら、個数を足す)
    public void Add(ItemData item, int count)
    {
        foreach (ItemStack stack in stacks)
        {
            if (stack.item == item)
            {
                stack.count += count;
                return;
            }
        }
        ItemStack newStack = new ItemStack();
        newStack.item = item;
        newStack.count = count;
        stacks.Add(newStack);
    }

    // どうぐを1つへらす(0になったら、リストから消す)。持っていなければ false
    public bool Remove(ItemData item)
    {
        for (int i = 0; i < stacks.Count; i++)
        {
            ItemStack stack = stacks[i];
            if (stack.item == item)
            {
                stack.count--;
                if (stack.count <= 0)
                {
                    stacks.RemoveAt(i);
                }
                return true;
            }
        }
        return false;
    }

    // 一覧に並べるどうぐ
    public ItemData[] Items()
    {
        ItemData[] items = new ItemData[stacks.Count];
        for (int i = 0; i < stacks.Count; i++)
        {
            items[i] = stacks[i].item;
        }
        return items;
    }
}
  • Add:もう持っているどうぐなら個数を足し、持っていなければ新しい行を足します。Inspectorに書いた最初の持ち物をそのまま使わず、new ItemStack()で作り直しているので、遊んで個数が減ってもInspectorの値は変わりません
  • Remove:個数が0になったら、その行をリストから消します。一覧から消えるので、0個のどうぐは選べません
  • Listの使い方は6-13 数が変わるデータを扱おう:Listを見てください
  • ボタンに出す個数は、あとの「個数とMPを右はしにそろえる」で足します
  • InventoryはMonoBehaviourではない、ふつうのクラスです。次の#17で、シーンをまたいで持ち越せるようにします

最初の持ち物

BattleのBattle LineupのInspector。Start Itemsが4つ:HerbTea 3、MagicCookie 2、AntidoteCandy 1、AlarmClock 1。下のBattle ManagerのItem MenuにItemWindow

BattleLineupに、最初の持ち物の欄と、持ち物を足します。欄はstartExpの下、プロパティはPartyViewsの下です。

    [SerializeField] private ItemStack[] startItems; // 最初の持ち物(#17 からは、ロビーから持ち越す)
    public Inventory Inventory { get; } = new Inventory(); // 持ち物

Setupの、敵を並べるforの上に、最初の持ち物を入れる行を足します。

        foreach (ItemStack stack in startItems)
        {
            Inventory.Add(stack.item, stack.count);
        }

BattleContextに、どうぐの一覧と持ち物を足します(SkillMenuの下)。

    public CommandMenu ItemMenu;          // どうぐの一覧
    public Inventory Inventory;           // 持ち物

BattleManagerにも欄を足して(skillMenuの下)、StartBattleAsyncでContextに渡します(context.SkillMenu = skillMenu;の下)。

    [SerializeField] private CommandMenu itemMenu;  // どうぐの一覧(ItemWindow)
        context.ItemMenu = itemMenu;
        context.Inventory = lineup.Inventory;
  • BattleのBattle LineupのStart Itemsを4つにして、上の図のように入れる(確認用。持ち物の画面は#17で作ります)

どうぐの一覧を開こう

フェアリーが「どうぐ」を選ぶと、6行の一覧が開く。名前は左、「× 3」「× 1」の個数は右にそろっている。↓キーで7つ目の ためしの どうぐ3 に進むと、一覧が1行ずつスクロールし、↑キーで一番上まで戻る

この節では、「どうぐ」を選んだら持ち物の一覧を開くようにします。

一覧のウインドは、#13のまほうの一覧(SkillWindow)と同じ大きさだと、3行しか入りません。どうぐが増えてくると、見渡しづらいですね。それに、まほうも、メドゥーサはLv7で4つ目を覚えて、ウインドからはみ出してしまいます・・・。そこで、ウインドを縦に大きくして6行見えるようにし、それでも入りきらないときはScroll Viewでスクロールできる一覧にします(9-6 Scroll ViewとLayout Groupでリストを作ろう)。

  • GIFの「ためしの どうぐ1〜4」は、スクロールを確かめるために足したものです(作り方は「動かして確認!」)

一覧のウインド

HierarchyでCanvasの子にItemWindowを選んでいる。子はViewport>Content、Scrollbar Vertical>Sliding Area>Handle。Gameビューの真ん中に、縦長のウインドとスクロールバー
  • Canvasの子にItemWindow(UI (Canvas)>Scroll View)
    • StatusWindowのすぐ下に置く
    • アンカー:bottom-left、Pos X:690、Pos Y:524、Width:560、Height:448(ステータスのウインドのすぐ上から、6行入る高さ)
    • Image:Source Image ui_window、Image Type:Sliced、Colorは白(Aも255)
    • Scroll Rect:Horizontalのチェックを外す、Movement Type:Clamped、Scroll Sensitivity:20、VerticalのVisibility:Auto Hide
    • Add Component>CommandMenu、Button PrefabにPrefabs/CommandButton、Scroll RectにItemWindow自身
  • 子のScrollbar Horizontalは消す(横にはスクロールしないので)
  • 子のViewport:Left 24、Top 24、Right 44、Bottom 24(右はスクロールバーの分だけ広く空ける)
  • Viewportの子のContent
    • Add Component>Grid Layout Group:Cell Size X 492・Y 60、Spacing Y 8、Constraint:Fixed Column Count、Constraint Count:1
    • Add Component>Content Size Fitter:Vertical Fit:Preferred Size(ボタンの数に合わせて、Contentの高さが変わる)
  • 子のScrollbar Vertical
    • Pos X:-16、Top:24、Width:12、Bottom:24
    • Image(背景)のColor:黒、A 90ぐらい。子のHandleのColor:うすい黄色(#F2E3B3)
    • Scrollbar:NavigationをNoneにする
      • Automaticのままだと、キーボードの→でスクロールバーが選ばれてしまいます
  • ItemWindowのチェックを外して、隠しておく
Canvas>ItemWindowのInspector。Scroll RectのContentにContent、Horizontalのチェックなし、Movement Type Clamped、Scroll Sensitivity 20、Vertical ScrollbarのVisibility Auto Hide。Command MenuのScroll RectにItemWindow(Button Prefabは、このあと作るListButtonに変えたところ)
ItemWindow>Viewport>ContentのInspector。Grid Layout GroupのCell Size 492×60、Spacing Y 8、Fixed Column Count 1。Content Size FitterのVertical FitがPreferred Size

まほうの一覧も、同じ形に作り直します。

  • 前のSkillWindowを消す
  • ItemWindowを複製(Ctrl+D)して、名前をSkillWindowにする
    • 複製すると、Command MenuのScroll Rectは、ちゃんと複製したSkillWindow自身になります
  • BattleのBattle ManagerのSkill MenuにSkillWindow、Item MenuにItemWindowをドラッグ

CommandMenuでスクロールする

どうぐの一覧が2つ。左は6つ目の ためしの どうぐ2 を選んでいて、一番上はハーブティー。右は↓で7つ目の ためしの どうぐ3 に進んだところで、一覧が1行上に動き、一番上がまほうのクッキーになっている

CommandMenuに、Scroll Rectの欄を足します(buttonPrefabの下)。

    [SerializeField] private ScrollRect scrollRect; // 一覧をスクロールするとき(まほう・どうぐ)。コマンドのウインドは空のまま

キーボードやゲームパッドで選ぶボタンが変わったら、そのボタンが見えるところまでスクロールします。Updateの最後のelseを、次のように書き換えます。

        else
        {
            if (scrollRect != null && current != lastSelected)
            {
                ScrollTo((RectTransform)current.transform); // 選ぶボタンが変わったら、見えるところまでスクロール
            }
            lastSelected = current;
        }

CreateButtonsの、buttons.Clear();の下に、ボタンの親を決める行を足します。

        // スクロールする一覧は Content の子に、そうでなければ自分の子にボタンを作る
        Transform parent = transform;
        if (scrollRect != null)
        {
            parent = scrollRect.content;
            scrollRect.content.anchoredPosition = Vector2.zero; // 一覧の一番上から見せる
        }
  • ボタンを作る行のInstantiate(buttonPrefab, transform)は、Instantiate(buttonPrefab, parent)に変えます(この下の「個数とMPを右はしにそろえる」で、CreateButtonsの全体をのせます)

CreateButtonsの下に、ScrollToを足します。

    // item のボタンが、見える枠(Viewport)からはみ出していたら、入るところまで一覧を動かす
    private void ScrollTo(RectTransform item)
    {
        RectTransform viewport = scrollRect.viewport;
        Vector3[] corners = new Vector3[4]; // 0:左下、1:左上、2:右上、3:右下
        item.GetWorldCorners(corners);
        float bottom = viewport.InverseTransformPoint(corners[0]).y; // ボタンの下(Viewport から見た高さ)
        float top = viewport.InverseTransformPoint(corners[1]).y;    // ボタンの上
        Rect view = viewport.rect;
        if (top > view.yMax)
        {
            scrollRect.content.anchoredPosition -= new Vector2(0f, top - view.yMax); // 上にはみ出した:一覧を下げる
        }
        else if (bottom < view.yMin)
        {
            scrollRect.content.anchoredPosition += new Vector2(0f, view.yMin - bottom); // 下にはみ出した:一覧を上げる
        }
    }
  • ボタンの親:Scroll Viewでは、ボタンをContentの子に並べます。Scroll Rectが空のとき(コマンドのウインド)は、今までどおり自分の子です
  • ScrollTo:GetWorldCornersでボタンの四すみの位置を取り、InverseTransformPointでViewportから見た高さに直しています。はみ出した分だけContentを動かすので、↓キーで下へ進むと、1行ずつスクロールします
  • マウスのホイールでのスクロールは、Scroll Rectがやってくれます
スライム

Scroll Viewは、マウスで動かすぶんには何もしなくていいんですが、キーボードで選んだときは自分でスクロールさせる必要があります・・・

フェアリー

ゲームパッドで遊ぶ人もいるから、ここは大事ね!

個数とMPを右はしにそろえる

どうぐの一覧を大きく写したところ。ハーブティー・まほうのクッキー・どくけしキャンディなどの名前は左につめてあり、右はしの「× 3」「× 2」「× 1」は、×と数字が縦に一列にそろっている

「ハーブティー ×3」のように、名前と個数を1つの文字にしてしまうと、名前の長さで個数の位置がずれてしまいます。それに、×9から×10に増えたときにも、見え方が変わってしまいますね。そこで、名前は左につめて、個数は右はしに、2けたの幅でそろえて出します。

まほうの一覧も、#13から「ヒール 3」のように名前とMPを1つの文字にしていたので、同じように直します。

ボタンの文字を2つに分けるので、一覧用のボタンのプレハブを作ります。

  • Prefabs/CommandButtonを複製(Ctrl+D)して、名前をListButtonにする
  • ListButtonを開いて、子のText (TMP)
    • Alignment:左(Left)
    • Extra SettingsのMarginsのLeft:44(▶のカーソルの分だけ空ける)
  • Text (TMP)を複製(Ctrl+D)して、名前をValueTextにする(ListButtonの一番下の子にする)
    • Textは空にする
    • Alignment:右(Right)
    • MarginsのLeftは0に戻して、Right:16
  • ItemWindowとSkillWindowのCommand MenuのButton Prefabを、ListButtonに変える(コマンドのウインドはCommandButtonのまま)

Inventoryに、ボタンに出す個数を作るメソッドを足します(Itemsの下)。

    // ボタンの右はしに出す個数(「× 3」「×12」のように、2けたの幅にそろえる)
    public string[] Counts()
    {
        string[] counts = new string[stacks.Count];
        for (int i = 0; i < stacks.Count; i++)
        {
            counts[i] = $"×<mspace=0.6em>{stacks[i].count,2}</mspace>";
        }
        return counts;
    }
  • {stacks[i].count,2}:,2を付けると、2文字の幅になるように、左に空白を足します。3なら「 3」、12なら「12」です
  • <mspace=0.6em>:TextMeshProのタグで、間の文字をすべて同じ幅(0.6文字分)で並べます。空白も数字と同じ幅になるので、「× 3」と「×12」の×と1の位が、縦にそろいます

CommandMenuのWaitForCommandAsyncに、右はしに出す文字を渡せるようにします。最初の4行を、次のように書き換えます。

    // user のコマンドでボタンを作り、どれかが押されるまで待つ(キャンセルなら null)
    // values を渡すと、ボタンの右はしにそれを出す(どうぐの個数、まほうの MP)
    public async Awaitable<BattleCommand> WaitForCommandAsync(BattleCommand[] commands, BattleUnit user, string[] values = null)
    {
        // 前の人の決定(Enter など)が、まだこのフレームに残っていることがあるので、1フレーム待ってから始める
        await Awaitable.NextFrameAsync(destroyCancellationToken);
        CreateButtons(commands, user, values);
  • string[] values = nullのように= nullを付けると、渡さなくてもよい引数になります。コマンドのウインドは今までどおり2つだけ渡せばOKです

CreateButtonsは、次のようになります。

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

        // スクロールする一覧は Content の子に、そうでなければ自分の子にボタンを作る
        Transform parent = transform;
        if (scrollRect != null)
        {
            parent = scrollRect.content;
            scrollRect.content.anchoredPosition = Vector2.zero; // 一覧の一番上から見せる
        }

        for (int i = 0; i < commands.Length; i++)
        {
            BattleCommand command = commands[i];
            Button button = Instantiate(buttonPrefab, parent);
            button.GetComponentInChildren<TMP_Text>().text = command.MenuLabel;
            if (values != null)
            {
                // 一覧のボタン(ListButton)は、右はしの ValueText に出す
                button.transform.Find("ValueText").GetComponent<TMP_Text>().text = values[i];
            }
            button.interactable = command.CanUse(user);
            button.onClick.AddListener(() => Choose(command));
            buttons.Add(button);
        }
    }
  • 何番目の文字かを使うので、foreachをforに変えています
  • GetComponentInChildren<TMP_Text>()は、上から探して最初に見つかったText (TMP)を返します。ValueTextを一番下の子にしたのは、こちらが名前のほうにならないようにするためです
  • transform.Find("ValueText")で、子の中から名前でValueTextを探しています
まほうの一覧を大きく写したところ。ヒール・まもりの風・みんなでヒール・めざめの歌の名前は左につめてあり、右はしの「MP 3」「MP 5」「MP 8」「MP10」は、1の位が縦にそろっている

まほうのMPも、右はしに出します。SkillCommandの、ボタンの文字を決めていた2行(MenuLabel)を、次のように書き換えます。

    // 一覧の右はしに出す MP(「MP 3」「MP12」のように、2けたの幅にそろえる)
    public string MpText => $"MP<mspace=0.6em>{mpCost,2}</mspace>";
  • ボタンの左の文字は、BattleCommandのMenuLabel(まほうの名前だけ)に戻ります

MagicCommandで、MPの文字を作って一覧に渡します。WaitForCommandAsyncの行を、次のように書き換えます。

            string[] mpTexts = new string[user.Skills.Count];
            for (int i = 0; i < user.Skills.Count; i++)
            {
                mpTexts[i] = user.Skills[i].MpText;
            }
            BattleCommand skill = await context.SkillMenu.WaitForCommandAsync(user.Skills.ToArray(), user, mpTexts);
  • どうぐのCountsと同じく、ボタンと同じ順番で、右はしに出す文字を並べています

どうぐのコマンド

メッセージに「どの どうぐを つかう?」。スライムちゃんの前に、どうぐの一覧が開いている

#4 コマンドをデータにして、メニューに並べようで、中身を空のままにしていた「どうぐ」のコマンドを作ります。ItemCommandを、次のように書き換えます。

using UnityEngine;

// どうぐ:持ち物の一覧を開いて、選んだどうぐの相手を選ぶ
[CreateAssetMenu(fileName = "Item", menuName = "TurnBasedRpg/Commands/Item")]
public class ItemCommand : BattleCommand
{
    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        while (true)
        {
            if (context.Inventory.Count == 0)
            {
                await context.Message.ShowAsync("どうぐを なにも もっていない!");
                return null; // コマンドを選び直す
            }

            context.MessageText.text = $"どの {CommandName}を つかう?";
            context.ItemMenu.gameObject.SetActive(true);
            BattleCommand item = await context.ItemMenu.WaitForCommandAsync(context.Inventory.Items(), user, context.Inventory.Counts());
            context.ItemMenu.gameObject.SetActive(false);

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

            BattleAction action = await item.SelectAsync(user, context);
            if (action != null)
            {
                return action; // どうぐと相手が決まった
            }
            // 相手を選ぶところでキャンセルしたら、もう一度どうぐの一覧を開く
        }
    }
}
  • #13のMagicCommandとほとんど同じ形です。ちがうのは、一覧に持ち物を並べることと、個数を渡すことだけ
  • ItemData[]は、そのままBattleCommand[]として渡せます(ItemDataはBattleCommandを受け継いでいるので)
  • 何も持っていないときは、一覧を開かずにメッセージを出して、コマンドを選び直します

倒れた仲間を起こそう

Lv8のフェアリーが まほう の一覧(右はしにMP 3・MP 5・MP 8・MP10)で めざめの歌 を選び、倒れているキョンシーを選ぶ。フェアリーが力をためると、キョンシーが +30 の緑の数字と一緒に起き上がる

この節では、めざましどけいと同じく倒れた仲間を起こす、フェアリーのまほう「めざめの歌」を作ります。

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

// 倒れた仲間を起こすまほう(めざめの歌)
[CreateAssetMenu(fileName = "NewReviveSpell", menuName = "TurnBasedRpg/Skills/Revive Spell")]
public class ReviveSpellCommand : SkillCommand
{
    public override async Awaitable<BattleAction> SelectAsync(BattleUnit user, BattleContext context)
    {
        return await SelectPartyMemberAsync(context, true); // 倒れている仲間から選ぶ
    }

    public override async Awaitable ExecuteAsync(BattleUnit user, BattleAction action, BattleContext context)
    {
        BattleUnit target = action.Target.Unit;
        SpendMp(user);
        await context.Message.ShowAsync($"{user.Name}は {CommandName}を うたった!");
        await user.View.PlayCastAsync();

        if (!target.IsDead)
        {
            // 同じターンに、ほかの仲間が先に起こしていた
            user.View.ResumeIdle();
            await context.Message.ShowAsync("しかし なにも おこらなかった。");
            return;
        }
        target.Revive();
        PlayEffect(target.View);
        await context.Effects.PlayHealAsync(target.View, target.Hp);
        user.View.ResumeIdle();
        await context.Message.ShowAsync($"{target.Name}は めを さました!");
    }
}
  • 相手を選ぶのは、ItemDataと同じSelectPartyMemberAsyncです。BattleCommandに書いておいたので、まほうからも使えます
  • 起こすところ(Revive→パーティクル→緑の数字)も、めざましどけいと同じです

データを作ります。

  • Data/Skillsに、Create>TurnBasedRpg>Skills>Revive Spell で「WakeSong」
    • Command Name:めざめの歌、Mp Cost:10、Effect Prefab:HealSparkles
  • フェアリーのSkillsに、Lv8 WakeSongを足す
Data/Skills/WakeSongのInspector。Command Nameにめざめの歌、Mp Cost 10、Effect PrefabにHealSparkles、Targets Allのチェックなし

どくの かみつく

本の虫が「こうげき!」でスライムちゃんにかみつくと、「どく」の文字が飛び出し、スライムちゃんの体が緑っぽい色になって名前の横にしずくのアイコンが付く。ターンのおわりに「スライムは どくで 10の ダメージ!」

この節では、どくけしキャンディを試せるように、本の虫の攻撃を「かみつく」にします。当たったときに、30%で どくにする攻撃です。

AttackCommandに、欄を2つ足します(クラスの最初)。

    [SerializeField] private StatusEffectData addEffect;      // 当たったときに、ときどきかける状態(本の虫の かみつく は どく)
    [SerializeField, Range(0, 100)] private int effectChance; // かける確率(%)

ExecuteAsyncの最後のif (target.IsDead)に、else ifを足します。

        if (target.IsDead)
        {
            await ShowFallAsync(target, context);
        }
        else if (addEffect != null && GameRandom.Range(0f, 100f) < effectChance)
        {
            target.AddEffect(addEffect);
            context.Effects.ShowText(target.View, addEffect.EffectName, addEffect.Color);
            await context.Message.ShowAsync($"{target.Name}は {addEffect.EffectName}に なった!");
        }
  • かける確率は、#15のStatusSpellCommandと同じ形です
  • Add Effectが空のとき(今までの「たたかう」)は、何も変わりません

データを作ります。

  • Data/CommandsのAttackを複製(Ctrl+D)して、名前を「Bite」にする
    • Command Name:かみつく、Add Effect:Poison、Effect Chance:30
  • Data/EnemiesのBookwormのCommandsを、AttackからBiteに変える
Data/Commands/BiteのInspector。Command Nameにかみつく、Add EffectにPoison、Effect Chanceのスライダーが30
  • 仲間のどくも、#15と同じく、ターンのおわりに最大HPの4分の1のダメージです。かなり痛いので、どくけしキャンディの出番ですね

動かして確認!

どくけしキャンディ・めざましどけい・まほうのクッキー・ハーブティーを順に使う
  • Start Itemsを、「最初の持ち物」の図のように4種類にしておく
  • スクロールを試すときは、Data/ItemsのHerbTeaを4つ複製(Ctrl+D)して、TestItem1〜4(Command Name:ためしの どうぐ1〜4)を作り、Start Itemsの下に1つずつ足す(全部で8種類)
  • たたかって、HPやMPが減ったら、ハーブティー・まほうのクッキーを使う
  • 本の虫に かみつかれて どくになったら、どくけしキャンディを使う
  • だれかが倒れたら、めざましどけいを使う
  • めざめの歌は、Battle LineupのStart Expを190(Lv8)にすると、フェアリーが覚えています

#16の動作確認

  • 「どうぐ」で持ち物の一覧が開き、名前は左、「× 3」のような個数は右はしにそろって出る
  • 「まほう」の一覧も、名前は左、「MP 3」のようなMPは右はしにそろって出る
  • ためしのどうぐを足して8種類にすると、↓キーで7つ目を選んだところで、一覧がスクロールする
  • ハーブティーで飲む、クッキー・キャンディで食べるポーズになり、HPは緑・MPは水色の数字が出る
  • 使うと個数が減り、0になると一覧から消える
  • めざましどけい・めざめの歌で、倒れた仲間がHP半分で起き上がり、次のターンからコマンドを選べる
  • 確かめたら、Start Expを0に、Start Itemsをハーブティー×3だけに戻す(TestItem1〜4は消してOK)

あとがき

今回は、どうぐをコマンドのデータにして、持ち物の一覧から使えるようにしました。まほうと同じBattleCommandを受け継いだので、相手を選ぶところや、すばやさの順に使うところは、何も足さずに動きましたね。次回は、図書館のロビーを作って、バトルで減ったHPや持ち物を持ち越せるようにしたいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次