| DevMaster.net |
| [[ Home | Forums | 3D Engines Database | Wiki | Articles/Tutorials | Game Dev Jobs | IRC Chat Network | Contact Us ]] |
| Using Trees for Sorted Rendering |
|
|
|
01/04/2004
|
||
IntroductionSuppose you want to make a 2D RPG, much like the old Final Fantasy games on the SNES (the good old days). And before you start any game, you have to examine the masters of old. Let's take a good look at a screen shot of Chrono Trigger shown on the right.
Pretty, isn't it? Notice, though, how our little Crono is partially obscured by the wall. How is that accomplished? It is accomplished several ways:
The first option is easier to implement - just simply remember to render your character before any walls are drawn. However, this does not lend itself to generalization. The second method, a render sort, is much more general, robust, and powerful solution. This method is what I call sorted rendering lists. And this method, itself, has many sub-methods. Here are the most popular ones:
The first method is extremely inefficient. Deleting, copying, reallocating every time you add an item to the array is not only a ugly hack, it has poor performance (imagine if you have to do that for hundreds or thousands of sprites). The second, although the code is cleaner, it still results in poor performance because in essence you are doing exactly the same thing as the first method. The last method is much, much better. What is a tree? A tree is binary in nature, consisting of nodes that have pointers to two sub-nodes, right and left. If you're an astute person, you can immediately tell right of the bat the advantage of using trees. If you've had experience with linked lists or trees before, you should have no problem understanding the rest of the tutorial. If you haven't, then I suggest a quick Google for trees and data structures, grasp the concept of them, and then come back. I'm a Tree HuggerHere are some off-the-head (best-case) algorithmic analyses of Binary Search Trees (BST): Insertion: O(log N) - By the way, when you insert into trees, you're
also taking care of the sorting step too. Generally, this is much better than the array approach: Insertion/Searching: O(N) - depending on what search/sort
algorithm you use. Again, algorithmic analysis proves that trees are good. OK, I'm in. Now show me some code!Let's get down to code. For this tutorial I'm only going to create a simple rendering tree, but it will be easily extendable. The smallest unit of a tree is a node. Or leaf... //....code... struct sNode { sSprite *sprite; //pointer to sprite its referring to int y; //the y values int x; //the x value sNode* left; //pointer to the child node to the "left" sNode* right; //pointer to the child node to the "right" }; There - we have our node. It's pretty transparent, but I'll still explain its internals: - sSprite* sprite - this is just the pointer to the sSprite object, whatever it may be. Note: if you're an inexperienced programmer (i.e. "noob"), don't copy and paste this code into your own programs, because sSprite isn't defined (unless you coincidentally defined an sSprite before...). So just replace your sSprite* sprite with whatever sprite object you happen to be using. - int x,int y - these are just the location of the of the node. - sNode* left, sNode* right - these are pointers to the left and right child nodes, respectively. the left points to the node which have lesser Y values than itself, and the right points to the node which have greater Y values than itself. The cool thing about trees is that subtrees retain the same properties of the parent tree, so when you insert nodes into the tree, its extremely easy to do so. The Tree ItselfIf you're still confused about the tree structure, all your confusion will be banished by the end of the tutorial, don't worry. Now we'll proceed onto the tree structure itself. It will be extremely painful. sNode* head; That's it. "WHAT!" you say. "Where are all the complex data structures and ugly fangled code that looks like it came from the depths of coding hell?" Don't worry, it'll come! So how is that the tree itself? Let's look at a diagram of a tree:
Yes, I agree, it is the crappiest tree you've ever seen in your entire life...but then again...I'm not known for my artistic abilities. The head node is represented by our pointer, sNode* head. The head has its two pointers, left child and right child, which have their own two pointers, left child and right child, and so on. Trees are recursive in nature, so that's how you can represent one tree with only one pointer. In essence, the sNode* head is "carrying" the whole tree. So, we've come up with the structure of the trees, but we still haven't figured out how do they work (man...now I'm feeling like I'm explaining trees instead of rendering trees...but they're one and the same I guess.). So if we've got only one pointer that represents our tree, how do we access all the nodes? We do that by traversal. There are three ways of traversing any tree:
Well that's great and all, but what the heck does that actually mean? Time for some pseudocode! //pre order code void PreOrder(sNode* node) { if (!node) //check if its valid return; //not valid, exit //since its preorder, visit the current node DoSomethingWithNode(node); //check if left pointer is valid. If it is, visit it! if (node->left) PreOrder(node->left); //traverse the left side //check if the right pointer is valid. If it is, visit it! if (node->right) PreOrder(node->right); //traverse right } So how does this work? Let's start with a PreOrder(head); It enters the function, check if the node is valid - it is, go on. It does something with the node (whatever that may be): process it, draw it, eat it, whatever. After that, it moves on to check if the left pointer is valid. This simply checks if this node has a left node "attached" to it. It does, then do pre-order all over again. It checks if the left node is valid, does something with the node, and then so on. Sooner or later it'll hit a dead end - the node isn't valid, so it jumps out of the function, and then it goes to the previous PreOrder "body" which called this PreOrder body, and proceeds to the right subtree. Did that make sense? It had better not, because recursion is simply like that. You have to visualize this in your mind...really strange stuff. If you don't understand recursion, you might be thinking, "Why doesn't this get stuck in an infinite loop by repeatedly calling itself?" Notice, however, in the beginning of the function it does a validity check. If it's not valid, it exits out of the function, so this validity check acts as a stopper for the infinite loop problem. But rest assured, if your tree is constructed correctly, then this type of recursion works. Now let's examine the other types of traversal: //In order code void InOrder(sNode* node) { if (!node) //check if its valid return; //not valid, exit //check if left pointer is valid. If it is, visit it! if (node->left) InOrder(node->left); //traverse the left side /* since its inoder, visit the current node AFTER it has visited the left subtree/node */ DoSomethingWithNode(node); //check if the right pointer is valid. If it is, visit it! if (node->right) InOrder(node->right); //traverse right } //Post order code void PostOrder(sNode* node) { if (!node) //check if its valid return; //not valid, exit //check if left pointer is valid. If it is, visit it! if (node->left) PostOrder(node->left); //traverse the left side //check if the right pointer is valid. If it is, visit it! if (node->right) PostOrder(node->right); //traverse right /* since its postoder, visit the current node AFTER it has visited both the left subtree/node AND the right subtree/node */ DoSomethingWithNode(node); } Not much difference, just the order in which you traverse the left subtree, right subtree and visit the node itself. So what's the big deal with all this inorder, postorder stuff? Well, the different types of traversals are used for different types of jobs. For example, for deleting the whole tree, we need to use PostOrder, because we can't delete the node itself before hand, or we would lose all of its children. We have to delete its left subtree, right subtree, and THEN delete itself. Now that we know how to traverse the tree, let's talk about insertion. Insertion Point: Sector AB01I love it how the military coins all these terms to define stuff. For example, they say, "Tango down," or, "Fire in the hole!" I mean, what the heck are those? Tango? Fire in the hole? Anyways, I'll dump some code on you (sorry about that, its the only way), and then make up for the shock by exhaustively explaining it. It won't be so bad, though :) But first, we'll have to have some initialization code first - our tree isn't even initialized! Node* head = NULL; //Init our tree void InitTree() { head = new sNode; if (!head) { printf("Error creating head node!\n"); return; } //end if head->left = NULL; //the left pointer doesn't point to anything head->right = NULL; //same //set the head's sprite pointer to NULL head->sprite = NULL; } Ok, our tree is properly initialized. We allocate a new sNode, called head. We then check to see if we allocated properly (basic error checking), followed by setting both left/right pointers to NULL. This is an important step, because when you allocate memory for anything, the value isn't defined, and it could be anything, so we have to set it to NULL just to be safe. Now to insertion. Let's examine how we would approach this. We have a function, InsertNode(sNode* node,sNode* child), and we pass the node we want to attach it to, and the node we want to attach. We will assume that you already alloc'd or new'd an sNode, and filled it with appropriate values. Then we will decide where to attach this new node. It will be a lot like our traversal functions. Let's examine what we have so far: sNode* head = NULL; //Init our tree void InitTree() { //.........InitTree code here.............. } void InsertNode(sNode* node,sNode* child) { //validate if (!node || !child) return; //we put something here...decide how to attach child to node } How do we decide where to attach this new node? Well we look at the criteria for sorting, the Y values: does this new node have a greater or lesser Y value than the node that was passed to us? If greater, then traverse the right tree - if lesser, then traverse the left tree. Here's code to show you what I mean. sNode* head = NULL; //Init our tree void InitTree() { //.........InitTree code here.............. } void InsertNode(sNode* node,sNode* child) { //validate if (!node || !child) return; if (child-> y >= node->y) { /* the child's y is bigger or equal than the node's y insert to the right but before we do, we have to check is the right tree valid? */ if (node->right) InsertNode(node->right,child); //insert into right tree else node->right = child; //nope - make node's right the child } else { //the child's y is less than the node's y if (node->left) InsertNode(node->left,child); //insert into left tree else node->left = child; } } That's it - pretty straightforward. As you can see, this function is recursive too - OMG! So much recursion! Yeah...I know...these trees suck :Grin:. So first we compare - is the child's Y value bigger than or equal to the node you want to insert to's Y? If so, then we have to insert it into the right subtree (if it exists). If the right subtree doesn't exist, then you just make the node's right pointer the child itself. So what happens if the right subtree does exist? Well you pass the node's right pointer as the node itself when you make the recursive call, and it keeps on comparing till it finds the perfect spot to place this pesky little child. See what I mean by how subtrees retain the properties of the parent? One more reason I love trees...anyways... The same with the left tree...no need to re-explain there. Now I'll wrap this insertion stuff up with the following code, which demonstrates how to build a tree: void main() { //load gfx //load input //whatever //initialize our tree InitTree(); //main loop while (1) { //retrieve input //now let's build our rendering tree sNode* temp = new sNode; temp->sprite = wallsprite; temp->x = 100; temp->y = 120; temp->left = temp->right = NULL; InsertNode(head,temp); //reuse temp = new sNode; temp->sprite = crono; temp->x = 100; temp->y = 100; temp->left = temp->right = NULL; InsertNode(head,temp); //draw the rendering list //destroy the rendering list } } Notice how I deliberately inserted the wall sprite first, and then Crono, even though Crono is behind the wall. This is to show that no matter how you insert it, it will come out all sorted and nice looking. So now we've got Insertion done and over with. All we have left is rendering the actual tree, and deletion of the tree. Notice how I've been sneaking up on you with the rendering list. I subtly (or maybe not so subtly) switched from explaining trees to rendering trees. But then again...its not that big of a jump. Rendering the actual treeSo now we've built our tree - we can render it now! It's nothing more than a problem of InOrder traversal. Why is it inorder? Because you have to draw the left subtree first (lesser Y values), itself, and right subtree last (greater Y values) - basically following painter's algorithm. So to follow my brutal techniques I used previously, I'll just perform a code dump on your head. So prepare for a gruesome mess of code. void RenderNode(sNode* node) { if (!node) return; if (node->left) //some redundant error checking RenderNode(node->left); //draw the node itself DrawNode(node); if (node->right) //some redundant error checking RenderNode(node->right); } You knew that was coming, didn't you? Wow - isn't that simple? The tree is already sorted (because of its inherent sorted/binary nature), so all you gotta do is follow the links and draw the nodes. Astute readers will notice that head isn't referring to any sprite - it just holds the tree in place. So in DrawNode, you need a mechanism for detecting an invalid node (by checking if the sprite is NULL or not - in InitTree I set the sprite to NULL). Of course, if you're conservative, then you can use head as a valid node, but I personally like the head to be empty. DeforestationNow we have the problem of deletion left. Is it hard, is it easy? Its just another matter of traversal, this time its Post-Order. void DeleteNode(sNode* node) { //post order //delete left branch if (node->left) //validate left pointer DeleteNode(node->left); //delete right branch if (node->right) //validate right pointer DeleteNode(node->right); //we can't delete ourselves...BUT, we can delete its children if (node->left) { //make sure it doesn't have any children if (!node->left->left && !node->left->right) { delete node->left; node->left = NULL; } } //do the same thing with the right node if (node->right) //does it have a right node { //make sure it doesn't have any children if (!node->right->left && node->right->right) { delete node->right; node->right = NULL; } } } I'm going crazy from the left right right left left right!!! Not that much, but looks a bit complicated with all those if statements and comments. So here's how you implement this: void DestroyResources() { //delete gfx, sound, etc DestroyNode(head); } That seems all fine and dandy. However, if we run this through a debugger, we'll find after calling DestroyNode head is still valid! How is this possible? Well remember that in DestroyNode, we destroy the children, not the node itself, so after all this recursive madness, we end up with a still living head...so...we know what to do boys. void DestroyResources() { //delete gfx, sound, etc DestroyNode(head); //SEEK AND DESTROY delete head; head = NULL; //MUAHAHAHA } So now our head is dealt with. "Is it over?" - Matrix RevolutionsWe've come a long way. For me, at least. Writing this took longer than I thought (about 4-5 hours). We have a fully functional tree in our hands, and its up to us what we want to do with it. We are just scratching the surface of data structures and trees, but for the most part, the tree we just went through is sufficient for most our needs. The thing is, all this recursion and new/delete will be sure to cause some memory problems in the future. I can't stress enough, ERROR CHECK, ERROR CHECK, ERROR CHECK. Go overboard with it. Only that way can your programs can be bullet proof. Or they can dodge bullets. Whichever it happens to be, just be sure to but a lot of redundant validation in your functions, especially insertion and deletion functions. And I'm sure that at least some of you will be going "huh? huh? huh" on the recursion parts. The trick to these are visualizing them either in your mind or even drawing these recursive graphs on paper. I did that when I first encountered these. They simplify the understanding of recursion SO MUCH. So with that, I leave you. Along with a sample console program that demonstrates trees. NOTE: I have left an intentional bug in the code somewhere up there. The solution is in the sample code, but its not obvious. So if you can find it, contact me! So in other words, noobs beware, don't copy and paste! Download source code for this article |
|
|
| © 2003-2004 DevMaster.net. All Rights Reserved. Terms of Use & Privacy Policy | Want to write for us? Click here |