見出し画像

NDI™で送信するRTAイベントタイマーをUnityで作る #03 ストップウォッチ編

メイン機能であるストップウォッチを作成する

下準備

名前はカウントダウンタイマーとの対比から、CountUp Timer にする

CountUp Timer Object を作成。CountUp Timer Background Image も作成。♯02と同じようにパーツを配置していく

動画の準備

タイマー(ストップウォッチ)開始時、終了時に動画を流したいので、動画を準備する。WebMファイル (コーデックがVP8のもの)を用意し、Assets > Videos に入れる。ファイル名はそれぞれ GoodLuck と GoodGame にしておく

NDI を設定したときのように、Assets > Textures に Render Texture をそれぞれに用意する。名前は GoodLuck Render Texture、GoodGame Render Texture とする。サイズはそれぞれ 960 x 270 にしておく

レンダーテクスチャの設定

動画の設定

ヒエラルキーにビデオプレイヤーを2つ追加する。場所は一番上の階層にしておく。名前は GoodLuck Video Player と GoodGame Video Player にする。
両方とも、ゲーム開始時に再生のチェックを外しておく。レンダーテクスチャはそれぞれ GoodLuck Render Texture、GoodGame Render Texture と対応したものに設定しておく。オーディオ出力モードを、念のためなしに変更

CountUp Object に [UI]→[Raw 画像] を2つ追加し、それぞれ GoodLuck VideoImage と GoodGame VideoImage にしておく。位置は(0,0)サイズは960 x 270 に設定する。テクスチャをそれぞれ GoodLuck Render Texture、GoodGame Render Texture にする

ヒエラルキー
ビデオプレイヤー
Raw 画像

CountUp Main

ステータスを管理してストップウォッチを動かす
ready : タイマー準備中。開始入力があると good_luck に遷移
good_luck : GL アニメーションを再生。そのまま running に遷移
running : 一時停止や終了入力があるまでタイマーを回し続ける
paused : 一時停止中。running や good_game に遷移可能。文字の色を灰色に
good_game : GG アニメーションを再生。そのまま finished に遷移
finished : タイマーストップ。リセットされない限りはそのまま。文字の色を黄色に

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video; // GL、GG 時に動画を流す用

public class CountUpMain : MonoBehaviour
{
    [SerializeField] Image hour1_image;
    [SerializeField] Image hour2_image;
    [SerializeField] Image colon1_image;
    [SerializeField] Image minute1_image;
    [SerializeField] Image minute2_image;
    [SerializeField] Image colon2_image;
    [SerializeField] Image second1_image;
    [SerializeField] Image second2_image;
    [SerializeField] Image period_image;
    [SerializeField] Image decimal_image;

    [SerializeField] Sprite[] number = new Sprite[10];

    [SerializeField] VideoPlayer goodluck_player;
    [SerializeField] VideoPlayer goodgame_player;

    private float start_time;
    public float now_time { get; private set; }
    public float offset_time { get; private set; }

    public enum TimerStatus
    {
        ready,
        good_luck,
        running,
        paused,
        good_game,
        finished,
    }

    public TimerStatus timer_status { get; private set; }

    public enum InputStatus
    {
        ready,
        start,
        stop,
        pause,
        resume,
        reset,
        up_1sec,
        up_10sec,
        up_1min,
        up_10min,
        up_1hour,
        down_1sec,
        down_10sec,
        down_1min,
        down_10min,
        down_1hour,
        updown_reset,
    }

    public InputStatus input_status;

    // Start is called before the first frame update
    void Start()
    {
        start_time = 0.0f;
        now_time = 0.0f;
        offset_time = 0.0f;

        input_status = InputStatus.ready;

        InitDisplay();

        timer_status = TimerStatus.ready;
    }

    // Update is called once per frame
    void Update()
    {
        // タイマーの状態で場合分け
        switch (timer_status)
        {
            case TimerStatus.ready:
                // 数字を白にしておく
                ChangeColor("#FFFFFFFF");

                // キー入力を待つ
                if (input_status == InputStatus.start)
                {
                    start_time = Time.time;
                    timer_status = TimerStatus.good_luck;
                    break;
                }
                // 停止中にF17が押されたらリセット
                if (input_status == InputStatus.reset)
                {
                    timer_status = TimerStatus.ready;
                    InitDisplay();
                    break;
                }
                break;
            case TimerStatus.good_luck:
                goodluck_player.Play();
                timer_status = TimerStatus.running;
                break;
            case TimerStatus.running:
                // 数字を白にしておく
                ChangeColor("#FFFFFFFF");

                now_time = Time.time - start_time;
                SetTime(now_time + offset_time);

                // F14が押されたら終了
                if (input_status == InputStatus.stop)
                {
                    if (now_time < 10.0f)
                    {
                        break;
                    }
                    timer_status = TimerStatus.good_game;
                    break;
                }
                // 一時停止機能をつける
                if (input_status == InputStatus.pause)
                {
                    timer_status = TimerStatus.paused;
                    break;
                }
                break;
            case TimerStatus.paused:
                // 数字をグレーにしておく
                ChangeColor("#808080FF");

                // F14が押されたら終了
                if (input_status == InputStatus.stop)
                {
                    timer_status = TimerStatus.good_game;
                    break;
                }
                // 停止中にF17が押されたらリセット
                if (input_status == InputStatus.reset)
                {
                    timer_status = TimerStatus.ready;
                    InitDisplay();
                    break;
                }
                // もう一度一時停止が押されたら、現時点から再開する
                if (input_status == InputStatus.pause)
                {
                    start_time = Time.time - now_time;
                    timer_status = TimerStatus.running;
                    break;
                }
                // 間違えて止めた時用に再開ボタンを用意する
                if (input_status == InputStatus.resume)
                {
                    timer_status = TimerStatus.running;
                    break;
                }
                break;
            case TimerStatus.good_game:
                goodgame_player.Play();
                timer_status = TimerStatus.finished;
                break;
            case TimerStatus.finished:
                // 数字を黄色にしておく
                ChangeColor("#EEB033FF");

                // 停止中にF17が押されたらリセット
                if (input_status == InputStatus.reset)
                {
                    timer_status = TimerStatus.ready;
                    InitDisplay();
                    break;
                }
                // 間違えて止めた時用に再開ボタンを用意する
                if (input_status == InputStatus.resume)
                {
                    timer_status = TimerStatus.running;
                    break;
                }
                break;
        }
        // 時間修正用オフセットを変更する。タイマーも一回描画する
        switch (input_status)
        {
            case InputStatus.up_1sec:
                ChangeOffset(1f);
                break;
            case InputStatus.down_1sec:
                ChangeOffset(-1f);
                break;
            case InputStatus.up_10sec:
                ChangeOffset(10f);
                break;
            case InputStatus.down_10sec:
                ChangeOffset(-10f);
                break;
            case InputStatus.up_1min:
                ChangeOffset(60f);
                break;
            case InputStatus.down_1min:
                ChangeOffset(-60f);
                break;
            case InputStatus.up_10min:
                ChangeOffset(600f);
                break;
            case InputStatus.down_10min:
                ChangeOffset(-600f);
                break;
            case InputStatus.up_1hour:
                ChangeOffset(3600);
                break;
            case InputStatus.down_1hour:
                ChangeOffset(-3600);
                break;
            case InputStatus.updown_reset:
                ChangeOffset(-offset_time);
                break;
        }

        input_status = InputStatus.ready;
    }

    bool isKeyDown(byte[] key, int code)
    {
        return ((key[code] & 0x80) != 0);
    }

    // 文字のカラーをすべて変更する
    private void ChangeColor(string color_hex)
    {
        if (color_hex == null)
        {
            return;
        }

        if (ColorUtility.TryParseHtmlString(color_hex, out var color))
        {
            // 変換成功
            hour1_image.color = color;
            hour2_image.color = color;

            minute1_image.color = color;
            minute2_image.color = color;

            second1_image.color = color;
            second2_image.color = color;

            decimal_image.color = color;
        }
    }

    private void ChangeOffset(float offset)
    {
        offset_time = offset_time + offset;
        SetTime(now_time + offset_time);
    }


    void InitDisplay()
    {
        hour1_image.enabled = false;
        hour2_image.enabled = false;
        colon1_image.enabled = false;
        minute1_image.enabled = false;
        minute2_image.enabled = false;
        colon2_image.enabled = false;
        second1_image.enabled = false;
        second2_image.enabled = true;
        period_image.enabled = true;
        decimal_image.enabled = true;

        start_time = 0.0f;
        offset_time = 0.0f;
        now_time = 0.0f;

        SetTime(0.0f);
    }

    void SetTime(float ftime)
    {
        if (ftime < 0.0f)
        {
            ftime = 0.0f;
        }

        ftime = ftime * 10;
        int itime = Mathf.FloorToInt(ftime);

        int hour = itime / 36000;
        int hour1 = hour / 10;
        int hour2 = hour - hour1 * 10;
        hour1_image.sprite = number[hour1];
        hour2_image.sprite = number[hour2];

        itime = itime - hour * 36000;

        int minute = itime / 600;
        int minute1 = minute / 10;
        int minute2 = minute - minute1 * 10;
        minute1_image.sprite = number[minute1];
        minute2_image.sprite = number[minute2];

        itime = itime - minute * 600;

        int second = itime / 10;
        int second1 = second / 10;
        int second2 = second - second1 * 10;
        second1_image.sprite = number[second1];
        second2_image.sprite = number[second2];

        int decimal1 = itime - second * 10;
        decimal_image.sprite = number[decimal1];

        // 表示非表示を設定する
        if (hour1 > 0)
        {
            hour1_image.enabled = true;
            hour2_image.enabled = true;
            colon1_image.enabled = true;
            minute1_image.enabled = true;
            minute2_image.enabled = true;
            colon2_image.enabled = true;
            second1_image.enabled = true;

            return;
        }
        else
        {
            hour1_image.enabled = false;
        }

        if (hour2 > 0)
        {
            hour2_image.enabled = true;
            colon1_image.enabled = true;
            minute1_image.enabled = true;
            minute2_image.enabled = true;
            colon2_image.enabled = true;
            second1_image.enabled = true;

            return;
        }
        else
        {
            hour2_image.enabled = false;
            colon1_image.enabled = false;
        }

        if (minute1 > 0)
        {
            minute1_image.enabled = true;
            minute2_image.enabled = true;
            colon2_image.enabled = true;
            second1_image.enabled = true;

            return;
        }
        else
        {
            minute1_image.enabled = false;
        }

        if (minute2 > 0)
        {
            minute2_image.enabled = true;
            colon2_image.enabled = true;
            second1_image.enabled = true;

            return;
        }
        else
        {
            minute2_image.enabled = false;
            colon2_image.enabled = false;
        }

        if (second1 > 0)
        {
            second1_image.enabled = true;
        }
        else
        {
            second1_image.enabled = false;
        }
    }
}

コントローラーの準備

コントローラーの識別に、Unity の InputSystem を使用する

パッケージマネージャーを開き、パッケージ:Unity レジストリ から Input System を選択。インストールする

これめっちゃ便利

インストールするとエディターが再起動する

Controller Input のオブジェクトをヒエラルキーの一番上の階層に作成する

ControllerInput のスクリプトを作成し、以下のように記述する。北ボタン(Xbox の Y、PS の △)が押されたら開始、もう一度押されたら終了するように設定しておく

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class ControllerInput : MonoBehaviour
{
    [SerializeField] CountUpMain countup;

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        // ゲームパッドが接続されていないとnullになる。
        if (Gamepad.current == null) return;

        if (countup != null)
        {
            if (Gamepad.current.buttonNorth.wasPressedThisFrame)
            {
                if (countup.timer_status == CountUpMain.TimerStatus.ready)
                {
                    countup.input_status = CountUpMain.InputStatus.start;
                }
                if (countup.timer_status == CountUpMain.TimerStatus.running)
                {
                    countup.input_status = CountUpMain.InputStatus.stop;
                }
            }
        }
#if UNITY_EDITOR
        if (Gamepad.current.buttonNorth.wasPressedThisFrame)
        {
            Debug.Log("Button Northが押された!");
        }
        if (Gamepad.current.buttonSouth.wasReleasedThisFrame)
        {
            Debug.Log("Button Southが離された!");
        }
#endif
    }

    private void OnGUI()
    {
        // ゲームパッドが接続されていないとnullになる。
        if (Gamepad.current == null) return;
#if UNITY_EDITOR
        GUILayout.Label($"leftStick: {Gamepad.current.leftStick.ReadValue()}");
        GUILayout.Label($"buttonNorth: {Gamepad.current.buttonNorth.isPressed}");
        GUILayout.Label($"buttonSouth: {Gamepad.current.buttonSouth.isPressed}");
        GUILayout.Label($"buttonEast: {Gamepad.current.buttonEast.isPressed}");
        GUILayout.Label($"buttonWest: {Gamepad.current.buttonWest.isPressed}");
        GUILayout.Label($"leftShoulder: {Gamepad.current.leftShoulder.ReadValue()}");
        GUILayout.Label($"leftTrigger: {Gamepad.current.leftTrigger.ReadValue()}");
        GUILayout.Label($"rightShoulder: {Gamepad.current.rightShoulder.ReadValue()}");
        GUILayout.Label($"rightTrigger: {Gamepad.current.rightTrigger.ReadValue()}");
#endif
    }
}

Input System ではボタンが東西南北で表されている。複数コントローラーを挿していても扱いが簡単。途中で抜き差しも可能。めっちゃ便利

コントローラーでタイマーが動くことを確認する。動画も透過できた

バッチリ!

おつかれさまでした

次はキーボード入力部分を作っていきます


この記事が気に入ったらサポートをしてみませんか?