开发者

Using inheritance and polymorphism to solve a common game problem

开发者 https://www.devze.com 2022-12-29 07:41 出处:网络
I have two classes; let\'s call them Ogre and Wizard. (All fields are public to make the example easier to type in.)

I have two classes; let's call them Ogre and Wizard. (All fields are public to make the example easier to type in.)

public class Ogre
{
  int weight;
  int height;
  int axeLength;
}

public class Wizard
{
  int age;
  int IQ;
  int height;
}

In each class I can create a method called, say, battle() that will determine who will win if an Ogre meets and Ogre or a Wizard meets a Wizard. Here's an example. If an Ogre meets an Ogre, the heavier one wins. But if the weight is the same, the one with the longer axe wins.

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
}

We can make a similar method for Wizards.

But what if a Wizard meets an Ogre? We could of course make a method for that, comparing, say, just the heights.

public Wizard battle(Ogre o)
{
  if (this.height > o.height) return this;
  else if (this.height < o.height) return o;
  else return this;
}

And we'd make a similar one for Ogres that meet Wizard. But things get out of hand if we have to add more character types to the program.

This is where I get stuck. One obvious solution is to create a Character class with the common traits. Ogre and Wizard inherit from the Character and extend it to include the other traits that define each one.

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;
  }
}

Is there a better way to organize the classes? I've looked at the strategy pattern and the mediator pattern, but I'm not sure how either of them (if any) could help here. My goal is to reach some kind of common battle method, so that if an Ogre meets an Ogre it uses the Ogre-vs-Ogre battle, but if an Ogre meets a Wizard, it uses a more generic one. Further, what if the characters that meet share no common traits? How can 开发者_StackOverflow中文版we decide who wins a battle?

Edit: Lots of great responses! I need to digest them and figure out which one works best for my situation.


The visitor pattern "is a way of separating an algorithm from an object structure it operates on".

For your example, you can have

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 
    }
    ...
}

And whenever a fight occurs:

targetChar.battle(new OgreBattleVisitor(ogre));

Define a Visitor implementation for a Wizard and a Dwarf and whatever appears. Also note that I define the result of the visit method to be boolean (won or lost) rather than to return the winner.

Thus, when adding new types, you will have to add:

  • a method to the visitor to handle fighting the new type.
  • an implementation for handling the fights for the new type

Now, here it turns out that you will have some duplication of code in case "Ogre vs Wizard" == "Wizard vs Ogre". I don't know if this is the case - for example there might be a difference depending on who strikes first. Also, you may want to provide totally different algorithm for, let's say "Swamp battle with ogre" compared to "village battle with ogre". Thus you can create a new visitor (or a hierarchy of visitors) and apply the appropriate one whenever needed.


What should happen in the case where two Ogres have the same height/weight and the same axe-length? According to your example, the one that was lucky enough to get called first would win.

I don't know if this is a suitable alternative, but what if you went for a completely different scheme and attributed a "battle-score" to each character instead of relying on comparing individual traits. You could use a character's attributes in a formula to give some integer that can be compared to another character's. Then you could use a generic battle method to compare the two scores and return the character with the higher one.

For example, what if an Ogre's "battle-score" was calculated by his height plus his weight times his axe-length and a Wizard's score was calculated by his age multiplied by his IQ?

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);
    }
 }


Sounds like you want double dispatch.

Basically your Ogre and Wizard would (probably) have a common base. When you call the battle method from Ogre's base it takes in the base of Wizard and invokes another function on that base that takes the Ogre as an argument. The polymorphic behaviour on both function calls effectively gives you polymorphism on the two types simultaneously.


What about separating the battle logic into its own class, with methods like

Battle(Ogre ogre, Wizard wizard)

Which would return an object containing the winner (or the winner itself, whatever). This would separate the battle logic from the battlers, and also allow you to generercize, i.e.:

Battle(Creature creat1, Creature creat2)

Would be a fallback method for any creature pairing (assuming Wizard/Ogre/etc all have 'Creature' as a base class) that doesn't have specific logic. This would allow you to add/edit/remove battle logic without modifying any of the creatures themselves.


I think you should rethink this whole thing.

Let's just take World of Warcraft as an example of how battle can be done, simply because it's a well-known game.

You have a number of different classes, which are capable of doing different things, and have their own strengths and weaknesses. However, all of them share some common types of statistics. For example, a Mage has more Intellect than a Warrior, but the Warrior is going to have a lot more Strength than the Mage.

So how do they actually battle? Well, regardless of the class, each character has a number of abilities at their disposal. Each ability does some amount of damage, and once the HP of one of the characters drop to 0, that character dies - they lose the fight.

You should use a similar approach: define a common base class with common attributes which apply to everyone - stuff like strength, spellpower, defense, and stamina. Then, when each type of character does battle, they can use any of a series of attacks or spells - the damage caused by each attack or spell will depend on the stats of both the attacker and defender, using some suitable formula (with some randomness to keep it interesting, it'll probably be no fun if it's impossible for a Wizard to ever beat an Ogre, or vice versa).

But here's something else to consider: Maybe you shouldn't use a class per type. It would be more preferable if you could use the same formula for everyone - even if they don't have the same set of abilities. Instead of having to code each ability in there, you would just have a list of abilities and their parameters in a file, and the Character class would use it to do all of these calculations. This makes it easier to adjust the formula (only that one place to look), and easier to adjust the abilities (just change the file). It's a bit harder to write that formula, because you might want to give the Ogre a bonus for having a high strength, while the Wizard would get a bonus for a high intellect, but it's better than having X almost identical formulae, one for each stat which can affect the output.


This is exactly the kind of problems Strategy is aim to solve.

Let's review the parts of Strategy

  • Context : The fight
  • Strategy: How would you define which will win
  • Concrete Strategy : Where the fight takes place and the decision is made.

So, instead of leaving that responsibility to the character them selves ( because they will always say, "I won!!, no I won, no I.." ) you can create a RefereeStrategy.

The concrete implementation will decide who wins.

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

diagram generated with http://yuml.me

You may either define common methods that all the characters agree to respond ( which doesn't seems like what you want, this is useful when all the characters have the same "attributes" or methods like defense():int, attack():int, heal():int ) or do "blind" strategy.

I'm doing the second ( "blind" strategy )

// 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;
    }

}

This allow you to plug new algorithms ( strategies - referees - what the pattern is good for ) as you need them.

It keeps the context ( your fight arena ) free of "if/elseif/else/if/else" constructs by forwarding to the referee the decision of the winner, and isolate your different characters from each other.


One way to do it would be to make a new interface to all of the character types like

public interface Fightable
{
    public Fightable doBattle(Fightable b);
}

And then from there you would implement doBattle in each class. For example, in the Ogre class, you could check whether b was an instance of Ogre (in which case do one thing), a Wizard (in which case another), etc...

The problem there is that every time you add a new type, you need to add code to every single class of character to every other character, which isn't particularly maintainable. Further you'd have to stress about making sure that the operations were properly maintained, i.e. if you changed the doBattle method in the Ogre class with respect to Wizards but not in the Wizard class with respect to Ogres, you could have a situation where the result differ whether you call anOgre.doBattle(aWizard) or aWizard.doBattle(anOgre).

What may be better would be to create a Battle class that accepts two Characters and contains the logic for fighting by seeing which two class types have been passed to it: that way, you only need to change your Battle class every time a new Character type is added! You want to encapsulate the behavior that is most likely to change most often.


You are going to have to define the unique logic (assuming that the logic IS unique) for every single combination of battle anyways - it doesn't matter what design pattern you choose to use. The only requirement is to separate this logic from the Ogre and Wizard class and create the battle methods in a different class. I think what you are doing currently is completely fine (once you move the battle logic somewhere else) without requiring a visitor pattern, which is what I would use if this was some enterprise game :)

Don't listen to all of this fluff about design patterns...


Instead of trying to resolve each battle for type of monster vs other type of monster, why not create some value for a monster based on it's attributes.

If it has a higher value than what it fights, it wins.

To compensate for certain enemies being better against other enemies, implement some kind of defence for each attack type

e.g Archer attacks from range, ogre is melee, and wizard is magic. Ogre has melee defence, ranged defence and magic defence.

The value of an monster can then be calculated based it's attack & and the enemies respective armour, as well as HP etc etc etc.

So you don't bother with a case by case basis.


Hmm, first of all, your first design is not good, because you let the fighters decide who wins. If you use Mediator, e.g. something like proposed Battle class, you´will be able to centralize the fighting logic and easily change any fighting rule at one place. Imagine having many creatures... once you sould want to change how two fight together, where will you go to search for the battle method? In first or in second class? Some superclass? So Mediator is a good idea. Yet another problem is deciding which rule to use. You might easily get to the multiple dispatch problem.


Something like this?

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)});
    }
}


try uncle bob's triple dispatch. see my answer to: Managing inter-object relationships


I know this is a little late, but Steve Yegge wrote an article a few years back on almost this exact problem (he even used a game example!).

0

精彩评论

暂无评论...
验证码 换一张
取 消