📄 items.htm
字号:
<html>
<head>
<title>Item Management System</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body bgcolor="#FFFFFF">
<div align="center">
<p><b>Item Management Systems <br>
</b></p>
<p><b>Victor Nicollet</b></p>
</div>
<blockquote>
<div align="center">
<p align="left"><i><b>Introduction</b></i></p>
</div>
</blockquote>
<div align="left">
<p>A lot of games allow the player to pick up, carry around, use, sell, buy,
drop, drink, wear various items. For such a thing to happen without becoming
an overwhelming task for the programmers, a coherent system for managing all
these actions becomes necessary. In this article, I will present and discuss
a simple and basic system for managing objects, that can easily be extended
to cover more complex cases.</p>
</div>
<blockquote>
<p><b><i>System architecture : what's what?</i></b></p>
</blockquote>
<p>Keeping all item information along with each item is overkill: all <i>small
health potions</i> have the same name, looks and properties. Sure, some objects
might have "personal" data such as ammunition, charges, enchantments,
nicknames or durability that must be stored on the item itself, but this can
be added later on.</p>
<p>The items will be represented by objects of a <font face="Courier New, Courier, mono">cItem</font>
class that will act as a black box - users of the class do not need to know
how it is working on the inside. These objects will actually contain an item
index which indicates what kind of item they are. To get the properties of an
object, there will be a <font face="Courier New, Courier, mono">cItemDatabase</font>
that holds all the information for all the possible item indices an object can
have, and which can be asked for this for information.</p>
<p>Finally, to represent piles of various items with various amounts (whether
it is a pile on the ground, a pile being traded to somebody else, or in the
player's backpack) there will be a <font face="Courier New, Courier, mono">cItemPack</font>
class which acts just like any pile of items: you can count the objects, add
some, or remove others.</p>
<blockquote>
<p><b><i>The items</i></b></p>
</blockquote>
<p>The first and foremost step is to implement the class that will represent the
items - since it's what we're going to move around and use most of the time
anyway. An "item" variable should obviously have the following properties
:</p>
<ul>
<ul>
<ul>
<ul>
<ul>
<li>Be set to another object</li>
<li>Compared to another object (to see if it's the same)</li>
</ul>
</ul>
</ul>
</ul>
</ul>
<p>Because we will need to manipulate the objects, we also need the object to
have the following (from a programming standpoint) :</p>
<ul>
<ul>
<ul>
<ul>
<ul>
<li>Be able to return an unique identifier for debugging purposes</li>
<li>Be able to be built from that unique identifier</li>
<li>All items must be fully ordered</li>
</ul>
</ul>
</ul>
</ul>
</ul>
<p>This leads to the following class definition:</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1">class cItem { </font></p>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1">unsigned long type;
</font></p>
</blockquote>
<p><font face="Courier New, Courier, mono" size="-1">public: </font></p>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1">cItem( unsigned
long );<br>
cItem & operator= ( const cItem & ); <br>
bool operator== ( const cItem & ) const; <br>
bool operator< ( const cItem & ) const; <br>
unsigned long getID( ) const; </font></p>
</blockquote>
<p><font face="Courier New, Courier, mono" size="-1">};</font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>This is it: a constructor (from an identifier), assignment and comparison operators,
and an identifier function. The private <font face="Courier New, Courier, mono">type</font>
variable is the item index that will be used later to get the item properties
from the item database.</p>
<p>Also note that the <font face="Courier New, Courier, mono">const</font> keyword
appears in a lot of places: it is quite a good programming practice to mark
as <font face="Courier New, Courier, mono">const</font> variables that should
not be modified by the function they are passed to, and as <font face="Courier New, Courier, mono">const</font>
functions those member functions that can be called without altering the object.
This way, when the <font face="Courier New, Courier, mono">cItem</font> class
will get bigger later on, it will become better to pass objects by reference,
in which case marking certain variables as <font face="Courier New, Courier, mono">const</font>
prevents those hard-to-spot bugs where one of those references is mistakenly
modified (they cause a compiler error instead).</p>
<p>As far as the behavior of the object goes, the implementation is quite straightforward:</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1"> cItem::cItem( unsigned
long t ) : type( t ) { } <br>
cItem &cItem::operator =( const cItem & o) { type = copy.type; return(
*this ); } </font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>The constructors and assignment operator are quite easy right now, since they
simply have to set the type to whatever is required. The type of the object
is used for comparisons as well:</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono">unsigned long cItem::getID(
void ) const { return( type ); }<br>
bool cItem::operator ==( const cItem & i ) const { return( type == i.type
); } <br>
bool cItem::operator <( const cItem &i ) const { return( type < i.type
); } </font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>As the system grows, so will the item class and its members, but it will happen
transparently: new things will be added, but the behavior of the previously
implemented methods will remain the same.</p>
<blockquote>
<p><b><i>Item Piles</i></b></p>
</blockquote>
<p>Joe user would expect an item pile to have the following properties:</p>
<ul>
<ul>
<ul>
<ul>
<ul>
<li>Turning an item into a pile with a single item</li>
<li>Creating an empty pile</li>
<li>Removing all the objects from a pile</li>
<li>Adding a specific amount of a certain object to a pile</li>
<li>Removing a specific amount of an object (if there are not enough,
then mention how many more objects would have been necessary).</li>
<li>Counting the objects of a certain type.</li>
<li>Combining two piles together.</li>
</ul>
</ul>
</ul>
</ul>
</ul>
<p>It is similar to associating to each item an integer (the amount of said item
present in the pile). As far as implementation goes, the user will need to access
(read and/or modify) at a moment's notice the amount of a particular item present
in the pile, which rules out all list containers and their <i>O(n)</i> random
access. </p>
<p>A vector where the indices are the item types would be the fastest method,
allowing <i>O(1)</i> access, but this approach has two problems : first, item
piles can contain as little as a single object, so allocating an array just
for one is wasted memory (especially when the vector's length is equal to the
number of different items in the game). Second, an item is not just a number:
it also has other properties than its type. Having two items with the same type
but with different properties in a single pile would lead to big problems (forcing
us into a vector of item containers later on). The only way to avoid this is
basing the access not on the type of object, but on the comparison functions
for the object.</p>
<p>The next best random access time is <i>O(log n)</i>, and can be implemented
with either sorted vectors or maps (or sets) without any serious memory hits.
Going the vector (or set) way would be impractical, because we would have to
store item-amount pairs in the container, making the implementation more complex
to code. A map, however, works perfectly.</p>
<p>This leads us to the class definition for item piles:</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono">class cItemPack {
</font></p>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono">std::map< cItem,
unsigned long > contents; </font></p>
</blockquote>
<p><font size="-1" face="Courier New, Courier, mono">public: </font></p>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono"> cItemPack( cItem
& , unsigned long ); <br>
void clear( ); <br>
unsigned long add( const cItem & , const unsigned long ); <br>
unsigned long remove( const cItem & , const unsigned long ); <br>
unsigned long getAmount( const cItem & ) const; <br>
const std::map< cItem, unsigned long > & getItems( ) const; <br>
cItemPack & operator= ( const cItemPack & ); <br>
cItemPack & operator+= ( const cItemPack & ); <br>
cItemPack operator+ ( const cItemPack & ) const; </font></p>
</blockquote>
<p><font size="-1" face="Courier New, Courier, mono">};</font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>I have chosen to allow only positive amounts of objects. While this could have
been useful to represent a NPC merchant's "plans" about what to buy
or sell, it only adds unnecessary complication, since in almost all cases an
item pile represents a physical entity where negative amounts are impossible.
Next up is the implementation:</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono"> cItemPack::cItemPack(
cItem & i, unsigned long a ) { contents[i] = a; }<br>
</font><font size="-1" face="Courier New, Courier, mono">void cItemPack::clear(
void ) { contents.clear( ); } <br>
cItemPack & cItemPack::operator=( const cItemPack & o ) { </font></p>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono">contents = o.contents;
<br>
return( *this );</font></p>
</blockquote>
<p><font size="-1" face="Courier New, Courier, mono"> }</font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>The two constructors, the clear function and the assignment operator are pretty
straightforward: there is nothing to specifically initialize. The "from
item" constructor showcases the ease of use associated with the map implementation:
items can actually be used as indices to access elements of the map. The following
function adds a definite amount of a certain object to the pile, and returns
the amount of objects after the addition:</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p> <font face="Courier New, Courier, mono" size="-1">unsigned long cItemPack::add(
const cItem & i, const unsigned long a ) {</font></p>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1">return( contents[i]
+= a );</font></p>
</blockquote>
<p><font face="Courier New, Courier, mono" size="-1">}</font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>A function that returns the amount of a certain type of object could work as
well, but it would be convenient if it could be used on a <font face="Courier New, Courier, mono">const</font>
object. The problem with this is that the [] operator on a map is not a <font face="Courier New, Courier, mono">const</font>
function, so the code needs to work around this by using a const iterator, setting
it to a possible amount of that item in the map, and if the item is not present,
return 0.</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1"> unsigned long cItemPack::getAmount(
const cItem & i ) const { </font></p>
<blockquote>
<p><font face="Courier New, Courier, mono" size="-1">std::map< cItem,unsigned
long >::const_iterator j;<br>
j = contents.find( i );<br>
if( j == contents.end( ) ) { return( 0 ); }<br>
else { return( j->second ); } </font></p>
</blockquote>
<p><font face="Courier New, Courier, mono" size="-1">}</font></p>
</blockquote>
</blockquote>
</blockquote>
</blockquote>
<p>However, a little bit more care needs to be taken of the removal function,
because we have to check if there are enough elements inside the pile to be
removed (you would not want the player to unequip 200 helmets and end up with
199 bonus headgear). The value returned by the function will be the amount of
items that could not be removed (because there were some missing) :</p>
<blockquote>
<blockquote>
<blockquote>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono">unsigned long cItemPack::remove(
const cItem & i, const unsigned long a ) { </font></p>
<blockquote>
<p><font size="-1" face="Courier New, Courier, mono">unsigned long t
= contents[i]; <br>
if( a > t ) { contents[i] = 0; return( a-t ); } <br>
else { contents[i] = t-a; return( 0 ); } </font></p>
</blockquote>
<p><font size="-1" face="Courier New, Courier, mono">}</font></p>
</blockquote>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -