问题 控制台和GUI的通用设计


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

我的真正目标是拥有一个界面 IPlayer 它将用于任何玩家:计算机或人,控制台或gui,本地或网络。我也打算有一个GameController,它只会处理两个 IPlayer秒。

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 阶级,本质上是同步的。我无法想象如何用相同的方式实现游戏的GUI变体 GameController 当。。。的时候 MakeAGuess 的方法 GUIHumanPlayer 必须等待,例如,一些鼠标移动和点击。当然,我可以启动一个等待用户输入的新线程,而主线程会阻塞,以便模仿同步IO,但不知怎的,这个想法令我感到厌恶。或者,或者,我可以将控制器和播放器设计为异步。在这种情况下,对于控制台游戏,我将不得不模仿异步,这似乎比第一个版本更容易。

您是否愿意评论我的设计以及我对选择同步或异步设计的担忧?另外,我觉得我对玩家类的责任比GameController类更多。等等

非常感谢你提前。

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


5354
2018-03-07 12:31


起源

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


答案:


而不是使用各种返回值 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'接口。


11
2018-03-07 12:58





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

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

如果您想保持所有单线程,您可能需要考虑不同的模型。游戏可以通过一个事件通知玩家轮到他们,但是留给玩家来启动游戏操作。


1
2018-03-07 13:27