Jump to content
  • Advertisement
Sign in to follow this  
kvee

Creating objects based on type identification

This topic is 3164 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Hi, I'm developing my first game, a simple 2D tile-based maze game, in C++ . I've gotten to the point where I have the basic stuff implemented; I have a map, I can draw the map, the player can move and interact etc. I'm now improving / redesigning the game. One of the features I need is loading a level from a file. The file will be a simple XML-based text file. Now, I have a simple class hierarchy that contains three classes: the base class for the entities from which two classes are derived, one for static objects (objects the player cannot interact with) and interactive objects. I have an EntityManager class that is responsible for the management of these objects. Now the problem is, when I parse the map file and I encounter the definition for an entity, how do I make my game to create the correct entity type based on some identification in the map file? The simplest approach apparently seems to be to have en enum that defines the types. Then my entity manager would simply use some if-else or switch construct to determine the type. But I don't like this approach much. It doesn't allow me to easily define new types. If I add a new type of entity I would have to go and change the enum and the if-else construct. Now, the enum part of the problem seems relatively easy to overcome, at least on the surface. But I don't quite see how I could solve the if-else part. So the question in short is this: Is there a way, in C++, to write one piece of code that is able to create the correct type of object in this case? How do people usually solve these kind of issues? Do you just live with the enums and if-elses? Would a language like Lua be able to accomplish this? So ideally I'd just write
EntityId id = entityManager.create(entityType); // or something
Elsewhere in the code I have a method such as
template <typename actionType>
void addAction()
{
    boost::shared_ptr<Action> temp(new actionType);
    m_actionList.append(temp);
}
This works nicely, but the same problem applies here. When reading from a file, somehow, somewhere, I must determine what to pass as actionType. Or perhaps you can suggest a better alternative overall design?

Share this post


Link to post
Share on other sites
Advertisement
You are looking for something termed a "factory".

The easiest way, since you're using stl/Boost, is to have a map<> of string to shared_ptr to "object maker". The "object maker" simply creates a new instance of the correct type of object and returns a base pointer.

Something like;

class IMaker { public: virtual IEntity *make(void)=0; }

typedef shared_ptr<IMaker> MAKERP;
map<string,MAKERP> makerMap;

You may be able to get away with templating your "create X" class which will save a lot of typing.

template<class X> class TMaker
{public: virtual IEntity *make(void) { return new X; } }

You just then fill the map with appropriate creators at the start of the program, but it's simple one liners saying

makerMap["shark"]=MAKERP(TMakeEntity<CShark>);

Hence, given the type in a string, you look in the map, and then call the creation function.

IEntity *newentity = makerMap[entitytypename]->make();

Share this post


Link to post
Share on other sites
Thanks for the suggestion. I *think* I got implemented approximately something you describe and it seems to do what I wanted it to do. Only problem is I cannot easily initialize the objects properly in the maker, but I can probably 'fix' this by implementing a pimpl hierarchy for my objects and accepting a base pimpl as an argument in the make() method... Well I guess no one said flexibility in C++ was easy.

Thanks.

Share this post


Link to post
Share on other sites
Initializing the objects should probably happen in the constructor, so as to obey RAII, and to make creating entities in-game run through the same code path as creating entities on-load.

What kind of data does an object need for initialization? You've said that you've got 'definitions' for entities in your map file. Can each definition be represented using a common type? A dictionary of (name, value) pairs, for example?

You could keep the templated maker function for entities that have no initialization (i.e. a constructor with no arguments), and then use partial template specialization to write makers that map entity-definition parts to entity-constructor arguments.

Share this post


Link to post
Share on other sites
Alright. This is the outline of the system at the moment. It is not actually a question of whether or not I can do it, it is just that the only way I see to do this requires downcasting. For my simple game that's probably all well and good but I'd still like to have compile-time errors rather than runtime errors.


#include <string>
#include <map>
#include <iostream>
#include <vector>
#include <typeinfo>

#include <boost/shared_ptr.hpp>

// The base class for all entities.

class BaseEntity
{
public:

BaseEntity(int hp = 1)
: m_hp(hp)
{}

virtual ~BaseEntity()
{
}

void setHp(int hp) { m_hp = hp; }
int getHp() const { return m_hp; }

private:

int m_hp;

// Copying prevented.
BaseEntity(const BaseEntity& other);
BaseEntity& operator=(const BaseEntity& other);

};

// A baddie
class BadEntity : public BaseEntity
{
public:

BadEntity(int hp = 1, std::string name = "")
: BaseEntity(hp), m_name(name)
{}

virtual ~BadEntity()
{
}

void setName(const std::string& name)
{
m_name = name;
}

std::string getName() const
{
return m_name;
}

private:

std::string m_name;

};

// A goodie
class GoodEntity : public BaseEntity
{
public:

GoodEntity(int hp = 1, int power = 100)
: BaseEntity(hp), m_power(power)
{}

virtual ~GoodEntity()
{
}

void setPower(int power)
{
m_power = power;
}

int getPower() const
{
return m_power;
}

private:

int m_power;

};



// The abstract base class for makers
class AbstractMaker
{
public:

virtual ~AbstractMaker() {}

virtual BaseEntity* create() = 0;

};

// A generic maker.
template <typename Entity>
class EntityMaker : public AbstractMaker
{
public:

virtual ~EntityMaker() {}

virtual Entity* create()
{
return new Entity;
}

};

// Maker specialization for BadEntity objects.
template <>
class EntityMaker<BadEntity> : public AbstractMaker
{
public:

virtual ~EntityMaker() {}

virtual BadEntity* create()
{
return new BadEntity;
}

virtual BadEntity* create(int hp = 1, std::string name = "")
{
return new BadEntity(hp, name);
}

};



std::map<std::string, boost::shared_ptr<AbstractMaker> > makerMap;



// The entity manager.
// This creates (and in the real game also destroys) entities.
class EntityManager
{
public:

// The entity creation method takes a type as parameter.
// If such a type is not registered, create() uses the "default" type.
BaseEntity* create(const std::string& type)
{
BaseEntity* newEntity = 0;
if (makerMap.find(type) != makerMap.end())
newEntity = makerMap[type]->create();
else
newEntity = makerMap["default"]->create();

return newEntity;
}

};



int main()
{
{
boost::shared_ptr<AbstractMaker> temp(new EntityMaker<BaseEntity>());
makerMap["BaseEntity"] = temp;
makerMap["default"] = temp;
}
{
boost::shared_ptr<AbstractMaker> temp(new EntityMaker<GoodEntity>());
makerMap["GoodEntity"] = temp;
}
{
boost::shared_ptr<AbstractMaker> temp(new EntityMaker<BadEntity>());
makerMap["BadEntity"] = temp;
}

std::vector<BaseEntity*> entityList;
EntityManager manager;

entityList.push_back(manager.create("BaseEntity"));
entityList.push_back(manager.create("GoodEntity"));
entityList.push_back(manager.create("BadEntity"));
entityList.push_back(manager.create("NewEntity"));

// Initialize a BadEntity using overloaded create().
entityList.push_back(
boost::dynamic_pointer_cast<EntityMaker<BadEntity> >(makerMap["BadEntity"])
->create(5, "bad man"));

std::cout << dynamic_cast<BadEntity*>(entityList[entityList.size() - 1])->getName() << "\n";

for (int i = 0; i < entityList.size(); ++i)
{
std::cout << "Type of entityList[" << i << "] : ";
std::cout << typeid(*(entityList)).name() << std::endl;
}

for (int i = 0; i < entityList.size(); ++i)
delete entityList;

return 0;
}




output:


bad man
Type of entityList[0] : 10BaseEntity
Type of entityList[1] : 10GoodEntity
Type of entityList[2] : 9BadEntity
Type of entityList[3] : 10BaseEntity
Type of entityList[4] : 9BadEntity


(By the way, is the name() output of type_info normal? What are the numbers? I mean, IF I was using typeid to determine types, how would I know what numbers to put before the class name? But that's not the point...)

So in the above example, how exactly would I go about correctly initializing the objects without the use of down-casting?

Another problem that arises is the EntityManager. There's no problem down-casting and calling an overloaded create() like here:


entityList.push_back(
boost::dynamic_pointer_cast<EntityMaker<BadEntity> >(makerMap["BadEntity"])
->create(5, "bad man"));


But it doesn't seem possible to do this when EntityManager is acting as a middle-man for the creation.



Quote:

What kind of data does an object need for initialization?

In the real game an object needs a position (which is a class type), a string of text and a boolean.

Quote:

Can each definition be represented using a common type? A dictionary of (name, value) pairs, for example?

I'm sorry, but I don't quite understand what you mean. A map of (name, value) wouldn't work I think, since objects need different types of data. Unless I'd use something like boost::any but I really don't want to go there.

Quote:

You could keep the templated maker function for entities that have no initialization (i.e. a constructor with no arguments), and then use partial template specialization to write makers that map entity-definition parts to entity-constructor arguments.

Hmm... So the maker for a specific type would take as extra template parameters the data that is needed, initialize them in the maker constructor and then, when calling create() (or make() ), would pass them to an object constructor? Sounds feasible, I'll have to test and see how it works.

Share this post


Link to post
Share on other sites
"is the name() output of type_info normal? What are the numbers? I mean, IF I was using typeid to determine types, how would I know what numbers to put before the class name?"

Yes. name() returns you the name of the class in an *implementation dependent* way. GNU C++ ones have even more junk attached to them. They are not designed or intended to be readable as they are. GNU C++ has an ABI method for "demangling" them and turning them into names more closely matching the source. Windows may have one somewhere but I've no idea what it is.

Yes, stormingly annoyingly this means it's pretty useless for these sorts of tasks.



"A map of (name, value) wouldn't work I think, since objects need different types of data."

If you're reading this information out of a file (while loading a level for example), chances are you already have the stuff as strings. So a pattern for doing this is to have a file format that contains lines like (for example)

SHARK pos=10,20 name="sammy" hungry=true

OCTOPUS pos=5,6 name="oliver" hungry=false

This makes it pretty easy to pull in subsequent data and push it through a "configure(string)" type interface.

Your base entity then just takes config strings, the sub-classes read the strings.

This makes it very much easier later on to add to the format, so you can decide to have;

CRAB pos=1,2 name="kenneth" hungry=true species=hermit

without needing to modify any of the loading code, just the "CCrab" class itself and the factory that turns CRAB instructions into CCrab instances.

Share this post


Link to post
Share on other sites
Quote:
Original post by kvee
IF I was using typeid to determine types, how would I know what numbers to put before the class name?

You're not supposed to care about the name. Just compare to a different typeid.

if (typeid(var) == typeid(std::string)) ...

Share this post


Link to post
Share on other sites
There is a good article about Creating a Generic Object Factory here on GameDev. It is pretty similar to the suggestions in this thread. When it comes to serialization and setting attribute values, I would suggest overloading the stream operators rather than passing arguments to the constructor. Of course, serialization can get pretty complicated. Depending on what is needed, it might be worth looking at a prefabricated solution (e.g. boost).

Share this post


Link to post
Share on other sites
Quote:
Original post by kvee
Quote:

What kind of data does an object need for initialization?

In the real game an object needs a position (which is a class type), a string of text and a boolean.
In that case, why doesn't your BaseEntity class take a position, string of text, and boolean as parameters to its constructor?

What are the string of text and boolean actually for?

Quote:

Quote:

Can each definition be represented using a common type? A dictionary of (name, value) pairs, for example?

I'm sorry, but I don't quite understand what you mean. A map of (name, value) wouldn't work I think, since objects need different types of data. Unless I'd use something like boost::any but I really don't want to go there.
Have all the names and values be strings, and use boost::lexical_cast to cast the values from strings to the appropriate strongly typed value.

Quote:

Hmm... So the maker for a specific type would take as extra template parameters the data that is needed, initialize them in the maker constructor and then, when calling create() (or make() ), would pass them to an object constructor? Sounds feasible, I'll have to test and see how it works.


It's more that you have something like:


template<class ObjectClass>
class ObjectMaker
{
public: static ObjectClass* CreateObject(std::map<string, string> description)
{
return new ObjectClass();
}
};

template<>
class ObjectMaker<EvilAccountant>
{
public: static EvilAccountant* CreateObject(std::map<string, string> description)
{
int hp = boost::lexical_cast<int>(description["hp"]);
bool hasBriefcase = boost::lexical_cast<bool>(description["hasBriefcast"]);

return new EvilAccountant(hp, hasBriefcase);
}
};

Share this post


Link to post
Share on other sites
Superpig and katie, you're right; somehow my brain didn't quite connect that if I read a text file I'm getting everything as text anyway... :)

The articles were also very insightful, especially the Industrial Strength Pluggable Factories. I will probably use something along the lines of std::map<std::string, std::string>.

Thanks!

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!