Creating lots of different enemies (Inheritance, polymorphism, etc.)

[spoiler]
Hey fellas,

I currently have run into a problem while creating lots of enemies - not that it doesn’t work, I’m just not sure what the best approach would be.

I have about 30 different enemy-types (ants, rats, orcs, dragons, etc.) and they all are described by the same fields. Therefore I have created a superclass called enemy:


public abstract class Enemy
{
	/* General Information */
	private String type;
	private int ac;
	
	/* Health related */
	private int maxHP;
	
	private int attackBonus;
	private int movement;
	
	/* Attack related */
	private String attType;
	private int attNo;
	private int damage;
	private int saveAs;
	private int morale;
	
	private int appearingNo;	// determines how many monsters of this type appear
	
	/* Drops */
	private String treasureType;
	private int exp;
	
	public Enemy(String type, int ac, int maxHP, int attackBonus, int movement, String attType, int attNo, int damage, int saveAs, int morale, int appearingNo, String treasureType, int exp) {
		this.type = type;
		this.ac = ac;
		this.maxHP = maxHP;
		this.attackBonus = attackBonus;
		this.movement = movement;
		this.attType = attType;
		this.attNo = attNo;
		this.damage = damage;
		this.saveAs = saveAs;
		this.morale = morale;
		this.appearingNo = appearingNo;
		this.treasureType = treasureType;
		this.exp = exp;
	}
}

Now when I want to create a giant ant for example I need to call the super.constructor:


public class MobGiantAnt extends Enemy
{
	public MobGiantAnt(String type, int ac, int maxHP, int attackBonus, int movement, String attType, int attNo, int damage, int saveAs, int morale, int appearingNo, String treasureType, int exp) {
		super(type, ac, maxHP, attackBonus, movement, attType, attNo, damage, saveAs, morale, appearingNo, treasureType, exp);
	}
}

Now here is where my question comes to play. All ants share the same stats (same type, same ac, same maxHP, etc.). I obviously don’t want to type in all parameters when I create a new ant but rather have new MobGiantAnt() automatically create an ant with the shared base values. Long story short: Is it better to use static fields (as they are shared by all objects created from that class to my understanding).


public class MobGiantAnt extends Enemy
{
	final private static String type = "Giant Ant";
	final private static int ac = 17;
	final private maxHP = ...
			.
			.
			.
			.
		
			
	public MobGiantAnt() {
		super(type, ac, maxHP, attackBonus, movement, attType, attNo, damage, saveAs, morale, appearingNo, treasureType, exp);
	}
}

Or to change the fields in the Enemy class to protected and do this:


public class MobGiantAnt extends Enemy
{			
	public MobGiantAnt() {
		this.type = "Giant Ant";
		this.ac = 17;
			.
			.
			.
	}
}

Excuse me if this is a stupid question, but I often get stuck on stuff like that :confused:

To my understanding protected means that only classes in the same package have access to the fields, but generally use of private fields and getters/setters is suggested. Is it good to have different subclasses for each enemy at all?
[/spoiler]

A static variable means that there is only one copy of that variable and it belongs to the class. So you can´t create a lot of objects containing a static “ac” and then expect to behave like you have a unique copy of “ac” for every instance of that class. So basically, if you change ac in MobGiantAnt you will change it for every MobGiantAnt. If it´s static every MobGiantAnt will always have the same ac. This might not be a problem in your game, but it´s a good thing to be aware of… :slight_smile:

Use enum.

Object wank IMO.
Your solution looks fine. Just use whatever sounds logical to you. When it doesn’t anymore, change to something more logical. Repeat.
The important thing is not how your code is structured, it’s how well and quick you can understand it, now and in 3 months.

Well i’d rather have everything set up inside the constructor than passing every value as a parameter megazord.
Yet the first seems easier to read/change. Why not mix both?

public class MobGiantAnt extends Enemy
{
   final private static String type = "Giant Ant";
   final private static int ac = 17;
   final private maxHP = ...
         ...  
         
   public MobGiantAnt() {
      super();
      this.type = type;
      this.ac = ac;
         ...
   }
}

Another option which might work for you is to keep all of your stats in a text file and then read it in to an object whose responsibility it is to just store the stats. For instance you could have a file like so…
[ANT] ac = 17 maxHp = 36 ... ... [DRAGON] ac = 255 ... ...

You could then have a simple object (Call it MonsterStats) that has fields for each of the parameters you have (type, ac, maxHp, etc). At start up you read in the file and create a hashmap of monster type to MonsterStats. Then in your Enemy class you read in the MonsterStats object as a whole rather than all of the individual values.



public Enemy(MonsterStats stats) {

   maxHp = stats.maxHp;
  ....
}

// sub class constructor

public GiantAnt() {
   super(MonsterStatLibrary.get("ANT"));
}

With this approach, if you want to add or remove a stat, you don’t have to go through every subclass and change every call to super, you just have to change Enemy and MonsterStats. Personally, I also prefer to have the data defined in an external file which makes it easier to see how various monsters compare and makes tweaking values easier.

EDIT
Here is another option that might give you some ideas that is somewhat along the lines you are looking for.

If you do not need to implement different behaviour, you do not need sub classes (now). Add individual classes later if required.

Instead of supplying 13 different parameters (unreadable code) for each object creation, rather invent some kind of enemy factory/builder which allows to get new objects by a, for example, simple createEnemy(enemyType) call. Then you don’t have to hide and spread parameters which will be adjusted a lot of times during development. Or add special create methods like

createBossEnemy()
createEnemy(enemyType, boolean juggleHitpoints)
createEnemey(enemyType, Map properties)

Whatever.

For constructors, only force to submit those parameters which must be specified to get a full working object, assign default values to others.

You could also just set up a small database with pre-created enemies stored within and then load them and all of their stats when needed.

if things like maxHP are the same per ant your static one looked fine imo, i’d just roll with it and get on with the game

I am new to java , but in other languages I think the suggestion given by 65k would be most correct IMO.
If you pass an enemy type, and level to the constructor, you can set all of that stuff up pretty easily.

In fact, I did a similar thing in a c class some years back. The code to setup the monsters stats need not even be in the enemy super class, and in fact it may be better to remove it for sanity sake. Use whatever means you want to pass the monster type to the constructor. An enum works. Then inside of the constructor, you can have the values be something like

this.ac = rpgKit.getACstat(MonsterType, Level);
this.hp = rpgKit.getHPstat(MonsterType,Level);

then in the rpgKit, getACstat can use a straightforward case selection(switch) statement to determine which stat to return on that call.

switch MonsterType
{
case 0:
//–rat AC–//
ac = level \ 2;
break;
case 1:
//–dasterFunk Zombie–//
ac = (level/3) + 5;
}

boss switch can be done in couple ways
Boss type monster, then use the level to determien stuff, spawnCritter(Boss,1);//–rat boss–//
regular monster type, with a level of -1,-2 etc to represent the Boss level spawnCritter(RAT,-20); lvl 2 rat boss

or, use a subclass for the bosses, which makes it possible to determine other behaviors like healing and summons, without having those things isntantiated with enemies that never use them.

You can also use something like json or xml so you can easily adjust stats later.

What benefits do you get from setting up enemy statistics through something like xml or a plain text file compared to simply coding the stats into the game? Just easier modifying?

None while developing, but it “looks” like it’s the proper way.

Once the game is released, it can be useful for players and modders though, or if non-coders need to tweak those values.

Yes, depends on how hard you will modify (adjust) it or if you’re on team of dev. However you’ll ofc hardcore it on release.

Some of it comes down to personal preference (I prefer to use this sort of set up when I can). I find having a list of options/parameters in a config file is easier for me to manage. You can also do some nice things like in this example, imagine having a CSVfile with each stat as a column and each row is a Monster. You could then easily see how various monsters compare. You do have to write some code to read and parse the file format but tends to be a pretty simple thing to do.

Thanks for all the replies they definitely helped!
I now use Properties to read the attributes from external text files.
This is what my system looks like now:

[spoiler]


public abstract class Entity
{
	final private FileHandler fh = FileHandler.getInstance();
	
	/* General Information */
	private String name;
	private String race;
	
	private int cb;
	private int cn;
	private int cr;
	private int in;
	private int mg;
	private int nt;
	private int st;
	private int wp;
	
	private int dv;
	private int mv;
	private int sz;
	private int lf;
	private int hp;
	
	/* Abstract methods */
	@Override
	public abstract String toString();
	
	/* Constructors */
	protected Entity() {}
	
	/* Concrete Methods */
	protected void initializeAbilities(String path) {
		race = fh.loadProperty("race", path);
		cb = Integer.parseInt(fh.loadProperty("cb", path));
		cn = Integer.parseInt(fh.loadProperty("cn", path));
		cr = Integer.parseInt(fh.loadProperty("cr", path));
		in = Integer.parseInt(fh.loadProperty("in", path));
		mg = Integer.parseInt(fh.loadProperty("mg", path));
		nt = Integer.parseInt(fh.loadProperty("nt", path));
		st = Integer.parseInt(fh.loadProperty("st", path));
		wp = Integer.parseInt(fh.loadProperty("wp", path));
		dv = Integer.parseInt(fh.loadProperty("dv", path));
		mv = Integer.parseInt(fh.loadProperty("mv", path));
		sz = Integer.parseInt(fh.loadProperty("sz", path));
		lf = Integer.parseInt(fh.loadProperty("lf", path));
	}
	
	/* Getters && Setters */
	public String getName() {
		return name;
	}
	.
	.
	.
	.
	.

}

public abstract class Enemy extends Entity
{
	final private FileHandler fh = FileHandler.getInstance();
	final private Random rnd = Randomizer.getInstance();
	
	/* Attributes */
	private int sp;		// Speed
	
	public Enemy(String namePath, String propPath) {
		setName(loadRandomName(namePath));
		initializeAbilities(propPath);
	}
	
	private String loadRandomName(String path) {
		ArrayList<String> names = fh.readCompleteFile(path);	// Stores all names from the file
		String name = null;			// The name to be returned
		int index;					// The index number of the item to pick
		
		/* Provide a random index in range of the array list and select the
		 * according item in the list */
		while (name == null) {
			index = rnd.nextInt(names.size());
			name = names.get(index).trim();
		}
		
		return name;
	}
	
	@Override
	protected void initializeAbilities(String path) {
		super.initializeAbilities(path);
		sp = Integer.parseInt(fh.loadProperty("sp", path));
	}
	
	@Override
	public String toString() {
// Ignore this - it's just for testing
		System.out.println("Name: " + getName());
		System.out.println("Race: " + getRace());
		System.out.println("CB: " + getCb());
		System.out.println("CN: " + getCn());
		System.out.println("CR: " + getCr());
		System.out.println("IN: " + getIn());
		System.out.println("MG: " + getMg());
		System.out.println("NT: " + getNt());
		System.out.println("ST: " + getSt());
		System.out.println("WP: " + getWp());
		System.out.println("DV: " + getDv());
		System.out.println("MV: " + getMv());
		System.out.println("SZ: " + getSz());
		System.out.println("LF: " + getLf());
		System.out.println("SP: " + getSp());
		return null;
	}
	
	/* Getters && Setters */
	public int getSp() {
		return sp;
	}
}

public class Zombie extends Enemy
{
	final private static String NAMEPATH = "files/enemies/names/zombie.name";
	final private static String PROPPATH = "files/enemies/races/zombie.prop";
	
	public Zombie() {
		super(NAMEPATH, PROPPATH);
	}
}

[/spoiler]