Detecting if 2 Iterators point to the same thing?

Started by
39 comments, last by Sean_Seanston 9 years ago

How should I go about storing some sort of reference to which inventory is the one containing the current item?

This is not the business of the inventory, this is the business of the inventory's *UI* (or view).
That UI can simply store the selected index as an integer, along with a reference to the inventory being viewed:

struct InventoryView {
    ...
    std::vector<InvEntry> * m_inventory;
    std::size_t m_selectedIndex;
};

Note that at the point where you are doing this, you should probably be encapsulating the "Inventory" into its own object rather than letting other things directly refer to it as a vector.

Advertisement

And yes, I'd occasionally want more than one item to share the same display name - I can think of at two unique situations where I'd want that, if the gameplay desired that feature.


Just the way I've been viewing it so far... is that if I ever had 2 types of item that theoretically could have the same display name... it seems like I'd be just as well to give them different names, even if it was something like "AK-47" vs "AK-47 (Gold)" or w/e.


I was thinking:
A) Quest related

If you have an item that you carry with you long-term through a quest sequence, and the item takes on new attributes as you progress through the quest, you might want to silently swap out the quest item for the next item in the sequence, but have them look identical and have the same name.

Example: You're required to get a "Official Pardon" signed by three judges. You have "Official Pardon" in your inventory, as given to you by the Judicial Administrator of Blahblah Sometown.

Item #221 = "Official Pardon". Description = "A bureaucratic form requesting pardon for Charlie the condemned blacksmith. It needs to be signed by three high-court judges."

Item #222 = "Official Pardon" (still the same display name). Description = "A bureaucratic form requesting pardon for Charlie the condemned blacksmith. It's been signed by The Honorable Justice McGuyver. It needs to be signed by two more judges."

...and so on. Yes, you could still give them different names (like "Official Pardon (1 signature)", but stylistically I'm not fond of that because, to me personally, it can be slightly immersion breaking.

B) Cursed items

Imagine a cursed helmet that disguises itself as a regular helmet of the same type, i.e. "Iron Helm"

In single-player games, I wouldn't mind the cursed item having a different name (like "Black Steel Bascinet"), which lures the player to try it on just by sounding cool. But in a more online-enabled game, news would spread much more quickly, and players would self-defeatingly avoid interesting experiences by knowing ahead of time that the item is cursed (my goal of the cursed item being to create interesting experiences and unique-ish personal stories - my goal is not to annoy or frustrate players).

Another example would be two mushroom items with the same name but slightly different appearances, one of which is edible and another poisonous.

C) Enchanted items

Imagine you got an item ("Short Sword"), and imagine you enchanted it so it had quicker attack speed ("Short Sword of Swiftinessitude"). But imagine that the strength of the enchantment was based on your current enchantment level ("Short Sword of Swiftinessitude +12"). But imagine you enchanted it in both attack power and attack speed ("+5 Damaging Short Sword of Swiftinessitude +12"?). Sometimes I want items to have dynamic attributes that can be altered by scripts ran in-game, and I'd rather the benefits be shown on the tooltip and not always in its display name.

So really, it comes down to gameplay needs. Your gameplay might not need it. But in general, given two different methods of equal code complexity, I prefer the one that doesn't constrain what the content creators can do with the engine.

I used to be a part of a small 2D online RPG (10-20 players online at once). The programmer and project leader made the smart decision to give the two mapmakers (me and another gent) access to the engine via a scripting interface. Boy, we put that thing through its paces! (I personally crashing the server probably forty times. He had to give me remote access to the server so I could restart it if he wasn't available tongue.png). Any and every feature he exposed to us through the scripting engine, we consumed and used in ways he hadn't ever thought about. It's a very basic 2D RPG, and I used the scripting engine to implement various minigames, and the other mapmaker added an auction house. The scripting engine we were using was a very very basic home-rolled thing the lead developer wrote - nothing fancy. And it exposed barely any functionality.

Anyway, I guess my point is that 'creativity reaches to and exceeds the constraints of the technology it is working with'. You go down an endless bunny trail if you start coding features you don't yet need, but at the same time, you hinder creativity if you unnecessarily exclude features you don't think you'll need.

What would you have in mind? Just read all item data from a file, then have the program automatically generate numerical item type IDs in sequence?


Nope, because if you remove an item later, then every item after the removed item gets accidentally reassigned a new ID.

What I do is I generate a unique ID when I create the resource, and then save the resource details and their IDs to the file.

I personally use a std::unordered_map<MyResourceID, MyResource> for that, and use a special serialization system to read/write the entire container to file.

But another thing I've done in the past in a simpler project was to use the unique ID as the filename itself, and store each resource in its own file (i.e. Item_22342.cfg), and then at loading just scan the entire "Item" directory and read the files and the filenames (though this is only good for smaller projects or for resources that you only have a few of - like music files). If you're talking about item templates, you can use this same method with std::string IDs.

So where I'm at right now regarding my inventory architecture is:
- My Inventory class contains a vector, the vector contains objects of the class InvEntry.
- InvEntry represents an inventory entry describing the item contained.
- InvEntry contains an Item object, and a quantity integer representing how many of that Item are stored.
- The Item class describes the item.


Sounds good.

Two thoughts:
- How are you handling 'condition' in this system?
- How do you prevent items of different conditions from stacking?

One possible way is to make "Short Sword" with condition 3, and "Short Sword" with condition 4 merge together and stack into a single "Short Sword" with 7 condition.
If acceptable gameplay-wise, basically each sword slash uses one "usage" of the sword. You could relabel "condition" in equipment and "item count" of potions to actually mean the same thing, relabelling them both as "uses left".

This means you'd have to give up your 1-5 'condition' range, and instead go to a >0 'usages', and if you pick up a sword, maybe it came with 5 usages, and when you pick up another sword of the same item, it comes with 5 more, giving you ten on that "stack".

This is the easiest way to go about doing it. In this design, you wouldn't need InvEntry, and you'd just have std::unordered_map<ItemID, Item> EveryItemInGame; or std::unordered_map<std::string, Item> EveryItemInGame; (or a std::vector).
And the player's inventory would just be a vector or map of struct { ItemID, numUsagesForThatItem }, or you could keep the usages in 'Item' itself, and just initialize every item's usages to 0.

If you do use a string for lookups, I suggest you either be super positive about normalizing your strings (i.e. converting them all the lowercase and stripping out whitespaces), or else you make your std::unordered_map or std::vector lookup functions be case-insensitive.

When I need to make a std::unordered_map case insensitive, I do so like this:


//Declared in a header someplace where it's easy to re-use.
//WARNING: Test before actually using, I can't copy+paste my real code because of dependencies.
class case_insensitive_equals
{
public:
   bool operator()(const std::string &strA, const std::string &strB) const
   {
      if(strA.size() != strB.size())
         return false;
      
      for(size_t i = 0; i < strA.size(); ++i)
      {
         if(std::tolower(strA[i]) != std::tolower(strB[i]))
            return false;
      }
      
      return true;
   }
};

typedef std::unordered_map<std::string, Item, std::hash<std::string>, case_insensitive_equals> ItemMap;

ItemMap EveryItemInGame;

You might need the hash itself to be case-insensitive too, I forget.


Note that at the point where you are doing this, you should probably be encapsulating the "Inventory" into its own object rather than letting other things directly refer to it as a vector.

Actually yes... I made a slip-up there, must have just been tired/not thinking straight... I do have an Inventory class, containing a vector of InvEntry objects, but for some reason I had started to implement my idea and had defined a pointer to an std::vector< InvEntry > rather than the containing Inventory for some reason... probably habit from how I'd been accessing the inventory up to this point... bleh, nvm.


A) Quest related

...

B) Cursed items

...
C) Enchanted items

Yes... I see how that makes sense now. Those are some interesting ideas that are definitely worth keeping open given the presumably trivial effort of allowing duplicate item names.

The enchanting example is much like the "+" idea I mentioned earlier; I think I stole the idea from Fallout New Vegas... but it was almost certainly in other games before that too IIRC. Just gives the player a clue that the item is unusual or more than it first seems, and invites them to investigate the description. Best of both worlds probably for something like enchantment.


Anyway, I guess my point is that 'creativity reaches to and exceeds the constraints of the technology it is working with'. You go down an endless bunny trail if you start coding features you don't yet need, but at the same time, you hinder creativity if you unnecessarily exclude features you don't think you'll need.

One of the most frustrating parts of software development, I reckon. That bunny trail was my undoing in that project I mentioned where I used XML, but at least I learned a valuable lesson. That balance can be so hard to find at times, but I think I'm getting better.


What I do is I generate a unique ID when I create the resource, and then save the resource details and their IDs to the file.

Hmmm... don't think I'm following the exact process involved here...

How is the resource created?

Are you loading in item data to create the resource, e.g. from an XML file, then assigning it an ID etc. and saving that to a separate file?

I'm probably wrong there but that's the best I can figure...


- How are you handling 'condition' in this system?
- How do you prevent items of different conditions from stacking?

- Condition is stored in the Item object.

- The Item object has an overloaded == operator that returns true if an Item has the same ItemType and condition.

So when adding an InvEntry to an Inventory, they get stacked only if the Item objects inside are equal. That way if I decide later that I want to stack them based on some additional criteria, I can just change the == operator.


This means you'd have to give up your 1-5 'condition' range, and instead go to a >0 'usages', and if you pick up a sword, maybe it came with 5 usages, and when you pick up another sword of the same item, it comes with 5 more, giving you ten on that "stack".

Well the main function of items in the game I'm working on is as commodities to be bought and sold, where the condition was added to expand the possibilities a bit (e.g. "Buyer X is only interested in Condition 5 of Weapon Y." or perhaps even "Consistently sell items of condition 2 and gain reputation for poor quality goods." or s/t) and alter the value of items. I do however feel it makes sense too that items could be equipped by player characters etc. in certain situations so I'll also want to be able to implement actual abilities/stats in some way.

Furthermore, perhaps the actual "usage" stats could be directly relevant to their functions as commodities. e.g. Someone tasks you with providing X number of weapons that use round Y or have a magazine capacity of Z.

Haven't thought it all through yet, I just have a reasonably general idea of where I want to go so far. I did think about making weapons for trading and weapons for equipping completely separate (or just massively restricting the weapons/items that can be used by characters) but I think I'll go ahead with using one type for both until I experience some major problem with that.

What I do is I generate a unique ID when I create the resource, and then save the resource details and their IDs to the file.

Hmmm... don't think I'm following the exact process involved here...

How is the resource created?

In my own project, I create resources within an editor I made.

Basically, to use images as an example, the process I use goes like this:

- Editor loads "Textures.dat" which contains [ID, TextureDetails] (and TextureDetails contains, among other things, the filepath to the texture itself).

- Editor puts it all in std::unsigned_map<TextureID, TextureDetails>

- At load-time, Editor runs through the map and finds the largest ID and sets it to 'nextID'.

- When I want to add a new texture to the editor, the editor assigns it an ID of "++nextID".

- The editor re-saves the entire map to 'Textures.dat', ID and data together.

Another solution, which I also use, goes like this:

- Editor reads Textures directory looking for every *.xml file (I'm using yaml, but the idea is the same) (file contains the texture details and the unique IDs).

- The editor loads all those .xml files into the std::unsigned_map<TextureID, TextureDetails>

- At load-time, Editor runs through the map and finds the largest ID and sets it to 'nextID'.

- When I want to add a new texture to the editor, the editor assigns it an ID of "++nextID".

- The editor re-saves the entire map to 'Textures.dat', ID and data together.

If you are creating resources outside of any editor instead of inside of it, you could use filename hashes or something - but one of the benefits of IDs is that it provides a level of indirection if you rename or move a file (e.g. fixing a typo). You could use creation-date instead, or manually enter the date/time as an ID (2015-04-11 4:51pm = 1504111651), but you risk messing up and accidentally assigning the same ID twice. Though your game could detect that at runtime, saving alot of debug hassle. Another option is writing a little command-line tool that runs through all the files, finds the largest ID, and gives you a new available ID to use. That might be over-complicating the process though.

So then for example, if you had 10 textures they would have the IDs {0,1,2,3,4,5,6,7,8,9}, and if you deleted the last 5 it would look like: {0,1,2,3,4}, then if you added another texture... am I right that it would be assigned ID 10 and 5-9 would never be used again?

If we take the xml example, I assume the original .xml data was created using the editor itself? So something like: Open editor, load texture, save as .xml with generated ID, then it saves the map in .dat form also?

So in this case, do the .xml files only exist as a second copy of the data, so as to be human-readable? (Then you could manually alter the .xml files directly and the editor would load it up and replace the .dat with the updated data).

I also wonder about how to refer to the resources by ID. In the first example, if the IDs are assigned by the editor and saved to a .dat, how do I know the IDs of each Item so I can e.g. give an item to the player at some specific point in the story? If it's saved to a .dat then it's not human-readable I assume.

Or do we read them from inside the editor once the data has been loaded?

Maybe I'm missing something obvious.

So then for example, if you had 10 textures they would have the IDs {0,1,2,3,4,5,6,7,8,9}, and if you deleted the last 5 it would look like: {0,1,2,3,4}, then if you added another texture... am I right that it would be assigned ID 10 and 5-9 would never be used again?

Sortof, yes. Since I check at load-time for the highest ID, it'd actually start counting from 5 again. But if you had {0,1,2,3,4,5,6,7,8,9}, and deleted all of them except '9', it'd continue counting at 10.

This isn't a big deal, because I'm using a map not an array, so the "wasted" IDs take up literally zero space. (If I was using a vector, then there would be wasted "empty" spots, but not with maps). And I don't care if I waste a couple ID numbers, because I use uint16_t's as my IDs (unsigned shorts - 2 bytes), giving me up to 65,535 IDs, which is plenty for my needs. If I needed more, I'd use uint32_t's (unsigned ints - 4 bytes), which would give me >4 billion IDs.

But if it became a big deal, it's equally easy to write code to have the editor find an available empty ID, reusing spots that have already been needed. It's just not necessary in my case.

If we take the xml example, I assume the original .xml data was created using the editor itself?

Yes, but they could be hand-created if desired. An editor isn't necessary.

So something like: Open editor, load texture, save as .xml with generated ID, then it saves the map in .dat form also?
So in this case, do the .xml files only exist as a second copy of the data, so as to be human-readable?

Yes, sortof. You don't actually need to save the .dat for smaller projects.

In my engine, when running the editor, the editor saves/writes the XML files. However, as an optimization, when the editor closes, it writes all the texture details into a single .dat file so the actual game doesn't have to go crawling through the directory looking for files, reading and parsing thousands of text human-readable files,

This is a premature optimization on my part, just to make the real game's start-up occur faster for end users. It's premature because it wasn't actually running slow, and I just was guessing by the time I get all my >10,000 different files added to the game, that it might run slow. This is why I say for smaller projects this probably isn't necessary. Or rather, for situations where the number of resources are fewer than a couple thousand.

In my case, the editor automatically scans the directory for textures, and also scans the directory for human-readable files (YAML, in my case).
If it finds textures but no matching YAML, then it auto-adds that texture to the list (but marks it as "New" so I can visually see it in the editor, so I can add any details I need to).
If it finds a YAML but no matching texture file, then it marks that texture's details as "Missing"

Here's my actual code, copy+pasted: (thankfully it's one of the better commented areas of my project)


//Crawls the editor resource directories looking for Yaml files describing what resources exist,
//as well as looking for new resources.
void Resources::CrawlDirectoryForEditorResources()
{
    ERROR_CONTEXT("When crawling the resource directories for YAML files...", "");

    //Load the tag redirects.
    this->Load_TagRedirectMap_ForEditor();
    this->Load_MaterialDetails_ForEditor();

    //==========================================================
    //Load the texture details and animation details.

    //---------------------------------------
    //Phase 1: Find all the image files.

    StringList missingTextures;
    StringList imageFilepaths = GetFilesInDirectory(this->Paths.Editor.TextureDirectory, SV_MatchesWildcard({"*.png", "*.jpg", "*.jpeg"}));

    //---------------------------------------
    //Phase 2: Load all the Yaml files.
    StringList yamlFilepaths = GetFilesInDirectory(this->Paths.Editor.TextureDirectory, SV_MatchesWildcard({"*.yaml"}));
    for(std::string &filepath : yamlFilepaths)
    {
        //Load the yaml filepath.
        this->priv_loadTextureAnimationDetailsFile(filepath);

        //Remove ".yaml" from the filepath, so we can compare the strings later.
        if(filepath.size() > 5)
        {
            //The image filepath is the same as the yaml filepath, except without ".yaml" as an extension.
            std::string imageFilepath = filepath.substr(0, (filepath.size() - 5));

            //Remove the image filepath from all the images we found when we crawled the directory.
            if(!RemoveAll(imageFilepaths, imageFilepath, PathsAreEquivilent))
            {
                //If we can't find this image filepath in the files we've just crawled, mark it as missing.
                missingTextures.push_back(imageFilepath);
            }
        }
    }

    //---------------------------------------
    //Phase 3: Process our missing and new textures.

    //Any remaining filepaths in 'imageFilepaths' were images we aren't yet aware of.
    StringList newTextures = imageFilepaths;

    //For every new texture, create a new TextureDetails.
    for(const std::string &filepath : newTextures)
    {
        this->CreateTextureAnimationDetailsFile(filepath);
    }

    //For every missing texture, modify the TextureDetails to mark it as missing.
    for(std::string &filepath : missingTextures)
    {
        Engine::TextureID textureID = MapGetKeyFromValue(this->Details.TextureDetails, Engine::TextureDetails(filepath), Engine::InvalidTextureID);
        if(this->Details.TextureDetails.count(textureID) > 0)
        {
            this->Details.TextureDetails[textureID].fileStatus = TextureDetails::FileStatus::Missing;
        }
    }

    //==========================================================
}

//Creates a new details for the image located at 'textureFilepath' (assumes there is no existing details for the image).
void Resources::CreateTextureAnimationDetailsFile(const std::string &textureFilepath)
{
    //Get an available ID.
    Engine::TextureID newTextureID = (this->Details.nextTextureID++);

    //Fill in the details.
    Engine::TextureDetails newTextureDetails;
    newTextureDetails.filepath = textureFilepath;
    newTextureDetails.tagSuggestions = this->GenerateTagsFromFilepath(textureFilepath);
    newTextureDetails.fileStatus = TextureDetails::FileStatus::New;

    //Add it to the details map.
    this->Details.TextureDetails[newTextureID] = newTextureDetails;

    //Save the details.
    this->Save_TextureDetail_ForEditor(newTextureID);
}

//Loads the details for the image located at 'textureFilepath', and returns the texture's ID.
Engine::TextureID Resources::priv_loadTextureAnimationDetailsFile(const std::string &yamlFilepath)
{
    ERROR_CONTEXT("When loading a TextureDetails/AnimationDetails editor-friendly file", yamlFilepath);

    //...yaml crap...
}

//============================================

typedef std::vector<std::string> StringList; //In some header somewhere

//============================================
//In some other file somewhere: (feel free to take!)

#include <boost/filesystem.hpp>

/*
	Returns a list of every file in the folder and (if desired) the files in each sub-folder.
	If 'timeThreshold' is specified, only files that have been written to *after* that threshold are returned.
	If 'fileFilterFunc' is specified, this filters out all file names (not the entire path) that don't pass the filter.
	If 'folderFilterFunc' is specified, entire subdirectories are filtered out.
	If 'pathPrefix' is specified, this determines what prefixes the returned paths.
	Suggestions for 'pathPrefix' are: Nothing ("FolderA"), "./" ("./FolderA") for relative paths, or passing in baseFolder for absolute paths.

	Example:
		"./File1.ext"
		"./File2.ext"
		"./FolderB/File3.ext"
		"./FolderB/File4.ext"
		"./FolderB/Folder2/File5.ext"
		"./File6.ext"
*/
StringList GetFilesInDirectory(const std::string &baseFolder, SubFoldersEnum subFoldersEnum, const std::string &pathPrefix, std::time_t timeThreshold)
{
	//Call the original function, but with filters that accept everything.
	return GetFilesInDirectory(baseFolder, IsAnything, IsAnything, subFoldersEnum, pathPrefix, timeThreshold);
}

StringList GetFilesInDirectory(const std::string &baseFolder, StringValidatorFunc fileFilterFunc, SubFoldersEnum subFoldersEnum, const std::string &pathPrefix, std::time_t timeThreshold)
{
	//Call the original function, but with a directory filter that accepts everything.
	return GetFilesInDirectory(baseFolder, fileFilterFunc, IsAnything, subFoldersEnum, pathPrefix, timeThreshold);
}

StringList GetFilesInDirectory(const std::string &baseFolder, StringValidatorFunc fileFilterFunc, StringValidatorFunc folderFilterFunc,
							   SubFoldersEnum subFoldersEnum, const std::string &pathPrefix, std::time_t timeThreshold)
{
	StringList listOfFiles;
	if(!DirectoryExists(baseFolder))
		return listOfFiles;

	boost::filesystem::directory_iterator directoryIterator(baseFolder);
	boost::filesystem::directory_iterator endOfDirectory; //An unitialized iterator is the end iterator.

	//Loop through everything in this directory.
	for(; directoryIterator != endOfDirectory; directoryIterator++)
	{
		//Make sure the item found is a file and not a folder.
		if(boost::filesystem::is_regular_file(directoryIterator->path()))
		{
			//Get the name of the folder.
			std::string filename = GetFilenameFromPath(directoryIterator->path().generic_string());

			//Check if the filename passes the filter.
			if(fileFilterFunc(filename))
			{
				//Check if it was written to after 'timeThreshold'.
				if(timeThreshold == 0 || timeThreshold < boost::filesystem::last_write_time(directoryIterator->path()))
				{
					//Add it to our list.
					listOfFiles.push_back(ConvertWinPathToUnix(pathPrefix + '/' + filename));
				}
			}
		}
		else if(boost::filesystem::is_directory(directoryIterator->path()))
		{
			//Check to make sure we want to crawl subfolders.
			if(subFoldersEnum == IncludeSubFolders)
			{
				//Get the name of the folder.
				std::string directoryName = GetFinalDirectoryFromPath(directoryIterator->path().generic_string());

				//Check if the directory name passes the filter.
				if(folderFilterFunc(directoryName))
				{
					//Get all subfolders and add them to the list.
					listOfFiles += GetFilesInDirectory(baseFolder + "/" + directoryName, fileFilterFunc, folderFilterFunc, IncludeSubFolders, (pathPrefix + '/' + directoryName + '/'), timeThreshold);
				}
			}
		}
	}

	return listOfFiles;
}


In this case, I'm just showing what I'm doing, so you can get an idea and more food for thought, and am not necessarily advising you do the same. Since you're talking about items, not textures, you don't need to keep track of textures AND yaml files, you can just have your item XML files.

If you don't use an editor, but edit the files by hand, you can do something like: Create an XML file manually, but don't create an <ID> field. When the game starts up, if it finds an XML file without an ID field, it assigns the next available ID and re-saves the file, so the file now has a permanent unique ID. Any missing fields could be initialized to defaults and re-saved, with the 'default' ID being a unique one.

I also wonder about how to refer to the resources by ID. In the first example, if the IDs are assigned by the editor and saved to a .dat, how do I know the IDs of each Item so I can e.g. give an item to the player at some specific point in the story?


In my specific case with the textures, the IDs are displayed by the editor, so I can see that #5742 = blah. In the small ORPG I mentioned previously, we just had our scripts do: AddItemToInventory(playerID, itemID). And really, for the several hundred items we had, this worked fine, because we had our editor open while we edited scripts (a different editor, different project, written by different programmer). Yea, we had to do some scrolling through lists of items to find what the ID was, but that really wasn't a problem.

In my new editor, I wouldn't even have to scroll, since the new editor can search lists of [textures, in my case] using wildcards (e.g. "potion" displaying "Red potion","Potion of healing", "HiPotion", and so on), to filter the lists by tags and display names, and filters instantly while typing in the search box.

Though you could also have your scripts lookup an item by name if you want to, but again you might accidentally later rename the item, or more than one item with the same name or typos in the name (though you can typo numbers too!).
This might look something like this:


AddItemToInventory(LookupItemByName("Bob's Quest Item"))

(Where 'LookupItemByName' would emit a descriptive error if the item isn't found, and a different descriptive error if more than one item with the same name exists).

Or do we read them from inside the editor once the data has been loaded?

Ah yes, this one. smile.png

If you don't have a fancy editor, and don't want to waste the development time making one (smart choice!), you could do this very easy hack:

When your game loads up, it reads the directory looking for all the item XML files (and fills in any missing IDs), it could then immediately output (while the game is still running) a very simple, very basic, CSV (comma-seperated-value) text file that contains just the display name and the ID of every item.


1,Potion
2,Sword of Pwning
17,Fluffy Kitten
18,Midnight Armor
142,Bob's Quest Item

This file wouldn't be used by the game at all, just auto-generated by the game at startup for your own use during development.

What you do then is just open it up in Excel or Google Docs or whatever (which you can have open in another window/tab while you work) - both of which can read CSV files, and whenever you want to know the ID of an item for your scripts or events or whatever, you just do a Ctrl+F search for the display name of the item.

(CSV is a real simple format. Feel free to hack apart my CSV outputting function to get started. My function has a few weird dependencies on my own libraries, but you can cut those out easily enough, especially since you don't need all those features)


Create an XML file manually, but don't create an field. When the game starts up, if it finds an XML file without an ID field, it assigns the next available ID and re-saves the file, so the file now has a permanent unique ID. Any missing fields could be initialized to defaults and re-saved, with the 'default' ID being a unique one.

Sounds like a good idea.


hen your game loads up, it reads the directory looking for all the item XML files (and fills in any missing IDs), it could then immediately output (while the game is still running) a very simple, very basic, CSV (comma-seperated-value) text file that contains just the display name and the ID of every item.

1,Potion
2,Sword of Pwning
17,Fluffy Kitten
18,Midnight Armor
142,Bob's Quest Item

This file wouldn't be used by the game at all, just auto-generated by the game at startup for your own use during development.

Makes sense. Should solve the problem with a minimum of hassle. Shouldn't have any major problems now once I get to properly implementing the item system.

The vector-based inventory system is causing a few unforeseen complications though. I've got to the point where I'm transferring items from one inventory to another, and specifically the method of not removing entries but instead flagging them as being erased has thrown up some bugs which, while not insurmountable, are making me wonder if perhaps a map might have been the more appropriate choice.

The main issue is that while "erased" entries (I decided to just introduce a bool for simplification) can be treated as not existing, the fact that they do exist requires quite a lot of manual accommodation of the fact that they need to be skipped/hidden/ignored.

One simple example is that if I transfer 2 of Gun X to an inventory that previously had none, it shows up as it should. However, if I then take those 2 and transfer them back, I wind up with a Gun X entry showing up a quantity of 0, which while accurate is rather messy. That can obviously be ignored when drawing the inventory takes place. But then I ALSO need to ensure that as I'm traversing the vector of items, I make sure these items are skipped because otherwise I'm selecting an invisible item and the cursor/caret disappears.

Another thing is that I've had it so an empty inventory will show a simple "(EMPTY)" message instead of a list of items. Fine, but what happens if there are no valid InvEntry objects in the vector, but the vector is not in fact empty due to the presence of one or more "erased" entries...? I know should be showing up "(EMPTY)" but instead I get nothing, as the vector is not empty but there are no items that will not be skipped when it comes to printing out their details.

Clearly, all of these can be worked around; for instance I could iterate through the entire vector to see if every entry was marked as having been erased, but by this point it all seems to be piling up and I have to wonder if the system is really working.

The way I see it... the problem, IIRC (and I may indeed have forgotten something... I'll go back and check after I post this), was that an iterator to a map element was no good if it was being compared with an iterator to an element in another map. However, as it stands I currently have both a simple integer as an index into the selected vector inventory, AND a pointer to the currently selected Inventory.

So it seems to me that if I used a map I could easily remove elements without all of the messy flagging of extant entries as being invalid and making sure they're always ignored appropriately (and potentially introducing hard to understand bugs that only show their heads much later down the line), and I would still just need a pointer to the current Inventory and an iterator to the relevant element in the map. When a different inventory is selected than the one the index refers to, the index is already rendered invalid and if not properly zeroed could go out of bounds etc. so I don't see that it's any safer with the vector. Either way I have to check to make sure I'm referring to the correct container, which I believe was the problem in the first place.

Does that sound reasonable at this point? It just feels quite hackish and like it could be dangerous if I fix it by doing what it seems I'll have to do in order to get the right behaviour...

A map is excellent for referring to unique items, like your "types" of items.

But you must separate the idea of the item type (e.g. Long Sword), from the items you hold in your inventory (Long Sword with a condition of '2'), because you want the items you hold in your inventory to have different states (the items' conditions).

An easy way to do this is to hold a vector of item instances, and a map of item types. You need unique IDs for each type, so you can refer to them. You use these IDs instead of iterators or pointers. But you don't need a unique identifier for each item in a player's inventory, because each instance of the struct is the item.


struct ItemType //The item details that don't change.
{
      std::string displayName;
      TextureID appearance;
      int price;
      int attackPower;
      float whatever;
};
 
//Loaded from XML or whatever. This is a part of the game's "content". It's the same for any player who downloads the game.
//It comes with the game, just like sound effects or textures, and isn't changed when players play the game.
std::unsigned_map<ItemTypeID, ItemType> itemTypes;
 
struct Item //The item details that vary between each "instance" of the same type of item.
{
     ItemTypeID typeID; //What type of item is it? (instead of a pointer)
     int condition; //What condition is it in?
};

//Part of the game's "state". It is created during a playing session based on the game's logic and the player's actions.
//It varies from player to player what items they have in their inventory and what conditions those items are in.
//It is saved when a player saves his game.
std::vector<Item> itemsInInventory; //No unique IDs needed.


To give a player an item, just add it to his inventory:


itemsInInventory.push_back(Item(shortSwordID, condition)); //The player has one short sword.
itemsInInventory.push_back(Item(shortSwordID, sameCondition)); //Now he has two.
itemsInInventory.push_back(Item(shortSwordID, differentCondition)); //And now a third one.

If you need to count how many items of the same type the player has, just iterate over the vector and count them (wrap that into a convenience function or use a standard algorithm).

To remove an item, just erase it from the vector.

Interesting...

I think all I'll have to do is change the ItemType map I already have to use an integer key instead of a string (though I may leave that until I start properly implementing the loading of item types instead of hardcoding them) then replace the InvEntry system with a vector of Items.

Though for something like this, would there be any danger of major slowdown etc.if you had a vector with many thousands of items? It's just that with my game idea being based largely on trading items, there's a very real possibility that someone might want to e.g. sell 1000 assault rifles, or even worse something like 100,000 rounds of ammunition type X. With the previous system it was no problem to just change the quantity from 1 to any arbitrary value that fit in an int, but it seems like a red flag for me when I think of making 100,000 copies of what are probably going to be the same item.

So if the problems in the first place were:

- My std::map solution was based on item condition always being the only differing factor, where there might be more in future,

- The std::vector solution caused problems by having "phantom" elements.

- Invalidation of iterators (though perhaps this isn't such a real problem when I could reset to the beginning after invalidation, given it's all currently just for UI purposes).

Maybe it would be best if I could somehow remove the issue of phantom elements and still store many multiples of the same object using a simple integer count?...

In fact, there wouldn't be a problem with removing vector elements if I made sure to always reset iterators/indices to 0 after every removal, indeed it would probably make logical sense to reset these when someone is altering inventory contents. I could even just point them to the preceding element.

I can't foresee any reason that I'd want to have an iterator/index into an inventory except for UI selection purposes, so I don't think this should be a major problem.

Hence, if I left it as it is... i.e. use a vector of InvEntry objects, where an InvEntry has an Item object and a quantity, then removed objects by simply erasing the elements of the vector, I THINK that would probably work fine as long as I always made sure to reset the iterators/indices to somewhere before the erased element, unless I've missed something.

Does that seem right?

EDIT: Make a few quick changes by getting rid of the erase bool and replacing it with just calling erase on the vector and it seems to be working after messing around with it for a while.

Though for something like this, would there be any danger of major slowdown etc.if you had a vector with many thousands of items? It's just that with my game idea being based largely on trading items, there's a very real possibility that someone might want to e.g. sell 1000 assault rifles, or even worse something like 100,000 rounds of ammunition type X. With the previous system it was no problem to just change the quantity from 1 to any arbitrary value that fit in an int, but it seems like a red flag for me when I think of making 100,000 copies of what are probably going to be the same item.


Different implementations are good for different game designs. I was thinking of your design differently (thinking of it more like a traditional JRPG), and so was suggesting something that fits that better.

If you are selling 100,000 bullets or 100,000 assault rifles, you'll want to use a different system.

Maybe I'm still thinking of your design wrong, but in this sense, it seems like 'conditions' is less like armor durability (which is how I was thinking of it), and seems more like different items entirely. "Rusty sword" vs "Polished sword", "Tainted potion" vs "Fresh potion", {"New","Like-new","Great","Good","Poor"}.

Does it make sense in your design to actually have different quality items be actual different items?

If so, player's inventory could be: std::unsigned_map<ItemTypeID, Count>.

This topic is closed to new replies.

Advertisement