Skip to content

Instantly share code, notes, and snippets.

@nekomimi-daimao
Last active July 8, 2021 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nekomimi-daimao/1dece355acf66b84aaaaa185cff6e271 to your computer and use it in GitHub Desktop.
Save nekomimi-daimao/1dece355acf66b84aaaaa185cff6e271 to your computer and use it in GitHub Desktop.
Unity. 最初の1文字を入力してから一定時間内に特定のキーを入力したかを判定する.途中で違うキーを押すかタイムアウトすると今まで入力した値はクリアされる.
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Assertions;
namespace Nekomimi.Daimao
{
/// <summary>
/// 最初の1文字を入力してから一定時間内に特定のキーを入力したかを判定する.
/// 途中で違うキーを押すかタイムアウトすると今まで入力した値はクリアされる.
/// </summary>
public class TypeChecker
{
/// <summary>
/// A-Z
/// </summary>
private static readonly KeyCode[] KeyCodeAlphabet =
{
KeyCode.A,
KeyCode.B,
KeyCode.C,
KeyCode.D,
KeyCode.E,
KeyCode.F,
KeyCode.G,
KeyCode.H,
KeyCode.I,
KeyCode.J,
KeyCode.K,
KeyCode.L,
KeyCode.M,
KeyCode.N,
KeyCode.O,
KeyCode.P,
KeyCode.Q,
KeyCode.R,
KeyCode.S,
KeyCode.T,
KeyCode.U,
KeyCode.V,
KeyCode.W,
KeyCode.X,
KeyCode.Y,
KeyCode.Z,
};
/// <summary>
/// インスタンスが要求するキーの順番.
/// </summary>
public readonly KeyCode[] Command;
/// <summary>
/// インスタンスに設定されたタイムアウト(秒).
/// 0以下の場合はタイムアウトしない.
/// </summary>
public readonly float TimeoutSecond;
/// <summary>
/// キーが要求通りに入力されたときのコールバック.
/// </summary>
public Action OnComplete = default;
/// <summary>
/// コンストラクタ.
/// </summary>
/// <param name="command"><see cref="Command"/></param>
/// <param name="timeoutSecond"><see cref="TimeoutSecond"/></param>
/// <param name="progress"><see cref="IProgress{T}"/> (分子/分母)</param>
/// <param name="token"><see cref="CancellationToken"/></param>
public TypeChecker(KeyCode[] command, float timeoutSecond,
IProgress<(int numerator, int denominator)> progress, CancellationToken token)
{
Assert.IsNotNull(command);
Assert.IsTrue(command.Length > 0);
Command = command;
TimeoutSecond = timeoutSecond;
CheckTypeLoop(progress, token).Forget();
}
private async UniTaskVoid CheckTypeLoop(IProgress<(int numerator, int denominator)> progress, CancellationToken baseToken)
{
var first = Command[0];
while (true)
{
await UniTask.Yield();
if (baseToken.IsCancellationRequested)
{
return;
}
if (CurrentPressed() != first)
{
// 最初の1文字が入力されるまではタイムアウトも以後の判定も行わない
progress?.Report((0, Command.Length));
continue;
}
// 最初の1文字が入力された
progress?.Report((1, Command.Length));
var cancelSource = CancellationTokenSource.CreateLinkedTokenSource(baseToken);
var token = cancelSource.Token;
bool result;
// 1文字目以降のキーが順番に入力されたら完了する
var type = TypeCommandAsync(progress, token);
if (TimeoutSecond > 0f)
{
// タイムアウトが設定されている場合はカウントする
var timeout = UniTask.Delay(TimeSpan.FromSeconds(TimeoutSecond), cancellationToken: token);
var (hasResultLeft, ret) = await UniTask.WhenAny(type, timeout);
// キー入力のtaskが先に完了してかつそれがtrue
result = hasResultLeft && ret;
}
else
{
// タイムアウトを待たない場合はキー入力の結果だけを待つ
result = await type;
}
if (result)
{
OnComplete?.Invoke();
}
if (!cancelSource.IsCancellationRequested)
{
cancelSource.Cancel();
}
}
}
/// <summary>
/// キー入力を監視して正しく入力されたかを判定する.
/// 最初の1文字は判定しない.
/// </summary>
/// <param name="progress"><see cref="IProgress{T}"/> (分子/分母)</param>
/// <param name="token"><see cref="CancellationToken"/></param>
/// <returns>true/false = 成功/失敗</returns>
private async UniTask<bool> TypeCommandAsync(
IProgress<(int numerator, int denominator)> progress, CancellationToken token)
{
for (var count = 1; count < Command.Length; count++)
{
while (true)
{
await UniTask.Yield();
if (token.IsCancellationRequested)
{
return false;
}
var current = CurrentPressed();
if (current == KeyCode.None)
{
// 何も入力していない場合は判定しない
continue;
}
if (Command[count] != current)
{
// 違うキーが入力されたので中断する
return false;
}
// 求めているキーが入力されたので進捗を通知して次のキーに進む
progress?.Report((count + 1, Command.Length));
break;
}
}
return !token.IsCancellationRequested;
}
/// <summary>
/// <see cref="KeyCodeAlphabet"/>に含まれている現在押されているキーを返す.
/// </summary>
/// <returns><see cref="KeyCode"/></returns>
private static KeyCode CurrentPressed()
{
if (!Input.anyKeyDown)
{
return KeyCode.None;
}
// every Update, no LINQ
foreach (var keyCode in KeyCodeAlphabet)
{
if (Input.GetKeyDown(keyCode))
{
return keyCode;
}
}
return KeyCode.None;
}
}
}
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace Nekomimi.Daimao
{
public class TypeCheckerExample : MonoBehaviour, IProgress<(int numerator, int denominator)>
{
private static readonly KeyCode[] Command =
{
KeyCode.N,
KeyCode.E,
KeyCode.K,
KeyCode.O,
KeyCode.M,
KeyCode.I,
KeyCode.M,
KeyCode.I,
};
[SerializeField]
private Slider _slider = default;
[SerializeField]
private Text _textSlider = default;
[SerializeField]
private Text _textComplete = default;
private void Start()
{
var typeChecker = new TypeChecker(Command, 5f, this, this.GetCancellationTokenOnDestroy());
typeChecker.OnComplete += () =>
{
_textComplete.text = "COMPLETE!";
Debug.Log("COMPLETE!");
};
}
public void Report((int numerator, int denominator) value)
{
var (numerator, denominator) = value;
_slider.maxValue = denominator;
_slider.value = numerator;
_textSlider.text = $"{numerator} / {denominator}";
Debug.Log($"progress {numerator} / {denominator}");
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment