Jump to content

  • Log In with Google      Sign In   
  • Create Account

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


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
28 replies to this topic

#1 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 06 February 2012 - 01:10 AM

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:

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

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

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.

Sponsor:

#2 LorenzoGatti   Crossbones+   -  Reputation: 2694

Like
0Likes
Like

Posted 06 February 2012 - 06:33 AM

  • 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 boost::noncopyable 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.

Produci, consuma, crepa

#3 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 06 February 2012 - 03:09 PM

  • 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 boost::noncopyable 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.


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?

#4 LorenzoGatti   Crossbones+   -  Reputation: 2694

Like
0Likes
Like

Posted 07 February 2012 - 09:11 AM

You should:
  • 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: the WeirdGraphNodeIterator 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.

Produci, consuma, crepa

#5 swiftcoder   Senior Moderators   -  Reputation: 9991

Like
0Likes
Like

Posted 07 February 2012 - 11:06 AM

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?

Tristam MacDonald - Software Engineer @Amazon - [swiftcoding]


#6 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 07 February 2012 - 04:18 PM

You should:

  • 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: the WeirdGraphNodeIterator 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.


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.

#7 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 07 February 2012 - 04:20 PM

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?


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.

#8 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 07 February 2012 - 05:54 PM

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! Posted Image )

(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.)

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.
						*/
};


#9 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 07 February 2012 - 06:04 PM

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.

#10 AllEightUp   Moderators   -  Reputation: 4210

Like
0Likes
Like

Posted 07 February 2012 - 08:06 PM

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.)


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:

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

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. :)

#11 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 07 February 2012 - 08:27 PM

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 any 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 may 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.

#12 AllEightUp   Moderators   -  Reputation: 4210

Like
0Likes
Like

Posted 08 February 2012 - 08:07 AM

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 any 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?

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:

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;
};

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.

Not to mention that I may want to generate some randomly. And it may 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.

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.

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.

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.

#13 Antheus   Members   -  Reputation: 2397

Like
1Likes
Like

Posted 08 February 2012 - 08:51 AM

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:
std::map<VertexID, VertexData> Vertices;
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.
typedef std::pair<VertexID, VertexID> Edge;
typedef std::vector<Edge> Graph;
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:
typedef std::set<Graph *> GraphIndex; // no smart pointers, just weak reference
typedef std::map<VertexID, GraphIndex> GraphLookup;

For algorithms:
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

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:
typedef std::map<StringID, Graph> Strings;

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

#14 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 08 February 2012 - 04:36 PM


Not to mention that I may want to generate some randomly. And it may 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.

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.


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:

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

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.

#15 LorenzoGatti   Crossbones+   -  Reputation: 2694

Like
0Likes
Like

Posted 09 February 2012 - 04:24 AM

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:

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


If this is the graph (levels as nodes; following level to play relationships as directed edges; data attached to both), the "strings" do not exist at all: 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.
Produci, consuma, crepa

#16 Antheus   Members   -  Reputation: 2397

Like
0Likes
Like

Posted 09 February 2012 - 08:33 AM

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).


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.

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


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.

#17 AllEightUp   Moderators   -  Reputation: 4210

Like
1Likes
Like

Posted 09 February 2012 - 08:39 AM

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:

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


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:

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>
  }
}

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:

typedef std::vector< std::string >   Levels_t;

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

// 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;
}

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.

#18 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 09 February 2012 - 03:13 PM

"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!):

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

(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:

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

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.

#19 LorenzoGatti   Crossbones+   -  Reputation: 2694

Like
0Likes
Like

Posted 10 February 2012 - 02:55 AM

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.
Produci, consuma, crepa

#20 mike3   Members   -  Reputation: 149

Like
0Likes
Like

Posted 10 February 2012 - 06:00 PM

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.


That's correct. That's what I've been trying 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 not invalidate iterators on deletion, or has some other type of stable node reference?




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS