【Unity】UniTask+C#7.0でモダンなUpdateLoopを作る

概要

Unity2018.3からC#7.3が使えるようになったので、ようやく堂々とUniTaskが使えるようになりました。

UniTask&async/awaitを使うことでコルーチンを(ほぼ)完全に置き換えることができるため、試しにUpdateを使わず、async/awaitだけでGameObjectを動かす処理を組んでみました。

f:id:notargs:20181219123049g:plain

コード

UpdateLoopメソッドがUpdateに相当する部分です。

MoveToメソッドを呼び出し、次の移動箇所を選定→その位置をへ移動を無限に繰り返しています。

async内で無限ループさせるとゲームが停止されてもスレッドが破棄されないため、CancellationTokenを伝搬させてUniTask.DelayFrameに渡し、GameObjectが死んだときに例外を投げるようにしています。

using System;
using System.Threading;
using UniRx.Async;
using UniRx.Async.Triggers;
using UnityEngine;
using Random = UnityEngine.Random;

public class MainBehaviour : MonoBehaviour
{
    async void Start()
    {
        try
        {
            await UpdateLoop(this.GetCancellationTokenOnDestroy());
        }
        catch (OperationCanceledException)
        {
        }
    }

    async UniTask UpdateLoop(CancellationToken cancellationToken)
    {
        while (true)
        {
            var nextPosition = new Vector3(
                Random.Range(-5.0f, 5.0f),
                Random.Range(-5.0f, 5.0f),
                Random.Range(-5.0f, 5.0f)
            );
            await MoveTo(nextPosition, cancellationToken);
        }
    }

    static float QuadraticEaseInOut(float t) => (1 - Mathf.Pow(1 - Mathf.Abs(t * 2 - 1), 2)) * Mathf.Sign(t - 0.5f) / 2 + 0.5f;
    
    async UniTask MoveTo(Vector3 targetPosition, CancellationToken cancellationToken)
    {
        var startPosition = transform.position;
        for (var time = 0.0f; time < 1.0f; time += Time.deltaTime)
        {
            transform.position = Vector3.Lerp(startPosition, targetPosition, QuadraticEaseInOut(time));
            await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
        }
    }
}

所感

XenkoというPure C#のゲームエンジンがこのような書き方をメインにサポートしていたため、「最新のUnityでもできるのでは?」と思って実装してみました。

やはりCancellationTokenの伝搬や例外処理が面倒ですが、UniTaskのサポートのおかげで少しだけ書きやすくはなっています。

StateMachineのような処理を書くときには便利そうな印象を受けたので、次はもう少し大きめのプロジェクトで試し書きしてみようと思いました。