GameObjects and Code Structure

Started by
3 comments, last by frob 6 years, 11 months ago

I'm making a text adventure game, but I've run into trouble when it comes to structuring my objects.

In my game, every item (objects, weapons) implements the GameObject interface.


public interface GameObject {
     void viewDetails();    
}

My ultimate goal is to store them in an ArrayList. If the player wanted to view them i can just loop through the list and pass it to a method that displays the data. Like so:


List<HealthPack> healthList = new ArrayList<HealthPack>();
healthList.add(new HealthPack(3));
healthList.add(new HealthPack(5));

display(healthList);

public static void display(List<GameObject> objDetails){
      //loop though the list and display.
}

Now, if I wanted methods that are more interactive I have to get a little more detail on the specific type. For example, not every game object is a health pack, therefore the GameObject interface should not contain a method called void replenishhealth(), however I can write a class that does this.


public abstract class HealthPack implements GameObject {       
   //the sub-class could be a ration, potion, or pills
   public abstract void replenishHealth(){}
}

My problem is, I would have to store it in a List collection specifically for that type, and I would have to do this for all types of GameObjects. For example Weapons:


List<HealthPack> healthList = new ArrayList<HealthPack>();
List <Weapons>  weaponsList = new ArrayList<Weapons>();

Questions:

1. Maybe I'm looking at it all wrong, but is there a more efficient way to restructure my code to not use so many ArrayLists? I don't mind if this is what I have to do, but there probably is a better way of creating, storing and using them.

Note: Ideally, I would like to only keep two arrayLists, one for GameObjects and the other for Weapons. The Weapon class is an abstract class that initializes the name & damage a weapon could do. The Weapon class also contains the abstract methods attack/replenish for the subclass weapons to implement.

2. Suppose I had a character interface that looked like this:


interface Character{   
    void pickUp(Weapon weapon);
    void pickUp(HealthPack healthItem);
    void use(Weapon weapon);
    void use(HealthPack healthItem);
}

Everytime I have a new GameObject the player can interact with, I would have to update my Character interface with pickup/use methods. The problem with this is, a GameObject sometimes can be used in different ways. For example, if I had the GameObject Paper:


public class Paper implements GameObject {
  public void writeText(String text);
  public void foldPaper();
  @Override
  public void viewDetails();   
}

I would then have to update the Character interface to accommodate all the different uses of the Paper object. Is there a better way of doing this? so that I don't have to keep updating my character interface every time I have a new GameObject?

Advertisement

Java or C#, right?

I see a lot of these baseclass (base interface) GameObject things, and from there running into all kinds of trouble, like you describe.

It originates (I think) from trying to make all things the same, in some way. As you're describing, different things are actually different, so yeah, somewhere it becomes a mess, as unifying truly different things isn't going to work.

So instead my suggestion is to honor that different things are different. Don't try to throw everything in one basket just because OO says you need a base-class, differentiate where it is useful. You're doing that already with eg Weapons (although you didn't write the precise properties that make a weapon a Weapon). Do it more. Nobody said you cannot have a Carryable interface for stuff you can carry around, or a LightGiver interface to provide light in dark matters. A big candle can have a Weapon, Carryable, and LightGiver interface, no problem whatsoever (as long as the candle isn't too heavy to lift).

As for storage in containers, this is really nice with interfaces, you can make a container for an interface, so you have collections of related stuff all for free, in every relation you make an Interface for, eg "ArrayList<Carryable> contents".

Now, as items generally have several interfaces, how do you access that? This is why "instanceof" exists.


if (item instanceof Carryable) { // Yay, I can carry 'item'!
    Caryable cy = (Carryable)item; // Cast to the allowed interface
    contents.add(cy); // Do your thing with that interface.
}

A second point is (which is probably much more important than the previous), don't design a hierarchy without need. It's very tempting to make a nice extensive generic interface for your items, and it's very good if you're doing a homework assignment to demonstrate your OO skills, but in real-life programming, don't build what you don't need. Make it all as simple as you can, and extend only if it improves the time to get to your goal.

Less code is less errors you can make.

As an example, couldn't a health-pack be a weapon with negative damage?

"Inheritance is the base class of evil."

-- Sean Parent

P.S.

OO doesn't "say you [need] a base-class".

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

Honestly, an easy way to handle this is to make a generic container that is basically a bag of holding It will hold anything of an item interface. Then to access data or to lost specific items, you allow the generic to filter out types.

You can trade off time and space in most problems. This is one.

A solution for C# and Java --- and also in C++ but with some caveats --- is to have one container, and upon request return a container of objects that implement the interface.

These dynamic casts are speedy if the conversion is to the base class or the leaf classes, but in all languages conversions anywhere in the middle of the tree has a potential performance cost. For a base or leaf conversion it is usually a single-digit nanosecond, and for mid-tree conversions it is either single-digit or double-digit nanoseconds. When you multiply by thousands of items those nanoseconds become microseconds, or maybe tens or hundreds of microsoeconds if you're doing many. That's enough to be a concern on big games, not so much on hobby projects.

You gain the space and difficulty of maintaining multiple lists, but it comes with the cost of doing tests for convertibility.

In C#, you can make a container of of the specific type, for each item in your source list see attempt to convert with the "as" operation, if the result is not null then add the item to the result container.

In C++, if your code has RTTI enabled, build the container for the target type, for each item in your source attempt to convert with dynamic_cast, if the result is not null then add the item to the result container.

In Java, you can similar make the container, for use instanceof for every item, if the test passes do the conversion and add it to the container. (It may have changed in newer versions because of demand, but Java does not have a convert-or-null operation, instead it has a test operation and a convert operation, if you convert something that would fail the test it throws.)

The caveat in C++ is that there are many different options and several ways to do things badly. Many code bases disable RTTI entirely because in the 1990s and early 2000s there were some serious issues on some compilers, and sadly people are slow to accept that compilers have gotten better.

Further in C++, comparing directly with typeid() is exactly the same speed as a well-written implementation of custom system that returns a unique value for each type, except typeid() is built in to the language. The drawback there is that it only works with exact comparisons, not across sibling types or child/parent types. (It also is potentially different on each compilation, which means you can't use the value for any serialization purpose, so game developers usually ignore its existence.)

This topic is closed to new replies.

Advertisement