🐡

【Unity】VContainer、MessagePipe、R3、DOTweenを用いた汎用的なTween処理

2024/05/19に公開

汎用的なTween処理を目標

現在戦車ゲーム MetalCityというUnityのゲーム開発をすすめていて、VContainer、MessagePipe、R3、DOTweenを用いた汎用的なTween処理を実装できればと思い、諸々設計してみました。まだ手探りですが、とりあえず形になったので備忘録として記事にしてみます。
まだ初期の処理しか記述していないので公開用Githubリポジトリはまだ準備しておりません。もう少し実装が進んでゲームとして成立してきたタイミングで公開用Githubのリポジトリも用意しようと思います

https://x.com/Cz_mirror/status/1791855954490974460

クラス図

Tween系処理

以下はTween系の処理を実装しています。他のプロジェクトへの移植を意識して、namespaceをプロジェクト名ではなく_Tweenとしています

ITweenEffect

Tween処理を定義したインターフェースです、Tweenのライブラリを変更しても対応できるようにインターフェースにしています

ITweenEffect.cs
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace _Tween.Scripts.Domain.UI.Tween
{
    /// <summary>
    /// Tween処理を実行するインターフェース
    /// </summary>
    public interface ITweenEffect
    {
        /// <summary>
        /// UIのalphaを0にする
        /// </summary>
        UniTask SetAlphaZero(CanvasGroup canvasGroup);

        /// <summary>
        /// 外部から受け取ったRectTransformのsizeDeltaを0にする
        /// </summary>
        UniTask SetSizeDeltaZero(RectTransform rectTransform);

        /// <summary>
        /// UIのフェードイン
        /// </summary>
        UniTask FadeIn(CanvasGroup canvasGroup, CancellationToken cancellationToken);

        /// <summary>
        /// UIのフェードアウト
        /// </summary>
        UniTask FadeOut(CanvasGroup canvasGroup, CancellationToken cancellationToken);

        /// <summary>
        /// UIのフェード処理
        /// </summary>
        /// <param name="canvasGroup"></param>
        /// <param name="endValue"></param>
        /// <param name="duration"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        UniTask Fade(CanvasGroup canvasGroup, float endValue, float duration, CancellationToken cancellationToken);

        /// <summary>
        /// UIのサイズ変更
        /// </summary>
        /// <param name="rectTransform"></param>
        /// <param name="endValue"></param>
        /// <param name="duration"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        UniTask SizeDelta(RectTransform rectTransform, Vector2 endValue, float duration,
            CancellationToken cancellationToken);
    }
}

DOTweenExecution

上記のITweenEffectで実際にTween処理を行う、DOTween用の実処理を行うクラスです

DOTweenExecution.cs
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using DG.Tweening;
using TMPro;
using UnityEngine;

namespace _Tween.Scripts.Domain.UI.Tween
{
    /// <summary>
    /// DOTweenのトゥイーン関連処理を実行するクラス
    /// </summary>
    public class DOTweenExecution : ITweenEffect
    {
        private const float defalutFadoInEndValue = 1f;
        private const float defalutFadoInDuration = 0.3f;
        private const float defalutFadoOutEndValue = 0f;
        private const float defalutFadoOutDuration = 0.3f;

        public UniTask SetAlphaZero(CanvasGroup canvasGroup)
        {
            canvasGroup.alpha = 0;
            return UniTask.CompletedTask;
        }

        public UniTask SetSizeDeltaZero(RectTransform rectTransform)
        {
            rectTransform.sizeDelta = Vector2.zero;
            return UniTask.CompletedTask;
        }

        /// <summary>
        /// フェード実行
        /// </summary>
        /// <param name="canvasGroup"></param>
        /// <param name="endValue"></param>
        /// <param name="duration"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask Fade(
            CanvasGroup canvasGroup,
            float endValue,
            float duration,
            CancellationToken cancellationToken
        )
        {
            return canvasGroup.DOFade(endValue, duration)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }

        /// <summary>
        /// フェードイン実行
        /// </summary>
        /// <param name="canvasGroup"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask FadeIn(
            CanvasGroup canvasGroup,
            CancellationToken cancellationToken
        )
        {
            return canvasGroup
                .DOFade(defalutFadoInEndValue, defalutFadoInDuration)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }

        /// <summary>
        /// フェードアウト実行
        /// </summary>
        /// <param name="canvasGroup"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask FadeOut(
            CanvasGroup canvasGroup,
            CancellationToken cancellationToken
        )
        {
            return canvasGroup.DOFade(defalutFadoOutEndValue, defalutFadoOutDuration)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }

        /// <summary>
        /// サイズ変更
        /// </summary>
        /// <param name="rectTransform"></param>
        /// <param name="endValue"></param>
        /// <param name="duration"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask SizeDelta(
            RectTransform rectTransform,
            Vector2 endValue,
            float duration,
            CancellationToken cancellationToken
        )
        {
            return rectTransform.DOSizeDelta(endValue, duration)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }

        /// <summary>
        /// X軸への移動
        /// </summary>
        /// <param name="rectTransform"></param>
        /// <param name="endValue"></param>
        /// <param name="duration"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask LocalMoveX(
            RectTransform rectTransform,
            float endValue,
            float duration,
            CancellationToken cancellationToken
        )
        {
            return rectTransform.DOLocalMoveX(endValue, duration)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }

        /// <summary>
        /// 文字の非表示
        /// </summary>
        /// <param name="textMeshProUGUI"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask HideText(TextMeshProUGUI textMeshProUGUI, CancellationToken cancellationToken)
        {
            return textMeshProUGUI.DOText(String.Empty, 0)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }

        /// <summary>
        /// 文字の表示
        /// </summary>
        /// <param name="textMeshProUGUI"></param>
        /// <param name="text"></param>
        /// <param name="speed"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public UniTask ShowText(TextMeshProUGUI textMeshProUGUI, string text, float speed, CancellationToken cancellationToken)
        {
            return textMeshProUGUI
                .DOText(text, text.Length * speed)
                .SetEase(Ease.Linear)
                .Play()
                .ToUniTask(cancellationToken: cancellationToken);
        }
    }
}

FeedLoopAnimation

上記のITweenEffectの処理を用いてフェードイン・フェードアウトのアニメーションのループ処理を実行するクラスです

FeedLoopAnimation.cs
using System;
using System.Threading;
using _Tween.Scripts.Domain.UI.Tween;
using Cysharp.Threading.Tasks;
using UnityEngine;

namespace _Tween.Scripts.Domain.UI.Effect
{
    /// <summary>
    /// フェードイン・アウトのループアニメーション処理
    /// </summary>
    public class FeedLoopAnimation
    {
        private readonly ITweenEffect _tweenEffect;
        private readonly CanvasGroup _canvasGroup;
        private readonly CancellationToken _cancellationToken;
        private float _duration = 1;

        public FeedLoopAnimation(
            ITweenEffect tweenEffect,
            CanvasGroup canvasGroup,
            CancellationToken cancellationToken,
            float duration = 1
            )
        {
            _tweenEffect = tweenEffect;
            _canvasGroup = canvasGroup;
            _cancellationToken = cancellationToken;
            _duration = duration;
        }

        public async UniTask FadeLoop()
        {
            try
            {
                while (!_cancellationToken.IsCancellationRequested)
                {
                    // Fadeで時間指定を行い、フェードイン・アウトを行う
                    await _tweenEffect.Fade(_canvasGroup, 1, _duration, _cancellationToken);
                    await _tweenEffect.Fade(_canvasGroup, 0, _duration, _cancellationToken);
                }
            }
            catch (OperationCanceledException)
            {
                // キャンセル
            }
        }
    }
}

TitleUI系処理

以下はゲームタイトルのUI系の処理を実装しています。こちらはゲーム内のみの実装のため、namespaceをゲーム名(_MetalCity)としています

TitleLifetimeScope

タイトル用のLifetimeスコープです、MessagePipeとTweenで使用するクラスをDIしています。

TitleLifetimeScope.cs
using _Tween.Scripts.Domain.UI.Tween;
using _MetalCity.Scripts.Structure.Signal;
using VContainer;
using VContainer.Unity;
using MessagePipe;

namespace _MetalCity.Scripts.Installer
{
    /// <summary>
    /// Title Lifetime Scope
    /// </summary>
    public class TitleLifetimeScope : LifetimeScope
    {
        protected override void Configure(IContainerBuilder builder)
        {
            base.Configure(builder);

            // MessagePipeの登録
            var options = builder.RegisterMessagePipe();

            // シグナルの登録
            builder.RegisterMessageBroker<PushStartButton>(options);

            // ITweenEffectにDOTweenExecutionを設定する
            builder.Register<ITweenEffect, DOTweenExecution>(Lifetime.Singleton);
        }
    }
}

StartButton

スタートボタン押下の処理を記述したクラスです、ボタンのクリックの定義はR3で行っています、ボタンはフェードイン・フェードアウトのループ処理を実現するため、FeedLoopAnimationクラスで実施しています。またボタン押下時にMessagePipeでPushStartButtonの構造体を発行しています。

StartButton.cs
using System.Threading;
using _Tween.Scripts.Domain.UI.Effect;
using _Tween.Scripts.Domain.UI.Tween;
using Cysharp.Threading.Tasks;
using MessagePipe;
using UnityEngine;
using UnityEngine.UI;
using VContainer;
using _MetalCity.Scripts.Structure.Signal;

namespace _MetalCity.Scripts.Domain.UI.Title
{
    /// <summary>
    /// タイトルUI スタートボタン押下処理
    /// </summary>
    [RequireComponent(typeof(Button), typeof(CanvasGroup))]
    public class StartButton : MonoBehaviour
    {
        [SerializeField] private Button _button;
        [SerializeField] private CanvasGroup _canvasGroup;
        [SerializeField] private float _duration = 5;

        private ITweenEffect _tweenEffect;
        private IPublisher<PushStartButton> _pushStartButtonPublisher;
        private CancellationTokenSource _cancellationTokenSource;

        [Inject]
        public void Construct(
            ITweenEffect tweenEffect,
            IPublisher<PushStartButton> pushStartButtonPublisher
            )
        {
            _tweenEffect = tweenEffect;
            _pushStartButtonPublisher = pushStartButtonPublisher;
        }

        private void Reset()
        {
            _button = GetComponent<Button>();
            _canvasGroup = GetComponent<CanvasGroup>();
        }

        private void Start()
        {
            _button.onClick.AddListener(PushButton);
            _cancellationTokenSource = new CancellationTokenSource();
            var feedLoopAnimation = new FeedLoopAnimation(_tweenEffect, _canvasGroup, _cancellationTokenSource.Token, _duration);
            feedLoopAnimation.FadeLoop().Forget();
        }

        private void OnDestroy()
        {
            _cancellationTokenSource.Cancel();
        }

        private void PushButton()
        {
            _pushStartButtonPublisher.Publish(new PushStartButton());

            // ボタンインタラクティブを無効化
            _button.interactable = false;
        }
    }
}

GameStartScreen

ゲームスタート後の暗転処理を実施するクラスです、MessagePipeでPushStartButtonの構造体を購読していて、受信するとアニメーション処理を実施します

GameStartScreen.cs
using System.Threading;
using _Tween.Scripts.Domain.UI.Tween;
using _MetalCity.Scripts.Structure.Signal;
using Cysharp.Threading.Tasks;
using MessagePipe;
using R3;
using UnityEngine;
using VContainer;

namespace _MetalCity.Scripts.Domain.UI.Title
{
    /// <summary>
    /// Game Start Screen
    /// </summary>
    [RequireComponent(typeof(CanvasGroup))]
    public class GameStartScreen : MonoBehaviour
    {
        [SerializeField] private CanvasGroup _canvasGroup;

        private float _duration = 1;
        private ITweenEffect _tweenEffect;
        private ISubscriber<PushStartButton> _pushStartButtonSubject;
        private CancellationTokenSource _cancellationTokenSource;

        [Inject]
        public void Construct(
            ITweenEffect tweenEffect,
            ISubscriber<PushStartButton> pushStartButtonSubject
            )
        {
            _tweenEffect = tweenEffect;
            _pushStartButtonSubject = pushStartButtonSubject;
        }

        private void Reset()
        {
            _canvasGroup = GetComponent<CanvasGroup>();
        }

        private async UniTask Start()
        {
            _tweenEffect.SetAlphaZero(_canvasGroup);
            gameObject.SetActive(false);

            _pushStartButtonSubject.Subscribe(foo =>
            {
                Show();

            }).AddTo(this);
        }

        public async UniTask Show()
        {
            gameObject.SetActive(true);
            _cancellationTokenSource = new CancellationTokenSource();
            await _tweenEffect.Fade(_canvasGroup, 1, _duration, _cancellationTokenSource.Token);
        }
    }
}

PushStartButton

ゲームスタートボタン押下時のMessagePipe用で、ボタン押下時のシグナルとしての用途のみの構造体です、

PushStartButton.cs
namespace _MetalCity.Scripts.Structure.Signal
{
    /// <summary>
    /// StartButton Push Signal
    /// </summary>
    public struct PushStartButton
    {

    }
}

Discussion