【问题标题】:Common Design for Console and GUI控制台和 GUI 的通用设计
【发布时间】:2011-03-07 12:31:17
【问题描述】:

为了我自己的乐趣和训练,我正在设计一个小游戏。游戏的真实身份与我的实际问题无关,假设它是Mastermind 游戏(实际上是:)

我在这里的真正目标是有一个界面IPlayer,它将用于任何玩家:计算机或人类、控制台或图形用户界面、本地或网络。我还打算有一个 GameController,它只处理两个 IPlayers。

IPlayer 界面看起来像这样:

class IPlayer
{
public:
    //dtor
    virtual ~IPlayer()
    {
    }
    //call this function before the game starts. In subclasses,
    //the overriders can, for example, generate and store the combination.
    virtual void PrepareForNewGame() = 0;
    //make the current guess
    virtual Combination MakeAGuess() = 0;
    //return false if lie is detected.
    virtual bool ProcessResult(Combination const &, Result const &) = 0;
    //Answer to opponent's guess
    virtual Result AnswerToOpponentsGuess(Combination const&) = 0;
};

GameController 类会做这样的事情:

IPlayer* pPlayer1 = PlayerFactory::CreateHumanPlayer();
IPlayer* pPlayer1 = PlayerFactory::CreateCPUPlayer();

pPlayer1->PrepareForNewGame();
pPlayer2->PrepareForNewGame();

while(no_winner)
{
   Guess g = pPlayer1->MakeAguess();
   Result r = pPlayer2->AnswerToOpponentsGuess(g);
   bool player2HasLied = ! pPlayer1->ProcessResult(g, r);
   etc. 
   etc.
}   

通过这个设计,我愿意让GameController类不可变,也就是我把刚才的游戏规则塞进去,没有别的,所以既然游戏本身成立了,这个类就不应该改变。对于主机游戏,这种设计可以完美运行。我会拥有HumanPlayer,它在其MakeAGuess 方法中会从标准输入中读取Combination,以及CPUPlayer,它会以某种方式随机生成它等等。

现在我的问题是:IPlayer 接口和GameController 类本质上是同步的。我无法想象当GUIHumanPlayerMakeAGuess 方法必须等待例如一些鼠标移动和点击时,我将如何使用相同的GameController 实现游戏的GUI 变体。当然,我可以启动一个等待用户输入的新线程,而主线程会阻塞,以模仿同步 IO,但不知何故,这个想法让我感到厌恶。或者,或者,我可以将控制器和播放器都设计为异步的。在这种情况下,对于主机游戏,我将不得不模仿异步,这似乎比第一个版本更容易。

您能否评论一下我的设计以及我对选择同步或异步设计的担忧?另外,我觉得我把更多的责任放在了玩家类上,而不是 GameController 类。等等等等。

提前非常感谢您。

附:我不喜欢我的问题的标题。随意编辑它:)

【问题讨论】:

  • 我喜欢这个标题 :-)
  • 我删除了 C# 标签,因为这个问题附近没有 C#。
  • 由于语言无关紧要,我也希望得到 C# 专家的意见
  • 然后在其上放置一个与语言无关的标签,而不是特定的语言标签。语言标签用于针对特定语言的问题。为什么不在阳光下用每种 OOP 语言标记它?
  • 一般来说,不鼓励使用标签来吸引更多的受众。标记用于对问题进行分类。

标签: c++ oop design-patterns language-agnostic


【解决方案1】:

考虑为IPlayer 对象引入一个观察者类,而不是使用各种IPlayer 方法的返回值,如下所示:

class IPlayerObserver
{
public:
  virtual ~IPlayerObserver() { }
  virtual void guessMade( Combination c ) = 0;
  // ...
};

class IPlayer
{
public:
  virtual ~IPlayer() { }
  virtual void setObserver( IPlayerObserver *observer ) = 0;
  // ...
};

IPlayer 的方法应该调用已安装的IPlayerObserver 的相应方法,而不是返回值,如下所示:

void HumanPlayer::makeAGuess() {
  // get input from human
  Combination c;
  c = ...;
  m_observer->guessMade( c );
}

然后,您的 GameController 类可以实现 IPlayerObserver,这样每当玩家做了一些有趣的事情时,它就会收到通知,比如猜测。

通过这种设计,如果所有IPlayer 方法都是异步的,那就太好了。事实上,这是意料之中的——他们都返回void!。您的游戏控制器在活动玩家上调用makeAGuess(这可能会立即计算结果,或者它可能会为多人游戏执行一些网络 IO,或者它会等待 GUI 执行某些操作)并且每当玩家做出选择时,游戏控制器可以放心,guessMade 方法将被调用。此外,玩家对象仍然对游戏控制器一无所知。他们只是在处理一个不透明的“IPlayerObserver”接口。

【讨论】:

    【解决方案2】:

    与控制台相比,GUI 的唯一不同之处在于您的 GUI 是事件驱动的。这些事件发生在 GUI 线程上,因此,如果你在 GUI 线程上托管游戏代码,你就会遇到问题:你让玩家移动的调用阻塞了 GUI 线程,这意味着你无法获得在该调用返回之前的任何事件。 [编辑:插入以下句子。] 但是调用不能返回,直到它得到事件。所以你陷入了僵局。

    如果您只是将游戏代码托管在另一个线程上,这个问题就会消失。您仍然需要同步线程,因此 MakeAGuess() 在准备好之前不会返回,但它肯定是可行的。

    如果您想保持一切都是单线程的,您可能需要考虑使用不同的模型。游戏可以通过事件通知玩家轮到他们了,但让玩家在游戏上启动操作。

    【讨论】:

      猜你喜欢
      • 2012-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-13
      • 2012-06-07
      相关资源
      最近更新 更多