この記事はUnityAdventCalendar 2024シリーズ3の記事です。
記事を書く余裕が出たのと枠が空いてたので参加しました。
12/30 更新
Unity6とUnity2022後期バージョンで動かなかったのでスクリプトに修正を入れました
- やること
- 開発環境
- UIの作成(動作確認用)
- 必要なコンポーネントの追加
- スクリプトの作成(シンプルなやつ)
- スクリプトの作成(UniTask + DOTWeen)
- おまけ
- Unity 6用の修正(12/30)
- 過去のUnity アドベントカレンダー記事
- 参考資料
やること
表示範囲からオーバーフローしてるテキストをスクロールしてループ表示する仕組みを作成します。
ソースコードをそのままコピペするだけで動きます。
開発環境
Unity 2022.3.14f1
TextMeshPro ver3.0.9
UIの作成(動作確認用)
動作確認用のUIを作成します。
CanvasとImageの作成
シーンにCanvasを作成(CanvasScalerはお好みで)
その子にImageを作成し大きさを調整します、これが文字の表示範囲になります。
Imageの色は文字が見やすいように半透明の黒にしておきます
Text(TMP)の作成
Imageの子にText(TMP)を作成。
余白が少し欲しいのでPosXを50に。
AnchorをLeft-Middleに、PivotのXを0に、AlignmentをLeft-Middleに設定します。
必要なコンポーネントの追加
RectMask2D
ImageにRectMask2Dコンポーネントをアタッチ
これでImageの範囲外に出たTextがマスクされるようになります
ContentSizeFitter
Text(TMP)にContentSizeFitterコンポーネントをアタッチ
HorizontalFitをPreferredSizeに変更
これでText(TMP)のWidthが文字数に応じて自動調整されるようになります
スクリプトの作成(シンプルなやつ)
ここから本題。最初はシンプルなスクリプトでやります。
UniTaskなどのライブラリやアセットを使っていないシンプルなやつなので、
このままコピペすればどのプロジェクトでも動作します(たぶん)
以下のTextScroller.csを作成。Text(TMP)にアタッチします。
using System.Collections; using UnityEngine; using UnityEngine.UI; using TMPro; /// <summary> /// テキストのスクロール /// TMP_Textにアタッチしてください /// </summary> [RequireComponent(typeof(CanvasGroup))] public class TextScroller : MonoBehaviour { [SerializeField] private float scrollSpeed = 100f; // スクロール速度 [SerializeField] private float scrollFinishLineAddValue = 50; // スクロール終了判定までのマージン、0だと全て表示された時点で終了する [SerializeField] private float waitTimeBeforeScroll = 2f; // スクロール開始前の待機時間 [SerializeField] private float waitTimeAfterScroll = 2f; // スクロール完了後、フェードアウトするまでの待機時間 [SerializeField] private float fadeDuration = 0.4f; // テキストのフェードインアウト時間 [SerializeField] private float waitTimeFade = 0.2f; // フェードアウトしてからフェードインするまでの待機時間 private CanvasGroup canvasGroup; private RectTransform textRectTransform; private RectTransform parentRectTransform; private Vector3 startPosition; // スタート位置 private float scrollValue; // スクロール値 private float finishLineValue; // スクロールが停止する位置、scrollValueがこの値を超えたら停止する private float textWidth; private float parentWidth; private bool isScrolling = false; private void Start() { Initialize(); } private void Update() { if (!isScrolling) return; // 停止位置までスクロールする if (scrollValue + parentWidth >= finishLineValue) { isScrolling = false; StartCoroutine(Fade(0, waitTimeAfterScroll)); Invoke(nameof(ResetScrollPosition), waitTimeAfterScroll + fadeDuration + waitTimeFade); } else { scrollValue += scrollSpeed * Time.deltaTime; textRectTransform.anchoredPosition = new Vector3(startPosition.x - scrollValue, startPosition.y, startPosition.z); } } /// <summary> /// スクロールの初期設定 /// </summary> private void Initialize() { if (!TryGetComponent<TMP_Text>(out var textComponent)) { Debug.LogWarning("TMP_Textがアタッチされていないのでスクロールは機能しません"); return; } // ContentSizeFitter手動更新 var contentSizeFitter = GetComponent<ContentSizeFitter>(); contentSizeFitter.SetLayoutHorizontal(); contentSizeFitter.SetLayoutVertical(); LayoutRebuilder.ForceRebuildLayoutImmediate(contentSizeFitter.GetComponent<RectTransform>()); canvasGroup = GetComponent<CanvasGroup>(); textRectTransform = textComponent.GetComponent<RectTransform>(); parentRectTransform = textComponent.transform.parent.GetComponent<RectTransform>(); textWidth = textRectTransform.rect.width; parentWidth = parentRectTransform.rect.width; startPosition = textRectTransform.anchoredPosition; finishLineValue = textWidth + startPosition.x + scrollFinishLineAddValue; if (textWidth + startPosition.x > parentWidth) { Invoke(nameof(StartScrolling), waitTimeBeforeScroll); } } /// <summary> /// スクロール開始 /// </summary> private void StartScrolling() { scrollValue = 0; isScrolling = true; } /// <summary> /// スクロールリセット /// </summary> private void ResetScrollPosition() { textRectTransform.anchoredPosition = startPosition; StartCoroutine(Fade(1, 0)); Invoke(nameof(StartScrolling), waitTimeBeforeScroll + fadeDuration); } /// <summary> /// フェード処理コルーチン /// </summary> private IEnumerator Fade(float targetAlpha, float delayTime) { var elapsedTime = 0f; var startAlpha = canvasGroup.alpha; yield return new WaitForSeconds(delayTime); while (elapsedTime < fadeDuration) { elapsedTime += Time.deltaTime; canvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, elapsedTime / fadeDuration); yield return null; } canvasGroup.alpha = targetAlpha; } }
Text(TMP)にアタッチすると、自動的にCanvasGroupもアタッチされます。
(パラメータの詳細はソースコードのコメントから)
この状態でゲームを実行するとこんな感じ。
デフォルトのパラメータだと、実行後2秒後に速度100でスクロールし、
一番最後の文字の位置+50までスクロールしたら停止、停止してから2秒後、0.4秒かけてフェードアウトし、
フェードアウトしきってから0.2秒後に位置をリセットした状態で0.4秒かけてフェードイン。
以降繰り返し....
スクリプトの作成(UniTask + DOTWeen)
次はUniTaskとDOTweenが入っているプロジェクト用のスクリプトです。
ProjectSettingsの設定
DOTweenの処理をawaitしたいので、ProjectSettingsを開き、
Player/ScriptCompilationのScriptingDefineSymbolsに、UNITASK_DOTWEEN_SUPPORTを追加。
スクリプト
以下のTextScroller_UniTask.csを作成。Text(TMP)にアタッチします。
using System; using System.Threading; using UnityEngine; using UnityEngine.UI; using TMPro; using Cysharp.Threading.Tasks; using DG.Tweening; /// <summary> /// テキストのスクロール /// TMP_Textにアタッチしてください /// </summary> [RequireComponent(typeof(CanvasGroup))] public class TextScroller_UniTask : MonoBehaviour { [SerializeField] private float scrollSpeed = 100f; // スクロール速度 [SerializeField] private float scrollFinishLineAddValue = 50; // スクロール終了判定までのマージン、0だと全て表示された時点で終了する [SerializeField] private float waitTimeBeforeScroll = 2f; // スクロール開始前の待機時間 [SerializeField] private float waitTimeAfterScroll = 2f; // スクロール完了後、フェードアウトするまでの待機時間 [SerializeField] private float fadeDuration = 0.4f; // テキストのフェードインアウト時間 [SerializeField] private float waitTimeFade = 0.2f; // フェードアウトしてからフェードインするまでの待機時間 private CanvasGroup canvasGroup; private RectTransform textRectTransform; private Vector3 startPosition; // スタート位置 private float finishLineValue; // スクロールが停止する位置、scrollValueがこの値を超えたら停止する private void Start() { if (!TryGetComponent<TMP_Text>(out var textComponent)) { Debug.LogWarning("TMP_Textがアタッチされていないのでスクロールは機能しません"); return; } // ContentSizeFitter手動更新 var contentSizeFitter = GetComponent<ContentSizeFitter>(); contentSizeFitter.SetLayoutHorizontal(); contentSizeFitter.SetLayoutVertical(); LayoutRebuilder.ForceRebuildLayoutImmediate(contentSizeFitter.GetComponent<RectTransform>()); canvasGroup = GetComponent<CanvasGroup>(); textRectTransform = textComponent.GetComponent<RectTransform>(); var textWidth = textRectTransform.rect.width; var parentWidth = textComponent.transform.parent.GetComponent<RectTransform>().rect.width; startPosition = textRectTransform.anchoredPosition; finishLineValue = startPosition.x - (textWidth + startPosition.x + scrollFinishLineAddValue - parentWidth); if (textWidth + startPosition.x > parentWidth) { Scroll(this.GetCancellationTokenOnDestroy()).Forget(); } } /// <summary> /// スクロール処理 /// </summary> private async UniTaskVoid Scroll(CancellationToken token) { while (true) { await UniTask.Delay(TimeSpan.FromSeconds(waitTimeBeforeScroll), cancellationToken: token); await textRectTransform.DOAnchorPosX(finishLineValue, scrollSpeed).SetSpeedBased().SetEase(Ease.Linear).ToUniTask(cancellationToken: token); // フェードインフェードアウト await UniTask.Delay(TimeSpan.FromSeconds(waitTimeAfterScroll), cancellationToken: token); await canvasGroup.DOFade(0, fadeDuration).SetEase(Ease.Linear); textRectTransform.anchoredPosition = startPosition; await UniTask.Delay(TimeSpan.FromSeconds(waitTimeFade), cancellationToken: token); await canvasGroup.DOFade(1, fadeDuration).SetEase(Ease.Linear).ToUniTask(cancellationToken: token); } } }
動作などはTextScroller.csと全く同じです。こっちの方が簡潔かも。
おまけ
もっといい感じにしたい人向け
背景をグラデーション画像にしMaskの境目を調整
Imageの画像をこんな感じのグラデーション画像に変更
RectMask2DのSoftnessの値を調整
これで、表示範囲との境界線で文字がフェードするので見栄えがちょっと良くなります
TextMeshProにRectMask2DのSoftnessが反映されない時(Unity2022用の追加対応)
※Unity6だと必要ありません
Unity2022、厳密にはTextMeshProのver3.0.9(Unity2022の最新バージョン)だと、
RectMask2DのSoftnessが反映されません。
こちらの記事に従い、TextMeshProを更新すれば反映されます
背景と表示範囲を分ける
確認用UIでは、ImageにRectMask2Dをつけていましたがこれを分けます。
これは後ろ側の余白をきちんと取りたい時に有効です。
まずImageのRectMask2Dを削除。
Imageの子に、空のGameObjectを作成し名前を「Mask」に変更。
このMaskにRectMask2Dコンポーネントをアタッチします。
Unity 6用の修正(12/30)
Unity6、および2022の後期バージョンだと発生する問題がありました。
これらでは、ContentSizeFitterのサイズ調整が即座に行われず、
1フレーム待たないと正常なWidthの値が取れない状態でした。
そこで上記のスクリプトに以下のコードを入れて対応しました。
これを呼び出すと即座にサイズが更新されます。
using UnityEngine.UI; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ContentSizeFitter手動更新 var contentSizeFitter = GetComponent<ContentSizeFitter>(); contentSizeFitter.SetLayoutHorizontal(); contentSizeFitter.SetLayoutVertical(); LayoutRebuilder.ForceRebuildLayoutImmediate(contentSizeFitter.GetComponent<RectTransform>());
過去のUnity アドベントカレンダー記事
参考資料
以上です