建模桌游的任何模式?

98

出于兴趣,我正在尝试将我儿子喜欢的一款棋盘游戏写成软件。最终我希望在其之上构建一个WPF UI,但现在我正在构建模拟游戏及其规则的机器。

在这个过程中,我发现了一些问题,我认为这些问题可能是许多棋盘游戏所共有的,也许其他人已经比我解决得更好。

(请注意,对于玩游戏的人工智能和高性能方面的模式不对我产生兴趣。)

到目前为止,我的模式如下:

  • 几个不可变类型来表示游戏盒中的实体,例如骰子、棋子、卡牌、棋盘、棋盘上的空格、金钱等。

  • 每个玩家都有一个对象,其中包含玩家的资源(例如金钱、得分)、他们的名字等。

  • 表示游戏状态的对象:玩家、轮到谁了、棋子在棋盘上的布局等。

  • 管理回合顺序的状态机。例如,许多游戏都有一个小的预备阶段,每个玩家都要掷骰子来决定谁先行动;那是开始状态。当一个玩家的回合开始时,首先他们掷骰子,然后他们移动,然后他们必须原地跳舞,然后其他玩家猜测他们是哪种品种的鸡,然后他们获得积分。

我可以利用一些已有的技术吗?

编辑: 我最近意识到游戏状态可以分为两个类别:

  • 游戏物件状态。 “我有10美元”或“我的左手在蓝色上”。

  • 游戏序列状态。 “我已经连续掷出两个相同的点数;下一个会把我关进监狱。” 在这里使用状态机可能是有意义的。

编辑:我的真正目的是寻找像国际象棋、Scrabble或大富翁这样的多人回合制游戏最佳实现方法。我确信可以通过从头开始逐步开发此类游戏,但与其他设计模式一样,可能存在一些不易于察觉需要仔细研究才能使事情变得更加顺利的方式。这就是我希望的。


3
你正在构建某种混合了Hokey Pokey、Monopoly和charades的东西? - Anthony Mastrean
对于任何依赖状态(呃……)的规则,比如大富翁游戏中的三个双倍规则,您都需要使用状态机。我想发表更详细的答案,但我没有做过这方面的经验。不过我可以妄加评论一番。 - MSN
8个回答

122

看起来这是一个两个月前的帖子,我现在才注意到,但是无所谓。我曾为一款商业网络版桌游设计和开发了游戏框架,我们与之合作非常愉快。

由于像A玩家有多少钱、B玩家有多少钱等各种排列组合的因素,您的游戏可能有(接近)无限数量的状态,因此我相信您应该避免使用状态机。

我们框架的理念是将游戏状态表示为具有所有数据字段的结构体,这些数据字段共同提供完整的游戏状态(即,如果您要将游戏保存到磁盘上,就会将该结构写出)。

我们使用命令模式来表示玩家可以执行的所有有效游戏操作。以下是一个示例动作:

class RollDice : public Action
{
  public:
  RollDice(int player);

  virtual void Apply(GameState& gameState) const; // Apply the action to the gamestate, modifying the gamestate
  virtual bool IsLegal(const GameState& gameState) const; // Returns true if this is a legal action
};
因此,你可以构造动作并调用其IsLegal函数来确定移动是否有效。如果它是有效的,并且玩家确认了该动作,那么您可以调用Apply函数来实际修改游戏状态。通过确保您的游戏代码只能通过创建和提交合法的操作(换句话说,Action :: Apply系列方法是直接修改游戏状态的唯一方法),然后确保您的游戏状态永远不会无效。此外,使用命令模式可以使玩家期望的移动序列化并通过网络发送到其他玩家的游戏状态中执行。
在这个系统中有一个注意事项,它最终拥有了一个相当优雅的解决方案。有时动作会有两个或更多阶段。例如,在大富翁中,玩家可能会落在某个财产上,现在必须做出新的决定。当玩家掷骰子之间和他们决定购买财产之间的游戏状态是什么?我们通过在游戏状态中添加“操作上下文”成员来处理这种情况。操作上下文通常为空,表示游戏当前未处于任何特殊状态。当玩家掷骰子并将掷骰子动作应用于游戏状态时,它会意识到玩家已经落在一个未拥有的财产上,并且可以创建一个新的“PlayerDecideToPurchaseProperty”操作上下文,其中包含我们正在等待决策的玩家的索引。到RollDice动作完成时,我们的游戏状态表示当前正在等待指定的玩家决定是否购买物业。现在,除了“BuyProperty”和“PassPropertyPurchaseOpportunity”动作外,所有其他动作的IsLegal方法都会返回false,这些动作只有在游戏状态具有“PlayerDecideToPurchaseProperty”操作上下文时才合法。
通过使用操作上下文,棋盘游戏的整个生命周期中从未存在过一点,即游戏状态结构不完全代表此时在游戏中发生的事情。这是您棋盘游戏系统非常理想的属性。当您只需检查一个结构就能找到有关游戏中发生的所有事情的信息时,编写代码会变得更加容易。此外,它非常适用于网络环境,客户端可以将其操作提交到主机机器上的网络中,主机机器可以将操作应用于“官方”游戏状态,然后将该操作回传给所有其他客户端以将其应用于其复制的游戏状态。
希望这些内容简明而有帮助。

4
我认为这不够简洁,但很有帮助!已点赞。 - Jay Bazuzi
很高兴能帮到你...哪些部分不够简明扼要?我很乐意进行澄清编辑。 - Andrew Top
我正在制作一款回合制游戏,这篇文章对我非常有帮助! - Kiv
我读到 Memento 是用于撤销的模式... Memento 模式和 Command 模式用于撤销,你的想法呢? - zotherstupidguy
这是我在Stackoverflow上读过的最好的答案。谢谢! - Papipo
我想指出的是,这里提出并推荐的解决方案实际上是一个状态机(虽然有点内外颠倒)。ActionContext 就是 当前状态(例如,可以很容易地创建一个DefaultActionContext而不是null)。考虑到以上情况,我不建议包括某种IsLegal类型的功能,因为它自然会孕育出一个严重贫血的模型,其中所有行为都与其作用的数据分离。相反,只需在当前状态中包含一个rollDice方法即可。 - user3347715

21
你的游戏引擎的基本结构使用状态模式。游戏盒子中的物品是各种类的单例。每个状态的结构可能使用策略模式模板方法
使用工厂模式创建玩家并将其插入到玩家列表中,另一个单例。GUI将使用观察者模式监视游戏引擎,并通过使用命令模式创建的多个命令对象之一与其交互。可以在被动视图的上下文中使用观察者和命令,但根据您的偏好,几乎可以使用任何MVP / MVC模式。保存游戏时,需要获取其当前状态的备忘录
我建议查看此site上的一些模式,看看它们是否成为起点。再次强调,您的游戏板的核心将是状态机。大多数游戏将由两个状态表示:预游戏/设置和实际游戏。但是,如果您正在建模的游戏具有几种不同的游戏模式,则可以添加更多状态。状态不必是连续的,例如战争游戏Axis&Battles有一个战斗版,玩家可以使用它来解决战斗。因此,有三个状态:预游戏,主板,战斗板,游戏不断在主板和战斗板之间切换。当然,回合顺序也可以表示为状态机。

18
我刚刚完成了一个基于状态的游戏设计和实现,使用了多态性。使用了一个名为GamePhase的抽象基类,这个类有一个重要的方法。
abstract public GamePhase turn();

这意味着每个GamePhase对象都保存了游戏的当前状态,而调用turn()方法会查看其当前状态并返回下一个GamePhase对象。

每个具体的GamePhase都有构造函数来保存整个游戏状态。每个turn()方法中都包含一些游戏规则。虽然这样会将规则分散开来,但它将相关规则保持在一起。每次turn()的最终结果就是创建下一个GamePhase并向其中传入完整的状态。

这使得turn()方法非常灵活。根据您的游戏,给定状态可以分支到许多不同类型的阶段。这形成了所有游戏阶段的图形。

在最高级别上,驱动它的代码非常简单:

GamePhase state = ...initial phase
while(true) {
    // read the state, do some ui work
    state = state.turn();
}

我非常喜欢这个方法,因为我现在可以轻松地为测试创建任何游戏状态/阶段。

现在回答你问题的第二部分,这在多人游戏中如何运作呢? 在需要用户输入的某些GamePhase中,从turn()调用将询问当前Player在给定当前状态/阶段下采取什么Strategy。 Strategy只是一个由所有可能的决策组成的Player接口。 这个设置还允许使用AI来实现Strategy!

此外,Andrew Top说:

由于像A玩家有多少钱,B玩家有多少钱等因素的排列组合,您的游戏可能处于(接近)无限数量的状态... 因此,我相当确定您希望远离状态机。

我认为那个陈述非常误导,虽然不同的游戏状态很多,但只有几种游戏阶段。 要处理他的示例,只需将整数参数添加到我的具体GamePhase构造函数中即可。

大富翁

一些GamePhase的示例如下:

  • 游戏开始
  • 玩家投掷骰子
  • 玩家落在属性上(FreeParking,GoToJail,Go等)
  • 玩家交易
  • 玩家购买物业
  • 玩家购买房屋
  • 玩家购买旅馆
  • 玩家支付租金
  • 玩家破产
  • (所有机会和社区宝箱卡)

在基本的GamePhase中,一些状态如下:

  • 玩家列表
  • 当前玩家(轮到谁了)
  • 玩家的钱/物业
  • 属性上的房屋/旅馆
  • 玩家位置

然后,某些阶段将根据需要记录自己的状态,例如PlayerRolls将记录玩家连续掷出骰子的次数。 一旦我们离开PlayerRolls阶段,我们就不再关心连续掷出的点数。

很多阶段可以被重复使用并链接在一起。 例如,GamePhase CommunityChestAdvanceToGo将创建下一个阶段PlayerLandsOnGo,并返回其当前状态。 在PlayerLandsOnGo的构造函数中,当前玩家将移动到Go,并增加200美元。


9
当然,关于这个主题有很多很多很多很多很多很多的资源。但是我认为你正在正确的道路上,将对象分开并让它们处理自己的事件/数据等。
在制作瓷砖棋盘游戏时,你会发现编写映射函数以在棋盘数组和行/列之间进行转换非常方便,还有其他功能。我记得我第一次制作棋盘游戏时(很久很久以前),我曾经苦苦思索如何从棋盘数组5中获取行/列。
1  2  3  
4 (5) 6  BoardArray 5 = row 2, col 2
7  8  9  

怀旧之情。 ;)

无论如何,http://www.gamedev.net/是一个获取信息的好地方。 http://www.gamedev.net/reference/


为什么不直接使用二维数组呢?这样编译器就可以为您处理了。 - Jay Bazuzi
我的借口是这是很久很久以前的事情。 ;) - Stefan
1
游戏开发有很多东西,但我没有看到我想要的。 - Jay Bazuzi
你使用的是哪种编程语言? - zotherstupidguy
基本,Basica,QB,QuickBasic等等。;) - Stefan

6

4

Three Rings提供LGPL的Java库。Nenya和Vilya是游戏相关的库。

当然,如果您的问题涉及平台或语言限制,会更有帮助。


最终我希望构建一个WPF用户界面,也就是.NET。至少在我看来是这样的。 - Mark Allen
我不知道的字母汤。 - jmucchiello
是的,我正在做.NET,但我的问题并不是特定于语言或平台的。 - Jay Bazuzi

3
我同意Pyrolistical的回答,我更喜欢他的做法(虽然我只是粗略地浏览了其他答案)。
巧合的是,我也使用了他的“GamePhase”命名。基本上,在回合制棋盘游戏的情况下,我会让你的GameState类包含一个由Pyrolistical提到的抽象GamePhase对象。
假设游戏状态如下:
1.掷骰子 2.移动 3.购买/不购买 4.监狱
你可以为每个状态创建具体的派生类。至少为以下内容创建虚函数:
StartPhase();
EndPhase();
Action();

在StartPhase()函数中,您可以设置状态的所有初始值,例如禁用其他玩家的输入等。
当调用roll.EndPhase()时,请确保将GamePhase指针设置为下一个状态。
phase = new MovePhase();
phase.StartPhase();

在MovePhase::StartPhase()中,你可以设置活动玩家剩余移动的次数为前一个阶段所摇到的点数。
现在有了这样的设计,你可以在Roll阶段解决“3次双倍=监狱”的问题。RollPhase类可以处理自己的状态。例如:
GameState state; //Set in constructor.
Die die;         // Only relevant to the roll phase.
int doublesRemainingBeforeJail;
StartPhase()
{
    die = new Die();
    doublesRemainingBeforeJail = 3;
}

Action()
{
    if(doublesRemainingBeforeJail<=0)
    {
       state.phase = new JailPhase(); // JailPhase::StartPhase(){set moves to 0};            
       state.phase.StartPhase();
       return;
    }

    int die1 = die.Roll();
    int die2 = die.Roll();

    if(die1 == die2)
    {
       --doublesRemainingBeforeJail;
       state.activePlayer.AddMovesRemaining(die1 + die2);
       Action(); //Roll again.
    }

    state.activePlayer.AddMovesRemaining(die1 + die2);
    this.EndPhase(); // Continue to moving phase. Player has X moves remaining.
}

我与Pyrolistical的不同之处在于,每个阶段都应该有一个阶段,包括当玩家落在社区宝箱或其他地方时。我会在MovePhase中处理所有这些内容。这是因为如果有太多的连续阶段,玩家很可能会感到过于“引导”。例如,如果有一个阶段,玩家只能购买房产,然后只能购买酒店,然后只能购买房屋,就像没有自由一样。将所有这些部分都合并到一个BuyPhase中,并赋予玩家购买任何想要的东西的自由即可。BuyPhase类可以轻松处理哪些购买是合法的。
最后让我们来看看游戏板。虽然2D数组很好,但我建议使用瓷砖图(其中瓷砖是板上的位置)。在monoppoly的情况下,它将更像是一个双向链接列表。然后每个瓷砖都会有:
  1. previousTile
  2. nextTile
这样做起来会更容易,比如:
While(movesRemaining>0)
  AdvanceTo(currentTile.nextTile);

AdvanceTo函数可以处理您的逐步动画或其他任何喜欢的内容。当然,还可以减少剩余的移动次数。

RS Conley在GUI上使用观察者模式的建议是很好的。

我以前没有发布过多少内容。希望这能帮到某个人。


2

有没有我可以利用的先前技术?

如果你的问题不是特定于语言或平台的,那么我建议你考虑使用AOP模式来处理状态、备忘录、命令等。

.NET中的AOP解决方案是什么?

还可以尝试找一些很酷的网站,比如http://www.chessbin.com


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接