1

I want to model an interaction between classes, e.g. there is a general class Hero and he can have some items. (I came up with this analogy so it is easier to understand) Some of them are e.g. potions and can modify hero's hp, regen, strength etc., and others can be equipped ( run method hero.setArmor(item) )

I want to know how to maintain loose coupling so hero is only running methods like hero.use(item) and in items I can implement various actions that affect hero's private sections and use hero's methods. Items must have known who called them (because they hold information on what to do on hero).

I already tried another approach, using items inside hero, e.g hero have a list of items he can use on himself. That would look like item.use(this) where this is an instance of hero. OK now items must implement their use action on various classes, have modification rights of heroes. I can declare friend class in hero for this, but items have derived classes which are not friends to hero and have no access to private section. Inheriting friendship would solve the case, but I know it is not allowed in C++. Looking for general solution.

Is making heroes setters public a good approach in that situation?

1 Answer 1

1

I'm really tempted to suggest entity-component systems, but that would be a total change in mindset and design strategy and possibly overkill. So sticking to the spirit of the question, I'll try to suggest some immediate steps.

The main goal I suggest to focus on is:

Make Each Entity Have as Little Knowledge as Possible

That is, don't make a hero know about more things than he needs to. Don't make a potion know about more things than it needs to in order to cause effects to some creature. This will ultimately reduce coupling quite effectively.

Given that goal, my first suggestion is to prefer this:

potion.apply(hero);

... over this:

hero.use(potion);

This is not a matter of syntactical preference (I actually prefer the second one). It's about coupling, cohesion, encapsulation.

I'm assuming the external system needs to know what a hero is carrying in his backpack, to list the items he possesses to the user. That's most likely a fundamental requirement.

Now, given that requirement, there is no need for a hero to know that he's using a disposable/consumable/usable item, like a potion. All that's needed is for the potion to know just enough about the hero to cause the desired effects for the type of potion involved (a temporary boost to strength, e.g.). Thus, I recommend potion.apply(hero), because otherwise you might simply end up with this:

void Hero::use(Disposable& item)
{
    item.apply(*this);
}

... which is just a pointless kind of forwarding function, and it's increasing the number of responsibilities the hero class has when he probably already has plenty.

Abstractions

Again, with the idea of minimizing the amount of objects that things have to have about other things, a potion can just be a concrete implementation of a Disposable item, like so:

class Disposable
{
public:
     virtual ~Disposable() {}
     virtual void apply(Hero& hero) = 0;
};

class MinorHealingPotion: public Disposable
{
public:
     virtual void apply(Hero& hero)
     {
         hero.set_health(hero.health() + 10);
     }
};

... but we can do better. Why does a potion need to know everything about a hero? Perhaps a hero himself models some abstract concept, like a Creature.

class Creature
{
public:
     virtual ~Creature()
     virtual int health() const = 0;
     virtual void set_health(int new_health) = 0;
     ...
};

class Hero: public Creature
{
public:
    ...
};

Now we can do like so:

class Disposable
{
public:
     virtual ~Disposable() {}
     virtual void apply(Creature& creature) = 0;
};

... and now we've increased the flexibility and loosened the coupling of our designs. We can even make health potions apply to a random NPC, e.g.

Interface Queries

But we can go even further. What if we had a base object which provides an interface query to see what interfaces they support? Maybe we can do this:

class Object
{
public:
     virtual ~Object() {}

     template <class Interface>
     Interface* query_interface()
     {
         return dynamic_cast<Interface*>(this);
     }
};

Now we can start doing things like this:

class Health
{
public:
     virtual ~Health() {}
     virtual int health() const = 0;
     virtual void set_health(int new_health) = 0;
     virtual void apply_poison(int poison_amount) = 0;
     virtual void apply_disease() = 0;
     virtual void resurrect() = 0;
     ...
};

class Hero: public Object, public Health
{
public:
     ...
};

Potions might implement interfaces like Item and Disposable.

class Item
{
public:
    virtual ~Item() {}
    int weight() const = 0;
};

class Disposable
{
public:
    virtual ~Disposable() {}
    virtual void apply(Object& obj) = 0;
};

class MinorHealingPotion: public Object, public Item, public Disposable
{
public:
     virtual int weight() const
     {
         return 20;
     }

     virtual void apply(Object& obj)
     {
         Health* health = obj.query_interface<Health>();
         assert(health && "The specified object has no notion of health!");
         health->set_health(health->health() + 10);
     }
};

With a COM-style approach you can design really narrow interfaces that focus on one thing, like Health, Disposable, HandEquipment, etc. A shield might implement Item and HandEquipment. You then pass objects around and query if they satisfy the necessary interface requirements.

This can be really helpful because game rules can go all over the place with what would otherwise be very painful design-breaking obscure requirements unless you apply a much more flexible design strategy upfront than usual which can respond to the wildest set of design requirements. You might find a new need for a disposable item, for example, like a magic scroll which randomly causes an item in the reader of the scroll's inventory to catch on fire. Now you need disposable items to know about inventories possessed by the creatures to which they are applied, and before you know it, a disposable item might need a super-abstraction like just an all-encompassing, monolithic Creature interface to cover the full range of use cases unless you apply this COM-style interface query approach*. The "query interface" approach is precisely what protects you from being tempted to design huge blobs of functionality all in one interface.

* What I've shown above is not exactly COM as it's not focused on ABI, but just conforms to the basic query_interface part -- is there a formal name for this?. Maybe just a component-based design?

Setters and Getters

And finally, on to the last question, it's probably perfectly okay in this case to use setters and getters for things like health and strength attributes.

Again, a game design can potentially do so many wild things with objects and really tax object-oriented designs to the brim that it's hard to get a richer design than that. You might try to come up with more meaningful functions like heal and hurt instead of set_health, but it'll likely just make your classes more complicated.

So I recommend just using getters and setters when there isn't an easy way out of it with a property-oriented mindset, but paying attention to this information above of minimizing unnecessary knowledge that one entity possesses about another. With the COM-style approach, you only have to couple things like heros to the basic notion of an Item without really knowing whether it's body equipment or hand equipment or leg equipment or a disposable/consumable or whatever. Things like disposable items only need to know about Health if they do nothing more than affect the health of the object to which they are applied.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.