見出し画像

【Unity】アニメーションするボタンとウィンドウを作る

UIがアニメーションするとかっこいいので、記事を参考に導入してみましたが、スクロールウィンドウが思ったより手こずってしまいました。


1.ボタン

下の動画のような押すとアニメーションするボタンを作りました。

コードは以下のようになりますが、これはAnimancer Proというアセットを使用しています。

 [Header("Animancer")][SerializeField]AnimancerComponent animancer;

 /////////////////中略///////////////

private void AnimePlay() {
        if (animancer == null) return;
        var state = animancer.Play(pressed, 0.1f);
        state.Events.OnEnd = () => animancer.Play(notPressed, 0.1f);
    }

Animancer Proを使用しない場合は、デフォルトのボタンコンポーネントのボタンアニメーションを使用することになると思います(こちらのほうが簡単かつ手軽)。
そのやり方は以下の記事を参考にしてください。
ねこじゃらシティさんの記事が特にわかりやすいです。

2.ウィンドウ(ダイアログ)

(1)非スクロールウィンドウ

以下のようなアニメーションして表示されるウィンドウを作成しました。

以下のねこじゃらシティさんの記事を参考に、UniTaskとAnimancer Proを使用したスクリプトを作成しました。

このスクリプトはウィンドウではなく、ボタンのほうにアタッチしてください(ウィンドウは始め、非アクティブだと思いますので)。
また、アニメーションクリップは「ループする」のチェックを必ず外してください。

using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using Animancer;
using Cysharp.Threading.Tasks;

public class AnimatedDialog : MonoBehaviour {
    [SerializeField] private AnimancerComponent _animancer;
    [SerializeField] private AnimationClip _openClip;
    [SerializeField] private AnimationClip _closeClip;
    [SerializeField] private Button [] _openButton;
    [SerializeField] private Button [] _closeButton;
    [SerializeField] private GameObject _dialog;
    private CancellationTokenSource _cts;
    // アニメーション中かどうか
    private bool IsTransition;
    // ダイアログは開いているかどうか
    public bool IsOpen => _dialog.activeSelf;
    
    private void Awake() {
        // キャンセルトークンを生成
        _cts = new CancellationTokenSource();
        // ボタンにリスナーを追加
        foreach (var button in _openButton)
            button.onClick.AddListener(() => OpenAsync().Forget());
        foreach (var button in _closeButton)
            button.onClick.AddListener(() => CloseAsync().Forget());
    }

    private void OnDestroy() {
        _cts.Cancel();// キャンセルトークンをキャンセル
        _cts.Dispose();// キャンセルトークンを破棄
        // ボタンのリスナーを削除
        foreach (var button in _openButton)
            button.onClick.RemoveAllListeners();
        foreach (var button in _closeButton)
            button.onClick.RemoveAllListeners();
    }

    public async UniTask OpenAsync() {
        if (IsOpen || IsTransition) return;
        IsTransition = true;
        _dialog.SetActive(true);
        //ボタンを押せなくする
        foreach (var button in _openButton)
            button.enabled = false;

        if (_animancer == null) return;
        
        // 開くアニメーションの再生、アニメーションのループは外す
        _animancer.Play(_openClip);
        
        // 開くアニメーションが終了したらIdleに戻る
        IsTransition = false;

        //ボタンを押せるようにする
        foreach (var button in _openButton)
            button.enabled = true;
    }

    public async UniTask CloseAsync() {
        if (!IsOpen || IsTransition) return;
        IsTransition = true;
        //ボタンを押せなくする
        foreach (var button in _closeButton)
            button.enabled = false;

        if (_animancer == null) return;

        _animancer.Play(_closeClip);
        
        _dialog.SetActive(false); // アニメーションが終了した後にダイアログを非表示
        IsTransition = false;

        //ボタンを押せるようにする
        foreach (var button in _closeButton)
            button.enabled = true;
    }
}

(2)スクロールビュー

スクロールビューも同じように、アニメーションさせて表示させようと思ったのですが…できませんでした。
理由はScrollResetterと絶望的に相性が悪かったからです。

↓ScrollResetterの内容についてはこちら

スクロールビューのScrollRectを一番上に戻すスクリプトです。(1)と同じ方法で、スクロールビューを表示させると、ScrollRectが中途半端な位置で止まってしまいました。
どうやらスクロールビュー全体のスケールの値をいじって、表示させているのに原因がありそうでしたが、上手く解消できませんでした。

ただ、表示時にアニメーションはさせたかったので、TimeLineを使用し、上から移動してくるようにしました。

クレジットボタンをクリックしたときにScrollResetterとConveniButtonのDelayPlayTimelineを実行しています。

↓ConveniButtonについてはこちら

2028.8.26追記

以下のScrollResetterを以下のように改良し、アニメーションするダイアログでも問題なく、スクロールビューが一番上に戻るようになりました。

using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;

public class ScrollResetter : MonoBehaviour{
    [Header("位置をリセットするツマミを登録")] public ScrollRect scrollRect;
    //クリックするとスクロールビューが開くボタン等にセット。
    //スクロールビューが開くと同時にSetActiveAndResetScrollを実行。
    private Button button;

    //このスクリプトが有効になったときにボタンを取得
    private void OnEnable() {
        button = GetComponent<Button>();
        // ボタンがあればイベントリスナーを追加
        if (button != null){
            button.onClick.AddListener(SetActiveAndResetScroll);
        }
    }

    private async void ResetScrollToTop() {
        Canvas.ForceUpdateCanvases();  // すべてのキャンバスの更新を強制
        await UniTask.Yield();  // 1フレーム待機してレイアウト更新を待つ
        // ScrollRectのcontentの位置をリセット
        if (scrollRect != null) {
            scrollRect.verticalNormalizedPosition = 1.0f;
        }
    }
    
    public async void SetActiveAndResetScroll() {
        // 親オブジェクトのスケールを変更するアニメーションを開始
        var parentTransform = scrollRect.transform.parent;
        if (parentTransform != null) {
            await ScaleParentObjectAsync(parentTransform);
        }
        
        // スケール変更後にスクロール位置をリセット
        ResetScrollToTop();
    }

    private async UniTask ScaleParentObjectAsync(Transform parentTransform) {
        Vector3 originalScale = parentTransform.localScale;
        Vector3 targetScale = new Vector3(1.0f, 1.0f, 1.0f);  // 目標のスケールを設定

        float duration = 0.1f;  // アニメーションの期間
        float elapsedTime = 0f;

        while (elapsedTime < duration) {
            elapsedTime += Time.deltaTime;
            float t = Mathf.Clamp01(elapsedTime / duration);
            parentTransform.localScale = Vector3.Lerp(originalScale, targetScale, t);
            await UniTask.Yield();  // フレームをまたいで処理
        }

        parentTransform.localScale = targetScale;  // 最終的なスケールを設定
    }
}

いいなと思ったら応援しよう!