【Unity】Timelineにテロップ用テキストデータを一括で流し込む

## 課題
- UnityのTimelineを用いたテキスト表示(字幕、テロップ、歌詞、etc..)で、大量のClipを手で打ち込む作業を効率化したい
- 表示するテキストが変わった際に一気に読み込み直せるようにしたい

## アウトプット

![area_area_short.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21406/3b234d2f-06b3-31c3-d622-f07cda872b41.gif)

## アプローチ

- カラオケ風の歌詞表示において、歌詞を一気にTimelineに流し込む機能を作る
- StartとEndを指定して(SectionTrackで指定)、その間に等分に文字表示Clipを配置していくことで、歌詞表示のたたき台を作り、その後微調整でタイミングを修正出来るようにする

![スクリーンショット 2022-12-17 22.56.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21406/c13a6966-02b7-81c6-2c6d-11b87875e5ef.png)


### 歌詞テキストデータ

Area Area feat. ZORNを引用
![スクリーンショット 2022-12-17 22.46.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21406/ebb7bc2f-4dc0-0133-d77f-0f361cf3fb1f.png)

### 歌詞モデルクラス

```csharp
using System.Collections.Generic;
using System.IO;
using UnityEngine.Assertions;

public class Lyric
{
    public class Bundle
    {
        public SectionType SectionType { get; set; }
        public List<string> LyricTexts { get; set; } = new List<string>();
    }

    public List<Bundle> LyricBundles { get; set; } = new List<Bundle>();

    public static Lyric Convert(string lyricText)
    {
        var lyric = new Lyric();
        var readingBundle = default(Bundle);
        var reader = new StringReader(lyricText);
        while (reader.Peek() > 0)
        {
            var line = reader.ReadLine();
            switch (line)
            {
                case "[Intro]":
                    readingBundle = new Bundle() { SectionType = SectionType.Intro };
                    lyric.LyricBundles.Add(readingBundle);
                    break;
                case "[Hook]":
                    readingBundle = new Bundle() { SectionType = SectionType.Hook };
                    lyric.LyricBundles.Add(readingBundle);
                    break;
                case "[Verse]":
                    readingBundle = new Bundle() { SectionType = SectionType.Verse };
                    lyric.LyricBundles.Add(readingBundle);
                    break;
                case "[Outro]":
                    readingBundle = new Bundle() { SectionType = SectionType.Outro };
                    lyric.LyricBundles.Add(readingBundle);
                    break;
                case "":
                    break;
                default:
                    Assert.IsNotNull(readingBundle);
                    readingBundle.LyricTexts.Add(line);
                    break;
            }
        }

        return lyric;
    }
}
```

### TimelineClipへの変換処理

```csharp
using System.Linq;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(UILyricTrack))]
public class UILyricTrackEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        if (GUILayout.Button("Import"))
        {
            var track = target as UILyricTrack;
            var sectionTrack = track.timelineAsset
                .GetOutputTracks()
                .FirstOrDefault(x => x.name == "Section");
            if (sectionTrack == null) return;
            var sectionTimelineClips = sectionTrack.GetClips();
            if (!sectionTimelineClips.Any()) return;
 
            var lyric = Lyric.Convert(track.LyricTextAsset.text);
            for (var bundleIndex = 0; bundleIndex < lyric.LyricBundles.Count; bundleIndex++)
            {
                var sectionTimelineClip = sectionTimelineClips.ElementAt(bundleIndex);
                if (sectionTimelineClip == null) continue;
                
                var sectionClip = (SectionClip)sectionTimelineClip.asset;
                var bundle = lyric.LyricBundles[bundleIndex];
                if (sectionClip.template.Type != bundle.SectionType) return;

                for (var lyricIndex = 0; lyricIndex < bundle.LyricTexts.Count; lyricIndex++)
                {
                    var clipAsset = track.CreateDefaultClip();
                    var duration = sectionTimelineClip.duration / bundle.LyricTexts.Count;
                    clipAsset.start = sectionTimelineClip.start + lyricIndex * duration;
                    clipAsset.duration = duration;
                    ((UILyricClip)clipAsset.asset).template.lyric = bundle.LyricTexts[lyricIndex];
                }
            }
        }
    }
}

```

### まとめ

- Clipを仮置き出来るだけでもだいぶ楽
- HIPHOPのような、BPMが一定の音楽であれば仮置段階でも十分に歌詞表示として機能しそう