Skip to main content
GameDev.net gamedev.net
🔒 Locked

Object-Oriented Scene Management

Started by Myopic Rhino May 2, 2002 at 12:34 AM 24 replies 9.2k views
Chris Hargrove
Chris Hargrove
I hate to sound like a bastard, especially to someone who''s made a tutorial (which I generally consider a Very Good Thing), but I''m sorry, there are so many issues in this tutorial it''s not even funny.

The key problem is that in the process of giving the basic description and intro of scene graphs, you are introducing other elements (as if they''re directly related) in a trivialized manner that is more likely to set an engine project up for failure in the long term rather than success. If you''re going to bring up big issues like good management of inheritance and use of custom RTTI solutions, you should either A) indicate that your examples are highly simplified and not suitable to a production environment, or B) provide examples that are suitable to such an environment. The lack of either of these two things is likely to introduce reader confusion and make peoples'' code worse rather than better.

Three key examples:

1) Scene graph object inheritance.

Your example uses a mesh object directly as a node in the scene graph, when in a real engine this is something you would never EVER want to do. Scene graphs are always supposed to use nodes representing instances of assets, NEVER the assets themselves. It is far to easy for the nodes to be moved around, duplicated where they refer to the same asset (such as with mirrors or other portals), and so on.

This may seem like a triviality for a tutorial example, but the people who read this tutorial may not know a thing about scene graphs, and if you lead them down the wrong path with things like this it could be extremely difficult for them to repair their engine later to fix the problems.


2) Scene graph object references and management.

You''re using direct pointers to parent and child objects, and forcing all child object destruction to be managed by the parent? Umm, no. This is unrealistic, and prone to loads of problems, or at the very least, completely ridiculous overhead.

If you want to use the scene graph as a parallel to your actor graph, what happens when a simple transient actor (like a projectile) wants to destroy itself (like when it explodes)? Does your system actually have to get the world''s permission to destroy a projectile? And how do you intend to tell the world what projectile to destroy? Pass in a pointer or handle and force it to find the right entry in its child list so it can delete it and clear the reference? There''d be no other way to clean up the parent''s pointer to the projectile child, and that means tons of unnecessary child list searches when you already KNOW what child you want to delete.

In other words, you are using an inappropriate data structure for your tree. A properly designed scene graph would allow node insertions and removals in constant time with all references cleaned up automatically. This is easy to do with a simple quad-linked tree structure, and it avoids all these problems. If a node chooses to move or otherwise delete itself, the parent and all siblings would be updated automatically without any cleanup searches required. It would also avoid the use of using std::vectors for your child lists, since std::vector is very suboptimal for small allocations that change in size frequently.


3) Hardcoded RTTI types.

No offense, but why in the hell would you ever want to hardcode your RTTI type identifiers like this? There are a ton of patterns in the factory family that avoid this process and allow adaptive behavior with regards to the object types available to the system. I posted one here in gdnet''s patterns section last year (called Reflective Factory), and it''s far from the only solution. Hardcoding your RTTI types in an enumeration generally means that this enumeration would have to be provided in a header used by anyone needing to use even one of the RTTI types (which generally means a large number of files). That makes it not only non-adaptive, but completely uninsulated adding a lot of needless compile-time dependency. In a large project this would be completely unacceptable. There is absolutely no benefit to doing it this way when the same identification can be given from a runtime class ID or pointer that is created with the associated class automatically.


I know it may sound like I''m nitpicking with this kind of stuff, and if any one of these things was present on its own I wouldn''t bother complaining about it (it is a tutorial after all, and doesn''t have to be perfect). But there are so many larger-scale design flaws in this tutorial, and novice readers who are first learning about scene graphs (i.e. your target audience) may not recognize these problems until they''ve already built a scene graph architecture around your examples only to find out later that they have to refactor the entire thing.

I know your intentions are good Jeff, but you know as well as I do that your examples won''t fly that easily in a real production codebase (or at least not a codebase that isn''t fraught with problems). The concepts from this article are good, but the context they''re provided in has a lot to be desired, and I hope readers don''t take it too literally.

- Chris
Void
Void
Chris, in the real world, you really see code like this in production. Sad but true.

For an nary STL container, take a look at this

http://www.damtp.cam.ac.uk/user/kp229/tree/#overview

It beats using a vector of pointers because STL containers are designed for storing copies, not pointers. A vector of pointers is not exception safe.

My main comments is about the RTTI implementation too. Each time you add a new type, you have to recompile the whole library.

Take a look at Modern C++ Design by Andrei Alexendrascu for a RTTI scheme that deals with C++''s "defective" RTTI.

If your base objects interfaces are well designed and you need to find out the type of object at runtime using RTTI, chances are you require a double dispatch scheme.

Check out MC++D again.
Jeff Kershner
Jeff Kershner
Thanks for your thoughts on my article. You are correct that the article does have a lot to be desired. The conundrum that I face is that I needed to explain an incredibly complicated topic in a short space that is renderer non-specific. The article really needs its own four chapters in a large book of engine creation.

Here is a clip from Chris''s post (verbatim):

Your example uses a mesh object directly as a node in the scene graph, when in a real engine this is something you would never EVER want to do. Scene graphs are always supposed to use nodes representing instances of assets, NEVER the assets themselves. It is far to easy for the nodes to be moved around, duplicated where they refer to the same asset (such as with mirrors or other portals), and so on.



What you can''t see is that the mesh would be loaded from a resource manager that allows instancing of resources. Since this article wasn''t about resource management, that was left out.

Chris''s second point talks about objects being owned by parents and being referenced by direct pointers. This can be a horrible thing and I understand your concerns. But deleting a child is just a recursive search for the node, a call to the the parent, and having the parent delete the child.

Again this article is not a game engine article but a scene graph article. If a person wants to set up a smart pointer system like I have to assure valid pointers and automatic deletion, that is fine; again not the focus of the article.

About Chris''s third point on the RTTI: It is true that the system has it''s problems; using the built in RTTI has even more. The point is that this system is quick to set up. There are a plethora of different ways of doing this. Having hardcoded enumerated types will cause the rebuilding of the library, but the frequency of doing this is quite small. And there are way around this as well.

I appreciate the comments,
/jk
Chris Hargrove
Chris Hargrove
Glad you didn''t take my comments too personally, Jeff. Like I said, I understand that it''s a tutorial and it''s really hard to capture all the issues of a large topic (like scene graphs) in only a couple pages of web text. So I totally know where you''re coming from there.

My key concern was not so much the simplicity with regards to the issues mentioned above, but rather the fact that they weren''t addressed more in the sense of "this is just an example; you wouldn''t really want to do it this way; look at my For Further Reading section for the proper way to set this up in a production situation". Even though it''s unrealistic to expect a small primer tutorial to address all the major issues, a big step in that direction is just the indication that the issues exist.

Anyway, sorry if I sounded too harsh; I just wanted to get the thoughts out there.

- Chris
Ecko
Ecko
How bad is it to use dynamic_cast for RTTI?
-----------------------------------------------------------"People who usualy use the word pedantic usualy are pedantic!"-me
yzzid
yzzid
Where is the paper you discussed located? I looked on your web page as stated in the article but couldn''t locate it.

-Dizzy
-dizzyGame Institute InternPlease rate me. :)
Jeff Kershner
Jeff Kershner
The link to Gil''s paper got lost in the new version of my web page. I will try to get it back, but here at shiny we are going through a serious crunch time. If anyone needs me to send it sooner, shoot me an email...
eli_pulsifer
eli_pulsifer
Ecko, the two main problems I have with using the build in RTTI are the following.

First, (I may be wrong here) every single object will get RTTI information associated with it which is unneeded overhead when you only really need RTTI for a select few objects. When working with console systems memory foot print is a big issue, well it used to be and still is to some extent.

Second, C++ RTTI is not portable.
Example:
VC 6.0
typeid(pMyClass).name() returns "class MyClass"
CodeWarrior 8
typeid(pMyClass).name() returns "MyClass"

That said, I still use the build in RTTI. It provides all the functionality I need and I've been able to over come all the portability issues so far with minimal effort.

I’m sure other people have reasons not to use the built in RTTI too, but I've not run into any others personally.


[edited by - eli_pulsifer on May 6, 2002 11:35:28 AM]
eli_pulsifer
eli_pulsifer
quote:
Original post by Chris Hargrove
In other words, you are using an inappropriate data structure for your tree. A properly designed scene graph would allow node insertions and removals in constant time with all references cleaned up automatically. This is easy to do with a simple quad-linked tree structure, and it avoids all these problems.



Chris, would you care to elaborate a bit on this quad linked tree data structure.
Chris Hargrove
Chris Hargrove
quote:
Chris, would you care to elaborate a bit on this quad linked tree data structure.


The basic idea uses links within circular doubly-linked lists, and embedding a pair of these links into each node of the tree.

Each link contains four pointers: The previous and next link pointers (for doubly-linked list behavior), and parent and child node pointers (which may be null in specific situations, as described below). Each node has two of these links contained in the node, the ParentLink and the ChildLink (along with whatever other data you want in the node of course).

At node creation, the links are initially constructed where they are self-linked (previous and next point back to the link itself). In addition, the ParentLink's child pointer points to the node, as does the ChildLink's parent pointer. The ParentLink's parent pointer and the ChildLink's child pointer are both set to null. After this construction, the node will be independent (not connected to any tree), but will be ready for tree functionality.

The node implements a "SetParent" method that takes another node it wants to be its parent. When this happens, the node's ParentLink's parent pointer is set to the new parent node, and the ParentLink is linked into the circular list of the parent node's ChildLink. This constant-time process automatically handles all sibling connections since they're part of this circular list. A similar process goes for unlinking, where the node sets its ParentLink's parent pointer to null and unlinks the ParentLink from whatever list it was in.

Iteration of all the children of a node is merely the iteration of all the links connected to the ChildLink of that node. This iteration can be performed starting with the ChildLink's next or previous link (depending on the direction you want), where each link's child node pointer will end up pointing to the child node you want (since these links are actually the ParentLinks of the children, and their child pointers were initialized at construction to point to the node). The iteration ends when you reach a child pointer of null, which only occurs in the original ChildLink.

All of this can be wrapped with relative ease into a few templates along the lines of Coplien's "Curiously Recurring Template" pattern, where the tree node template class is inherited from and the name of the subclass is passed into the template. For example:


  
class CMyTreeNode
: public TTreeNode<CMyTreeNode>
{
// regular node contents go here

};


In this case, CMyTreeNode would inherit all the basic functionality of a tree item (like TreeGetParent(), TreeGetFirstChild() or whatnot), with all the pointers being returned of the proper type. In addition, you can build an iterator template class to automatically go through the children of a node, which usage like:


  
CMyTreeNode* parent; // passed in from somewhere else

for (TTreeIterator<CMyTreeNode> iter(parent); iter; ++iter)
{
// overloads operator * and -> to use node pointer

CMyTreeNode* child = *iter;
iter->DoSomething(); // same as child->DoSomething()

}


A quad-linked tree has the nice benefit of being entirely built up by connection data embedded directly in the nodes themselves, without the need for any kind of dynamic allocation (like internal child pointer arrays). The only things that need to be allocated are the nodes themselves, making memory management much simpler, and alleviating a lot of fragmentation concerns. In addition, you can do a large number of operations in constant time, since its built on circular linked lists (which have a lot of constant-time operations). Data access isn't an issue since every node knows its parent, its siblings, and its children at all times. At runtime the tree is very malleable, but with a slightly higher cost for iteration. The only other real additional cost is a bit more memory, due to the two links per node (on current machines this is generally 16 bytes per link, so you get an additional 32 bytes per node of size overhead). In many situations this cost is trivial, and the benefits of using the structure outweigh the drawbacks.

As always, whether this structure is appropriate depends on the situation at hand. Scene graphs may very well be one such situation.

[Editing to put the code into source blocks]

- Chris

[edited by - Chris Hargrove on May 7, 2002 5:09:17 PM]
Countach
Countach
Thanks for the info, Chris.
Much appreciated.

I''ll adopt your system (I was using linked lists in the parent to manage the siblings, much like in this tutorial).
-------homepage - email
Infinite Monkey
Infinite Monkey
Is there a reason why the parent and child links need to be seperate from the node? Can we not just create a node class that has previous, next, parent and child pointers, and then do without the parentlinka and child link objects?
Chris Hargrove
Chris Hargrove
quote:
Is there a reason why the parent and child links need to be seperate from the node? Can we not just create a node class that has previous, next, parent and child pointers, and then do without the parentlinka and child link objects?


You can do that, but there are a number of drawbacks.

For example, it''s not as easily self-cleaning, since having only a single child pointer means that if that first child decides to delete itself it needs to clean up the reference in the parent to point to another sibling, etc.

Another example is the added complexity of making an iterator. If you start the iterator at a particular child and want it to know when to stop automatically, it will either need to hold on to that starting child (to check when it''s reached again), or do similar checks by jumping up to the parent and then comparing the result against the first child pointer and seeing if they match. Unfortunately, this can become complicated if a node deletes itself during iteration, since it can easily throw off the state of the iterator. In contrast, the quad-linked approach never loses its stopping condition (due to the always-null child pointer of the parent''s ChildLink), so it only needs to track the next node for iteration-with-deletion purposes and the rest takes care of itself.

That said, the tree form you''re talking about does only have half the overhead, so the additional complexity may be justified in certain situations where the node count is very high and memory is tight.

I never intended to imply that the quad-linked tree is the only way to do a decent tree structure (there are tons of ways to make trees). I just find it to be a simple approach which, for only a marginal additional cost in memory, takes care of a lot of issues automatically.

- Chris
eli_pulsifer
eli_pulsifer
quote:
Original post by Chris Hargrove
All of this can be wrapped with relative ease into a few templates along the lines of Coplien's "Curiously Recurring Template" pattern, where the tree node template class is inherited from and the name of the subclass is passed into the template.


Unless I misunderstand how this works, wouldn’t there need to be a non-template base class that would be used by the link object. Otherwise you could only have trees containing one type of derived node.

I guess having only one node type is fine as long as nodes are simply containers and don’t store any actual data, just a pointer to the data.

Edited to remove extra quote tag

[edited by - eli_pulsifer on May 8, 2002 10:05:30 PM]
Chris Hargrove
Chris Hargrove
The class of the node that inherits from the template (as given in the example) is your base class. The use of subclasses in the tree is perfectly fine, since the links use pointers. As long as all the tree nodes are inherited from the base node class (the one passed into the template) then you''re fine. The node functions (like GetParent(), the iterator functions, etc) will all return the base node type and not a specific subclass of course... but a good abstract interface in the base class and/or a reasonably thought-out RTTI mechanism will cover that situation (no different than any other type of scene graph).

- Chris
eli_pulsifer
eli_pulsifer
Well, I had a few hours tonight to play around so I coded up this node thing. I haven’t exactly tested this yet but it looks good

Here is my implementation:


      
template <class _TDerived>
class TNode
{
protected:

template <class _TNode>
struct TLink
{ // link for circular node linked list

typedef TLink<_TNode> _MyType;

TLink() : m_pParent(NULL), m_pChild(NULL)
, m_pNext(NULL), m_pPrev(NULL)
{}

void Init(_TNode *pParent, _TNode *pChild, _MyType *pNext, _MyType *pPrev)
{ // initialize node and link pointers

m_pParent = pParent; m_pChild = pChild;
m_pNext = pNext; m_pPrev = pPrev;
}

_TNode *m_pParent;
_TNode *m_pChild;
_MyType *m_pNext;
_MyType *m_pPrev;
};

typedef TNode<_TDerived> _MyType;
typedef TLink<_MyType>* _Linkptr;
typedef TLink<_MyType>*& _Linkpref;
typedef _TDerived& _Vref;
typedef _TDerived* _Vptr;

protected:
static _Linkpref _NextSibling(_Linkptr pLink)
{ // return reference to successor pointer in node

return ((_Linkpref)(*pLink).m_pNext);
}

static _Linkpref _PrevSibling(_Linkptr pLink)
{ // return reference to predecessor pointer in node

return ((_Linkpref)(*pLink).m_pPrev);
}

static _Vref _Myval(_Linkptr pLink)
{ // return links owner node

return (_Vref)(*(*pLink).m_pChild);
}

public:
class iterator;
friend class iterator;

class iterator
{ // iterator for sibling nodes

public:

iterator()
: m_pLink(0)
{ // construct with null node pointer

}

iterator(_Linkptr pLink)
: m_pLink(pLink)
{ // construct with node pointer _Pnode

}

_Vref operator*() const
{ // return designated value

return _Myval(m_pLink);
}

_Vptr operator->() const
{ // return pointer to class object

return (&**this);
}

iterator& operator++()
{ // preincrement

m_pLink = _NextSibling(m_pLink);
return (*this);
}

iterator operator++(int)
{ // postincrement

iterator iTmp = *this;
++*this;
return (iTmp);
}

iterator& operator--()
{ // predecrement

m_pLink = _PrevSibling(m_pLink);
return (*this);
}

iterator operator--(int)
{ // postdecrement

iterator iTmp = *this;
--*this;
return (iTmp);
}

bool operator==(const iterator& iRight) const
{ // test for iterator equality

return (m_pLink == iRight.m_pLink);
}

bool operator!=(const iterator& iRight) const
{ // test for iterator inequality

return (!(*this == iRight));
}

_Linkptr _Mynode() const
{ // return node pointer

return (m_pLink);
}


protected:
_Linkptr m_pLink; // pointer to node

};

public:
TNode()
{ // initialize links

m_lnkParent.Init(NULL, this, &m_lnkParent, &m_lnkParent);
m_lnkChild.Init(this, NULL, &m_lnkChild, &m_lnkChild);
}

virtual ~TNode()
{ }

void attach_parent(_MyType *pParent)
{ // link to parent

m_lnkParent.m_pParent = pParent;

m_lnkParent.m_pPrev = pParent->m_lnkChild.m_pPrev;
m_lnkParent.m_pNext = &pParent->m_lnkChild;

pParent->m_lnkChild.m_pPrev->m_pNext = &m_lnkParent;
pParent->m_lnkChild.m_pPrev = &m_lnkParent;
}

void detach_parent()
{ // un-link from parent

m_lnkParent.m_pParent = NULL;
m_lnkParent.m_pPrev->m_pNext = m_lnkParent.m_pNext;
m_lnkParent.m_pNext->m_pPrev = m_lnkParent.m_pPrev;
m_lnkParent.m_pPrev = m_lnkParent.m_pNext = &m_lnkParent;
}

_MyType* parent()
{ // return parent node

return m_lnkParent.m_pParent;
}
iterator child_begin()
{ // return begin iterator for children

return iterator(m_lnkChild.m_pNext);
}
iterator child_end()
{ // return end iterator for children

return iterator(&m_lnkChild);
}
iterator begin()
{ // return begin iterator for siblings

return (NULL == m_lnkParent.m_pParent) ? iterator(m_lnkChild.m_pNext) :
iterator(m_lnkParent.m_pParent->m_lnkChild.m_pNext);
}
iterator end()
{ // return end iterator for siblings

return (NULL == m_lnkParent.m_pParent) ? iterator(&m_lnkChild) :
iterator(&m_lnkParent.m_pParent->m_lnkChild);
}

protected:
TLink<_MyType> m_lnkParent; // link to parent node and sibling list

TLink<_MyType> m_lnkChild; // link to child node list

};


There are probably bugs in there, but I figured someone may find this useful.

[edited by - eli_pulsifer on May 10, 2002 3:00:12 AM]
nias
nias
Hi,

I really enjoyed the discussion on scene graphs. I have gotten some good ideas from it, which I will use in my own projects.

What I''m wanted to ask is the following: is a scenegraph like this combinable with a BSP/ oct-tree solution ?

I''m trying to write a basic 3d renderer in opengl myself, and sofar I''ve stuck to this simple scenegraph. But in a later version I''d also like to integrate BSP, without losing the simpler structure.

So, is this possible ? Or should I seperate the two, and provide a seperate structure for BSP ?

I''ve been looking for articles on scenegraphs in general, but haven''t been very lucky. So any help/hints are very much appreciated.

Thanks,

Nias
Qatal
Qatal
Chris, your comment on when a node (projectile example) wants to destroy itself is invalid.

Provided that the scene graph is constructed carefully, the node already KNOWS what its parent is.

Recall the FFObject::setParent() function?

All that is needed is for the node to delete itself from the parent''s child list like obj->parent->m_childlist.popObj(obj)
or something like that. (STL is not my thing)

And then the node frees the memory it''s allocated to.

nothing more to it.

If i''ve missed something, please point it out.

Qatal


die or be died...i think
die or be died...i think
Lotus
Lotus
Well, finding the parent of a child won''t be a problem, however, a problem appears when the parent tries to locate the child in its list to delete its pointer and free its memory. For example if an object is owned by the whole world such as a bullet, when it wants to get destroyed, it will tell its parent "Kill me" and pass a reference to itself (or some other sort of identification). Since a great number of childs may be attached to the same parent, the search in its child list could consume some time.

Of course this search could be optimized by modifying the objects system a little. But that''s mainly Hargrove''s point in this area.

-Mokhtar M. Khorshid
Check out War to the Core the world domination space MOBA-RTS hybrid.
Join us on Discord.
Into sci-fi novels? Then check out Spectral Legends.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.