使用继承和多态来解决一个常见的游戏问题

Posted

技术标签:

【中文标题】使用继承和多态来解决一个常见的游戏问题【英文标题】:Using inheritance and polymorphism to solve a common game problem 【发布时间】:2011-02-15 15:50:23 【问题描述】:

我有两节课;让我们称他们为食人魔和巫师。 (所有字段都是公开的,以使示例更易于输入。)

public class Ogre

  int weight;
  int height;
  int axeLength;


public class Wizard

  int age;
  int IQ;
  int height;

在每个类中,我都可以创建一个名为battle() 的方法,该方法将确定如果食人魔相遇并且食人魔或巫师遇到巫师,谁将获胜。这是一个例子。如果食人魔遇到食人魔,则较重的人获胜。但如果重量相同,则斧头较长者获胜。

public Ogre battle(Ogre o)

  if (this.height > o.height) return this;
  else if (this.height < o.height) return o;
  else if (this.axeLength > o.axeLength) return this;
  else if (this.axeLength < o.axeLength) return o;
  else return this;    // default case

我们可以为巫师制作类似的方法。

但是如果巫师遇到食人魔怎么办?我们当然可以为此制定一种方法,例如仅比较高度。

public Wizard battle(Ogre o)

  if (this.height > o.height) return this;
  else if (this.height < o.height) return o;
  else return this;

我们会为遇到巫师的食人魔制作一个类似的。但是,如果我们必须在程序中添加更多字符类型,事情就会失控。

这就是我卡住的地方。一个明显的解决方案是创建一个具有共同特征的 Character 类。食人魔和巫师从角色继承并扩展它以包含定义每个特征的其他特征。

public class Character

  int height;

  public Character battle(Character c)
  
    if (this.height > c.height) return this;
    else if (this.height < c.height) return c;
    else return this;
  

有没有更好的方法来组织课程?我已经查看了策略模式和中介模式,但我不确定它们中的任何一个(如果有的话)在这里有什么帮助。我的目标是达到某种常见的战斗方法,这样如果食人魔遇到食人魔,它会使用食人魔对食人魔的战斗,但如果食人魔遇到巫师,它会使用更通用的战斗方式。此外,如果相遇的角色没有共同特征怎么办?我们如何决定谁会赢得一场战斗?

编辑:很多很棒的回应!我需要消化它们并找出最适合我的情况。

【问题讨论】:

所以你想让所有的Character 都有一堆int 字段,其中所有的共同特征都应该按照一定的顺序进行比较? 您可以在向导类中放置一个魔术/更魔术的布尔值 你不得不为食人魔和巫师使用一个通用类型。当您在公共向导战斗(Ogre o)中直接比较它们时,有一次您返回 Ogre 类型的“o”并且该方法需要一个向导。有时自然会推动一个具有良好的对象层次结构。但在我们的日常项目中看到这种现象并不容易。还有好问题tbw :) 【参考方案1】:

visitor pattern“是一种将算法与其操作的对象结构分离的方法”。

对于你的例子,你可以有

class Character 
    boolean battle(BattleVisitor visitor) 
       return visitor.visit(this);
    


class Ogre extends Character ..
class Wizard extends Character ..
class Dwarf extends Character ..

interface BattleVisitor 
    boolean visit(Ogre character);
    boolean visit(Wizard character);
    boolean visit(Dwarf character);


class OgreBattleVisitor implements BattleVisitor 
    private Ogre ogre;
    OgreBattleVisitor(Ogre ogre)  this.ogre = ogre; 
    boolean visit(Ogre ogre) 
      // define the battle 
    

    boolean visit(Wizard wizard) 
      // define the battle 
    
    ...

每当发生战斗时:

targetChar.battle(new OgreBattleVisitor(ogre));

为巫师和矮人以及任何出现的东西定义一个访客实现。另请注意,我将visit 方法的结果定义为boolean(赢或输),而不是返回赢家。

因此,在添加新类型时,您必须添加:

访问者处理新类型战斗的方法。 处理新型战斗的实现

现在,事实证明,如果“Ogre vs Wizard”==“Wizard vs Ogre”,您将有一些重复的代码。我不知道是不是这种情况——例如,根据谁先出手,可能会有所不同。此外,您可能希望为“与食人魔的沼泽战斗”与“与食人魔的村庄战斗”提供完全不同的算法。因此,您可以创建一个新的访问者(或访问者的层次结构)并在需要时应用适当的访问者。

【讨论】:

能不能只用构造函数传入访问者,以避免每次战斗都必须将其作为参数传递? @mgroves:是的,但这允许不同的战斗场景:在不同的情况下,可能需要不同的结果。 所以...每当您想添加一个新角色时,您必须为每个访问者中的每个现有角色添加一个额外的方法?这比更改所有其他角色有什么好处? @WilhelmTell of Purple-Magenta 无论如何你都必须添加这些方法,因为它们是必需的逻辑。更好,因为你不改变字符 - 即结构。您只是在另一个地方添加操作 @Oscar Reyes 这是根据要求 - 每个角色都应该知道如何与其他角色战斗,因为角色的具体情况。但是因此,您不会将角色与他与他人的战斗方式联系起来。也就是说,角色(巫师、食人魔)本身根本不与其他角色耦合。而且,访问者模式是 GoF,不是我的发明 :)【参考方案2】:

如果两个食人魔具有相同的身高/体重相同的斧头长度会发生什么?根据您的示例,有幸被称为 first 的人将获胜。

我不知道这是否是一个合适的替代方案,但如果你采用完全不同的方案并为每个角色分配一个“战斗分数”而不是依赖于比较个人特征会怎么样。您可以在公式中使用字符的属性来给出一些可以与另一个字符进行比较的整数。然后您可以使用通用的battle 方法来比较两个分数并返回分数较高的字符。

例如,如果一个食人魔的“战斗分数”是由他的身高加上他的体重乘以他的斧头长度来计算的,而一个巫师的分数是由他的年龄乘以他的智商来计算的呢?

abstract class Character 
    public abstract int battleScore();

    public Character battle(Character c1, Character c2) 
        (c1.battleScore() > c2.battleScore()) ? return c1 : c2;
    


class Ogre extends Character 
    public int battleScore() 
        return (height + weight) * axeLength;
    
 

 class Wizard extends Character 
    public int battleScore() 
        return height + (age * IQ);
    
 

【讨论】:

100 岁智商 200 的巫师和 200 岁智商 100 的巫师一样强大吗?我认为这些只是示例,但是将想法(年龄,智商,...)扁平化为整数存在问题。尽管如此,我个人更喜欢这个方向,而不是访问者模式。 这只是一个简单的例子,所以你可以说年龄值x%,智商值y【参考方案3】:

听起来你想要double dispatch。

基本上你的OgreWizard (可能)有一个共同的基础。当您从Ogre 的基址调用battle 方法时,它接受Wizard 的基址并调用该基址上的另一个函数,该函数将Ogre 作为参数。两个函数调用的多态行为有效地同时为您提供了两种类型的多态性。

【讨论】:

-1 您将创建对每个类的相互依赖。高耦合是不可取的。 @Oscar:它不会在类之间创建依赖关系。每个类只是为它想要为其指定行为的每种类型重载一个方法。实际的逻辑可以推迟到其他地方的函数,该函数同时接受这两种类型。据我了解,OP 正在寻找一种方法来获取两个通用 Character 对象,并简单地在其中一个对象上调用 battle 之类的方法并让它做正确的事情。【参考方案4】:

如何将战斗逻辑分离到自己的类中,使用类似的方法

Battle(Ogre ogre, Wizard wizard)

这将返回一个包含获胜者(或获胜者本身,等等)的对象。这会将战斗逻辑与战斗人员分开,并允许您进行通用化,即:

Battle(Creature creat1, Creature creat2)

对于没有特定逻辑的任何生物配对(假设巫师/食人魔/等都具有“生物”作为基类),这将是一种备用方法。这将允许您在不修改任何生物本身的情况下添加/编辑/删除战斗逻辑。

【讨论】:

这种方法的问题是Java会查看编译时类型来确定调用哪个版本的Battle方法。例如:Creature c1 = new Ogre(); Creature c2 = new Wizard(); Battle(c1,c2); 会调用 Battle(Creature,Creature)not Battle(Ogre,Wizard)【参考方案5】:

我认为你应该重新考虑整个事情。

让我们以魔兽世界为例来说明如何进行战斗,因为它是一款知名游戏。

你有许多不同的职业,它们能够做不同的事情,并且有自己的长处和短处。但是,它们都共享一些常见的统计数据类型。例如,法师的智力比战士高,但战士的力量比法师强很多。

那么他们实际上是如何战斗的呢?好吧,无论职业如何,每个角色都有许多可以使用的能力。每个技能都会造成一定程度的伤害,一旦其中一个角色的 HP 降至 0,该角色就会死亡——他们会输掉这场战斗。

您应该使用类似的方法:定义一个具有适用于所有人的通用属性的通用基类——例如力量、法术强度、防御和耐力。然后,当每种类型的角色进行战斗时,他们可以使用一系列攻击或法术中的任何一种 - 每次攻击或法术造成的伤害将取决于攻击者和防御者的统计数据,使用一些合适的公式(具有一些随机性为了保持有趣,如果巫师不可能打败食人魔,那可能就没有乐趣了,反之亦然。

但还有一点需要考虑:也许您不应该为每种类型使用一个类。如果您可以对每个人都使用相同的公式,那就更好了——即使他们没有相同的能力集。不必在其中编写每个能力的代码,您只需在文件中拥有能力列表及其参数,Character 类将使用它来完成所有这些计算。这使得调整公式更容易(只看一个地方),更容易调整能力(只需更改文件)。写这个公式有点难,因为你可能想给食人魔一个高强度的奖励,而巫师会因为智力高而得到奖励,但这比有 X 个几乎相同的公式要好,每个公式stat 会影响输出。

【讨论】:

我明白你在说什么,但我也认为你基本上是在创造一个不同于他试图创造的游戏。 @mgroves:我并不是说涵盖《魔兽世界》涵盖的每个方面都是正确的方法,但是如果您没有任何共同的属性,则必须指定一种不同的战斗方式对于每种类型的组合 - 所有这些都必须单独指定。纯粹因为所有这些组合,它很可能变得无法维护,这就是为什么你最好尝试集中这些东西。 如果个别战斗方法最终非常相似/重复,我认为像你这样的方法将是最好的方法。然而,从这个问题看来,每个战斗配对都可能非常非常独特,我认为访客模式最适合这种情况。【参考方案6】:

这正是Strategy想要解决的问题。

让我们回顾一下Strategy的部分

背景:战斗 策略:如何 你会定义哪个会赢 具体策略:战斗的地方 发生并做出决定。

因此,与其把责任留给他们自己(因为他们总是会说,“我赢了!!,不,我赢了,不,我..”)你可以创建一个@ 987654325@。

具体实施将决定谁获胜。

strategy in action http://bit.ly/cvvglb

用http://yuml.me生成的图表

您可以定义所有角色都同意响应的通用方法(这看起来不像您想要的,当所有角色都具有相同的“属性”或类似 defense():int, attack():int, heal():int 的方法时,这很有用)或执行“盲目”策略。

我正在做第二个(“盲目”策略)

// All the contenders will implement this.
interface Character 
    public String getName();    

// The context
class FightArena 
    Character home;
    Character visitor;

    // The strategy 
    Referee referee;

    Character fight()
        this.referee = RefereeFactory.getReferee( home.getName(), visitor.getName() );
        Character winner = referee.decideFightBetween( home, visitor );
        out.println(" And the winner iiiiss...... " + winner.getName() );
    


interface Referee 
    Character decideFightBetween( Character one, Character two );


class RefereeFactory 

        static Referee getReferee( Character one, Character two ) 
             .... return the appropiate Refereee... 
            


// Concrete Referee implementation 
// Wizard biased referee, dont' trust him
class OgreWizardReferee implements Referee 
    Character decideFightBetween( Character one, Character two ) 
        if( one instanceof Wizard )
            return one;
        else
            return two;
        
    

class OgreReferee implements Referee 
    Character decideFightBetween( Character one, Character two ) 
        Ogre a = ( Ogre ) one;
        Ogre b = ( Ogre ) two;

        if( a.height > b.height || a.axeLength > a.axeLength ) 
            return a;
        
        return b;
    


这允许您根据需要插入新的算法(策略 - 裁判 - 模式的好处)。

它通过将获胜者的决定转发给裁判,并隔离你的不同角色彼此。

【讨论】:

我不会说我喜欢这个选项,因为它依赖于字符串常量作为映射键。所以类似于 if/elseif/else/if 的东西其实是隐藏在 map 中的,但它仍然存在。 我明白为什么这对于支持多态的语言来说是一个很好的解决方案。 @Troubadour。具体策略需要多态性才能发挥作用。 @Bozho 实际上,我在 RefereeFactory 中将其视为插件架构,其中可以更改新的裁判,添加新的 jar 文件,但我不会编写所有这些代码。这是一种简单易懂的展示方式。 @Oscar:不,具体的策略在这里根本不需要多态性。关键是您已经使用映射来完全绕过多态性。您在示例中所做的只是从工厂获取Referee 并在其上调用decideFightBetween。你也可以把decideFightBetween 做成工厂方法。返回裁判只是在完全绕过多态后的象征性尝试。【参考方案7】:

一种方法是为所有字符类型创建一个新界面,例如

public interface Fightable

    public Fightable doBattle(Fightable b);

然后您将在每个类中实现 doBattle。例如,在 Ogre 类中,您可以检查 b 是否是 Ogre 的实例(在这种情况下做一件事)、向导(在这种情况下做另一件事)等等......

问题在于,每次添加新类型时,都需要将代码添加到每个字符类的每个其他字符中,这不是特别可维护的。此外,您还必须强调确保操作得到正确维护,即,如果您在 Ogre 类中更改了与 Wizards 相关的 doBattle 方法,但没有在 Wizard 类中针对 Ogres 更改方法,您可能会遇到以下情况:无论您调用 anOgre.doBattle(aWizard) 还是 aWizard.doBattle(anOgre),结果都会有所不同。

最好的办法是创建一个接受两个角色的 Battle 类,并通过查看传递给它的两种类类型来包含战斗逻辑:这样,您只需在每次添加了新的字符类型!您希望封装最有可能最常更改的行为。

【讨论】:

如果某些字符没有实现Fightable但实现Communicative等,这种方法很有用。您还可以抽象您的结果函数的输入参数并安排每个实现字符根据自身特征返回一个值。 -1 这样做的问题是您在每个字符之间创建了依赖关系。【参考方案8】:

无论如何,您都必须为每一种战斗组合定义独特的逻辑(假设逻辑是独特的)——无论您选择使用哪种设计模式都没有关系。唯一的要求是将此逻辑与 Ogre 和 Wizard 类分开,并在不同的类中创建战斗方法。我认为你目前正在做的事情完全没问题(一旦你将战斗逻辑移到其他地方),不需要访问者模式,如果这是一些企业游戏,我会使用它:)

不要听那些关于设计模式的吹毛求疵...

【讨论】:

【参考方案9】:

与其尝试解决每个怪物类型与其他类型怪物的战斗,不如根据怪物的属性为怪物创造一些价值。

如果它的价值高于它的战斗价值,它就会获胜。

为了补偿某些敌人对其他敌人更好,为每种攻击类型实施某种防御

例如,弓箭手是远程攻击,食人魔是近战,法师是魔法。食人魔有近战防御、远程防御和魔法防御。

然后可以根据怪物的攻击力和敌人的盔甲以及HP等来计算怪物的价值。

因此您不必逐案处理。

【讨论】:

【参考方案10】:

嗯,首先,你的第一个设计不好,因为你让战士决定谁赢。如果您使用调解器,例如类似于提议的战斗课程,您将能够集中战斗逻辑并在一个地方轻松更改任何战斗规则。想象一下有很多生物……一旦你想改变两个人一起战斗的方式,你会去哪里寻找战斗方法?在一等还是二等?一些超类?所以Mediator是个好主意。还有一个问题是决定使用哪个规则。您可能很容易遇到多分派问题。

【讨论】:

什么是“多重调度问题”?多次派遣不是解决方案吗? :-) @Ken 是的,这是一个解决方案,在许多语言中实现起来都有问题:)【参考方案11】:

这样的?

class Trait

    enum Type
    
        HEIGHT,
        WEIGHT,
        IQ
    
    protected Type type;
    protected int value;

    public Trait(int value, Type type)
    
        this.type = type;
        this.value = value;
    

    public boolean compareTo(Trait trait)
    
        if(trait.type != this.type)
            throw new IllegalArgumentException(trait.type+" and "+this.type+" are not comparable traits");
        else
            return this.value - trait.value;
    


class Character

    protected Trait[] traits;

    protected Character(Trait[] traits)
    
        this.traits = traits;
    

    public Trait getTrait(Trait.Type type)
    
        for(Trait t : traits)
            if(t.type == type) return t;
        return null;
    

    public Character doBattleWith(Character that)
    
        for(Trait thisTrait : traits)
        
            otherTrait = that.getTrait(thisTrait.type);
            if(otherTrait != null)
            
                int comp = thisTrait.compareTo(otherTrait);

                if(comp > 0)
                    return this;
                else if (comp < 0)
                    return that;
            
        
        return null;
    


class Ogre extends Character

    public Ogre(int height, int weight)
    
        super(new Trait[]
            new Trait(Type.HEIGHT,height),
            new Trait(Type.WEIGHT,height));
    

【讨论】:

【参考方案12】:

试试鲍勃叔叔的三重调度。看我的回答:Managing inter-object relationships

【讨论】:

【参考方案13】:

我知道这有点晚了,但史蒂夫·耶格(Steve Yegge)几年前写了an article 几乎就这个问题(他甚至使用了一个游戏示例!)。

【讨论】:

以上是关于使用继承和多态来解决一个常见的游戏问题的主要内容,如果未能解决你的问题,请参考以下文章

继承和多态常见的面试问题

继承和多态常见的面试问题

继承和多态常见的问题

C++ 继承多态关系中的赋值运算符的重载=operator()

C++ 继承多态关系中的赋值运算符的重载=operator()

继承和多态