回合制游戏的设计,其中动作有副作用


19

我正在编写游戏Dominion的计算机版本。这是一种基于回合制的纸牌游戏,其中动作卡,宝物卡和胜利点卡累积在玩家的个人卡组中。我已经很好地开发了类结构,并且我开始设计游戏逻辑。我正在使用python,稍后可能会在pygame中添加一个简单的GUI。

玩家的回合顺序由一个非常简单的状态机控制。顺时针旋转通过,玩家无法在游戏结束之前退出游戏。单圈游戏也是一种状态机。通常,玩家经历“行动阶段”,“购买阶段”和“清理阶段”(按此顺序)。根据问题的答案,如何实现回合制游戏引擎?在这种情况下,状态机是一种标准技术。

我的问题是,在玩家的动作阶段,她可以使用对自己或其他一个或多个玩家有副作用的动作卡。例如,一张动作卡允许玩家在当前回合结束后立即进行第二回合。另一张动作牌会使所有其他玩家从其手上弃掉两张牌。在当前回合中,还有另一张动作卡不起作用,但允许玩家在下一回合中抽出额外的卡。为了使事情变得更加复杂,游戏中经常会有新的扩展添加新的牌。在我看来,将每个动作卡的结果硬编码到游戏的状态机中既丑陋又不适应。回合制策略循环的答案 并未详细介绍解决该问题的设计。

我应该使用哪种编程模型来包含一个事实,即转弯时的一般模式可以通过转弯内发生的动作来修改?游戏对象是否应该跟踪每张动作卡的效果?或者,如果卡应实现自己的效果(例如,通过实现接口),则需要什么设置才能赋予它们足够的功率?我已经想出了一些解决此问题的方法,但是我想知道是否存在解决该问题的标准方法。具体来说,我想知道哪个对象/类/什么负责跟踪每个玩家由于打出一张动作卡而必须执行的动作,以及与正常顺序中的临时更改有何关系转弯状态机。


2
您好Apis Utilis,欢迎来到GDSE。您的问题写得很好,并且引用了相关的问题也很棒。但是,您的问题涵盖了许多不同的问题,要完全涵盖它,一个问题可能需要庞大。您可能仍然会得到很好的答案,但是如果您进一步解决问题,您自己和站点都将受益。也许从建立一个简单的游戏开始,然后发展到Dominion?
michael.bartnett

1
我将从给每张卡一个脚本来修改游戏状态开始,如果没有任何奇怪的事情发生,请使用默认的转弯规则...
Jari Komppa 2013年

Answers:


11

我同意Jari Komppa的观点,即使用强大的脚本语言定义卡片效果是必须的。但是我认为最大灵活性的关键是可编写脚本的事件处理。

为了允许卡片与以后的游戏事件进行交互,您可以添加脚本API,以向某些事件(例如游戏阶段的开始和结束)或玩家可以执行的某些操作添加“脚本挂钩”。这意味着在玩纸牌时执行的脚本能够注册下次到达特定阶段时称为的功能。每个事件可以注册的功能数量应该是无限的。如果有多个,则按照注册顺序调用它们(当然,除非有核心游戏规则说出不同的意思)。

应该有可能为所有玩家或仅某些玩家注册这些挂钩。我还建议增加挂钩的可能性,以自行决定是否应继续调用它们。在这些示例中,hook函数的返回值(true或false)用于表达这一点。

然后,您的双回合卡会执行以下操作:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(我不知道Dominion是否还有“清理阶段”之类的东西-在此示例中,这是玩家转弯的最后阶段)

一张允许每个玩家在抽奖阶段开始时再抽一张纸牌的卡片看起来像这样:

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

使目标玩家每当打出一张牌都会失去生命值的牌看起来像这样:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

您不会硬编码一些游戏动作,例如抓牌或丢失生命值,因为它们的完整定义(即“抓牌”的确切含义)是核心游戏机制的一部分。例如,我知道一些TCG,当您出于任何原因不得不抽出一张牌并且卡组为空时,您就会输掉比赛。此规则并非印在每张让您抓牌的卡上,因为它在规则簿中。因此,您也不必在每张卡的脚本中检查该丢失情况。检查类似的事情应该是硬编码drawCard()功能的一部分(顺便说一句,它也很可能成为可钩事件的候选对象)。

顺便说一句:您不太可能为未来版本可能会出现的每一个晦涩难懂的机械装置进行规划,因此,无论您做什么,您仍然需要不时地为以后的版本添加新功能。情况下,掷五彩纸屑的迷你游戏)。


1
哇。那混乱的五彩纸屑的东西。
2013年

@Philipp是一个很好的答案,它可以处理Dominion中完成的许多工作。但是,在玩一张纸牌时,有些动作必须立即发生,即,一张纸牌被迫迫使另一位玩家交出其牌库顶牌并允许当前玩家说出“保留”或“放弃”。您会编写事件挂钩来处理此类即时操作,还是需要提出其他编写卡脚本的方法?
2013年

2
当需要立即进行操作时,脚本应直接调用适当的函数,而不要注册钩子函数。
菲利普

@JariKomppa:Unglued组合故意是荒谬的,充满了毫无意义的疯狂牌。我最喜欢的是一张卡片,它使每个人在说特定单词时都受到伤害。我选择了“ the”。
杰克·艾德利

9

我给了这个问题-灵活的电脑纸牌游戏引擎-前段时间有些思考。

首先,像Chez Geek或Fluxx(我相信是Dominion)这样的复杂纸牌游戏要求纸牌具有可编写脚本的功能。基本上,每张卡都会带有自己的一堆脚本,这些脚本可能会以各种方式改变游戏状态。这将使您对系统有所适应,因为脚本可能能够执行您目前无法想到的事情,但可能会在将来进行扩展。

其次,僵化的“转弯”可能会引起问题。

您需要某种包含“特殊转弯”的“转弯堆叠”,例如“丢弃2张牌”。当纸叠为空时,默认的正常转弯将继续。

在Fluxx中,转弯完全有可能像这样:

  • 选择N张卡(按照当前规则说明,可以通过卡更改)
  • 播放N张牌(按照当前规则说明,可以通过卡更改)
    • 其中一张牌可能是“拿3张,其中2张”
      • 其中一张卡很可能会“轮流”
    • 其中一张牌可能是“放弃并抽签”
  • 如果您更改规则以选择比开始时更多的卡,请选择更多的卡
  • 如果您更改手牌数量较少的规则,则其他所有人必须立即丢弃卡
  • 回合结束时,弃牌直到拥有N张牌(可再次通过牌更换),然后再转一圈(如果您在上述混乱中有时打过“另一轮”牌)。

..等等等等。因此,设计一个可以处理上述滥用行为的转弯结构可能会非常棘手。除此之外,还有许多带有“任何时候”卡牌的游戏(例如“ chez geek”中的游戏),其中“任何时候”卡牌都可以通过取消最后玩过的任何卡牌来干扰正常的游戏流程。

因此,基本上,我将从设计一个非常灵活的回合结构开始,对其进行设计,以便可以将其描述为脚本(因为每个游戏都需要使用自己的“主脚本”来处理基本的游戏结构)。然后,任何卡都应可编写脚本;大多数卡可能不会做任何奇怪的事情,但是其他人会做。卡片也可以具有各种属性-是否可以手持,“随时”使用,是否可以作为资产存储(例如fluxx“管理员”或食物中的“ chez geek”中的各种物品)...

我从未真正开始执行任何此类操作,因此在实践中您可能会发现许多其他挑战。最简单的开始方法是从您对要实施的系统的了解开始,并以可编写脚本的方式实施它们,并尽量减少设置,因此在进行扩展时,您无需进行修改基本系统-很多。=)


这是一个很好的答案,如果可以的话,我会接受的。我通过接受信誉
欠佳

没有问题,我现在已经习惯了。=)
Jari Komppa 2013年

0

炉石传说似乎做得很相关,老实说,我认为实现灵活性的最佳方法是通过具有面向数据设计的ECS引擎。一直试图制作一个炉石克隆,但事实证明这是不可能的。所有的边缘情况。如果您面对许多此类奇怪的情况,那可能是解决问题的最佳方法。不过,根据最近尝试该技术的经验,我颇有偏见。

编辑:根据您想要的灵活性和优化类型,甚至可能甚至不需要ECS。这只是完成此操作的一种方式。DOD我错误地认为它是过程编程,尽管它们关系密切。我的意思是。您应该考虑完全或至少不使用OOP,而应将注意力集中在数据及其组织方式上。避免继承和方法。而是专注于公共功能(系统)来操纵您的卡数据。每个动作都不是模板化的事物或逻辑,而是原始数据。然后系统在哪里使用它执行逻辑。整数切换大小写或使用整数访问函数指针数组有助于有效地从输入数据中找出所需的逻辑。

遵循的基本规则是,应避免将逻辑与数据直接捆绑在一起,应避免使数据尽可能相互依赖(可能适用例外),并且当您想要灵活的逻辑感到无法承受时...考虑将其转换为数据。

这样做有好处。每张卡可以具有一个枚举值或一个或多个字符串来表示其动作。该实习生允许您通过文本或json文件设计卡,并允许程序自动导入它们。如果您将玩家的动作作为数据列表,则可以提供更大的灵活性,尤其是在一张牌依赖炉石传说等过去的逻辑,或者您想随时保存游戏或游戏重播的情况下。有可能更轻松地创建AI。特别是在使用“实用程序系统”而不是“行为树”时。建立网络也变得更加容易,因为无需找出如何获取整个可能的多态对象以在线上进行传输以及事后如何设置序列化,您已经拥有的游戏对象不过是简单的数据而已,最终变得非常容易移动。最后但并非最不重要的一点是,这使您可以更轻松地进行优化,因为您不必浪费时间担心代码,而是可以更好地组织数据,从而使处理器可以更轻松地处理数据。Python可能在这里有问题,但会查找“缓存行”及其与游戏开发者的关系。也许对原型设计并不重要,但是在将来,它将很方便。

一些有用的链接。

注意:ECS允许在运行时动态添加/删除变量(称为组件)。ECS可能的外观示例c程序(有很多实现方法)。

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

创建一堆具有纹理和位置数据的实体,最后销毁一个具有纹理成分的实体,该实体恰好位于纹理成分数组的第三个索引处。看起来很古怪,但却是做事的一种方式。这是一个如何渲染具有纹理组件的所有内容的示例。

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}

1
如果它详细介绍了如何建议您设置面向数据的ECS并将其应用于解决此特定问题,则此答案会更好。
DMGregory

更新,谢谢您指出这一点。
Blue_Pyro

总的来说,我认为告诉别人“如何”设置这种方法是很不好的,而是让他们设计自己的解决方案。事实证明,这既是实践的好方法,又是解决该问题的潜在更好方法。当以这种方式考虑数据而不是逻辑时,最终会发现有很多方法可以完成同一件事,而这一切都取决于应用程序的需求。以及程序员的时间/知识。
Blue_Pyro
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.