• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
mike3

What is the best way to implement this "weird" kind of graph in C++ for a game?

28 posts in this topic

Hi.

I've asked this on a couple of other forums and got some useful responses but I'm still not 100% sure as to the best way to proceed with this. I've got this problem of representing a "weird" kind of graph structure, which is like a normal graph but it has an additional layer of structure called "strings" which are composed of nodes connected together and ordered in a linear order. The purpose of this graph is to represent a system of levels in a game. In one of the responses I got, it was suggested I should implement this using a conventional graph type (i.e. just an implementation of a conventional graph, like Boost's graphs). But the problems appear when considering the "strings". Namely, I have something like this:

[code]class WeirdGraph {
private:
class NodeString {
private:
<...>
std::vector<WeirdGraphNodeIterator> nodes;
<...>
public:
<...>
};

<... (there is a graph variable declared here) ...>
std::vector<NodeString *> strings;
<...>
public:
<...>

};[/code]

i.e. the weird graph object contains a private class to represent the strings. Problems are that: 1. iterators in the graph get invalidated. Each node string stores the nodes comprising it as iterators into the graph. When a node is deleted, the iterators get invalidated, and this seems to require an update to the NodeStrings. What can be done? Obviously, one could just use a graph that does not invalidate iterators (though this may lead to a penalty accessing/traversing the graph, hence why I'm a little hesitant, since I'm going to be accessing more than I'm going to be deleting stuff, though I doubt this is used in performance-critical code in any case so it probably doesn't matter.). But I'm curious to know if there may be a technique that would work even in the case of invalidating iterators. 2. To update, the node strings need to know what nodes belong to them. So the nodes contain a string ID field, which is user-specified when the user creates a string and the WeirdGraph enforces uniqueness. Is it "good form in C++" to have WeirdGraph do the enforcing and not NodeString? It feels iffy. 3. Regarding that, is there an alternative to using the user-specified string IDs that won't fail (e.g. incrementing a counter may fail long off into the future, but I doubt one will be creating and deleting billions of times. But is having that "latent failure" possibility in there OK? As it feels iffy too.)? 4. NodeString constructs nodes in its constructor, then deletes them in the destructor. This last bit makes me feel iffy as then it's not okay to pass one by value -- though we can always not do that, but I'm not sure whether having such requirements which are enforced only by user understanding is a good idea (although does it matter here since NodeString is a private internal of WeirdGraph?).

Any comments on these issues? Right now I'm really leaning toward using a non-invalidating graph type, as that would entirely eliminate 3 out of the 4 issues above, but I'd still like to know what the deal is on the 4th. The 4th is also why I'm using pointers above (perhaps I should use std::auto_ptr instead of raw pointers there? hmm) instead of objects directly, since that would necessitate some copying/by-value passing when filling that vector.
0

Share this post


Link to post
Share on other sites
[list]
[*]You shouldn't delete things through iterators; you need iterators to store pointers or IDs of the items you need to delete, but after you have finished the search you don't need the iterators any more.
[*]Can't you store node IDs in the NodeString instead of NodeString ids in the node? How do you represent the order of nodes in NodeString?
[*]C++ is full of objects that should not be copied for various reasons. If you are afraid of making a mistake, you can either enforce noncopyability with something like [url="http://www.boost.org/doc/libs/1_49_0_beta1/libs/utility/utility.htm"]boost::noncopyable[/url] or make your class safely copyable; in your case you could define a copy constructor that doesn't cause trouble, probably using smart pointers to avoid deleting the same memory twice.
[/list]
0

Share this post


Link to post
Share on other sites
[quote name='LorenzoGatti' timestamp='1328531624' post='4910116'][list]
[*]You shouldn't delete things through iterators; you need iterators to store pointers or IDs of the items you need to delete, but after you have finished the search you don't need the iterators any more.
[*]Can't you store node IDs in the NodeString instead of NodeString ids in the node? How do you represent the order of nodes in NodeString?
[*]C++ is full of objects that should not be copied for various reasons. If you are afraid of making a mistake, you can either enforce noncopyability with something like [url="http://www.boost.org/doc/libs/1_49_0_beta1/libs/utility/utility.htm"]boost::noncopyable[/url] or make your class safely copyable; in your case you could define a copy constructor that doesn't cause trouble, probably using smart pointers to avoid deleting the same memory twice.
[/list]
[/quote]

1. The iterators are so the string knows what its nodes are and can easily access them. And the graph type needs to be passed an invalidatable iterator (in the case of my home made graph class) or "descriptor" (in the case of Boost's graph which I've thought of switching to) to tell it what node to delete.

2. Store node IDs, not iterators? Then we'd need a costly search through the graph to look up the corresponding node's ID just to access it. Iterators provide O(1) access. As for order, there's a vector of node iterators in NodeString. (Should go and update the OP to show that. Just did it.) Then the order of nodes is simply the order in which those iterators come. The nodes are also numbered internally, and the graph has an order to its nodes as one iterates over them, that is like a vector.

3. Thanks for the answer there, but "safe copying" looks to be out of the question, considering the destructor deletes the nodes from the graph, that has nothing to do with "smart pointers", but with the graph. Where else should I delete the nodes of a destroyed string?
0

Share this post


Link to post
Share on other sites
You should:[list]
[*]Explain the operations of your abstract data type more precisely, which is crucial to finding a solution that covers all needs.
[*]Post complete code samples. Details can be important, but in this case you are omitting non-details too: [font=arial,helvetica,sans-serif]the WeirdGraphNodeIterator[/font] type, where are Node objects stored, what exactly is allocated in constructors and deallocated in destructors to make your classes unsafe to copy, and many other large gaps between guessing what you are doing and having something to give advice about.
[/list]
0

Share this post


Link to post
Share on other sites
If I understand your description correctly, you have a standard graph, and then you have a set of 'strings', each of which is an ordered subset of nodes in the graph. Is that correct?

Now, you mention deleting nodes from the graph. Under exactly what constraints can deletion occur: can you delete a node that is referenced by one or more strings? Does that deletion also cause the relevant strings to be deleted?
0

Share this post


Link to post
Share on other sites
[quote name='LorenzoGatti' timestamp='1328627488' post='4910521']
You should:[list]
[*]Explain the operations of your abstract data type more precisely, which is crucial to finding a solution that covers all needs.
[*]Post complete code samples. Details can be important, but in this case you are omitting non-details too: [font=arial,helvetica,sans-serif]the WeirdGraphNodeIterator[/font] type, where are Node objects stored, what exactly is allocated in constructors and deallocated in destructors to make your classes unsafe to copy, and many other large gaps between guessing what you are doing and having something to give advice about.
[/list]
[/quote]

1. The operations are:
add string,
remove string,
connect two strings (needs to have level numbers of the levels in the strings that are connected specified and what this does is form an edge in the
graph joining the two strings at the indicated nodes (indicated by the level numbers)),
fetch a map from a level in a string,
get/set various web attributes and string attributes (latter not yet implemented).
Strings should be referenceable by both array index (e.g. like a "vector" of strings would be) and by their special ID code (which does not change
when deleted and is the preferred method for referencing them elsewhere).

2. I've got about ~800 lines in the current implementation, and that's not counting the graph class. Do you want me to post all that code here?
But I can tell what makes strings unsafe to copy: The constructor is passed the graph on which to construct the string, by reference. It then constructs the nodes
corresponding to the given string (via, of course, the graph class's interface) in there. The destructor does the opposite, deletes all referenced nodes. It is not safe to
copy, since that would cause additional nodes to be created, with the same ID as the previous nodes, and once that copy destructor is invoked it would destroy both sets
of nodes due to them having the same IDs. Now, changing IDs on copy would mitigate the last concern, but you're still altering the graph structure on copy, and that
might not be what you want when passing those strings to other functions (so it seems by-reference argument passing is _mandatory_).
"WeirdGraphNodeIterator" is just an instance of an iterator in template that the general graph class uses. That general class gives objects of the form
Graph where V is the type of object to store in each node and E is the type to store in each edge, e.g. Graph would store an int on
each node and edge. In this program, WeirdGraphNodeIterator is Graph<LevelElement, LevelConnectionData>::NodeIterator, where the two
mentioned items store the data that defines respectively the levels and the properties of connections between them, and the NodeIterator is just the
graph's node iterator type.
0

Share this post


Link to post
Share on other sites
[quote name='swiftcoder' timestamp='1328634379' post='4910553']
If I understand your description correctly, you have a standard graph, and then you have a set of 'strings', each of which is an ordered subset of nodes in the graph. Is that correct?

Now, you mention deleting nodes from the graph. Under exactly what constraints can deletion occur: can you delete a node that is referenced by one or more strings? Does that deletion also cause the relevant strings to be deleted?
[/quote]

Yes, you are right in your basic understanding.

No, you can only delete whole strings right now, though having individual-node deletion capability may be useful.
0

Share this post


Link to post
Share on other sites
Though I'm hesitant to post the full 800-line behemoth, I can post the class definition for the current implementation. Here it is:

(and OH CRAP, the forums seem to have bungled the formatting! [img]http://public.gamedev.net//public/style_emoticons/default/sad.png[/img] )

(Note here that some terminology is different since this is a concrete realization. The "weird graph" is obviously not really called that,
so here it's called "LevelWeb", and the "strings" are called "LevelBranch"es.)

[code]class LevelWeb {
private:
/* Level element. This is used to form the nodes of the level web's graph. */
class LevelElement {
private:
int branchID; /* ID code of branch this level belongs
* to.
*/
int levelNumber; /* Level number of this level in its
* associated branch.
*/
LevelGenerator *levelGenerator; /* Level generator used to generate
* this level.
*/
bool isGenerated; /* Flag indicating whether this level
* has been generated.
*/
std::string fileName; /* Filename of file to store map in */
public:
LevelElement(); /* Default constructor */
LevelElement(int, int, LevelGenerator *,
std::string); /* Construct to specified parameters */
~LevelElement(); /* Destructor */
int getBranchID(); /* Get this level's branch ID code */
int getLevelNumber(); /* Get this level's level number */
void setLevelNumber(int); /* Set this level's level number */
bool isLevelGenerated(); /* Returns whether this level has been
* generated.
*/
LevelMap *retrieveMap(); /* Retrieve the level map. Generates
* the map if it's not already generated
* or there's no map stored.
*/
XXXXXError saveMap(LevelMap &); /* Save a level map to the file */
};

/* Level connection data. This is stored in the graph's edges. */
struct LevelConnectionData {
PortalType portalType; /* Type of portal used for this
* connection.
*/

/* Constructors */
LevelConnectionData() : portalType(PORTAL_UPSTAIRS)
{}
LevelConnectionData(PortalType portalType_) : portalType(portalType_)
{}
};

/* Graph data types */
typedef Graph<LevelElement, LevelConnectionData> WebGraph;
typedef Graph<LevelElement, LevelConnectionData>::NodeIterator WebGraphNodeIterator;
typedef Graph<LevelElement, LevelConnectionData>::EdgeIterator WebGraphEdgeIterator;

/* Level branch. The web is composed of branches, linked together in
* the underlying graph.
*/
class LevelBranch {
private:
int parentWebID; /* Parent web ID code */
int branchID; /* Branch ID code. Must be unique
* for each branch in the web.
* Used to identify the branch
* _and_ to tag the nodes in the
* graph.
*/
std::string branchAbbrev; /* Abbreviated description of this
* branch.
*/
std::string branchDescriptionText; /* Text description of this branch. */
WebGraph *parentGraph; /* Graph containing the nodes comprising
* the branch.
*/
std::vector<WebGraphNodeIterator> nodes; /* Graph nodes containing the
* levels in the branch.
*/
public:
LevelBranch(); /* Default constructor */
LevelBranch(int, int,
std::string, std::string,
WebGraph *,
LevelGenerator *,
PortalType,
PortalType,
std::size_t); /* Construct to specified parameters */
~LevelBranch(); /* Destructor */
int getBranchID(); /* Get branch ID */
std::size_t getNumLevels(); /* Get number of levels in this branch */
std::string getAbbrev(); /* Get abbreviated description of this
* branch
*/
std::string getDescriptionText(); /* Get branch description text */
bool isLevelGenerated(std::size_t); /* Returns whether a given level has
* been generated.
*/
LevelMap *retrieveMap(std::size_t); /* Retrieve a level map from the
* branch
*/
XXXXXError connectTo(LevelBranch &,
PortalType,
std::size_t,
std::size_t); /* Connect this branch one-way to
* another branch.
*/
void recalibrate(); /* Recalibrate this branch to the
* parent graph. Necessary after
* removing nodes from the parent
* graph (or any other operation that
* invalidates graph iterators).
*/
};

int webID; /* Web ID code */
std::string webAbbrev; /* Abbreviated description of this web */
std::string webDescriptionText; /* Text description of this web. */
WebGraph graph; /* The underlying graph containing all
* the level data.
*/
std::vector<LevelBranch *> branches; /* The level branches, which are defined
* on the graph.
*/

/* Routines defined in levelmgmt.cpp */
std::size_t searchForBranch(int); /* Private member: searches for a
* level branch with a given ID code
* and returns its index (or -1 if
* no branch found)
*/
public:
/* Routines defined in levelmgmt.cpp */
LevelWeb(); /* Default constructor */
LevelWeb(int,
std::string, std::string);/* Construct with specified parameters */
~LevelWeb(); /* Destructor */
void addBranch(int,
std::string, std::string,
LevelGenerator *,
PortalType,
PortalType,
std::size_t); /* Add a new level branch to this web. */
XXXXXError removeBranch(std::size_t); /* Remove a level branch from the
* web.
*/
XXXXXError removeBranchByID(int); /* Remove a level branch indicated by
* its ID code.
*/
XXXXXError connectBranches(std::size_t, std::size_t,
std::size_t, std::size_t,
PortalType, PortalType); /* Connect two level
* branches
* together.
*/
XXXXXError connectBranchesByID(int, int,
std::size_t, std::size_t,
PortalType, PortalType); /* Connect two level
* branches,
* indicated by
* their ID codes.
*/
LevelMap *retrieveMap(std::size_t, std::size_t); /* Retrieve a level map. */
LevelMap *retrieveMapByID(int, std::size_t); /* Retrieve a level map
* with branch indicated by
* its ID code.
*/
};
[/code]
0

Share this post


Link to post
Share on other sites
Also, note that in the above code that routines not marked "byID" take indices which index into the vector of branches. Adding and removing branches is then like adding and removing stuff to/from a vector.
0

Share this post


Link to post
Share on other sites
[quote name='mike3' timestamp='1328658880' post='4910693']
Though I'm hesitant to post the full 800-line behemoth, I can post the class definition for the current implementation. Here it is:

(and OH CRAP, the forums seem to have bungled the formatting! )

(Note here that some terminology is different since this is a concrete realization. The "weird graph" is obviously not really called that,
so here it's called "LevelWeb", and the "strings" are called "LevelBranch"es.)
[/quote]

My first thought is that storing iterators (or pointers) is "evil" in the case of an editable structure such as this. I would (assuming it is possible for you) start by separating an editable version from a final version. This is not solving your problem directly but instead solving via alternative thinking. I always setup my build environment to have a "retail" build which defines XXX_BUILD_RETAIL as a 1 or 0 depending on the build. In the case of this code, I would separate everything into map/multi_map's of data each with unique ID's as the "editable" (well, I usually call it "Mutable"). Then, storing tuples referencing the id's instead of iterators. (Tuple is a vector of id's. Reference a level with "level id", a level element with "level id, element id", etc.)

Ok, that slows things down, makes it all larger and sucks if you used the separated id mapped data directly within the game. But that is not what you do at all. Any "edit" to the mutable structure calls a "Save" function which blows away the engines current world graph and rewrites a completely new version all optimized for engine usage which includes pointers/iterators or whatever is best for the "real" data. All the "mutable" code compiles out in retail and you end up just loading the optimized items directly instead of using the mutable version to control the loading. The general code description is the following:

[code]
class LevelMap
{
... the retail build required functionality and data ...
basically your current code removing all non-const functionality.
};

#if !BUILD_RETAIL
class MutableLevelMap : public LevelMap
{
editing functions and complete duplication of all data in a non-optimized manner.
the "save" function rewrites the LevelMap data.
};

typedef MutableLevelMap LevelMap_t;
#else
typedef LevelMap LevelMap_t;
#endif
[/code]

That is just one way to solve the problem of mutable during dev versus const at release. I can think of a lot of other variations but this is my old stand by when possible. Of course if the save takes too long you can always look into proper selective updating of the final data. If memory is a problem, get a bigger machine since this is only for development purposes. (Really, get a bigger machine! :))

Sorry if this isn't usable for you, I just like posting the "right angle view of the problem" first as it can often break loose other ideas to attack something this complicated in terms of dependencies. I can see a couple ways to reorganize the data which could help but my suggestion for that direction goes into another of my favorite 90 degree view points. Ask at your own risk. :)
0

Share this post


Link to post
Share on other sites
Interesting idea. But I don't get what you mean by "mutable during dev versus const at release" here, the way this works is that there is a set-up routine that uses those add functions (in [i]any[/i] version) to set up the graphs for the level structures. That initialization needs to be done somewhere, anyways -- how would you propose it would be done? Not to mention that I may want to generate some randomly. And it [i]may[/i] even be that the graphs need to be altered during the game process (currently, I don't know of a place where that would happen, but I'd like to leave the door open for it.). I.e. there is no "dev vs. release" distinction I can see here. What I was hoping for here was something like a regular graph type, but with the added structure of "strings"/"branches" overlaid. And the suggestion I got on another forum I asked this on was to use just that: a regular graph, and build off that, and that's what I was wondering how to do. Yet if that's not possible, then maybe I would need something like this.

Though I kind of, sort of, like this approach since it does address the whole thing about what to do when deleting a node or whatever: since we re-build the entire graph from scratch, then when we do a delete we don't need to mess around with updating the strings or whatever, we just blow them away and write out fresh new ones.
0

Share this post


Link to post
Share on other sites
[quote name='mike3' timestamp='1328668068' post='4910737']
Interesting idea. But I don't get what you mean by "mutable during dev versus const at release" here, the way this works is that there is a set-up routine that uses those add functions (in [i]any[/i] version) to set up the graphs for the level structures. That initialization needs to be done somewhere, anyways -- how would you propose it would be done?

[/quote]
So the mutable versus const thing is that during development I tend to have various tools enabled which allow me to mess with the world. Those tools/hacks/tweaks would all live in the mutable wrapper. Now, if they do something simple they can reach down into the presentation layer (i.e. the data/interface which the game engine actually uses) and make the change directly or, if it is something big and complicated they can use the un-optimized data and just rewrite the entire presentation so I don't need to write potentially buggy edit functionality. Big changes can/will cause hitching of performance but for these purposes I don't care normally.

Now, as to the initialization function. I try to avoid such things in cases where it is possible. (Doesn't seem you want to limit yourself though, so this may not be appropriate.) Basically the way I try to do things is that the presentation data can be serialized in/out without going through the mutable layer. I.e. it is complete, initialized and "ready to go" in all ways and requires no functionality from the mutable layer to load/save off of disk. The intention here is to eventually remove the mutable interface and run with the constant non-modifiable data.

Now, if this is supposed to be run time dynamic even in the final game, that does throw a wrench into the works. You can keep the mutable variation in the final build and look at it slightly differently. Instead of writing out a complete copy with pointers and such, you could split your data into the raw data bits and a relational container. I.e. just for example:

[code]
struct Element
{
.. just some data here, no pointers or reference information beyond the mentioned generic "tuple" type thing.
};

struct ElementRef
{
Element* mpData;
Element* mpRelatedElement;
StringData* mpWhatever;
};
[/code]

So, in this case the "save" becomes just a process of optimization of the data so the engine can use it efficiently and you don't have to duplicate all the data. Again, this is just one possible example, there are several possible which would not require duplicating the bulk of the data in two different places but this is probably the easiest and best solution to the problem.

[quote name='mike3' timestamp='1328668068' post='4910737']
Not to mention that I may want to generate some randomly. And it [i]may[/i] even be that the graphs need to be altered during the game process (currently, I don't know of a place where that would happen, but I'd like to leave the door open for it.). I.e. there is no "dev vs. release" distinction I can see here. What I was hoping for here was something like a regular graph type, but with the added structure of "strings"/"branches" overlaid. And the suggestion I got on another forum I asked this on was to use just that: a regular graph, and build off that, and that's what I was wondering how to do. Yet if that's not possible, then maybe I would need something like this.
[/quote]
The first thing I would do if you want to just fix up the data structure itself is diagram it out and run use cases through the data. Understanding your data and the usage of it first usually helps more than anything else. While cheesy, one of the tools I use is MySql Workbench to model the data into an sql form and then I write all the operations as sql. If I see the sql getting too complicated, I look at ways to rework the data layout to simplify things. You don't need "real" data for this process, just a reasonable representation of something organized in the expected manner.

The gain from doing this process is a better understanding of your data, the access patterns required and which pieces of functionality will be the most difficult. Mapping the changed data layout back to code is a simple process and very often you can collapse it back to a very understandable structure, though it rarely is organized in any manner similar to what you started with. More than likely the suggestion to do the graph separate and tack on the strings separately would be the end of such an experimental process, but you may see a better solution just by looking at the data and relationships in a different manner.

[quote name='mike3' timestamp='1328668068' post='4910737']
Though I kind of, sort of, like this approach since it does address the whole thing about what to do when deleting a node or whatever: since we re-build the entire graph from scratch, then when we do a delete we don't need to mess around with updating the strings or whatever, we just blow them away and write out fresh new ones.
[/quote]
There are many things I like about this pattern myself. The simplification and separation of editing code/data from the actual game used data is a great benefit. And, as mentioned above, you can make variations where the mutable data continues to exist. Being able to simply rewrite the data in an optimized manner without having to figure out all the little details of which pointers to update, what items are invalidated etc saves a lot of time. Of course, if you do dynamically changing content through the mutable interface you might eventually have to go in and optimize the functions and potentially end up with doing the complicated modifications of invalidating the correct things etc, but you can do that later when your data and usage patterns have stabilized. Doing those optimizations too early could lock you into a bad data layout and make it very painful to fix at a later time.
0

Share this post


Link to post
Share on other sites
A graph is a set of vertices. Some vertices may be connected by a pair of edges.

String mentioned above is a graph as well.

First part:[code]std::map<VertexID, VertexData> Vertices;[/code]Vertices represents all vertices used by Strings and main graph itself.

Now we need an edge list. We have two choices here, depending on how they will be used. For in-order traversal, we could just list vertices, but for general graph, we need pairs of vertices. For frequently updated items we need to be able to find edges which connect a specific vertex. Unfortunately, the required structure is missing from standard library, one could use boost multiindex.

Instead, we'll do a bit of a workaround.[code]typedef std::pair<VertexID, VertexID> Edge;
typedef std::vector<Edge> Graph;[/code]Graph is a heap, ordered container, making operations log(n).

Finally, to remove items, we need a reverse index. Using smart pointers and similar has been shown to be problematic, so instead we track things manually:[code]
typedef std::set<Graph *> GraphIndex; // no smart pointers, just weak reference
typedef std::map<VertexID, GraphIndex> GraphLookup;[/code]

For algorithms:[code]addVertex(VertexID, VertexData) // log(n)
- Put VertexData in Vertices

addEdge(Graph g, VertexID a, VertexID b) // 3 * log(n)
- insert edge into Graph
- add g to GraphLookup[a]
- add g to GraphLookup[b]

removeVertex(VertexID v) // log(n) + m * log(n)
- get GraphIndex g from GraphLookup[v]
- remove v from all g
- remove v from Vertices
[/code]

Those should be enough to fully realize all operations. Iteration is omitted since it's simple enough, so is merging of graphs.

Graph is a vector, but using <algorithm> one can update it as a heap, which uses lexicographic ordering (1-1, 1-3, 1-5, 2-1, 2-3, ...). Not perfect, but doesn't require external libraries for partial edge matches (such as searching for all nodes that start at edge 1. Searching by destination edge isn't efficient.

Note that there is no special "String" class, a string is defined simply as an edge list, same as main graph. For small graphs (<64 edges), linear search would work just fine.

Strings can also be maintained by another list:[code]typedef std::map<StringID, Graph> Strings;[/code]

All modification operations should be log(n), so it will work well to thousands, even tens of thousands of vertices and edges.
1

Share this post


Link to post
Share on other sites
[quote name='AllEightUp' timestamp='1328710077' post='4910895']
[quote name='mike3' timestamp='1328668068' post='4910737']
Not to mention that I may want to generate some randomly. And it [i]may[/i] even be that the graphs need to be altered during the game process (currently, I don't know of a place where that would happen, but I'd like to leave the door open for it.). I.e. there is no "dev vs. release" distinction I can see here. What I was hoping for here was something like a regular graph type, but with the added structure of "strings"/"branches" overlaid. And the suggestion I got on another forum I asked this on was to use just that: a regular graph, and build off that, and that's what I was wondering how to do. Yet if that's not possible, then maybe I would need something like this.
[/quote]
The first thing I would do if you want to just fix up the data structure itself is diagram it out and run use cases through the data. Understanding your data and the usage of it first usually helps more than anything else. While cheesy, one of the tools I use is MySql Workbench to model the data into an sql form and then I write all the operations as sql. If I see the sql getting too complicated, I look at ways to rework the data layout to simplify things. You don't need "real" data for this process, just a reasonable representation of something organized in the expected manner.

The gain from doing this process is a better understanding of your data, the access patterns required and which pieces of functionality will be the most difficult. Mapping the changed data layout back to code is a simple process and very often you can collapse it back to a very understandable structure, though it rarely is organized in any manner similar to what you started with. More than likely the suggestion to do the graph separate and tack on the strings separately would be the end of such an experimental process, but you may see a better solution just by looking at the data and relationships in a different manner.
[/quote]

And that's just it. If I try to look at the data, try to "diagram" it, I get the following:

1. We need to store parameters for each level.
2. We need to store what levels a level is connected to.
3. We need to store parameters for the connections.
4. Finally, we need to be able to group levels together in linear "strings" so as to be able to store parameters for those strings and the levels in a string are all strung together.

Looking at 1 and 2, I see "graph". If we tried to write out a simplified description, like this:

[code]
Primary String:
Level 1:
Name (a parameter): "Alpha"
Type (a parameter): Generic level
Connected to:
Level 2 of primary string
Connection params:
Type: Stairway
Level 2:
Name (a parameter): "Beta"
Type (a parameter): Generic level
Connected to:
Level 1 of primary string
Connection params:
Type: Stairway
[/code]

Or something like that. Even just writing that down seems to scream "graph" to me. We've got levels and connections stored with them, telling what levels are connected to what other levels.

But when we go to use a graph, we run up against the problem I mentioned: how to represent the "strings". If we keep the strings as lists of iterators to the corresponding graph nodes, then we run into the problem of iterator invalidation in deletion and what to do in that case. Seems to me like what we've got so far is a choice of:

1. use my original code,
2. use a different graph implementation, one that does not have invalidating iterators that invalidate on any of the operations we perform on this web,
3. scrap the idea of using a standard graph -- but I've heard that it's often better to stick to proven data structures when possible, and it doesn't seem like it's _impossible_ to use a standard graph here, just tricky,
4. go to the "mutable" approach. But the main difficulty I see here is added complexity. It seems, looking at it, that what we'd need are two kinds of graph class: one that lists nodes and what not with ID codes and stores connections by referencing ID codes (i.e. an adjacency list with the levels storing tuples of ID codes for the nodes connected to + associated connection data), which is easy to edit (ID codes are stable so there is no invalidation), and one that stores with iterators and what not, which has the trouble of invalidation but doesn't require searches to look up nodes and whatnot based on ID codes, i.e. an "editable" graph and an "optimized" graph, with functions to "bake" the former to the latter. So then we need another graph implementation. Whereas in the other approaches we could even not write our own graph class at all and instead just use something like Boost's graph functionality (wow! and I'm thinking of switching to the Boost graph thing just to offload that extra code in my own program for the graph).

But this is where it stands, as far as I can tell. To me, it seems I'm leaning on option #2, but I'm curious what the best way is that's in the vein of 1 and 2, i.e. using just a standard graph, and also am curious how it can be done with invalidating iterators, if there's a way to address the concerns mentioned in my OP in that case.
0

Share this post


Link to post
Share on other sites
[quote name='mike3' timestamp='1328740580' post='4911100']


1. We need to store parameters for each level.
2. We need to store what levels a level is connected to.
3. We need to store parameters for the connections.
4. Finally, we need to be able to group levels together in linear "strings" so as to be able to store parameters for those strings and the levels in a string are all strung together.

Looking at 1 and 2, I see "graph". If we tried to write out a simplified description, like this:

[code]
Primary String:
Level 1:
Name (a parameter): "Alpha"
Type (a parameter): Generic level
Connected to:
Level 2 of primary string
Connection params:
Type: Stairway
Level 2:
Name (a parameter): "Beta"
Type (a parameter): Generic level
Connected to:
Level 1 of primary string
Connection params:
Type: Stairway
[/code]

[/quote]

If this is the graph (levels as nodes; following level to play relationships as directed edges; data attached to both), the "strings" [i]do not exist at all[/i]: you simply have one or more first levels and then you can just follow graph edges.
Note that a graph allows game levels to have multiple exits (leading to different levels), multiple entrances (reached from different levels) and loops of connections allowing the player to return to already visited levels.
0

Share this post


Link to post
Share on other sites
[quote]But the main difficulty I see here is added complexity. It seems, looking at it, that what we'd need are two kinds of graph class: one that lists nodes and what not with ID codes and stores connections by referencing ID codes (i.e. an adjacency list with the levels storing tuples of ID codes for the nodes connected to + associated connection data), which is easy to edit (ID codes are stable so there is no invalidation), and one that stores with iterators and what not, which has the trouble of invalidation but doesn't require searches to look up nodes and whatnot based on ID codes, i.e. an "editable" graph and an "optimized" graph, with functions to "bake" the former to the latter. So then we need another graph implementation. Whereas in the other approaches we could even not write our own graph class at all and instead just use something like Boost's graph functionality (wow! and I'm thinking of switching to the Boost graph thing just to offload that extra code in my own program for the graph).[/quote]

You're mixing up everything together, from tiny syntactic details of particular classes to various design issues to some specific fixation on the need for something that is a String.

A graph is a graph and is incredibly trivial. It is a list of vertices and a list of edges.

boost::graph is a disaster. Template MPL overload. Flexible, but incredibly verbose to get even the basic case running.

[quote]
Primary String:
Level 1:
Name (a parameter): "Alpha"
Type (a parameter): Generic level
Connected to:
Level 2 of primary string
Connection params:
Type: Stairway
Level 2:
Name (a parameter): "Beta"
Type (a parameter): Generic level
Connected to:
Level 1 of primary string
Connection params:
Type: Stairway
[/quote]

What is the purpose of all of this? Looking at things I don't understand anything here.

A "level" for me is a game level. You know: Mario, level 1-1. When you complete that, you go to level 1-2. After completing 1-8, you go to world 2 and start with level 2-1.
What is alpha/beta? What is a stairway? And why are there strings?

Is this supposed to be some sort of pathfinding? Because technically, any relation can be expressed as a graph, some can just be simplified to a tree or list. Yet graph structures are rarely used because they simply aren't needed.
0

Share this post


Link to post
Share on other sites
[quote name='mike3' timestamp='1328740580' post='4911100']
And that's just it. If I try to look at the data, try to "diagram" it, I get the following:

1. We need to store parameters for each level.
2. We need to store what levels a level is connected to.
3. We need to store parameters for the connections.
4. Finally, we need to be able to group levels together in linear "strings" so as to be able to store parameters for those strings and the levels in a string are all strung together.

Looking at 1 and 2, I see "graph". If we tried to write out a simplified description, like this:

[code]
Primary String:
Level 1:
Name (a parameter): "Alpha"
Type (a parameter): Generic level
Connected to:
Level 2 of primary string
Connection params:
Type: Stairway
Level 2:
Name (a parameter): "Beta"
Type (a parameter): Generic level
Connected to:
Level 1 of primary string
Connection params:
Type: Stairway
[/code]
[/quote]

With that more simplified outline of the problem I don't see a graph, I see a fairly simple db schema. Ok, it is a graph (anything can be called a graph for the most part) but this is a lot more simplified than I thought earlier. It seems you should be able to normalize this in the db term and look at it from a different perspective which may work out better or give you further ideas on how to rework things. Basically I would start by considering (assuming this works for your needs) that the level and the connections can both be described in a generic manner: A named set of generic parameters. So, you could do something like:

[code]
typedef std::string PropType_t;
struct Property
{
PropType_t Type;
PropType_t Data;
};

typedef std::multimap< std::string, Property > PropMap_t;

// C++11 initializer pseudo code.
PropMap_t Levels
{
{ "Level1", { "Name", "Alpha" } },
{ "Level1", { "Type", "Generic Level" } },
{ "Level1", { "Connections", "Level1Connections" } },

{ "Level1Connections", { "Stairwell", "Level2" } },
{ "Level1Connections", { "Corridor", "Level2" } },
{ "Level1Connections", { "Bridge", "Level3" } },
{ "Level2",
<etc>
}
}
[/code]

So, you now have a generic set of attributes in a single structure and they can refer to each other via "Name" within the map. You loose iteration ability of the "Levels" themselves so you probably want to re-add the concept of a list of levels:

[code]
typedef std::vector< std::string > Levels_t;
[/code]

Now, if you want to get all connections from Level1:

[code]
// Convert sql to C++
// SELECT Data FROM tblProperties WHERE Name = (SELECT Data FROM tblProperties WHERE Name='Level1' AND Type = 'Connections')

Levels_t FindConnections( const std::string& levelName )
{
Levels_t result;
typedef std::pair< PropMap_t::const_iterator, PropMap_t::const_iterator > ItPair_t;

ItPair_t props = Levels.equal_range( levelName );
for( ; props.first!=props.second; ++props.first )
{
if( props.second->Type == "Connections" )
{
ItPair_t connections = Levels.equal_range( props.first->Data );
for( ; connections.first!=connections.second; ++connections.first )
result.push_back( connections.first->Data );
break;
}
}

return result;
}
[/code]

Most other operations you might need would end up along the same basic lines. Yup, I think very database oriented, if i can't represent something with fairly simple SQL I go back to the drawing board and rework the data.

And of course you can use this data as the "mutable" version and build your optimized (with iterators/pointers) version after edits.
1

Share this post


Link to post
Share on other sites
"strings do not exist at all"

"don't see a graph"

OK, so maybe my given example was a little too simple. Here's a more complicated one with data fields closer to what we'd actually have in the game (e.g. individual levels are not named, but strings are!):

[code]
Web1 (the "weird graph" I'm referring to)
Name: "Mega Caverns"
String 1:
Name: "Main"
Level 1:
Type: Generic level
Connections:
Level 2, String 1
Type: Stairway
Level 2:
Type: Generic level
Connections:
Level 1, String 1
Type: Stairway
Level 1, String 2
Type: Stairway
Level 3, String 1
Type: Stairway
Level 3:
Type: Generic level
Connections:
Level 2, String 1
Type: Stairway
String 2:
Name: "Side Branch"
Level 1:
Type: Generic level
Connections:
Level 2, String 2
Type: Stairway
Level 2, String 1
Type: Stairway
Level 2:
Type: Generic level
Connections:
Level 1, String 2
Type: Stairway
Level 3, String 2
Type: Stairway
Level 3:
Type: Generic level
Connections:
Level 2, String 2
Type: Stairway
[/code]

(The actual game version also has file name tags and flags indicating whether a given level has been generated, the file names being for the files that store the levels.)

Why I think of graphs is because I usually diagram something like this in this way:

[code]
Let "X" denote that connected levels belong to string 1. Let "Y" denote that
connected levels belong to string 2. Let "-" denote an inter-string connection.

Level 1
X
X Stairway
X
Level 2 ------------ Level 1 YYYYYYYYYYYY Level 2 YYYYYYYYYYYY Level 3
X Stairway Stairway Stairway
X Stairway
X
Level 3
[/code]

and it looks like a graph with nodes connected by edges. More sophisticated examples could even have cycles in them. But it's this diagramming that gave me the graph idea.

Though I suppose you could implement this with the d/b as well, so then it's a toss-off between d/b and graph.
0

Share this post


Link to post
Share on other sites
It seems that, in your more complex example, the true graph is still levels and directed reachability links, and the strings are a completely superfluous and artificial grouping of levels; globally unique node identifiers are available and you are already using them.

If grouping your levels makes sense, for example because you have regions in space or episodes in time, you can use collections of pointers to nodes or collections of node names that aren't part of the graph data structure. Freestanding collections of pointers or names are nice because your objects can belong to multiple ones; they don't "own" objects.
0

Share this post


Link to post
Share on other sites
[quote name='LorenzoGatti' timestamp='1328864115' post='4911600']
It seems that, in your more complex example, the true graph is still levels and directed reachability links, and the strings are a completely superfluous and artificial grouping of levels; globally unique node identifiers are available and you are already using them.

If grouping your levels makes sense, for example because you have regions in space or episodes in time, you can use collections of pointers to nodes or collections of node names that aren't part of the graph data structure. Freestanding collections of pointers or names are nice because your objects can belong to multiple ones; they don't "own" objects.
[/quote]

That's correct. That's what I've been [i]trying[/i] to do. The problem is that the iterators to the nodes can be invalidated upon deletion, and that leads to needing to update them in all strings every time a string is deleted. And this brings some issues, which I mention in my original post. Would the best approach, then, be just to use some kind of graph that does [i]not[/i] invalidate iterators on deletion, or has some other type of stable node reference?
0

Share this post


Link to post
Share on other sites
[quote name='mike3' timestamp='1328918404' post='4911825']
Would the best approach, then, be just to use some kind of graph that does not invalidate iterators on deletion, or has some other type of stable node reference?[/quote]
You just told me that nodes don't ever get deleted, only strings get deleted. In which case, node iterators can never be invalidated. Which is it?
0

Share this post


Link to post
Share on other sites
[quote name='swiftcoder' timestamp='1328919549' post='4911833']
[quote name='mike3' timestamp='1328918404' post='4911825']
Would the best approach, then, be just to use some kind of graph that does not invalidate iterators on deletion, or has some other type of stable node reference?[/quote]
You just told me that nodes don't ever get deleted, only strings get deleted. In which case, node iterators can never be invalidated. Which is it?
[/quote]

Deleting a string deletes the associated nodes. That happens in the string object's destructor. If you were thinking of this post:

http://www.gamedev.net/topic/619851-what-is-the-best-way-to-implement-this-weird-kind-of-graph-in-c-for-a-game/page__view__findpost__p__4910656

I was trying to say you could not delete nodes [i]individually[/i], not that they couldn't be deleted at all, as deleting a string entails deleting all the nodes that belong to it. Sorry if that wasn't clear.
0

Share this post


Link to post
Share on other sites
[quote name='mike3' timestamp='1328927187' post='4911857']
Deleting a string deletes the associated nodes. That happens in the string object's destructor.[/quote]
But surely, the same node can occur in more than one string? If so, what happens to the other strings when a node they depend on is deleted by another string?

If not, then your strings are disjoint subsets of the graph, and there is no particular advantage to storing the nodes in a shared container.
0

Share this post


Link to post
Share on other sites
[quote name='swiftcoder' timestamp='1328928202' post='4911861']
[quote name='mike3' timestamp='1328927187' post='4911857']
Deleting a string deletes the associated nodes. That happens in the string object's destructor.[/quote]
But surely, the same node can occur in more than one string? If so, what happens to the other strings when a node they depend on is deleted by another string?

If not, then your strings are disjoint subsets of the graph, and there is no particular advantage to storing the nodes in a shared container.
[/quote]

Yes, they are disjoint. But they can be connected together by connecting nodes from different strings with an edge, and thus I use one graph, to represent the whole "web" of nodes. And also, because building on a standard graph type was suggested on one of the other forums I asked this question on, as it's a "standard" kind of data structure.
0

Share this post


Link to post
Share on other sites
::iterator types are for iteration. While some may be held on to, it causes aforementioned problems.

In most newer languages, iterators are explicitly checked for modification and will cause failures when trying to use them as references.

Iterators are not identity types and cannot be used for such purpose, even though in some corner cases, depending on implementation, they can temporarily take such role.

Instead of insisting on such approach, use a different approach. Establish strong identity (NodeID, StringID, EdgeID). Keep track of those separately as well as their connections. Then build structures on top of that.


There really are no difficulties here. It's just a basic list/tree structure, which iterators make impossible to implement if any of them can be mutated, something that iterators, as a concept, cannot handle. Only general solution to this problem is garbage collection, which doesn't solve cross-linking issues.
0

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0