[solved] Need some advice regarding class design

Started by
13 comments, last by ToohrVyk 16 years ago
Hello, Don't know if this is really the right forum, but I could use some good advice. Currently I'm working with 2 other students on our final schoolproject that will determine whether or not we succeed in graduating this year. Our subject is a 3D MMORPG. I know this is a huge project, and not something to be taken lightly, but we have agreed with the teachers that the basics should be enough. This means a loading screen, login feature, character selection and a map with one opponent to move around on. Just enough to demonstrate the key features of MMORPG gameprogramming. I'll give a small example of the current class design that i'm working with for the client side of the mmorpg aspect:

#ifndef CREATURE_H_
#define CREATURE_H_

#include <string>
#include "../tools/list.cpp"
#include "../items/item.h"
#include "../items/equipable.h"
#include "../skills/skill.h"
using namespace std;

namespace characters {
	// Abstract class Creature
	// Derived classes must implement the virtual void methods, or becomes
	// abstract themselves.
	class Creature {
	private:
		string mName;
		string mMesh;
		unsigned int mId;
		unsigned int mLevel;
		unsigned int mCurrentLife;
		unsigned int mCurrentSynergy;
		unsigned int mStrength;
		unsigned int mAgility;
		unsigned int mVitality;
		unsigned int mDexterity;
		unsigned int mWisdom;
		unsigned int mLuck;
		tools::dList<items::Item*>* mInventory;
		tools::dList<items::Equipable*>* mEquipment;
		
	public:
		Creature(string name, string mesh, unsigned int level, unsigned int current_life, unsigned int current_synergy, unsigned int strength, unsigned int agility, unsigned int vitality, unsigned int dexterity, unsigned int wisdom, unsigned int luck, tools::dList<items::Item*>* inventory, tools::dList<items::Equipable*>* equipment);
		Creature(const Creature& c);
		virtual ~Creature();
		
		string getName() const;
		string getMesh() const;
		unsigned int getId() const;
		unsigned int getLevel() const;
		unsigned int getCurrentLife() const;
		unsigned int getCurrentSynergy() const;
		unsigned int getStrength() const;
		unsigned int getAgility() const;
		unsigned int getVitality() const;
		unsigned int getDexterity() const;
		unsigned int getWisdom() const;
		unsigned int getLuck() const;
		tools::dList<items::Item*>* getInventory() const;
		tools::dList<items::Equipable*>* getEquipment() const;
		
		void setName(string name);
		void setMesh(string mesh);
		void setId(unsigned int id);
		void setLevel(unsigned int level);
		void setCurrentLife(unsigned int currentLife);
		void setCurrentSynergy(unsigned int currentSynergy);
		void setStrength(unsigned int strength);
		void setAgility(unsigned int agility);
		void setVitality(unsigned int vitality);
		void setDexterity(unsigned int dexterity);
		void setWisdom(unsigned int wisdom);
		void setLuck(unsigned int luck);
		void setInventory(tools::dList<items::Item*>* inventory);
		void setEquipment(tools::dList<items::Equipable*>* equipment);
		
		void Equip(items::Equipable* item);
		items::Equipable* Unequip(items::Equipable* item);
		
		virtual void Move() const;
		virtual void Rotate() const;
		virtual void Animate() const;

		virtual unsigned int CalculateAttack() const = 0;
		virtual unsigned int CalculateDefense() const = 0;
		virtual unsigned int CalculateMagicAttack() const = 0;
		virtual unsigned int CalculateMagicDefense() const = 0;
		virtual unsigned int CalculateMaximumLife() const = 0;
		virtual unsigned int CalculateMaximymSynergy() const = 0;
	};
}
#endif /*CREATURE_H_*/




#ifndef FIGHTER_H_
#define FIGHTER_H_

#include <string>
#include "creature.h"
#include "../tools/list.cpp"
#include "../items/item.h"
#include "../items/equipable.h"
#include "../skills/fighterskill.h"
using namespace std;

namespace characters {
	// This derived class Must override all virtual functions from it's
	// parent class, or become abstract as well.
	class Fighter : public Creature {
	private:
		tools::dList<skills::FighterSkill*>* mSkills;
		
	public:
		Fighter(string name, string mesh, unsigned int level, unsigned int current_life, unsigned int current_synergy, unsigned int strength, unsigned int agility, unsigned int vitality, unsigned int dexterity, unsigned int wisdom, unsigned int luck, tools::dList<items::Item*>* inventory, tools::dList<items::Equipable*>* equipment, tools::dList<skills::FighterSkill*>* skills);
		Fighter(const Fighter& f);
		~Fighter();
		
		tools::dList<skills::FighterSkill*>* getSkills() const;
		void setSkills(tools::dList<skills::FighterSkill*>* skills);
		
		// TODO: Delete if no overwrite is needed.
		void Move() const;
		void Rotate() const;
		void Animate() const;
		
		// Override functions
		unsigned int CalculateAttack() const;
		unsigned int CalculateDefense() const;
		unsigned int CalculateMagicAttack() const;
		unsigned int CalculateMagicDefense() const;
		unsigned int CalculateMaximumLife() const;
		unsigned int CalculateMaximymSynergy() const;			
	};
}
#endif /*FIGHTER_H_*/



This is also done for the skills, items etc. What is the problem I experience with this, I can't use the creature baseclass as a general pointer in other classes due it beeing abstract. Now should I change my design, and make the creature class not abstract so it can be used as a pointerclass in function parameters, or should I go for a completly different design? p.s : I hope it's clear what I try to ask [Edited by - Airslash on March 31, 2008 4:43:40 AM]
Advertisement
Being abstract doesn't stop you passing it as a pointer. It just means that the instances you pass must be one of the derived classes. I think you've misunderstood your problem.
Sorry,

but I've tried to create a function in the creature class that looks like this :

void Attack(Creature* target) {}


and it simply refuses to compile. Saying that target cannot be of the unnamed type and no reference is specified. Also I browsed some cpp sites and they claimed that it's not allowed to use an abstract class as parameter in a function.

edit: I'll add the code again and try to compile for the exact error message.
Have you actually tried it?
I see no reason why you shouldn t be able to store the base class pointer "Creature*".
http://www.8ung.at/basiror/theironcross.html
ok aplogisies,

it does work. beat me dead but I don't know why the compiler protested on it the previous times.

I'll probably have typed something wrong in the previous attempts.


Thanks for the assistance though, now I can keep programming with my design till my next problem :)
You can use a pointer to an abstract class. If your code doesn't work, the error is coming from somewhere else. However...

First modification: replace the get/set pairs with the following.

struct Attributes{   int dexterity, agility, vitality, strength, wisdom, luck;  // Apply a set of attribute modifiers to another  Attributes &operator += (const Attributes & o);};class AttributeOwner{  private:     Attributes basic, effective;  public:     // Initialize with some attributes    AttributeOwner(const Attributes&);    // Returns "basic"    const Attributes &getBasicAttributes() const;     // Returns "effective"    const Attributes &getEffectiveAttributes() const;    // Adds some modifiers (this alters the effective    // attributes, not the basic ones).     void addModifiers(const Attributes &);     // Add some permanent modifiers (this alters both    // basic and effective attributes).    void addPermanentModifiers(const Attributes &);    // Clear all non-permanent modifiers (sets effective = basic)    void clearModifiers();};class Creature : private AttributeOwner { public:  using AttributeOwner::getBasicAttributes;  using AttributeOwner::getEffectiveAttributes;  using AttributeOwner::addModifiers;  using AttributeOwner::addPermanentModifiers;  using AttributeOwner::clearModifiers;  // [rest of the class]};


Now, you've replaced 18 members with only 7 (and only 5 public interface functions) yet you now have additional semantics which allow you to apply temporary modifiers to your characters, and are also more flexible.

Second modification: don't use pointers, because the ownership policy is unknown. Does a Creature own its inventory and equipment, or does it merely reference an externally available object? What happens when you copy-construct a creature? In this particular case, I would personally use std::list and just be done with it. I would also drop the get/set pairs and use:

class InventoryOwner{public:  // boost::shared_ptr is useful to get sane and easily implemented   // behavior. Use naked pointers if you wish, but it will be harder.  typedef boost::shared_ptr<items::Item> item_type;  typedef boost::shared_ptr<items::Equipable> equip_type;private:  std::list< item_type > items;  std::list< equip_type > equipment;public:  void equip(const equip_type &);  void remove(const equip_type &);     void pickUp(const item_type &);  void drop(const item_type &);  const std::list< item_type > &carried() const;  const std::list< equip_type > &equipped() const;};class Creature : private AttributeOwner, private InventoryOwner{public:  using AttributeOwner::getBasicAttributes;  using AttributeOwner::getEffectiveAttributes;  using AttributeOwner::addModifiers;  using AttributeOwner::addPermanentModifiers;  using AttributeOwner::clearModifiers;  using InventoryOwner::equip;  using InventoryOwner::remove;  using InventoryOwner::pickUp;  using InventoryOwner::drop;  using InventoryOwner::carried;  using InventoryOwner::equipped;  // [Rest of the class]};


Third modification: remove the stats computation from the object. You don't want to have to inherit an entire fat class just to overload a few bits of behavior. Plus, you'll reduce coupling.
class Stats{public:  virtual unsigned int attack(const class Creature &) const = 0;  virtual unsigned int defense(const class Creature &) const = 0;  virtual unsigned int magicAttack(const class Creature &) const = 0;  virtual unsigned int magicDefense(const class Creature &) const = 0;  virtual unsigned int maxLife(const class Creature &) const = 0;  virtual unsigned int maxSynergy(const class Creature &) const = 0;  virtual ~Stats();};class StatsOwner{  boost::shared_ptr< Stats > stats;  // Weak pointer  const class Creature *c;  unsigned int current_life, current_synergy;public:   StatsOwner(const class Creature*, const boost::shared_ptr<Stats> &);  unsigned int attack() const;  unsigned int defense() const;  unsigned int magicAttack() const;  unsigned int magicDefense() const;  unsigned int maxLife() const;  unsigned int maxSynergy() const;  unsigned int life() const;  unsigned int synergy() const;  void addLife(int);  void addSynergy(int);  // Runtime change of the stats.  void changeStats(const boost::shared_ptr<Stats> &);};class Creature : private AttributeOwner,                  private InventoryOwner,                 private StatsOwner,{public:  using AttributeOwner::getBasicAttributes;  using AttributeOwner::getEffectiveAttributes;  using AttributeOwner::addModifiers;  using AttributeOwner::addPermanentModifiers;  using AttributeOwner::clearModifiers;  using InventoryOwner::equip;  using InventoryOwner::remove;  using InventoryOwner::pickUp;  using InventoryOwner::drop;  using InventoryOwner::carried;  using InventoryOwner::equipped;  using StatsOwner::attack;  using StatsOwner::defense;  using StatsOwner::magicAttack;  using StatsOwner::magicDefense;  using StatsOwner::maxLife;  using StatsOwner::maxSynergy;  using StatsOwner::life;  using StatsOwner::synergy;  using StatsOwner::addLife;  using StatsOwner::addSynergy;  using StatsOwner::changeStats;  // [Rest of the class]};


Fourth step: move the 'Move/Rotate/Animate' virtual functions to a controller class which independent of the underlying model. This way, you can change the corresponding behavior at will. I would use the same process as for the StatsOwner here.

Once this is done, your Creature class has no reason to be an abstract base class anymore. Simply configure its behavior by providing it with a construction-time description of tis properties.
Wow, really usefull remarks.
I'll be honest that I don't understand every bit of it yet, but I will definitly give it a go and look up the parts I don't quiet get yet.
Quote:Original post by Airslash
Our subject is a 3D MMORPG. I know this is a huge project, and not something to be taken lightly, but we have agreed with the teachers that the basics should be enough. This means a loading screen, login feature, character selection and a map with one opponent to move around on. Just enough to demonstrate the key features of MMORPG gameprogramming.


Your scope is more or less OK, but please stop trying to use this "MMORPG" buzzword. It misleads people into thinking you have unbelievably unrealistic expectations.

What you have there are key features of an RPG, more or less. The work involved just in putting things online represents a radical shift in the project. Dealing with large numbers of users introduces huge numbers of other complications in both the gameplay and the networking. The reason these things cost millions of dollars in production isn't because of detailed character models or interesting quest scripts; it's because of giant worlds, database administration costs, server load balancing etc.

Quote:I'll give a small example of the current class design that i'm working with for the client side of the mmorpg aspect:


You didn't say anything above about client/server architecture. Even the basics of this are far from trivial.

Quote:*** Source Snippet Removed ***


Don't "design" that much at once. You don't know what you're going to need yet. Make as little as possible functionality at a time that will let you demonstrate progress.

And what on earth is a "tools::dlist"? You're aware that there is a doubly-linked list already provided for you in the standard library, yes?

Also, everything ToohrVyk said.
Well, I didn't ask the question, but wanted to thank ToohrVyx for such detailed information that will probably help me in the long run.

I was wondering, however, is there any reason to use private inheritance over composition in this case?
Quote:Original post by joshuanrobinson2002
I was wondering, however, is there any reason to use private inheritance over composition in this case?


The two are equivalent in terms of semantics, but the message forwarding is easier.

This topic is closed to new replies.

Advertisement