Unity3D用のシンプルな非同期タスクマネージャー

はじめに



読者の皆様、ご挨拶。 この記事では、 Unity3d開発向けのシンプルな非同期タスクマネージャーの実装について説明します。 このマネージャーは基本的に、エンジンに存在するいわゆるCoroutineを使用します。



いつものように、実装を説明して詳細に入る前に、私たちが何をしているのか、なぜそれが必要なのかを理解する必要があります。



ゲームを開発するときに多くの人が出会ったと思う簡単な例を考えてみましょう。 一連のアクションを実行する必要のある特定のキャラクターがいます。ポイントAに移動し、オブジェクトを取得し、ポイントBに移動し、オブジェクトを配置します。 ご覧のとおり、これは通常のシーケンスです。 条件チェック付きの1つの更新で最も基本的なオプションとして、さまざまな方法でコードに実装できます。 ただし、このようなキャラクターがたくさんあり、アクションもたくさんあると事態は複雑になります。 コードにそのようなキャラクターを伝え、一連のアクションを順番に実行し、完了したら知らせてほしいのですが、今のところは他のことを行います。 この場合、非同期アプローチが役立ちます。 現時点では、これを可能にする多くの異なるシステム( Unityを含む)があります。たとえば、 UniRx (リアクティブ非同期プログラミング)です。 しかし、初心者の開発者にとっては、こうしたことをすべて理解して習得することは非常に難しいため、エンジン自体が提供するもの、つまりCoroutineを活用してみましょう。



:上記の例は多くの内の1つに過ぎませんが、同様の状況を説明できる多くの領域があります:ネットワーク上のリソースの順次(または並列)ロード、相互依存サブシステムの初期化、UIでのアニメーションなど。



実装



コードを記述してC#の詳細に入る前に、アーキテクチャと用語について詳しく説明します。



そのため、上記で私が例として書いたのは、彼が演じなければならないキャラクターの行動です。 この記事で説明するシステムのフレームワークでは、このアクションはキャラクターが実行しなければならない特定のタスクです。 この概念を一般化する場合、タスクはゲームロジックの任意のエンティティによって実行できるアクションです。 タスクは次のルールに従う必要があります。





これらのルールは、インターフェースを介して説明します。
public interface ITask { void Start(); ITask Subscribe(Action completeCallback); void Stop(); }
      
      







SubscribeがITaskを返すのはなぜですか? ビューデザインを作成できるため、使いやすさが向上します。



 ITask myTask; myTask.Subscribe(() => Debug.Log(“Task Complete”)).Start();
      
      





タスクのインターフェイスは作成されましたが、重要なものが1つ欠落しています。これは実行優先度です。 それは何のためですか? キャラクターにタスクを設定するときの状況を想像してください。論理的には、彼はすべてのタスクを停止し、ゲームプロセスにとって重要な別のタスクを完了する必要があります。 この場合、現在のチェーンを完全に停止し、新しいタスクを完了する必要があります。 説明されている例は、いくつかの動作の1つにすぎません;さらに、優先順位には次のものが含まれる場合があります。





優先順位を考慮して、タスクインターフェイスは最終的な形式を取ります。
 public enum TaskPriorityEnum { Default, High, Interrupt } public interface ITask { TaskPriorityEnum Priority { get; } void Start(); ITask Subscribe(Action feedback); void Stop(); }
      
      







そのため、タスクが何であるかを共通に理解することにしました。今、特定の実装が必要です。 前述のように、このシステムではコルーチンが使用されます。 Coroutineは、単純な意味では、コルーチン(文字通り翻訳された場合)であり、基本的にスレッドを実行しますが、ブロックはしません。 イテレータ(IEnumerator)の使用により、 yield return呼び出しが内部で行われる場合、このコルーチンへの戻りは各フレームで発生します。



ITaskインターフェイスを実装するTaskクラスを実装します
 public class Task : ITask { public TaskPriorityEnum Priority { get { return _taskPriority; } } private TaskPriorityEnum _taskPriority = TaskPriorityEnum.Default; private Action _feedback; private MonoBehaviour _coroutineHost; private Coroutine _coroutine; private IEnumerator _taskAction; public static Task Create(IEnumerator taskAction, TaskPriorityEnum priority = TaskPriorityEnum.Default) { return new Task(taskAction, priority); } public Task(IEnumerator taskAction, TaskPriorityEnum priority = TaskPriorityEnum.Default) { _coroutineHost = TaskManager.CoroutineHost; _taskPriority = priority; _taskAction = taskAction; } public void Start() { if (_coroutine == null) { _coroutine = _coroutineHost.StartCoroutine(RunTask()); } } public void Stop() { if (_coroutine != null) { _coroutineHost.StopCoroutine(_coroutine); _coroutine = null; } } public ITask Subscribe(Action feedback) { _feedback += feedback; return this; } private IEnumerator RunTask() { yield return _taskAction; CallSubscribe(); } private void CallSubscribe() { if (_feedback != null) { _feedback(); } } }
      
      







コードに関する簡単な説明:





残りについては、飾り気のない、かなりシンプルで理解可能なコードです。



そのため、タスクインターフェースについて説明し、それを実装するクラスの実装を作成しました。ただし、これは本格的なシステムには十分ではありません。優先順位に従ってチェーン内のタスクの実行を監視するマネージャーが必要です。 どのゲームでも、独自のタスクマネージャーを必要とするサブシステムが多数存在する可能性があるため、通常のクラスの形式で実装し、必要なすべてのユーザーがそのコピーを作成して保存します。



タスクマネージャクラスの実装。
 public class TaskManager { public ITask CurrentTask { get { return _currentTask; } } private ITask _currentTask; private List<ITask> _tasks = new List<ITask>(); public void AddTask(IEnumerator taskAction, Action callback, TaskPriorityEnum taskPriority = TaskPriorityEnum.Default) { var task = Task.Create(taskAction, taskPriority).Subscribe(callback); ProcessingAddedTask(task, taskPriority); } public void Break() { if(_currentTask != null) { _currentTask.Stop(); } } public void Restore() { TaskQueueProcessing(); } public void Clear() { Break(); _tasks.Clear(); } private void ProcessingAddedTask(ITask task, TaskPriorityEnum taskPriority) { switch(taskPriority) { case TaskPriorityEnum.Default: { _tasks.Add(task); } break; case TaskPriorityEnum.High: { _tasks.Insert(0, task); } break; return; case TaskPriorityEnum.Interrupt: { if (_currentTask != null && _currentTask.Priority != TaskPriorityEnum.Interrupt)) { _currentTask.Stop(); } _currentTask = task; task.Subscribe(TaskQueueProcessing).Start(); } break; } if(_currentTask == null) { _currentTask = GetNextTask(); if (_currentTask != null) { _currentTask.Subscribe(TaskQueueProcessing).Start(); } } } private void TaskQueueProcessing() { _currentTask = GetNextTask(); if(_currentTask != null) { _currentTask.Subscribe(TaskQueueProcessing).Start(); } } private ITask GetNextTask() { if (_tasks.Count > 0) { var returnValue = _tasks[0]; _tasks.RemoveAt(0); return returnValue; } else { return null; } } }
      
      







以下のコードを分析してみましょう。





それ以外の場合、 Taskクラスの場合のように、コードは非常に原始的ですが、これがこの記事の目的でした。



使用する



上記のシステムを使用する方法と場所の簡単な例を見てみましょう。
 public class TaskManagerTest : MonoBehaviour { public Button StartTaskQueue; public Button StopTaskQueue; public Image TargetImage; public Transform From; public Transform To; private TaskManager _taskManager = new TaskManager(); private void Start() { StartTaskQueue.onClick.AddListener(StartTaskQueueClick); StopTaskQueue.onClick.AddListener(StopTaskQueueClick); } private void StartTaskQueueClick() { _taskManager.AddTask(MoveFromTo(TargetImage.gameObject.transform, From.position, To.position, 2f)); _taskManager.AddTask(AlphaFromTo(TargetImage, 1f, 0f, 0.5f)); _taskManager.AddTask(Wait(1f)); _taskManager.AddTask(AlphaFromTo(TargetImage, 0f, 1f, 0.5f)); _taskManager.AddTask(MoveFromTo(TargetImage.gameObject.transform, To.position, From.position, 2f)); } private void StopTaskQueueClick() { if (_taskManager.CurrentTask != null) { _taskManager.Break(); }else { _taskManager.Restore(); } } private IEnumerator Wait(float time) { yield return new WaitForSeconds(time); } private IEnumerator MoveFromTo(Transform target, Vector3 from, Vector3 to, float time) { var t = 0f; do { t = Mathf.Clamp(t + Time.deltaTime, 0f, time); target.position = Vector3.Lerp(from, to, t / time); yield return null; } while (t < time); } private IEnumerator AlphaFromTo(Image target, float from, float to, float time) { var imageColor = target.color; var t = 0f; do { t = Mathf.Clamp(t + Time.deltaTime, 0f, time); imageColor.a = Mathf.Lerp(from, to, t / time); target.color = imageColor; yield return null; } while (t < time); } }
      
      







それで、このコードは何をしますか。 StartTaskQueueボタンをクリックすると、 画像TargetImage )オブジェクトを操作するための一連のタスクが起動します。





[ StopTaskQueue ]ボタンをクリックすると、マネージャーにアクティブなタスクがある場合は現在のタスクチェーンが停止し、アクティブでない場合はタスクチェーンが復元されます(可能な場合)。



おわりに



コードが比較的単純であるにもかかわらず、このサブシステムを使用すると、開発中の多くの問題を解決できます。 このようなマネージャーや他の類似の(より複雑な)マネージャーを使用する場合、柔軟性が得られ、オブジェクトに適用されたアクションが正しい順序で完了することが保証されます。このプロセスを中断する必要がある場合、これは「タンバリンと踊る」ことはありません 私のプロジェクトでは、説明したシステムのより複雑なバージョンを使用しています。これにより、 Actionとc YieldInstructionおよびCustomYieldInstructionの両方で作業できます。 とりわけ、タスク実行の優先度にはより多くのオプションを使用し、 Funcを使用してマネージャー外およびキュー外でタスクを起動するモード(タスクの結果を返すことができます)を使用します。 これらの実装は難しくありません。また、上記のコードを使用してこれを行う方法を自分で簡単に理解できます。




All Articles