feature image

2023年4月19日 | ブログ記事

文字送りを作りたい!!!

この記事は新歓ブログリレー42日目の記事です。

はじめに

こんにちは!20Bのurturnです。普段は麻雀やポーカーをしている人です。
ゲーム制作をしていて速度可変の文字送りを実装したいと思ったことがあったので記事にしました。

文字送りとは

ここで指す文字送りとは以下のようなものです。

はい、よくあるアレです。これ自体は以下のようなコードで実装できます。
これを入力に対して速度が可変な文字送りにしたい!というのが今回の記事の目的です。

using TMPro;
using UnityEngine;

public class CaptionManager : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI text;
    [SerializeField] private float timePerCharacter = 0.1f;
    [SerializeField] private string caption = "Hello World!";

    private int _count;
    private float _time;
    
    void Start()
    {
        text.text = caption;
        text.maxVisibleCharacters = 0;
    }
    
    private void Update()
    {
        _time += Time.deltaTime;
        if (_time > timePerCharacter)
        {
            _time = 0;
            _count++;
            text.maxVisibleCharacters = _count;
        }
    }
}

ちなみに、DOTweenのDOTextがとても便利なので、基本はそちらを使った方がいいです。

記事の流れ

環境

ライブラリとしてUniTaskを使用しています。

入力で全表示する文字送り

これはUpdateで入力を受け取ったらmaxVisibleCharactersにtextのLengthを代入するだけでほぼできます(最初のコードだと上書きしてしまうのでカウント方法を変えてください)。
ただ、後半でUniTaskを使った実装をするので、こちらでもそれに則った実装にします。
まず、最初のコードを以下のように書き換えます。

~~
using Cysharp.Threading.Tasks;
~~
    void Start()
    {
        text.text = caption;
        ShowText().Forget();
    }

    private async UniTask ShowText()
    {
        for (int i = 0; i <= caption.Length; i++)
        {
            text.maxVisibleCharacters = i;
            await UniTask.Delay((int)(timePerCharacter * 1000));
        }
    }

Forgetは関数内でawaitしない時につけます。UniTask.Delayで1文字増やすごとにtimePerCharacter秒待っています。
次に以下のようにコードを変えます。

using System.Linq;
using System.Threading;
~~
    // 長くするためなので変えなくても良い
    private string caption = string.Concat(Enumerable.Repeat("Hello World!", 10));
~~
    private CancellationTokenSource _cts;
    
    async void Start()
    {
        text.text = caption;
        _cts = new CancellationTokenSource();
        try
        {
            await ShowText(_cts.Token);
        }
        catch (Exception e) when (!(e is OperationCanceledException))
        {
            Debug.LogException(e);
        }
        catch (OperationCanceledException)
        {
            Debug.Log("OperationCanceledException");
        }
        finally
        {
            text.maxVisibleCharacters = caption.Length;
        }
    }
~~
    private async UniTask ShowText(CancellationToken token = default)
    {
        token.ThrowIfCancellationRequested();
~~
    private void Update()
    {
        _time += Time.deltaTime;
        if (_time > timePerCharacter)
        {
            _time = 0;
            _count++;
            if (_count > 10)
            {
                _cts.Cancel();
                _cts.Dispose();
                _cts = null;
            }
        }
    }

CancellationTokenSourceを用意することで任意のタイミングで終わらせることが可能になっています。finallyにはCancelされた時の処理を記述します。
async-awaitはUniTaskを待つための記述です。UniTaskではForgetで非同期処理を投げっぱなしに出来るのですが、例外をcatchできないのでawaitしています。
実行結果は以下の通りです。

Updateの処理をクリック入力を受け付けるように書き換えます。

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            _cts.Cancel();
            _cts.Dispose();
            _cts = null;
            Debug.Log("Cancel");
        }
    }

できました!!(このままだと二回目以降のクリックでエラーが起こるのできちんと対処してください)

入力がある間速度を変える文字送り

これに関しては色々やり方があります。二つ例を挙げましょう。
1.Update内部で文字速度のパラメータをいじる

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            timePerCharacter = 1f;
        }

        if (Input.GetMouseButtonUp(0))
        {
            timePerCharacter = 0.1f;
        }
    }

2.速度が変更されたものを呼ぶ

以下のように変更すると同様の挙動を再現できます。要約すると、交互に読んでキャンセル、読んでキャンセルをしています。これは、finallyで呼ぶような実装にも書き換えられます。

using System;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using TMPro;
using UnityEngine;
using OperationCanceledException = System.OperationCanceledException;

public class CaptionManager : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI text;
    private string _caption = string.Concat(Enumerable.Repeat("Hello World!", 10));

    private CancellationTokenSource _cts;
    private int _nowCount;
    
    void Start()
    {
        _nowCount = 0;
        text.text = _caption;
        ShowTextFast().Forget();
    }
    
    private async UniTaskVoid ShowTextFast()
    {
        _cts = new CancellationTokenSource();
        try
        {
            await ShowText(0.1f, _cts.Token);
        }
        catch (Exception e) when (!(e is OperationCanceledException))
        {
            Debug.LogException(e);
        }
        catch (OperationCanceledException)
        {
            Debug.Log("OperationCanceledException");
        }
    }

    private async UniTaskVoid ShowTextSlow()
    {
        _cts = new CancellationTokenSource();
        try
        {
            await ShowText(1f, _cts.Token);
        }
        catch (Exception e) when (!(e is OperationCanceledException))
        {
            Debug.LogException(e);
        }
        catch (OperationCanceledException)
        {
            Debug.Log("OperationCanceledException");
        }
    }

    private async UniTask ShowText(float timePerCharacter, CancellationToken token = default)
    {
        token.ThrowIfCancellationRequested();
        for (int i = _nowCount; i <= _caption.Length; i++)
        {
            text.maxVisibleCharacters = i;
            _nowCount = i;
            await UniTask.Delay((int)(timePerCharacter * 1000), cancellationToken: token);
        }
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            _cts.Cancel();
            _cts.Dispose();
            _cts = null;
            ShowTextSlow().Forget();
        }
        else if (Input.GetMouseButtonUp(0))
        {
            _cts.Cancel();
            _cts.Dispose();
            _cts = null;
            ShowTextFast().Forget();
        }
    }
}

入力によって速度を分岐させる文字送り

ここまで読んでくれた人なら何をするのかわかる気がするので、コードと結果だけ載せます。(二回クリックするとエラー起きるコードになってるので使う場合はいい感じに直してください。単純でいいなら_ctsのnull判定するだけで直ります。)

~~
    void Start()
    {
        _nowCount = 0;
        text.text = _caption;
        ShowTextSlow().Forget();
    }
    
    private async UniTaskVoid ShowTextFast(){
    ~~
        await ShowText(1f, _cts.Token);
    ~~
    }

    private async UniTaskVoid ShowTextSlow()
    {
    ~~
        await ShowText(0.1f, _cts.Token);
    ~~
    }
    ~~
    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            _cts.Cancel();
            _cts.Dispose();
            _cts = null;
            text.maxVisibleCharacters = _caption.Length;
        }
        else if (Input.GetKeyDown(KeyCode.Space))
        {
            _cts.Cancel();
            _cts.Dispose();
            _cts = null;
            ShowTextFast().Forget();
        }
    }

さいごに

ゲームの表現はたくさんあって面白いですね!
皆さんもいい表現ができたら共有してください!
お願いします!!
同じ表現だとしても異なる実装があるって結構大事なので!!
。。。では、またどこかで。

urturn icon
この記事を書いた人
urturn

20B ユアターン 麻雀とポーカーしてプログラム書く人です 生きてます

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2023年11月21日
School Breakin' Tag -新感覚おにごっこ-
s9 icon s9
2023年4月17日
ポケモンを飼いたい夢を叶える
tqk icon tqk
2023年9月3日
タイピング&アクション『TypeTheCode』作りました
wal icon wal
2023年4月25日
【驚愕】作曲4年目だった男が大学3年間ゲームサウンドに関わった末路...【ゲームサウンドのお仕事について】
tenya icon tenya
2023年3月20日
traPグラフィック班の活動紹介(Ver.2023)
NABE icon NABE
2023年4月27日
Vulkanのデバイスドライバを自作してみた
kegra icon kegra
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記