Переключение случая против событий: при обновлении состояния игры (UNITY, C #) - PullRequest
0 голосов
/ 08 мая 2018

Допустим, у меня есть игра Unity с несколькими менеджерами (Game, GUI, Sounds, Camera, Inputs, Terrain, ...), созданная с помощью шаблона Singleton .

Когда GameManager вызывает (или получает вызов), чтобы изменить состояние игры (Intro, Tutorial, Gameplay, ...), он должен информировать других менеджеров о новом состоянии игры через события или, скорее, обрабатывать его и выполнить определенные действия для каждого менеджера в регистре коммутатора ?

Использование корпуса переключателя :

public static GameState currentGameState;
public static GameState previousGameState;

public void ChangeGameState(GameState newGameState)
{
    switch(state)
    {
    case GameState.Intro:
    //do stuff
    break;

    case GameState.Tutorial:
    //do stuff
    break;

    case GameState.Gameplay:
    //do stuff
    break;

    //and so on

    }

    previousGameState = currentGameState;
    currentGameState = newGameState;
}

Или используя события :

public static GameState currentGameState;
public static GameState previousGameState;

public delegate void GameStateChanged(GameState prev, GameState curr, GameState new);
public static event GameStateChanged OnGameStateChanged;

public void ChangeGameState(GameState newGameState)
{
    OnGameStateChanged(previousGameState, currentGameState, newGameState);

    previousGameState = currentGameState;
    currentGameState = newGameState;
}

Какая модель наилучшей практики / поддерживаемости здесь?

Ответы [ 2 ]

0 голосов
/ 10 мая 2018

Ваш первый подход менее предпочтителен, так как он создает жесткую зависимость между вашими классами, он кажется безвредным, но часто приводит к взаимозависимостям, которые потом трудно нарушить.Очевидно, что вам нужно сделать ссылку где-нибудь, но GameManager с жестко заданными упоминаниями о классах, имеющих дело со звуком или камерой, является явным нарушением разделения интересов, в идеале игровому менеджеру не нужно заботиться о том, что делают другие классы, гораздо лучше иметьcameraManager и soundManager подписываются на события gameManager - по крайней мере, вы создаете две точки, где один класс ссылается на другой класс, что гораздо лучше, чем создание одной точки, где один класс ссылается на два других, - представьте себе позже, где у вас есть 10 менеджеров, если у вас есть список их где-то в коде, это не очень хороший знак, так как это будет становиться все более и более сложным для управления в будущем.

Если вы разделите его, вы можете инкапсулировать функциональность, связанную с вещами, которые выхотите сделать с камерой внутри переключателя в сценарии камеры, и инкапсулировать функциональность звука в другой переключатель в звуковом сценарии.Это похоже на дублирование кода со случаями переключения, но эти другие случаи переключения будут намного проще, чем основной, который управляет графиком изменения состояния, например, возможно, вы будете воспроизводить звук только при переходе из состояния C в состояние D, пока вы толькопереместите камеру в состояния A и C, постарайтесь не писать код, который перекрывает некоторые проблемы.

Пока мы обсуждаем эту тему (я потратил много времени на размышления о различных решениях, и у большинства подходов есть хорошие и плохие стороны), вот мой предпочтительный способ (это личное предпочтение:)

Учтите, что вам все еще нужно хранить переменные, чтобы знать, каково было предыдущее и следующее состояние, потому что могут быть другие сценарии, которые просыпаются позже,и пропустить первые x событий, они должны быть в состоянии подписаться на будущие события изменения состояния и знать, каким было текущее и предыдущее состояние.Это означает, что вам все еще нужны статические переменные, и это можно понимать как отсутствие единого источника правды.Допустим, вы допустили ошибку позже, когда вы забыли запустить событие или (как упоминалось выше) целевой сценарий еще не подписан на событие при запуске, и этот сценарий получает информацию о состоянии из переменных, в то время как другиеСценарии берут его на основе собственного кэша состояния (как сообщается делегатом).Это может привести к противоречивым состояниям в вашем приложении (определенный модуль может думать, что вы находитесь в другом состоянии, чем другие).Это не так, просто кажется немного опасным.

Я часто решаю это, чтобы иметь событие без параметров (System.Action), которое срабатывает при изменении состояния (вы даже можете связать эти два, чтобы событиесрабатывает автоматически через установщик, т.е.

    public static System.Action OnGameStateChange;
    protected static GameState _currentGameState;
    public static GameState previousGameState;
    public static GameState currentGameState
       { get { return _currentGameState;} 
         set { previousGameState=_currentGameState;
              _currentGameState=value;
              if (OnGameStateChange!=null) OnGameStateChange.Invoke(); } } 

Таким образом, вы всегда получаете событие, если вы изменяете поле, но меньше риск оказаться в несовместимом состоянии (например, те же объекты пропускают обновление) сценарии всегда могут безопасно ссылаться на открытое поле (это будет единственный источник истины для этого правильного значения), и все слушатели будут уведомлены независимо от того, как вы измените значение. Вы также можете сделать установщик защищенным, чтобы его нельзя было вызывать изза пределами вашего игрового менеджера.

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

0 голосов
/ 08 мая 2018

Исходя из того, как выглядит ваша игра в настоящее время, я думаю, что было бы предпочтительнее обрабатывать изменения состояния игры в GameManager (например, с помощью оператора switch), а не сообщать другим менеджерам о новое состояние игры через события. Это потому, что я не думаю, что другие менеджеры должны знать о состоянии игры. Например, предположим, что вы хотите изменить состояние обучающей игры, чтобы задействовать действия с камерой и менеджерами звука:

public void ChangeGameState(GameState newGameState)
{
    switch(state)
    {
    case GameState.Intro:
        //do stuff
        break;

    case GameState.Tutorial:
        cameraManager.MoveToTutorialPosition();
        soundManager.PlayTutorialAudio();
        break;

    case GameState.Gameplay:
        //do stuff
        break;
    }

    previousGameState = currentGameState;
    currentGameState = newGameState;
}

Я думаю, что вышеупомянутое предпочтительнее, чем сообщать об изменениях состояния игры через события каждому из ваших других менеджеров, потому что каждый из ваших менеджеров должен будет знать, как обращаться с каждым GameState. Сравните с другим подходом ниже:

public class CameraManager
{
    public void UpdateGameState(GameState gameState)
    {
        switch (gameState)
        {
            case GameState.Intro:
                // do stuff
                break;
            case GameState.Tutorial:
                MoveToTutorialPosition();
                break;
            case GameState.GamePlay:
                // do stuff
                break;
        }
    }
}

public class SoundManager
{
    public void UpdateGameState(GameState gameState)
    {
        switch (gameState)
        {
            case GameState.Intro:
                // do stuff
                break;
            case GameState.Tutorial:
                PlayTutorialAudio();
                break;
            case GameState.GamePlay:
                // do stuff
                break;
        }
    }
}

Обратите внимание, что если в вашем GameManager много логики, возможно, имеет смысл начать рефакторинг, чтобы еще больше разложить класс. Например:

  • Вместо оператора switch вы можете использовать шаблон проектирования State для представления каждого случая switch как своего собственного класса, а не как значение enum. (Подробнее см. главу о паттерне состояний в паттернах игрового программирования от Nystrom .)
  • Несмотря на то, что здесь я утверждал, что другие менеджеры, кроме GameManager, не должны знать о GameState, для каждого менеджера также может быть целесообразно иметь свои собственные состояния, в зависимости от сложности менеджера. Например, CameraState может быть полезным.

(Надеюсь, я правильно понял ваш вопрос. Я не уверен на 100% в этом, поэтому я хотел бы услышать и другие ответы или отзывы.)

...