この記事は新歓ブログリレー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がとても便利なので、基本はそちらを使った方がいいです。
記事の流れ
- 環境
- 入力で全表示する文字送り
- 入力がある間速度を変える文字送り
- 入力によって速度を分岐させる文字送り
環境
- MacOS Ventura 13.2.1
- Unity 2021.3.15.f1
ライブラリとして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();
}
}
さいごに
ゲームの表現はたくさんあって面白いですね!
皆さんもいい表現ができたら共有してください!
お願いします!!
同じ表現だとしても異なる実装があるって結構大事なので!!
。。。では、またどこかで。