1

For context, I'm trying to make a game something along the lines of Pokemon. You obtain, train and fight monsters.

Each species of monster is a class inheriting from an abstract base class (so they can have unique behaviour), and hopefully there will be a very large number of different species throughout the game. Ex:

abstract class Monster {
  int hp;
  void attack();
  //Etc.
}

public class FireBreathingDragon extends Monster {
  static String species_name = "Fire Breathing Dragon";
  //Blah
}

So when the player is exploring, they will encounter monsters local to an area at random. The game then needs to create a monster at random from a list of species that live in that area. Now to make this code reusable between areas (and make it easy to create monsters dynamically in other places in the code) I don't want to hardcode the possibilities into the area. Instead I think I'd like something along the lines of a factory that creates a monster of a given species on demand, something like:

public class MonsterFactory {
  Monster createMonster(
    String species_name,
    //Possibly other paramters
  );
}

The problem is then implementing createMonster in a "nice" or "elegant" way when you have (potentially) tens or hundreds of different Monster classes. Of course you could use a very very long if-else if-else or switch statement, but that's horrible to write and extend. Is there a nice way to do this? It would also be good if it was relatively easy to extend when adding more monsters.

Or is there some totally different design I should be using instead?

Disclaimer: My java is a little rusty, syntax may not be perfect, sorry about that.

mrhthepie
  • 11
  • 3
  • 1
    Why even have so many classes? You could have one monster class with fields like species name and type and you wouldnt even have to have different classes. – taronish4 Aug 13 '13 at 18:16
  • 1
    What I would recommend is having only one Monster class, but having it load stats dynamically (from config files or the like). Then you could override the attack() action by using functors (classes that have only one method). For example, WaterMonster would have an AttackAction that is called WaterAttack, and WaterMonster's `attack` method delegates to its `AttackAction`. – scott_fakename Aug 13 '13 at 18:16
  • Have you considered storing all the types of Pokemon in a file (CSV, XML, JSON, YAML, ... - take your pick)? Your code would then read this file once to initialise all the types and then be able to dynamically create the right one in `createMonster`. – andersschuller Aug 13 '13 at 18:17
  • My initial idea is to have each species be unique/different, using polymorphism. But this may be overcomplicating. – mrhthepie Aug 13 '13 at 18:35
  • @scott_fakename I'm not sure I understand your comment. Is WaterMonster or WaterAttack a functor? Is WaterAttack a nested class or method in WaterMonster? Also I'm not familiar with this use of the word functor. I take it's different from the [mathematical one](http://en.wikipedia.org/wiki/Functor)? – mrhthepie Aug 13 '13 at 18:36
  • @mrhthepie You were so close. http://en.wikipedia.org/wiki/Function_object#In_Java – asawyer Aug 13 '13 at 18:37
  • I've updated my answer below with an example of scanning for .class files and loading them using reflection. It worked great in my own tests. It requires no predefined lists of monsters in any form. – Jason C Aug 13 '13 at 19:01
  • WaterAttack would be the function object. I could be nested but there is no need for it to be since I'm sure there would be many different types of monster who could use it. In fact if you use the single monster class idea then making it nested would be bad. – scott_fakename Aug 13 '13 at 19:09

6 Answers6

4

You could register all your Monster implementation classes in a List.

List<Class<? extends Monster>> monsterTypes = new LinkedList<>();
monsterTypes.add(FireBreathingDragon.class);
// more

This doesn't have to be hardcoded. You can externalize it to some XML, Json, or other file format.

The factory instance or class can then choose a monster type from the list at a random index. You can then use reflection to instantiate the type.

Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • This sounds promising. I didn't know you could treat classes this way in java (operating on the classes as data). Does anyone know of good resources to learn about this? This is generics right? I don't know much about that. – mrhthepie Aug 13 '13 at 18:21
  • [This article](http://docs.oracle.com/javase/tutorial/reflect/) is the official tutorial. – Sotirios Delimanolis Aug 13 '13 at 18:22
  • 1
    @mrhthepie "Generics" here refers to the `List` parameterized type. The use of `FireBreathingDragon.class` is not generics, its part of Java's reflection support. – Jason C Aug 13 '13 at 18:26
  • @mrhthepie Yeah, sorry, didn't read that part of your comment. The part List>` has some Generics in it. A Java resource for that is [here](http://docs.oracle.com/javase/tutorial/java/generics/). – Sotirios Delimanolis Aug 13 '13 at 18:27
  • I don't know much about either generics or reflection. Thanks for your help Sotirios and Jason. – mrhthepie Aug 13 '13 at 18:31
  • @mrhthepie You're welcome. A lot Java frameworks use Generics and Reflection heavily. If you're planning to do anything in Java, I recommend you familiarize yourself with both. – Sotirios Delimanolis Aug 13 '13 at 18:32
  • 1
    @mrhthepie If you do find yourself wanting to use reflection, also read up on annotations. Custom annotations are a very concise and powerful way to attach metadata to classes, fields, and methods, and you can get that info using reflection. – Jason C Aug 13 '13 at 19:37
  • IMHO, reflection is not an elegant way. Quite on the contrary. Just on the practical side (i.e. putting elegancy apart), it disrupts many forms of static analysis and, very importantly, refactoring. When used simplistically, it can also increase time requirements by a very high multiplier (100x or more; I invite you to check this because it depends on how reflection is exactly used and it could also have been recently optimize). Reflection is always the very last resource that I use, and fortunately this has happened in few cases. – Mario Rossi Aug 19 '13 at 03:46
  • @MarioRossi: AFAIK, all dependency inversion frameworks need reflection to allow configuration-based object instantiation. Your example adds a whole lot of plumbing to achieve the same thing, create a list of class constructors. Of course, this also means that your example allows more complex instantiation scenarios for each class than simply invoking its constructor. – vgru Aug 19 '13 at 08:29
  • @Groo I'd say that's correct. And many other kinds of "frameworks" too. But 1) that doesn't make reflection the best solution, and 2) it could fit the "if there is no other alternative" criterion. I still prefer code generation much over reflection. But then, why reflection is so much more popular? IMO, because it is much older than anottations (the sactioned mechanism for CG), and also because it is much easier to use. In the long run, the ability to quickly identify all the points where a specific class is instantiated, for example, is much more valuable than reducing boilerplate code. – Mario Rossi Aug 19 '13 at 08:50
  • @Groo If that doesn't convince you, I quote the Reflection API Tutorial (http://docs.oracle.com/javase/tutorial/reflect/): "Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it." To justify this, Sun/Oracle's authors resort to extremely technical reasons internal to Java. I agree with them, but my main reason is long-term code maintenance and tooling. – Mario Rossi Aug 19 '13 at 08:53
2

The simplest solution is to have a data driven monster class. This means you only have one class (or a small number) and this class can be used for a wide variety of monsters with different attributes and abilities.

You could have a CSV file which contains each species and all the attributes and abilities fr that species. This way you could add a species by adding a line in a spreadsheet.

Peter Lawrey
  • 525,659
  • 79
  • 751
  • 1,130
  • Well, the idea was that the different species of monsters would be different and unique enough that they would warrant their own classes. This way would greatly simplify the factory, although whoever is creating the monsters would have to know a lot more about them ahead of time. – mrhthepie Aug 13 '13 at 18:19
  • @mrhthepie Can you give me an example? AFAIK, This is how most games of this type are developed. eg Diablo. – Peter Lawrey Aug 13 '13 at 18:21
  • @mrhthepie it would also mean adding new critters means a recompile / redistribute – asawyer Aug 13 '13 at 18:22
  • An example would be something on the lines of: most monsters have a fixed elemental type (fire, water, air etc). But some could change their type at will to gain an advantage, or be forced to change as a side effect of some other action. And hopefully such interesting or unique abilities would be the rule rather than the exception. – mrhthepie Aug 13 '13 at 18:29
  • @mrhthepie So I would have an elemental type which can also be generated from data. You could add new element types like lightening or magma or undead. SO now you have two classes, total. They can be unique attributes with unique names, but this doesn't require unique code. – Peter Lawrey Aug 13 '13 at 18:35
  • @PeterLawrey How would you define monster behavior as data? – Sotirios Delimanolis Aug 13 '13 at 18:38
  • So then how do you handle the monster which can temporarily masquerade as magma or lightning etc? – mrhthepie Aug 13 '13 at 18:38
  • You have a another table you add to for different element types with colours, effects, how it interacts with other elements etc. You can add as many as you need. You can have a table for abilities, how much damage they do over what times etc, etc. Tables for treasure, potions, armour, helms etc. You end up with a few dozen tables and one or two class each and that is all you ever need. – Peter Lawrey Aug 13 '13 at 18:46
  • Data alone has a limit, especially in terms of *future* behavior, unless, of course, code is considered data too (which it is, but in a different context). Dynamic/Scripting languages and its support in Java make things even more diffuse. – Mario Rossi Aug 19 '13 at 05:12
1

This solution uses Class Factories without any form of reflection. Why is this important in the context of the question ("the most elegant way")? From a very interesting exchange with another contributor: I quote Sun/Oracle's Reflection API Tutorial "Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it." To justify this, Sun/Oracle's authors resort to extremely technical reasons internal to Java. I agree with them, but my main reason is long-term code maintenance and tooling. And what's the main alternative to reflection? Annotation-based automatic code generation. I can't do something like that in this short space, but I can produce what should be, more or less, the resulting code:

public interface Factory<T> {
    T make();
}
public static class BasicMonster {
}
public static class Monster1 extends BasicMonster {
    public static final Factory<Monster1> getFactory() {
        return new Factory<Monster1>() {
            public Monster1 make() { return new Monster1() ;  }
        };
    }
}
public static class Monster2 extends BasicMonster {
    public static final Factory<Monster2> getFactory() {
        return new Factory<Monster2>() {
            public Monster2 make() { return new Monster2() ;  }
        };
    }
}
List<Factory<? extends BasicMonster>> monsterFactories= new ArrayList<Factory<? extends BasicMonster>>();
{
    monsterFactories.add(Monster1.getFactory());
    monsterFactories.add(Monster2.getFactory());
}
...
BasicMonster newMonster= monsterFactories.get(aRandomOne).make() ;

Form static class used to indicate classes not intended as inner.

Even if list monsterFactories were initialized through reflection, the presence of factory objects in the code permits a higher level of static analysis than reflective constructor invocation.

Mario Rossi
  • 7,651
  • 27
  • 37
0

You could put all the classes in a specific package, then scan that directory for class files, load them, and track the ones that extend Monster. You could even define some custom annotations to help manage this, e.g. @IgnoreMonster to temporarily disable some without having to change the location of the file. This is similar to the way e.g. Hibernate scans source to find entity mappings.

Here is an example. All the Monster classes are placed in package dload.monsters. First, here's the base class I'm using for this example:

package dload.monsters;

public abstract class Monster {

    public abstract String getName ();

}

Then, a MonsterFactory which scans for all classes in the dload.monsters package (sorry its a little sloppy, and I skimped out on exception handling):

package dload.monsters;
import java.io.*;
import java.net.*;
import java.util.*;

public class MonsterFactory {

    private static final List<Class<? extends Monster>> monsterClasses = new ArrayList<Class<? extends Monster>>();
    private static final Random random = new Random();

    @SuppressWarnings("unchecked") // <- for monsterClasses.add((Class<? extends Monster>)cls);
    public static void loadMonsters () throws Exception {

        // in this example, Monster is in the same package as the monsters. if
        // that is not the case, replace "." with path relative to Monster.
        File folder = new File(Monster.class.getResource(".").toURI());

        for (File f : folder.listFiles()) {
            if (f.getName().endsWith(".class")) {
                String name = f.getName().split("\\.")[0];
                // replace "dload.monsters." below with package monsters are in
                Class<?> cls = ClassLoader.getSystemClassLoader().loadClass("dload.monsters." + name);
                // if Monster is not in same package as monsters, you can remove
                // cls.equals(Monster.class) check. this check makes sure the loaded
                // class extends Monster, but is not the Monster class itself (since
                // its also in that package).
                if (Monster.class.isAssignableFrom(cls) && !cls.equals(Monster.class)) {
                    System.out.println("Found " + cls.getSimpleName());
                    monsterClasses.add((Class<? extends Monster>)cls);
                }
            }
        }

        // at this point all Class's for monsters are in monsterClasses list.

    }

    public static Monster randomMonster () throws Exception {

        // choose a class at random
        int n = random.nextInt(monsterClasses.size());
        Class<? extends Monster> cls = monsterClasses.get(n);

        // instantiate it
        return cls.newInstance();

    }

}

Then, when you want to use it:

public static void main (String[] args) throws Exception {

    // load monsters; only need to do this once at startup
    MonsterFactory.loadMonsters();

    // create 10 random monsters
    for (int n = 0; n < 10; ++ n) {
        Monster m = MonsterFactory.randomMonster();
        System.out.println("name is " + m.getName());
    }

}

Note that at any time you can check the monster's Class for relevant annotations.

Another option, if the classes are already loaded (which they won't be if they've never been used or explicitly loaded) is to use Instrumentation.getAllLoadedClasses() to get a list of all classes currently loaded, then scan through all classes looking for ones that are assignable to a Monster.

Note: I do feel like there is a cleaner way to do the actual scan, and I haven't tested this in a JAR. Suggestions welcome.

All that being said, if a Monster's behavior could be entirely defined by data, I also support and recommend the data driven approach described above.

Jason C
  • 38,729
  • 14
  • 126
  • 182
0

You should take a look at the Cartesian Product Algorithm. It will generate every combination of products and you can then choose one at random.

Essentially the algorithm will take arrays of attributes and create unique combinations of the different attributes and add them to an array. You can then randomly select a key from the array when you create the enemy. That way every enemy has a random chance to have any number of attributes.

Community
  • 1
  • 1
Matthew R.
  • 4,332
  • 1
  • 24
  • 39
0

have an interface or base class that provides a monster.

I thought I'd include this wiki-bit, "The factory method pattern is an object-oriented creational design pattern to implement the concept of factories and deals with the problem of creating objects (products) without specifying the exact class of object that will be created."

This lets you use superclass methods or interfaces exclusively without ever needing to know the specific subtype of the interface. This is important because you cannot call new base_monster();

abstract class base_monster  {
  abstract base_monster factory();
}

/// make sure every monster has a name...
//
abstract class Monster extends base_monster { 
   String name; 
   static int object_counter = 0;

    Monster factory() { 
       name = Integer(object_counter).toString(); 
       object_counter();
       return this; 
    }

    /// this class has a useful setter
    void object_counter( int c ) { object_counter++; out.println( object_counter );    }
}

class Griffon extends Monster {
  Monster factory() { return new Griffon(); }
}


class Harpy extends Monster {
  Harpy() { name = "Grizelda WhuttleThut III"; }
  Harpy factory() { return new Harpy(); }
}


class BlackHarpy  extends Harpy {
  BlackHarpy  factory() { super.factory(); return new BlackHarpy(); }
}


// we assume that each class has a default constructor. But, 
// if the array is filled with monsters of different subclasses we
// would have to use reflection or nasty instanceof switches to be
// able to call a (specific) defined constructor.

ArrayList<Monster> monsters = new ArrayList<Monster>();    

monsters.add( new BlackHarpy() );
for( int I = 0; I < ave_monsters_appearing; I++ )
    monsters.add( new Harpy() );
//
// an array of ten harpies and a boss Harpy.

///
// how can this array of monsters be copied into the other array?
// (we want object copies, not reference copies)
///

ArrayList<Monster> local_monsters = new ArrayList<Monster>();    

/// solution: use the factory method
for( Monster m : monsters ) 
   local_monsters.add( m.factory() ); 

.
. Hope this solves the problem of not having a static method.

Dru
  • 1,398
  • 9
  • 6
  • 1
    And how do you create the first monster of each concrete class? `make()` is an instance method, so it requires a preexisting object. – Mario Rossi Aug 19 '13 at 03:32
  • good point Mario, I'll edit the post and an abstract modifier. – Dru Aug 19 '13 at 04:59
  • hey! fancy, that doesn't work either! I'll have to test out my posts in future, never occurred to me that you wouldn't have a default constructor. – Dru Aug 19 '13 at 05:06