Отмена сопрограммы при нажатии кнопки Home или возвращении главного меню - PullRequest
3 голосов
/ 11 июня 2019

предлог того, что я делаю; В настоящее время я блокирую свои кнопки навыка, установив интерактивный = false в сопрограммах. Отображение текста оставшихся секунд через textmeshpro и установка их в неактивное состояние после окончания обратного отсчета. Но у меня проблема при нажатии кнопки домой / возвращении главного меню. Я хотел бы обновить время восстановления моих кнопок и остановить сопрограммы при ее нажатии. Но он остается в положении блокировки.

это моя перезарядка сопрограмма

static List<CancellationToken> cancelTokens = new List<CancellationToken>();

...

public IEnumerator StartCountdown(float countdownValue, CancellationToken cancellationToken)
    {
        try
        {
            this.currentCooldownDuration = countdownValue;
            // Deactivate myButton
            this.myButton.interactable = false;
            //activate text to show remaining cooldown seconds
            this.m_Text.SetActive(true);
            while (this.currentCooldownDuration > 0 && !cancellationToken.IsCancellationRequested)
            {

                this.m_Text.GetComponent<TMPro.TextMeshProUGUI>().text = this.currentCooldownDuration.ToString(); //Showing the Score on the Canvas
                yield return new WaitForSeconds(1.0f);
                this.currentCooldownDuration--;

            }
        }
        finally
        {
            // deactivate text and Reactivate myButton
            // deactivate text
            this.m_Text.SetActive(false);
            // Reactivate myButton
            this.myButton.interactable = true;
        }

    }
static public void cancelAllCoroutines()
   {
       Debug.Log("cancelling all coroutines with total of : " + cancelTokens.Count);
       foreach (CancellationToken ca in cancelTokens)
       {
           ca.IsCancellationRequested = true;
       }
   }


void OnButtonClick()
    {

    CancellationToken cancelToken = new CancellationToken();
        cancelTokens.Add(cancelToken);

        Coroutine co;
        co = StartCoroutine(StartCountdown(cooldownDuration, cancelToken));
        myCoroutines.Add(co);

    }

это то место, где я ловлю, когда нажата / возвращена главная кнопка главного меню. когда поймать и выскочить pauseMenu

public void PauseGame()
    {
        GameObject menu = Instantiate(PauseMenu);
        menu.transform.SetParent(Canvas.transform, false);
        gameManager.PauseGame();
        EventManager.StartListening("ReturnMainMenu", (e) =>
        {
            Cooldown.cancelAllCoroutines();
            Destroy(menu);
            BackToMainMenu();
            EventManager.StopListening("ReturnMainMenu");
        });
        ...

Я также прекращаю время, когда игра на паузе

public void PauseGame() {
        Time.timeScale = 0.0001f;
    }

Ответы [ 3 ]

3 голосов
/ 21 июня 2019

В этом случае вы используете CancellationToken неправильно.CancellationToken - это структура, которая упаковывает CancellationTokenSource следующим образом:

public bool IsCancellationRequested 
{
    get
    {
        return source != null && source.IsCancellationRequested;
    }
}

Поскольку это структура, она передается вокруг по значению , то есть той, которую вы храните в своем списке. не совпадает с экземпляром, который есть у вашего Coroutine.

Типичный способ обработки отмены - создать CancellationTokenSource и передать его Tokenвокруг.Всякий раз, когда вы хотите отменить его, вы просто вызываете метод .Cancel() для CancellationTokenSource.Причина этого заключается в том, что CancellationToken может только быть отменено через ссылку «источник», а не потребителями токена.

В вашем случае высоздание токена без источника вообще, поэтому я бы предложил внести следующие изменения:

Прежде всего, измените список cancelTokens на:

List<CancellationTokenSource>

Далее измените свойOnButtonClick() метод выглядит следующим образом:

public void OnButtonClick()
{
    // You should probably call `cancelAllCoroutines()` here  
    cancelAllCoroutines();

    var cancellationTokenSource = new CancellationTokenSource();
    cancelTokens.Add(cancellationTokenSource);
    Coroutine co = StartCoroutine(StartCountdown(cooldownDuration, cancellationTokenSource.Token));
    myCoroutines.Add(co);
}

И, наконец, измените ваш метод cancelAllCoroutines() на этот:

public static void CancelAllCoroutines()
{
   Debug.Log("cancelling all coroutines with total of : " + cancelTokens.Count);
   foreach (CancellationTokenSource ca in cancelTokens)
   {
       ca.Cancel();
   }

   // Clear the list as @Jack Mariani mentioned
   cancelTokens.Clear();
}

Я бы предложил прочитать документы по ОтменаТокены или, как предложено @JLum, используйте метод StopCoroutine , который предоставляет Unity.

РЕДАКТИРОВАТЬ: я забыл упомянуть, что рекомендуется утилизировать CancallationTokenSources прибольше не используется, чтобы предотвратить утечки памяти.Я бы порекомендовал сделать это в OnDestroy () хук для вашего MonoBehaviour, например, так:

private void OnDestroy()
{
    foreach(var source in cancelTokens)
    {
        source.Dispose();
    }
}

РЕДАКТИРОВАТЬ 2: Как @Jack Mariani упомянул в своем ответе, несколько CancellationTokenSourcesв этом случае излишне.Все, что вам действительно нужно было бы сделать, это иметь более детальный контроль над тем, что Coroutine отменяется.В этом случае вы отменяете их все за один раз, так что да, оптимизация будет заключаться в создании только одного из них.Здесь можно сделать несколько оптимизаций, но они выходят за рамки этого вопроса.Я не включил их, потому что чувствовал, что это раздувает этот ответ больше, чем необходимо.

Однако я бы поспорил с его утверждением о том, что CancellationToken «в основном предназначен для Задачи».Выделяются прямо из первой пары строк в MSDN docs :

Начиная с .NET Framework 4, .NET Framework использует унифицированную модель для совместного отмены асинхронных или длительные синхронные операции .Эта модель основана на облегченном объекте , который называется токеном отмены

CancellationTokens - это облегченные объекты.По большей части они просто Structs, которые ссылаются на CancellationTokenSource.«Накладные расходы», о которых упоминается в его ответе, ничтожны и, на мой взгляд, полностью стоят того, чтобы учесть читабельность и намерение.

Вы могли бы передать нагрузку booleans синдексы или подписка на события с использованием литералов string, и эти подходы будут работать.Но какой ценой?Запутанный и трудно читаемый код?Я бы сказал, что оно того не стоит.

Хотя выбор в конечном итоге за вами.

1 голос
/ 24 июня 2019

ОСНОВНОЙ ВЫПУСК

В своем вопросе вы просто используете bool cancellationToken.IsCancellationRequested и никаких других функций токена отмены.

Итак, следуя логике вашего метода, вы можете просто захотетьиметь List<bool> cancellationRequests и передать их, используя ref ключевое слово .

Тем не менее, я бы не стал продвигаться ни к этой логике, ни к логике, предложенной Дарреном Руэйном, потому что у них есть однаглавный недостаток .

FLAW => эти решения продолжают добавлять объекты в 2 списка cancelTokens.Add(...) и myCoroutines.Add(co), даже не очищая их.


public void OnButtonClick()
{
    ...other code
    //this never get cleared
    cancelTokens.Add(cancelToken);
    ...other code
    //this never get cleared
    myCoroutines.Add(co);
}

Если вы хотите пойти по этому пути, вы можете удалить их вручную, но это сложно, потому что вы никогда не знаете, когда они потребуются (Coroutine можно вызывать через много кадров после CancelAllCoroutines метода).


SOLUTION

Использование статического события вместо списка

Чтобы удалить список и сделать класс еще более отделенным, вы можете использовать статическое событие,создан и вызван в сценарии, где вы вызываете PauseGamemethod.

//the event somewhere in the script
public static event Action OnCancelCooldowns;

public void PauseGame()
{
    ...your code here, with no changes...
    EventManager.StartListening("ReturnMainMenu", (e) =>
    {
        //---> Removed ->Cooldown.cancelAllCoroutines();
        //replaced by the event
        OnCancelCooldowns?.Invoke();

        ...your code here, with no changes...
    });
    ...

Вы будете слушать статическое событие в вашей сопрограмме.

public IEnumerator StartCountdown(float countdownValue, CancellationToken cancellationToken)
{
    try
    {
        bool wantToStop = false;
        //change YourGameManager with the name of the script with your PauseGame method.
        YourGameManager.OnCancelCooldowns += () => wantToStop = true;

        ...your code here, with no changes...

        while (this.currentCooldownDuration > 0 && !wantToStop)
        {
            ...your code here, with no changes...
        }
    }
    finally
    {
        ...your code here, with no changes...
    }
}

EASIER SOLUTION

Или вы можете просто остаться простыми и использовать MEC сопрограммы вместо Unity (они также более эффективны ).

MEC Free предлагает функцию тегов, которая полностью решает проблему.

START SINGLE COROUTINE

void OnButtonClick() => Timing.RunCoroutine(CooldownCoroutine, "CooldownTag");  

STOP ALL COROUTINE с определенным тегом

public void PauseGame()
{
    ...your code here, with no changes...
    EventManager.StartListening("ReturnMainMenu", (e) =>
    {
        //---> Removed ->Cooldown.cancelAllCoroutines();
        //replaced by this
        Timing.KillCoroutines("CooldownTag");

        ...your code here, with no changes...
    });
    ...

Далее примечания к тегам (учтите, что в MEC free есть только теги, а не слои, но вам требуетсятолько теги в вашем случае использования).

РЕДАКТИРОВАТЬ: После некоторой мысли я просто решил удалить детали решения bool, это может запутать ответ, и он выходит за рамки этого вопроса.

0 голосов
/ 21 июня 2019

Unity имеет StopCoroutine() специально для раннего завершения сопрограмм.

Вы захотите создать функцию, которую вы можете вызывать для каждого объекта кнопки навыка, когда вы хотите сбросить их все, как это:

void resetButton()
{
    StopCoroutine(StartCountdown);
    this.currentCooldownDuration = 0;
    this.m_Text.SetActive(false);
    this.myButton.interactable = true;
}
...