作者:Victor Nicollet 翻译:杨冰(源代码之光) 个人主页:http://www.ngame2000.com 发布:GameRes http://www.gameres.com 创建时间:2004-11-18
介绍
很多游戏都允许玩家捡取、携带、使用、买卖、丢弃、饮用,穿戴各种物品。这绝对是个庞大的系统,里面有太多需要关联的东西。在这篇文章中,会展现一个基础的物件管理系统给大家。
系统构架
面对的第一个问题是如何将物品信息对应到各自的身上:比如血瓶有它的名字、外观以及属性;某些物品拥有特定属性,如数量、魔法属性、个性名字,磨损度等等,而且这些属性有可能是在稍后被添加的。
那么,我们用类cItem来表示某一个物品。这个类包含了一个可以表示物品类型的标示。如果我们需要获得某个物品的属性,可通过cItemDatabase来获得,这是个物品数据库,它中包含所有物品的信息。
因为物品会出现在不同的地方,,比如在地上,在交易中,或是玩家的背包里,我们要展现这些物品,就需要一个集合来统一表现,类cItemPack就是物品的集合,我把它翻译成物品包,你可以遍历这包中的物品,添加或移除他们。
物品
第一步,我们来实现物品类,这是我们所用功夫最多的地方。首先考虑物品属性,他应该拥有如下属性: 可以设置到另一个物品上 和另外一个物品对比(看他们是否一样) 物品的指针 可以返回其唯一的ID 可以创建一个拥有唯一ID的物品 根据ID排序
现在我们得出了如下类定义: class cItem { unsigned long type;
public: cItem( unsigned long ); cItem & operator= ( const cItem & ); bool operator== ( const cItem & ) const; bool operator< ( const cItem & ) const; unsigned long getID( ) const; };
上面的类有一个构造函数(参数ID),指针,比较操作,获取ID的函数。私有成员变量是物品的ID,通过这个ID可以从数据库中得到该物品的属性。
这里要注意的是,const关键字出现在很多地方,在编程中要养成这种好习惯(具体作用大家都知道,呵呵)。通过const修饰,当cItem变得很大的时候,可以增加更多的安全性。
下面是实现: cItem::cItem( unsigned long t ) : type( t ) { } cItem &cItem::operator =( const cItem & o) { type = copy.type; return( *this ); }
unsigned long cItem::getID( void ) const { return( type ); } bool cItem::operator ==( const cItem & i ) const { return( type == i.type ); } bool cItem::operator <( const cItem &i ) const { return( type < i.type ); }
这个类的基础工作就做完了,可以在上面添加自己所需要的东西。接下来我们来谈谈cItemPack
物品包
用户希望物品包拥有如下属性: 将一个物品放入一个包中 创建一个空包 从一个包中将所有物品移除 添加某一物品到包中 从包中移除某一类物品 计算物品的某种类型 将两个包合并成为一个包
包中有列表存放物品的类型。以后我们就可以通过列表容器来访问集合中的物品。不过这里的时间消耗为O(n)。
也可以使用vector来实现,只需要O(1)的访问时间。但如果包中只存在一个物品,那就有点浪费。另外,如果物品很复杂,包含很多属性,或是两个物品有相同类型但不同属性,那么,依靠类型来获得物品,就会出问题,所以我们用map,下面是类定义:
cItemPack类: class cItemPack { std::map< cItem, unsigned long > contents;
public: cItemPack( cItem & , unsigned long ); void clear( ); unsigned long add( const cItem & , const unsigned long ); unsigned long remove( const cItem & , const unsigned long ); unsigned long getAmount( const cItem & ) const; const std::map< cItem, unsigned long > & getItems( ) const; cItemPack & operator= ( const cItemPack & ); cItemPack & operator+= ( const cItemPack & ); cItemPack operator+ ( const cItemPack & ) const; };
通过getAmount()接口获取物品的数量,可以很方便的展现商品清单等功能。通常cItemPack会表示一个实体,所以获取物品数量的接口是非常重要的。下面是cItemPack实现部分:
cItemPack::cItemPack( cItem & i, unsigned long a ) { contents[i] = a; } void cItemPack::clear( void ) { contents.clear( ); } cItemPack & cItemPack::operator=( const cItemPack & o ) { contents = o.contents; return( *this ); }
两个构造函数,一个清除函数,一个=操作:这里没有具体的初始化,构造是依靠传进来的物品。接下来的函数可以向集合中添加某种物品。
unsigned long cItemPack::add( const cItem & i, const unsigned long a ) { return( contents[i] += a ); }
下面的函数会返回某一种类型物品的数量,注意,因为map中的[]操作符并不是const,而这个函数的参数却是const,所以必须用map 的const iterator。 unsigned long cItemPack::getAmount( const cItem & i ) const { std::map< cItem,unsigned long >::const_iterator j; j = contents.find( i ); if( j == contents.end( ) ) { return( 0 ); } else { return( j->second ); } }
这里我们还需要一个移除函数,代码如下: unsigned long cItemPack::remove( const cItem & i, const unsigned long a ) { unsigned long t = contents[i]; if( a > t ) { contents[i] = 0; return( a-t ); } else { contents[i] = t-a; return( 0 ); } }
接下来是两个联合集合的函数: cItemPack & cItemPack::operator+=( const cItemPack & o ) { std::map< cItem,unsigned long >::const_iterator i; for( i = o.contents.begin( ); i != o.contents.end( ); ++i ) { add( i->first, i->second ); } return( *this ); }
cItemPack cItemPack::operator+( const cItemPack & o ) const { return( cItemPack(*this) += o ); }
最后,将map的接口提供出来: const std::map< cItem,unsigned long > & cItemPack::getItems( void ) { return( contents ); }
物品数据库
我们在这里制定的物品都有名字,有简短的描述,以及值和重量,他们保存在下面的结构中:
struct sItemData { std::string name, description; unsigned long value, weight; };
我们使用monostate来表现这个数据库,这个对象类似单件,只存在一份,不过不能直接进行全局访问。
下面是该对象的定义: namespace cItemDatabase { const sItemData & getData( const cItem & ); cItem create( const std::string & ); void initialize( const std::string & ); void unload( void ); };
第一个函数是获得物品数据 第二个是创建一个物品,参数是该物品的名字
不过这里需要注意的是,用这些函数操作物品的时候,都必要要保证被操作的物品是已经被初始化的。不过要注意的是,如果操作失败的话,这里并没有一个返回错误的机制。第二,创建函数在创建物品的时候,并没有判断穿过来的物品名是否有效;第三,用户在获取某一个物品时,如果该物品并不存在于数据库中,同样没有一个返回错误的机制来通知用户。那么我们现在来创建一个返回错误的机制:
enum eDatabaseError { IDBERR_NOT_INITIALIZED, IDBERR_INVALID_NAME, IDBERR_INVALID_ITEM };
这是数据库的定义: namespace cItemDatabase { std::deque< sItemData > item_database_entries; bool item_database_initialized = false; };
Deque用在这里的好处就用不着再讲了。呵呵,看老外一大段都是废话我就懒得翻译了。接下来是实现:
void cItemDatabase::initialize( const std::string & s ) { item_database_entries.clear( ); //FILE LOADING SEQUENCE item_database_initialized = true; }
void cItemDatabase::unload( void ) { item_database_entries.clear( ); item_database_initialized = false; }
这里并没有具体的调用数据文件的代码,大家都会有自己的一套机制。本文提供的源代码里有作者的一套调用系统,大家可以参考。
getData函数的实现: const sItemData & cItemDatabase::getData( const cItem & i ) { if( item_database_initialized ) { unsigned long type = i.getID( ); if( type >= item_database_entries.size( ) ) { throw IDBERR_INVALID_ITEM; } else { return( item_database_entries[type] ); } } else { throw IDBERR_NOT_INITIALIZED; } }
创建函数的实现: cItem cItemDatabase::create( const std::string & s ) { if( !item_database_initialized ) { throw IDBERR_NOT_INITIALIZED; } long i; for( i = item_database_entries.size( )-1; i >= 0; --i ) { if( item_database_entries[i].name == s ) { return( cItem(i) ); } } throw IDBERR_INVALID_NAME; }
结论
这篇文章展示了一个高效的基本物品系统。文章末尾提供了示例文件。
我也在这里做个结论,这个物品系统很简单,但非常直观,几乎所有的游戏物品系统都可以在此上面进行延伸。我已经打算把这个物品系统用到现在的项目上了。翻译这篇文章,希望对大家有点作用。欢迎访问的主页: http://www.ngame2000.com
|