考虑一个纸牌游戏,例如《炉石传说》。
有成百上千种卡片可以做各种各样的事情,其中有些甚至对于一张卡片来说都是独一无二的!例如,有一张牌(称为Nozdormu)可将玩家回合减少到仅15秒!
当您具有如此广泛的潜在影响时,如何在整个代码中避免幻数和一次性检查?如何避免PlayerTurnTime类中的“ Check_Nozdormu_In_Play”方法?以及如何组织代码,使得当您添加更多效果时,您无需重构核心系统即可支持以前从未支持过的东西?
考虑一个纸牌游戏,例如《炉石传说》。
有成百上千种卡片可以做各种各样的事情,其中有些甚至对于一张卡片来说都是独一无二的!例如,有一张牌(称为Nozdormu)可将玩家回合减少到仅15秒!
当您具有如此广泛的潜在影响时,如何在整个代码中避免幻数和一次性检查?如何避免PlayerTurnTime类中的“ Check_Nozdormu_In_Play”方法?以及如何组织代码,使得当您添加更多效果时,您无需重构核心系统即可支持以前从未支持过的东西?
Answers:
您是否研究了实体组件系统和事件消息传递策略?
状态效果应该是某种类型的组件,可以在OnCreate()方法中应用其持久性效果,在OnRemoved()中使它们的效果失效,并订阅游戏事件消息以应用对某些事件的反应而产生的效果。
如果效果是永久性的(持续X圈,但仅在某些情况下适用),则您可能需要在各个阶段检查这些条件。
然后,只需确保您的游戏也没有默认的幻数。确保可以更改的所有内容都是数据驱动变量,而不是带有用于任何异常的变量的硬编码默认值。
这样,您永远不会假设转弯长度将是多少。它始终是一个不断检查的变量,可以通过任何效果更改,并且在过期时可以通过效果撤消。在默认使用幻数之前,您永远不会检查异常。
RobStone走在正确的轨道上,但是我想详细说明一下,因为这正是我写《地下城与地下城》时所做的事情,《地下城与地下城》拥有非常复杂的武器和法术效果系统。
每张卡都应附有一组效果,其定义方式可以指示效果是什么,目标是什么,作用时间和持续时间。例如,“损害对手”的效果可能看起来像这样。
Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana
然后,当效果触发时,让通用例程处理效果的处理。像个白痴一样,我使用了一个巨大的case / switch语句:
switch (effect_type)
{
case DAMAGE:
break;
}
但是,通过多态性是一种更好,更模块化的方法。创建一个包装所有这些数据的效果类,为每种类型的效果创建一个子类,然后使该类重写特定于该类的onExecute()方法。
class Effect
{
Object source;
int amount;
public void onExecute(Object target)
{
// Do nothing
}
}
class DamageEffect extends Effect
{
public void onExecute(Object target)
{
target.health -= amount;
}
}
因此,我们将拥有一个基本的Effect类,然后是一个具有onExecute()方法的DamageEffect类,因此在我们的处理代码中,我们将继续;
Effect effect = card.getActiveEffect();
effect.onExecute();
知道正在发生什么的方法是创建一个Vector / Array /链表/等。附加到任何对象(包括运动场/“游戏”)的活动效果(效果类型,基类)的数据,因此不必检查是否正在播放特定效果,只需遍历附加到该对象的所有效果对象并让它们执行。如果效果未附加到对象上,则说明该效果不起作用。
Effect effect;
for (int o = 0; o < objects.length; o++)
{
for (int e = 0; e < objects[o].effects.length; e++)
{
effect = objects[o].effects[e];
effect.onExecute();
}
}
我将提供一些建议。其中一些相互矛盾。但是也许有些有用。
考虑列表与标志
您可以遍历整个世界并检查每个项目上的标志,以决定是否执行标志操作。或者,您可以仅保留那些应该做标记的项目的列表。
考虑列表和枚举
您可以继续向项目类isAThis和isAThat添加布尔字段。或者,您可以具有字符串或枚举元素的列表,例如{“ isAThis”,“ isAThat”}或{IS_A_THIS,IS_A_THAT}。这样,您可以在枚举(或字符串const)中添加新项,而无需添加字段。并不是说添加字段确实有什么问题...
考虑函数指针
除了标志或枚举的列表外,还可以具有要在不同上下文中对该项目执行的动作的列表。(像实体的…)
考虑对象
有些人更喜欢数据驱动的,脚本化的或组件实体的方法。但是老式的对象层次结构也值得考虑。基类需要接受动作,例如“在B相阶段使用这张牌”或其他动作。然后,每种卡都可以覆盖并适当地响应。可能还存在一个玩家对象和一个游戏对象,因此游戏可以执行以下操作:if(player-> isAllowedToPlay()){做游戏…}。
考虑调试能力
关于一堆标记字段的一件好事是,您可以以相同的方式检查和打印每个项目的状态。如果状态由不同的类型,成组的组件,功能指针或位于不同的列表中表示,则仅查看项目的字段可能不够。这都是权衡。
最终,重构:考虑单元测试
无论您对体系结构进行多少概括,您都可以想象它没有涵盖的内容。然后,您必须进行重构。也许一点,也许很多。
一种更安全的方法是进行单元测试。这样,您可以确信,即使重新布置了下面的内容(也许很多!),现有功能仍然有效。通常,每个单元测试如下:
void test1()
{
Game game;
game.addThis();
game.setupThat(); // use primary or backdoor API to get game to known state
game.playCard(something something).
int x = game.getSomeInternalState;
assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}
如您所见,保持游戏(或玩家,纸牌和&c)上的顶级API调用稳定是单元测试策略的关键。