ターン制RPGの作り方#5|だれの番か、ひと目で分かるようにしよう

スライムちゃんが薄い黄色に光って点滅し、フェアリーとキョンシーが暗い。コマンドを決めると点滅と明るさがフェアリーに移り、Escでスライムちゃんに戻る。キョンシーまで選ぶと全員が明るく戻って「コマンドが きまった!」

前回、3人の仲間のコマンドを順番に選べるようになりました。ただ、今のままだと「今だれのコマンドを選んでいるのか」が、上のメッセージを読まないと分かりません。RPGのバトルでは、画面のどこを見ればいいか、だれの番なのかが、ひと目で分かることが大事です。今回は、選んでいる仲間を光らせて点滅させ、ほかの仲間を暗くして、静止画でもパッと分かるようにしてみたいと思います!

目次

今回作るもの

  • 選んでいる仲間を、光らせて点滅させる
  • ほかの仲間を暗くする
  • ステータスのウインドの名前を黄色にする
スクリプト役割付ける場所
UnitView光らせる・暗くするメソッドを足す仲間3人・敵のプレハブ
StatusRow名前を黄色くするメソッドを足すStatusRowのプレハブ
BattleLineup選んでいる人を光らせ、ほかを暗くするBattle

選んでいる仲間を目立たせよう

だれのコマンドを選んでいるのかが、静止画でもひと目で分かるように、2つのことをします。

  • 選んでいる仲間を光らせて点滅させる
  • ほかの仲間を暗くする
フェアリーの番。フェアリーだけが薄い黄色に光り、スライムちゃんとキョンシーは暗い。敵はふつうの色。ステータスでは「フェアリー」の名前が黄色

光らせるのは、光らせたい人にだけ「光って」「やめて」と言えばいい形にします。あとで全体まほうのように何人もまとめて選ぶときも、対象の全員に「光って」と言うだけで済みます。暗くするのは、まわりを少し落ち着かせて、光っている人に目が行くようにするためです。

白く光らせるマテリアル

UIのImageのColorは「絵の色に掛け算」なので、白(255,255,255)にしても元の絵のままで、白くは光りません。そこで、絵の形だけを使って1色で塗るマテリアルを使います。

  • Projectウインドの TurnBasedRpg/Effects で右クリック>Create>Material、名前をWhiteFlash
  • Shader:GUI/Text Shader
Effects/WhiteFlashのInspector。ShaderがGUI/Text Shader

GUI/Text Shaderは、もともと文字を描くためのシェーダーです。絵の透明なところ(形)だけを使い、色はImageのColorで塗るので、「形はそのまま、色は真っ白」に出来ます。

UnitViewに、白く光る絵を重ねる

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

using UnityEngine;
using UnityEngine.UI;

// 仲間や敵の見た目。データを受け取って、絵を表示する
public class UnitView : MonoBehaviour
{
    [SerializeField] private Image image;
    [SerializeField] private IdleBounce idleBounce;  // 仲間だけ。敵は空のまま
    [SerializeField] private Material flashMaterial; // 白く光らせるマテリアル(WhiteFlash)
    [SerializeField] private Color flashColor = new Color(1f, 0.92f, 0.5f); // 光る色(白い敵でも分かるよう、少し黄色)
    [SerializeField] private float blinkSpeed = 6f;  // 点滅の速さ
    [SerializeField] private float minFlash = 0.15f; // 一番弱いときの白さ
    [SerializeField] private float maxFlash = 0.45f; // 一番強いときの白さ
    [SerializeField] private Color dimColor = new Color(0.4f, 0.4f, 0.45f); // 暗くするときの色

    private Image flashImage; // 自分の絵の上に重ねる、白いシルエット
    private bool isHighlighted;

    private void Awake()
    {
        // 自分の絵と同じ大きさの Image を子に作って、白く光らせるのに使う
        GameObject flash = new GameObject("Flash", typeof(RectTransform), typeof(Image));
        flash.transform.SetParent(transform, false);
        RectTransform flashRect = (RectTransform)flash.transform;
        flashRect.anchorMin = Vector2.zero;
        flashRect.anchorMax = Vector2.one;
        flashRect.offsetMin = Vector2.zero;
        flashRect.offsetMax = Vector2.zero;
        flashImage = flash.GetComponent<Image>();
        flashImage.material = flashMaterial;
        flashImage.preserveAspect = true;
        flashImage.raycastTarget = false;
        flashImage.enabled = false;
    }

    private void Update()
    {
        if (isHighlighted)
        {
            UpdateFlash();
        }
    }

    // 光る絵を、体と同じ形・今の白さにする
    private void UpdateFlash()
    {
        // 待機のコマが変わっても、同じ形で光るようにする
        flashImage.sprite = image.sprite;
        // Sin で 0〜1 をくり返し、白さを minFlash〜maxFlash の間で変える
        float wave = (Mathf.Sin(Time.time * blinkSpeed) + 1f) / 2f;
        Color color = flashColor;
        color.a = Mathf.Lerp(minFlash, maxFlash, wave);
        flashImage.color = color;
    }

    // 仲間のデータで見た目を作る
    public void SetupCharacter(CharacterData data)
    {
        name = data.CharacterName;
        idleBounce.SetFrames(data.IdleFrames);
    }

    // 敵のデータで見た目を作る
    public void SetupEnemy(EnemyData data)
    {
        name = data.EnemyName;
        image.sprite = data.Sprite;
    }

    // 選ばれている間、白く点滅させる
    public void SetHighlight(bool isOn)
    {
        isHighlighted = isOn;
        if (isOn)
        {
            // 表示する前に、形と色を入れておく(入れる前に表示すると、1フレームだけ白い四角が出る)
            UpdateFlash();
        }
        flashImage.enabled = isOn;
    }

    // 暗くする(false で、もとの色に戻す)
    public void SetDimmed(bool isDimmed)
    {
        image.color = isDimmed ? dimColor : Color.white;
    }
}
  • Awake で Flash を作る
    • 自分の子に、同じ大きさのImageを作って、WhiteFlashのマテリアルを付ける。体の絵の上に重なる「白いシルエット」になる
    • Hierarchyで子を作ってつなぐ手間がないので、仲間にも敵にも、UnitViewを付けるだけで使えます
  • UpdateFlash()
    • 光る絵を、体と同じ絵(flashImage.sprite = image.sprite)と、今の白さにそろえる。仲間は待機のコマがくり返し変わるので、光っている間は毎フレーム呼ぶ
  • Mathf.Sin と Lerp
    • Sinは-1〜1をくり返す。1を足して2で割ると0〜1になる。その値で、白さ(Alpha)を0.15〜0.45の間で行ったり来たりさせる
    • 一番弱いときも0にしないので、どの瞬間を切り取っても、少し光って見えます
  • flashColor
    • 光る色。真っ白にすると、地下1階の白い紙ひこうきのような白っぽい敵では、光っても見分けがつかないので、少し黄色にしています
  • SetHighlight(bool isOn)
    • 光らせる・やめるを切り替える。呼ぶのは、選んでいる人にだけ
    • 光らせるときは、表示する前にUpdateFlash()で形と色を入れておく。Flashは最初、絵が空っぽで色が真っ白なので、そのまま表示すると、次のUpdateで形が入るまでの1フレームだけ、白い四角が見えてしまいます
  • SetDimmed(bool isDimmed)
    • ImageのColorは「絵の色に掛け算」なので、白なら元の色のまま、灰色(0.4)にすると全体が暗くなる。暗くするのは、色を変えるだけで出来ます

UnitViewを付けている仲間3人(Fairy・Slime・Jiangshi)と、PrefabsのEnemyのFlash Materialに、WhiteFlashをドラッグします。

SlimeのUnit View。Flash MaterialにWhiteFlash、Flash Colorが薄い黄色、Blink Speed 6、Min Flash 0.15、Max Flash 0.45、Dim Colorが灰色

Flash Materialを入れ忘れても、エラーにはなりません。ただ、ふつうのマテリアルで絵が重なるだけなので、白く光らず、少し濃く見えるだけになります。光らないときは、Flash Materialが入っているかを確かめましょう。

スライム

光らせたい人にだけ命令すればいいので、分かりやすいですね

この「白いシルエットを重ねる」仕組みは、#8で攻撃が当たったときに、敵を一瞬だけ真っ白に光らせる演出にも使います。

ステータスの行も、選んでいる人の名前を黄色にします。#3で作ったStatusRowに次のメソッドを足します。

    // 選んでいる人の名前を黄色にする
    public void SetHighlight(bool isOn)
    {
        nameText.color = isOn ? new Color(1f, 0.86f, 0.35f) : Color.white;
    }

キャラクターの動きと、ステータスの名前の色の2か所で「今の番」を見せています。キャラクターが重なって見えにくいときや、敵の攻撃の演出が入ったあとでも、どちらかで分かるようにしておくと親切です。

選んでいる人を光らせよう

#4のBattleLineupに、光らせる・暗くする処理を足します。

using TMPro;
using UnityEngine;

// データから仲間と敵を並べる(バトルの流れは #9 で BattleManager に移す)
public class BattleLineup : MonoBehaviour
{
    [SerializeField] private CharacterData[] party;  // バトルに出る3人
    [SerializeField] private UnitView[] partyViews;  // PartyArea の3人
    [SerializeField] private EnemyData[] enemies;    // 出てくる敵(1〜4体)
    [SerializeField] private UnitView enemyPrefab;
    [SerializeField] private RectTransform enemyArea;
    [SerializeField] private float enemySpacing = 300f; // 敵の横の間隔
    [SerializeField] private StatusWindow statusWindow;
    [SerializeField] private CommandMenu commandMenu;
    [SerializeField] private TMP_Text messageText;

    private StatusRow[] statusRows;
    private BattleContext context;

    private async void Start()
    {
        statusRows = new StatusRow[party.Length];
        for (int i = 0; i < partyViews.Length; i++)
        {
            CharacterData character = party[i];
            partyViews[i].SetupCharacter(character);
            statusRows[i] = statusWindow.AddRow(character);
        }

        for (int i = 0; i < enemies.Length; i++)
        {
            UnitView view = Instantiate(enemyPrefab, enemyArea);
            // 真ん中をそろえて横に並べ、1体おきに上下へずらす
            float x = (i - (enemies.Length - 1) / 2f) * enemySpacing;
            float y = (i % 2 == 0) ? 60f : -60f;
            ((RectTransform)view.transform).anchoredPosition = new Vector2(x, y);
            view.SetupEnemy(enemies[i]);
        }

        // コマンドに渡す道具をまとめる
        context = new BattleContext();
        context.MessageText = messageText;

        try
        {
            while (true)
            {
                await SelectActionsAsync();
                await Awaitable.WaitForSecondsAsync(2f, destroyCancellationToken);
            }
        }
        catch (System.OperationCanceledException)
        {
            // オブジェクトが消えたら、そこで終わり
        }
    }

    // 3人のコマンドを順に選ぶ(キャンセルで前の人に戻る)
    private async Awaitable SelectActionsAsync()
    {
        BattleAction[] actions = new BattleAction[party.Length];
        int index = 0;
        while (index < party.Length)
        {
            messageText.text = $"{party[index].CharacterName}は どうする?";
            partyViews[index].SetHighlight(true);
            statusRows[index].SetHighlight(true);
            DimPartyExcept(index);

            BattleCommand command = await commandMenu.WaitForCommandAsync(party[index].Commands);
            BattleAction action = null;
            if (command != null)
            {
                action = await command.SelectAsync(context);
            }

            partyViews[index].SetHighlight(false);
            statusRows[index].SetHighlight(false);

            if (command == null)
            {
                // コマンドでキャンセルしたら、前の人に戻る(1人目なら、そのまま1人目)
                if (index > 0)
                {
                    index--;
                }
                continue;
            }

            if (action == null)
            {
                // 相手選びなどでキャンセルしたら、同じ人のコマンドを選び直す
                continue;
            }

            actions[index] = action;
            index++;
        }

        BrightenParty();
        messageText.text = $"コマンドが きまった!({ActionText(actions[0])}・{ActionText(actions[1])}・{ActionText(actions[2])})";
    }

    // index の人以外の仲間を暗くする(index が -1 なら、全員暗くする)
    private void DimPartyExcept(int index)
    {
        for (int i = 0; i < partyViews.Length; i++)
        {
            partyViews[i].SetDimmed(i != index);
        }
    }

    // 仲間を全員、もとの明るさに戻す
    private void BrightenParty()
    {
        foreach (UnitView view in partyViews)
        {
            view.SetDimmed(false);
        }
    }

    // 画面に出す説明(相手がいれば、相手の名前も付ける)
    private string ActionText(BattleAction action)
    {
        if (action.Target != null)
        {
            return $"{action.Command.CommandName}({action.Target.name})";
        }
        return action.Command.CommandName;
    }
}
  • 光らせる → コマンドを選ぶ → 光らせるのをやめる
    • この順番が上から読んだとおりに動く。asyncで書くうれしさが出るところです
  • DimPartyExcept(index)
    • 3人それぞれに「選ばれている人以外か」(i != index)を伝えて、選ばれている人は明るく、ほかの2人は暗くする
    • 次の人に移ったときも、この1行で明るさがまとめて入れ替わる
  • BrightenParty()
    • 3人分が決まったら、全員をもとの明るさに戻す

光らせるのも、やめるのも、選んでいる人(partyViews[index])にだけ命令しています。光らせる処理は「光らせたい人」だけ、暗くする処理は「まわり」だけを受け持つので、どちらかを変えても、もう一方に影響しません。

動かして確認!

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

キーボードで3人のコマンドを選ぶ。選んでいる人が光って点滅し、ほかの2人は暗い。Escで前の人に戻ると、点滅と明るさも前の人に戻る

#5の動作確認

  • 再生すると、スライムちゃんが光って点滅し、フェアリーとキョンシーが暗くなる。ステータスの名前も黄色くなる
  • コマンドを決めると、点滅と明るさが次の仲間に移る。Escで前の人に戻ると、前の人に戻る
  • どの瞬間で止めても(静止画でも)、だれの番か分かる
  • 3人分決まると、全員がもとの明るさに戻る
スライム

どこを見ればいいか、すぐ分かるようになりましたね

フェアリー

次の#6では「たたかう」で敵を選ぶよ。そのときは、仲間を全員暗くして、ねらっている敵を光らせるんだ

あとがき

今回は、だれの番かがひと目で分かるように、選んでいる仲間を光らせて、ほかの仲間を暗くしました。光らせたい人にだけ命令する形にしたので、敵を選ぶときや、全体まほうで何人もまとめて選ぶときにも、そのまま使えます。次回は「たたかう」を選んだあとに、どの敵をねらうかを選べるようにしたいと思います!

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

コメント

コメントする

目次