Приветствую! Сегодня я хотел бы поделиться своей наработкой, которую я создал около двух лет назад и использую в проектах и сегодня.
Оговорюсь, что все что описано ниже - придумано мной, так что идею я нигде не украл. Также в результате поисков в интернете ничего подобного не нашел. Если для вас это актуально - пользуйтесь на здоровье =)
Все описанное ниже будет основано на понятии промиса для компонентной ориентированного подхода - простыми словами в данном случае мы будем создавать не класс промиса, а компонент.
А в силу того, что на дворе 2023 год, мы в след за js-ом сделаем этот промис awaitable.
Я не буду останавливаться на работе async/await, по этому поводу и так достаточно много написано, не только на официальном сайте, но и много где в интернете. Обозначим только основные пункты.
Для обеспечения работы этого механизма от нас требуется в типе:
Реализовать INotifyCompletion
Добавить IsCompleted свойство (bool)
Добавить метод GetResult (можно вернуть из него войд)
Реализовать метод GetAwaiter без параметров - возвращающий этот же тип (можно extension`ом)
Также наш промис будет уметь принимать в себя параметры.
Для удобства в понимании того, как это работает, я рекомендую ознакомиться с моей статьей по поводу проброса зависимостей в компоненты десятком строк очень простого кода
Итак, начнём. Сначала опишем механизм приема параметров в самом компоненте. В нашем случае будет вот так:
public abstract class AwaitableBehaviour : MonoBehaviour { private static object[] _parameters; private void Awake() { try { OnAwake(_parameters); } catch (Exception e) { Debug.LogError("Initialization failed due to " + e); } } protected virtual void OnAwake(params object[] parameters) { } public static TAwaitable Run<TAwaitable>( GameObject container, params object[] parameters) where TAwaitable:AwaitableBehaviour { _parameters = parameters; return container.AddComponent<TAwaitable>(); } }
Тут мы с помощью статики пробросим параметры для новых компонентов в этом же типе. В целом механизм очень простой - засовываем наши параметры в статику, а вводимый в среду компонент во время инициализации посмотрит в эту статику. Вся прелесть тут в методе Run, он понадобится нам в нашем компоненте-промисе.
Следующим шагом реализуем INotifyCompletion и все описанное выше:
public abstract class AwaitableBehaviour : MonoBehaviour,INotifyCompletion { private static object[] _parameters; private bool _isCompleted; private Action _continuation; public virtual bool IsCompleted { get => _isCompleted; set { _isCompleted = value; if (!_isCompleted) return; Destroy(this); _continuation.Invoke(); } } public void GetResult() { } public virtual void OnCompleted(Action continuation) { _continuation = continuation; } public AwaitableBehaviour GetAwaiter() { return this; } private void Awake() { try { OnAwake(_parameters); } catch (Exception e) { Debug.LogError("Initialization failed due to " + e); } } protected virtual void OnAwake(params object[] parameters) { } public static TAwaitable Run<TAwaitable>( GameObject container, params object[] parameters) where TAwaitable:AwaitableBehaviour { _parameters = parameters; return container.AddComponent<TAwaitable>(); } }
Очень простой код. Немного его разберем - внесем ясность. Когда мы вызовем команду Run (с помощью await), мы добавим компонент, который вернется в методе GetAwaiter. Далее будет опрошено свойство IsCompleted, а за ним вызовется INotifyCompletion метод OnCompleted. Он вызовется до выполнения основного кода, и его параметр - это точка дальнейшего выполнения программы - мы должны будем запустить его самостоятельно, поэтому сохраним его. После того, как мы выполним все что хотели, нужно уничтожить компонент и продолжить выполнение программы. Сделаем это в сеттере свойства IsCompleted.
Злые языки скажут: "А как же вернуть результат?" И мы вернем им результат:
public abstract class AwaitableBehaviour<TResult>:AwaitableBehaviour { public TResult Result { get; protected set; } public new TResult GetResult() { return Result; } public new AwaitableBehaviour<TResult> GetAwaiter() { return this; } }
Все, теперь у нас есть все необходимое для запуска компонента через ключевое слово await!(И все это занимает около 35 строк кода, не считая пробелов между строками)
Напишем наш первый awaitable компонент. Для удобства и простоты примера создадим таймер который отсчитает для нас 5 секунд.
public class Example : AwaitableBehaviour { public float Duration = 5f; private void Update() { Duration -= Time.deltaTime; if (Duration > 0) { return; } IsCompleted = true; } }
public class ExampleWithResult : AwaitableBehaviour<float> { public float Duration = 5f; protected override void OnAwake(params object[] parameters) { Duration = (float)parameters[0]; } private void Update() { Duration -= Time.deltaTime; if (Duration > 0) { return; } Result = Duration; IsCompleted = true; } }
Сразу отмечу, что можно не только писать awaitable таймеры, можно показывать попапы, открывать сцены и т.д. В такой записи все, что может быть выражено компонентом, может быть выполнено в await стиле.
У нас есть архитип, есть его конкретные дочерние объекты, осталось только запустить их. Сделаем это:
public class Test : MonoBehaviour { async void Start() { await AwaitableBehaviour.Run<Example>(gameObject); Debug.Log("1"); await AwaitableBehaviour.Run<Example>(gameObject); Debug.Log("2"); var result = await AwaitableBehaviour.Run<ExampleWithResult>(gameObject,5f); Debug.Log(result); } }
Вот и все. Логи будут выведены с интервалом в пять секунд. Надеюсь Вам понравилось и этот подход найдет свое применение на ваших проектах. Спасибо за внимание!
