ターン制RPGの作り方#6|たたかう相手を選ぼう

スライムちゃんで「たたかう」を決めると、仲間が全員暗くなり、こぼれたインクが光って「だれを ねらう?」。→キーで紙ひこうき、本の虫と光が移り、→でこぼれたインクに戻る。Escでコマンドに戻り、もう一度「たたかう」→→Enterで本の虫に決まる。フェアリーは「ぼうぎょ」、キョンシーは「たたかう」で紙ひこうきを選び、「コマンドが きまった!(たたかう(本の虫)・ぼうぎょ・たたかう(紙ひこうき))」

「たたかう」を選んだら、次はどの敵をねらうかを決めます。敵が何体もいるとき、ねらっている敵がはっきり分からないと、思っていたのと違う敵をたたいてしまいますよね。今回は、#4で作った「たたかう」のコマンドに自分で相手を選ぶ仕事を持たせて、矢印キーやゲームパッド、マウスで攻撃する相手を選べるようにしてみたいと思います!

目次

今回作るもの

  • 「たたかう」を選んだら、ねらう敵を選ぶ
    • 左右(上下)のキーで相手を変える。端まで行ったら反対の端に戻る
    • 決定で決まり、キャンセルでコマンドに戻る
    • マウスを乗せた敵をねらい、クリックで決める
  • 敵を選んでいる間は、仲間を全員暗くして、ねらっている敵を光らせる(#5の点滅を敵にも使う)
スクリプト役割付ける場所
TargetSelector相手が選ばれるまで待って、選んだ相手を返すTargetSelector(新しく作る)
TargetClickマウスを乗せた・クリックしたを知らせる敵のプレハブ
AttackCommand選ばれたら、相手を選ぶ(#4のものに中身を足す)(アセット)
BattleContext相手を選ぶ道具・仲間・敵を足す-
BattleLineupBattleContextに道具を入れるBattle

相手を選ぶTargetSelectorを作ろう

コマンドは「ボタン」だったので、EventSystemが矢印キーで選択を動かしてくれました。敵はボタンではないので、矢印キーの入力を自分で読んで、ねらいを動かします。

  • Hierarchyで空のGameObjectを作り、名前を「TargetSelector」にする
  • 「TargetSelector」スクリプトを作って、TargetSelectorに付ける
using UnityEngine;
using UnityEngine.EventSystems;

// たたかう相手を選ぶ。決まるまで待って、選んだ相手を返す(キャンセルなら null)
public class TargetSelector : MonoBehaviour
{
    // 選んだ相手が入る箱
    private readonly AwaitableCompletionSource<UnitView> targetSource = new AwaitableCompletionSource<UnitView>();
    private InputSystem_Actions inputActions;
    private UnitView[] targets; // 選べる相手
    private int index;          // 今ねらっている相手の番号
    private bool isWaiting;

    private void Awake()
    {
        inputActions = new InputSystem_Actions();
    }

    private void OnEnable()
    {
        inputActions.Enable();
    }

    private void OnDisable()
    {
        inputActions.Disable();
    }

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

        // 敵を選んでいる間は、仲間を全員暗くして、点滅も止める(敵の方に目が行くように)
        foreach (UnitView member in party)
        {
            member.SetHighlight(false);
            member.SetDimmed(true);
        }

        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);
        isWaiting = false;
        return target;
    }

    private void Update()
    {
        if (!isWaiting)
        {
            return;
        }

        // 右か下で次の相手、左か上で前の相手
        if (inputActions.UI.Navigate.WasPressedThisFrame())
        {
            Vector2 move = inputActions.UI.Navigate.ReadValue<Vector2>();
            if (move.x > 0.5f || move.y < -0.5f)
            {
                ChangeTarget(index + 1);
            }
            else if (move.x < -0.5f || move.y > 0.5f)
            {
                ChangeTarget(index - 1);
            }
        }

        if (inputActions.UI.Submit.WasPressedThisFrame())
        {
            targetSource.TrySetResult(targets[index]);
        }
        else if (inputActions.UI.Cancel.WasPressedThisFrame())
        {
            targetSource.TrySetResult(null);
        }
    }

    // ねらう相手を変える(端まで行ったら、反対の端へ)
    private void ChangeTarget(int newIndex)
    {
        targets[index].SetHighlight(false);
        index = (newIndex + targets.Length) % targets.Length;
        targets[index].SetHighlight(true);
    }

    // マウスを乗せた敵をねらう(TargetClick から呼ぶ)
    public void PointAt(UnitView target)
    {
        if (!isWaiting)
        {
            return;
        }
        int found = System.Array.IndexOf(targets, target);
        if (found >= 0)
        {
            ChangeTarget(found);
        }
    }

    // クリックした敵に決める(TargetClick から呼ぶ)
    public void Choose(UnitView target)
    {
        if (isWaiting && System.Array.IndexOf(targets, target) >= 0)
        {
            targetSource.TrySetResult(target);
        }
    }
}

コードのポイント

CommandMenuとTargetSelectorは、どちらも「AwaitableCompletionSourceの箱に答えが入るまで待つ」形です。待ち方を同じ形にそろえておくと、コマンドの側からは「コマンドを待つ」「相手を待つ」が同じように読めます。

  • Awaitable(待ち方は6-16 待つ処理をすっきり書こう)
    • 選んだ敵のUnitViewを返す。キャンセルされたらnullを返すので、呼ぶ側は「nullならコマンドに戻る」と書ける
  • await Awaitable.NextFrameAsync(…)
    • 「たたかう」をEnterで決めると、相手選びはEnterを押したのと同じフレームの中で始まることがある。そのままだと、Updateが「このフレームでEnterが押された」と受け取って、相手を選ぶ前に1体目に決まってしまう。#4のCommandMenuと同じく、1フレーム待ってから始めます
  • party を暗くする
    • 敵を選んでいる間は、仲間を全員暗くして点滅も止める。明るくて光っているのは敵の側だけになるので、「今は敵を選ぶ時間」だと目で分かる。だれのコマンドかは、ステータスの黄色い名前で分かります
  • inputActions.UI.Navigate
    • 矢印キー・WASD・十字キー・スティックの入力(Vector2)。WasPressedThisFrame()で「押した瞬間」だけを受け取り、ReadValue<Vector2>()でどちらに押したかを見る
    • 敵は横に並んでいるので右と左を使うけど、下や上でも動くようにしておくと、ゲームパッドのスティックを少し斜めに倒したときにも反応してくれます
  • (newIndex + targets.Length) % targets.Length
    • 3体なら、0の左は2、2の右は0。端まで行ったら反対の端に戻る計算。+ targets.Lengthは、-1が出たときにマイナスにならないようにするため
  • SetHighlight
    • #5で作ったUnitViewの点滅を、そのまま敵に使っている。ねらいを変えるときは、前の相手を消して、新しい相手を光らせるだけ
  • EventSystem.current.SetSelectedGameObject(null)
    • コマンドのボタンが選ばれたままだと、矢印キーでコマンドの▶も一緒に動いてしまう。相手を選ぶ間は、ボタンの選択を外しておく

1フレーム待つのを外して試すと、「たたかう」をEnterで決めた瞬間に1体目の敵に決まり、そのまま次の人へ進んでしまいました(Unity 6.3で確認)。待ち始めるときは、まず1フレーム待つ、と覚えておきましょう。

マウスでも選べるようにしよう

敵は「Image」なので、スタートガイドの9-7 UIをドラッグ&ドロップしようでやったのと同じように、イベントのインターフェースでマウスを受け取れます。

  • 「TargetClick」スクリプトを作る
using UnityEngine;
using UnityEngine.EventSystems;

// 敵にマウスを乗せた・クリックしたを、TargetSelector に知らせる
public class TargetClick : MonoBehaviour, IPointerEnterHandler, IPointerClickHandler
{
    [SerializeField] private UnitView unitView;

    private TargetSelector selector;

    private void Start()
    {
        // シーンに1つだけある TargetSelector を探しておく
        selector = FindAnyObjectByType<TargetSelector>();
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        selector.PointAt(unitView);
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        selector.Choose(unitView);
    }
}
  • FindAnyObjectByType()
    • シーンの中から、TargetSelectorが付いたオブジェクトを探す。敵はスクリプトから作るので、Inspectorでドラッグしておけない。そこで、自分で探しにいきます
  • PointAt / Choose
    • 選んでいる最中かどうかは、TargetSelectorの方で確かめている。TargetClickは「乗せた」「押した」を伝えるだけ

FindAnyObjectByTypeは、シーンに1つだけあるものを探す書き方です。TargetSelectorをシーンに2つ置くと、どちらが見つかるか分かりません。TargetSelectorはシーンに1つだけにしておきましょう。

PrefabsのEnemyを開いて、TargetClickを付け、Unit ViewにEnemy(自分自身)をドラッグします。

Enemyのプレハブの編集画面。Unit ViewのFlash MaterialにWhiteFlash、Target ClickのUnit ViewにEnemyが入っている

マウスのイベントは、敵のImageの「Raycast Target」にチェックが入っているときだけ届きます(最初から入っています)。#5で作った光る絵(Flash)はRaycast Targetを外してあるので、光っている間もクリックの邪魔をしません。

「たたかう」が自分で相手を選ぶようにしよう

いよいよ、#4で中身を空っぽにしておいたAttackCommandに、相手を選ぶ仕事を書きます。

BattleContextに道具を足す

コマンドが相手を選べるように、BattleContextに「相手を選ぶ道具」と「仲間・敵の一覧」を足します。

using TMPro;

// コマンドが相手を選んだり、メッセージを出したりするときに使う、バトルの道具をまとめたもの
public class BattleContext
{
    public TMP_Text MessageText;          // 上のメッセージ
    public TargetSelector TargetSelector; // 相手を選ぶ道具
    public UnitView[] Party;              // 仲間の見た目
    public UnitView[] Enemies;            // 敵の見た目
}

AttackCommand

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

using UnityEngine;

// たたかう:敵を1体選ぶ
[CreateAssetMenu(fileName = "Attack", menuName = "TurnBasedRpg/Commands/Attack")]
public class AttackCommand : BattleCommand
{
    // 敵を1体選ぶ。キャンセルされたら null を返す
    public override async Awaitable<BattleAction> SelectAsync(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);
    }
}
  • override SelectAsync
    • 親のBattleCommandのSelectAsync(何も選ばずに決まる)を、「敵を1体選ぶ」に書き換えた。ぼうぎょ・にげるは書き換えていないので、今までどおり何も選ばずに決まる
  • return null
    • 相手選びでキャンセルしたらnullを返す。#4のBattleLineupは「actionがnullなら、同じ人のコマンドを選び直す」と書いてあったので、BattleLineupの選ぶ流れは直さなくていい
スライム

たたかうのクラスを書き換えただけで、ほかのコマンドは何も変わらないんですね

フェアリー

それが1つずつのクラスに分けたうれしさだよ!#13のまほうでも、MagicCommandだけを書き換えて、まほうの一覧を開けるようにするんだ

BattleLineupで道具を入れる

最後に、BattleLineupでBattleContextに道具を入れます。#5のBattleLineupに、次の3か所を足します。

  • 変数を2つ足す(using System.Collections.Generic;も一番上に足す)
    [SerializeField] private TargetSelector targetSelector;

    private readonly List<UnitView> enemyViews = new List<UnitView>();
  • 敵を並べるループの最後で、作った敵をListに入れる
            view.SetupEnemy(enemies[i]);
            enemyViews.Add(view);
  • BattleContextを作るところで、道具を入れる
        // コマンドに渡す道具をまとめる
        context = new BattleContext();
        context.MessageText = messageText;
        context.TargetSelector = targetSelector;
        context.Party = partyViews;
        context.Enemies = enemyViews.ToArray();
  • enemyViews
  • target.name
    • 決まったときのメッセージの「たたかう(本の虫)」は、#2で敵のGameObjectの名前をデータの名前にしておいたので、そのまま出る

BattleのBattle LineupのTarget Selectorに、TargetSelectorをドラッグします。

「だれを ねらう?」の画面。仲間3人は全員暗く、敵のうち紙ひこうきだけが薄い黄色に光っている。こぼれたインクと本の虫はふつうの色。ステータスでは「スライム」の名前が黄色
BattleのBattle LineupのTarget SelectorにTargetSelectorが入っている

動かして確認!

フェアリー

マウスでも試してみてね。敵にマウスを乗せると光が移って、クリックで決まるよ

再生して、キーボードで操作してみましょう。

キーボードで相手を選ぶ。敵を選ぶ間は仲間が全員暗くなり、ねらっている敵だけが光る。Escでコマンドに戻れる

#6の動作確認

  • 「たたかう」を選ぶと「だれを ねらう?」と出て、仲間が全員暗くなり、1体目の敵が光って点滅する
  • 左右のキー(十字キー)でねらいが変わり、端まで行くと反対の端に戻る
  • Enter(下のボタン)で決まり、次の仲間へ。Esc(右のボタン)でコマンドに戻る
  • マウスを乗せた敵が光り、クリックで決まる
  • 「たたかう」以外のコマンドを選んだときは、相手を選ばずに次の仲間へ進む

あとがき

今回は、「たたかう」の相手を選べるようにしました。変えたのは「たたかう」のクラスだけで、コマンドを選ぶ流れには手を入れずに済みましたね。これで3人分の「何をするか」「だれに」が決まるようになりました。次回は、いよいよ攻撃して、ダメージを計算してメッセージを出したいと思います!

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

コメント

コメント一覧 (1件)

コメントする

目次