切换案例与事件:关于游戏状态更新(UNITY,C#)

时间:2018-05-08 08:46:14

标签: c# unity3d events switch-statement state

假设我有一个 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 :(得分:1)

从关于游戏当前结构的声音来看,我认为最好在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中有很多逻辑,那么开始重构以便更多地分解类可能是有意义的。例如:

  • 您可以使用State设计模式将每个switch case表示为自己的类,而不是使用枚举值,而不是switch语句。 (有关详细信息,请参阅the chapter on the State pattern in Game Programming Patterns by Nystrom。)
  • 虽然我在此认为除了GameManager之外的其他管理者不应该知道GameState,但每个经理也可能有自己的状态,具体取决于经理的复杂性。例如,CameraState可能很有用。

(我希望我能正确理解你的问题。我对此并不是100%肯定,所以我很乐意听到其他答案或反馈。)

答案 1 :(得分:1)

你的第一种方法不太可取,因为它会在你的类之间产生一种硬依赖关系,它似乎无害但往往导致后来很难打破的相互依赖性。显然你需要在某个地方建立链接,但GameManager有硬编码提到声音或摄像机的类别明显违反了关注点的分离,游戏管理员理想情况下不应该关心其他类正在做什么,更好的是cameraManager和soundManager订阅了gameManager事件 - 至少你创建了两个点,其中一个类引用另一个类,这比创建一个点,其中一个类引用另外两个更好 - 想象一下后面你有10个经理,如果你在代码中有一个这样的列表,这不是一个好兆头,因为这将变得越来越复杂,以便在路上管理。

如果将其拆分出来,则可以在相机脚本的开关中封装与想要对相机进行操作相关的功能,并将声音功能封装在声音脚本中的另一个开关中。这听起来像是用开关盒重复代码,但那些其他开关盒将比控制状态变化图的主要情况简单得多,例如,当你只从状态C变为状态D时你可能只会发出声音在状态A和C中移动相机,尽量不要编写重叠很少的代码。

关于这个问题(我花了很多时间考虑各种解决方案,大多数方法都有好的和坏的方面),这是我的首选方式(这是个人偏好:)。

考虑到你仍然需要保持变量以了解上一个和下一个状态是什么,因为可能有其他脚本稍后会变得清醒,并且错过了事件的第一个x个火,他们需要能够同时订阅未来的状态变化事件,并了解当前和以前的状态。这意味着你仍然需要静态变量,这可以被理解为没有单一的事实来源。让我们说你以后犯了一个错误,你忘了触发事件,或者(如上所述)目标脚本在你触发时尚未订阅事件,并且该脚本从变量获取状态信息,而其他脚本根据自己的状态缓存(由委托通知)来获取它。这可能会导致应用程序中的状态不一致(某个模块可能认为您处于与其他模块不同的状态)。这没有错,只是感觉有点危险。

我经常解决这个问题的方法是让一个无参数的事件(System.Action)在状态改变时被触发(你甚至可以将这两个事件联系起来,以便通过setter自动触发事件,即

    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(); } } 

这样,如果更改字段,您总是会收到事件,但是处于不一致状态的风险较小(例如,相同的对象缺少更新) 脚本始终可以安全地引用公共字段(对于正确的值,它将是单一的事实来源),无论您如何更改值,都将通知所有侦听器。你也可以让setter受到保护,这样就无法从你的游戏经理课外调用它。

总是最好有一点自我不信任,假设你是一个白痴,并试图保护代码免受你自己试图在六周后弄乱它,因为你可能不完全记得你决定选择哪种方法为什么,最好是代码引导您以一致的方式编写新代码。