Merry Christmas and happy holidays! “And now for something completely different...”
The Next Post:
Before we start, I've been looking into discussions that are similar to this and realized that they usually end when people start discussing implementation (people get way too side tracked). So as a general rule, I'd say we avoid discussing particulars of the implementation except in situations where a given implementation will make things a lot easier. At the same time don’t hold back any implementation details that you want to share. Of particular interest are unique uses of the language you use.
I've got a few things planned for today’s post (don’t worry we will begin to write code today). The first few sections are going to be a bit boring but I think they are necessary. The good thing is that once we go over them, we never have to look at them again. Here’s a list:
- Code growth:
How and why code grows.
- Programmer growth:
"Use the force" of design.
- Naming:
Some stuff on my naming convention
(many out there; I will go over two
conventions I’ve used).
- Before you start coding:
Programming tools galore.
- The design tree/process:
Putting order/expanding to our previous
list of subsystems.
- The context management subsystem:
Finally some code. In this section I
will provide a description of the
requirements of this subsystem. Believe
it or not, this is at the bottom of our
tree.
Code Growth:
Code grows. In fact growth is usually a good thing because it means that your design is scaling to meet the problem. Growth however, has its own problems in normal practice (i.e. growing pains) and you should deal with them. The sooner the better; the cost in time required to solve overgrown code grows exponentially. This is because code you develop using your overgrown code will also have to be changed and this pattern snowballs over time. The cost for not accommodating your code growth is maintenance. Code becomes harder and harder to maintain if you ignore code growth, and in unfortunate situations you may loose the ability to complete the project successfully.
How does code grow? As far as I can tell there are two main ways code can grow. It grows on a macro scale and a micro scale. Your design should consider the macro scale of code before you begin implementing, but it’s not unusual for your initial considerations to change throughout the development. Here is a list of different macro scales for different complexities of solutions:
1. single function program (hello world)
2. multi function/class program (school projects)
3. multi subsystem program compiled into a single app (this project)
4. single app that uses dlls or plugins
5. user/client apps (usually networked games)
6. multi apps on one cpu (Microsoft Office/Windows)
7. multi apps on multi cpus (think mmorpg servers)
8. Protocols (the internet)
The scale you select depends on the solution you are targeting, so you usually have an idea of what you will need before you start the project. You divide your project into more and more pieces to make it maintainable, and as a result feasible. At each division you can test and grow your code independent of the other levels.
The other type of code growth is micro growth. If you ever had a situation where you needed to use only part of a class or a bit of a large function in another part of your code then you've run into micro growth (or the lack there of). Micro growth happens whenever you add more code, particularly to older parts of your project. This type of growth requires the most maintenance and by maintaining it you ensure that your project grows as you intended.
How do you maintain code growth? Like many things in programming, it’s not so clear cut and requires a bit of instinct and experience. I'll try to provide a few pointers. There are two good approaches that I know of when dealing with macro growth. The first is decomposition. Break the parts of your solution down and try to fit the pieces into the levels I mentioned previously. It’s probably best if you choose the solution that fits most naturally and reduces your work load. This approach however, requires a bit of instinct and experience. The second approach is research. Look at similar programs and see how they design their architecture. When choosing the macro design, give yourself breathing room. If you think it might be done without dlls, but it will be a big executable then you should probably go with dlls. Most importantly, make this decision before you start coding. You are currently estimating the size that your code will grow to. For this project we should only need a single app with multiple subsystems. Our subsystems should be modular and independent at the base levels. This way we can easily turn them into separate dlls just in case our project gets too big.
Managing the growth at the micro level should be easier to explain because we should have all had similar experiences. There is a constant dynamic that occurs between the interfaces and implementation of your code as your project progresses. The implementation grows as a natural part of your development. You must actively change your interface to accommodate for this growth. If the interface grows too much then you may need to reconsider your macro scale (this is usually rare). When do you need to change? There are a few indicators. The main reason you change is to avoid code duplication. If you have ever had a large class and you wished you had a part of that class to code a solution then you need to grow your interface. In this case the right solution is to divide your large class into subclasses and reconstruct the interface. Copying and reusing the implementation is a very bad idea (this is why copy and pasting code is bad) because you produce a maintenance nightmare. Similarly if you have a large function and need only a piece of the function, then divide it up. Another good indicator is the situation where writing new code using your current interface is exceedingly difficult. This is usually indicative of malformed growth. Why have an interface designed as it is if you can’t use it to do the job? Redoing the interface can save you some time later on. This type of change however, depends more on the situation. If only one part of your code is hard to write and the others are easy then the interface is doing its job, so don’t change it.
Programmer growth:
A bit of a side track: It seems as though a few of my friends have had similar experiences when it comes to our growth as programmers. In general, we all seem to be developing an instinct for what I think is good design. This instinct tends to be directly related to the level you can program/engineer. I encourage you to share your experiences on this thread as it seems the development of this instinct is different from programmer to programmer.
As far as I can tell, the things associated with this instinct are what I enjoy about programming/engineering/math/science. It has to do more with how pieces fit together than what the actual pieces are. There is a good discussion on programmer development at http://www.gamedev.net/reference/articles/article832.asp and I recommend you read it (not too long ;) ). As you progress through the stages mentioned in the article the programming tools available to you increase, subconsciously at first, and these tools become increasingly abstract. Here are a few things I can do that I don’t do consciously (or at least mostly consciously):
- Foresight:
I don’t quite remember when it started but now
when I look at a problem, I instinctively
think of a design usually off the top of my
head. It tends to be the case that when I
implement the solution the benefits of the
solution become apparent. Oddly enough, the
situations where foresight comes into play
most often occur when there is a big problem
ahead. I usually don’t see the problem. Instead
I have a strong urge to program or design
the solution a certain way. Then there is
usually a point where I say, "wow, that was
close".
- Understanding:
I'm fairly sure I had this earlier than foresight.
I remember an episode of Dilbert where this is
called the engineering gene. Its the ability to
figure out how something works without any extra
knowledge. I think we all share this ability at
our current programming level. The reason I mention
it here is because this ability seems to be growing.
Instead of being able to figure out how something
works I can figure out (or at least make good guesses)
at how it was designed...in greater and greater detail.
I think the design element is key to this ability.
We can figure out how something works because we
have a general notion as to how we would design
and build it.
- Effortless effort:
Have you ever had a feeling that the code was
writing itself? It seems to be happening more
frequently especially when the design of a system
seems particularly elegant and natural. The only
downfall of this ability is that it detaches you
from time...and when you regain consciousness,
its 4:00 am...
There have been a few names given to the driving force behind this phenomenon. This article (http://www.schedler.org/cookie/the-tao-of-programming.html) calls it the "tau of programming". The lead architect on my team calls it his "design spidy sense". I call it "the force" (a very original name ;) ). How does this "force" relate to our project? I think the best thing we can get from this project is growth in this area.
Naming:
Why is naming important and what is it used for? From my experience naming can be used for a variety of things. It's simply another programming tool and its importance depends on what you are using it for. In this section I'll discuss two naming conventions I've run into. When designing a naming convention the most important element is consistency. Naming conventions tend to be affected by the size of the team working on the project. The size of the team is important because as the team grows larger it becomes harder to enforce the convention. Successful conventions make it easy to be consistent because there are fewer special cases.
Large team:
Naming conventions have to be more flexible as the size of the team grows. Here are the rules of a large scale convention I've used:
- All names must begin with a lower case.
- Any successive words in the name must start with upper case. For example the name "Chess Programming Tutorial" would look like "chessProgrammingTutorial".
You can determine easily where a name starts. You can also easily read the name since the beginning of each successive word is obvious. The following rules are optional:
- All function names should begin with a verb
- All class names should begin with a noun
You'll notice that there is nothing that specifies which subsystem a function or class belongs to. This is because large companies usually invest a lot of time developing coding tools. A tool to help a programmer parse code would make subsystem naming obsolete.
Small team:
When the team size is relatively small (1-10 programmers) then maintaining a strict naming convention is easier. Moreover, since we don’t have time to develop coding tools we can use the naming convention to solve some of our problems. Here are the rules of my personal naming/formatting system:
- All files need to be preformatted using a macro (will provide mine in the tools section of this post).
- Names for functions/classes are all lower case.
- Successive words in the name are separated by underscores. For example the name "Chess Programming Tutorial" would look like "chess_programming_tutorial".
- Your code needs to be organized into subsystems (also called slices or units).
- Each subsystem is assigned a three to four letter acronym that precedes any global names used in that subsystem. For example a window creation function might look like "wnd_create_window();".
- If there are further divisions in your subsystem repeat the same pattern of assigning an acronym. For example a gl creation function might look like "wnd_gl_create_gl_window()".
There are two reasons for the acronyms. The first is visual sorting; you can easily tell which area of the code the function/class came from. You also know where to look to find similar members of the same subsystem as well as documentation on how to use them (via comments). The second reason is that it lets you determine the scope of a function/class. You can easily tell which parts of your code are local (i.e. declared right then and there) or global because local names don’t have a prefix and tend to be shorter. Just remember that the programmer you are trying to stop from wrecking your code is yourself :)
Before you start coding:
In this section I'm going to introduce you to the world of programming tools (...unless of course you've been there before :) ...). A programming tool, in general, is anything that makes your development efforts a bit easier. Your IDE (integrated development environment) for example is a programming tool. In this discussion I'll go over three main tools I use (all downloadable). The first is a macro I use in my ide (VC++ 6.0) that lets me setup a .h or .cpp file by hitting one key. The second is dos prompt basecode that I use for unit testing. The third is a gl basecode I use for testing effects ect.
What is a macro? I'm not talking about preprocessor commands. I mean macro in a VC++ sense. A macro is a vb script that can be triggered a number of ways to do a variety of things. In this case, we are using the macro to setup our headers with the appropriate preprocessor commands. It will also provide a commented structure that tells us where to put our declarations ect. This way we get a consistent physical structure throughout all of our code making it very easy to find what we are looking for. You can download it here:
http://www.eng.uwaterloo.ca/~rramraj/Gamedev/code_tools/code_template.dsmA few changes to the script (namely replacing my name with yours) and you are good to go. To integrate the macro in VC++ 6.0 follow these steps:
1. Open VC++ 6.0
2. Go Tools->Macro...
3. Go Options>>
4. Go Loaded Files
5. Go Browse and load the .dsm
6. Your macro should appear in the "Add-ins and macro files" list
7. Check your macro
Optional:
8. Go to Keyboard
9. In the "Category" combo box select "Macros"
10. Select HTemplate and assign it a key; this key will be used
to format your headers
11. Select SRCTemplate and assign it a key; this key will be used
to format your cpp files
You can then create new .h and .cpp files and try them out. Unfortunately, I have not yet found a way to convert them to .NET or other IDEs...
The second and third tools I mentioned earlier are essentially the same type of thing. The dos prompt basecode is essentially a console app with the following code:
#include <iostream>using namespace std;void main(void){ //test code here}
Although this does look simple it is actually very useful. Using iostream, you can easily whip up an interface for almost anything you want to test. Testing is a big part of almost any project. It lets you eliminate smaller problems when and where they start so that they do not become larger problems. Have you ever run into the situation where you had to write a program but you felt that you had to finish the program before you could test it? Well this is the solution to that problem and we will use this basecode later on in the post. The only down side is that this setup does not let you test your gl effects. Nehe to the rescue :) . Download the nehe basecode and you're good to go.
If you guys want I can post the dos basecode with the workspace and project files built and packaged. I can also post my own version of the nehe basecode. It contains other libraries that do texture loading ect...
These three tools are the ones I use on a regular basis. If you are interested in other tools available, I recommend Python:
http://www.python.org/. Python is a platform independent scripting language with many uses. It can even be embedded into your game as your native scripting language.
The design tree/process:
The process of designing and developing is one of selecting a path from the numerous ones available at a given moment. You reduce the many possibilities that exist until you end up with your final project. At any given level you make assumptions that determine the path you will ultimately end up taking. You must make a conscious effort to ensure that you don’t end up taking a path that eliminates some of your features. I'll try to point out examples of this as we go along (just need to remind me ;) ).
Now that all of the preamble stuff is out of the way we can finally move on to the game...Here’s a pictorial representation of our project so far...at least my interpretation :) :
You'll notice that there is a big node called "The Great Unknown". The nice thing about design is that you don’t necessarily have to plan the entire project beforehand. We can design, implement, and test in small units. In our case, when we know more about our lower level, the resource manager in particular, we can start laying out the design for the unknown elements. This dynamic lets us change the design (hopefully for the best) as we progress. In our case, the unknown elements consist mainly of making the individual contexts work, which when we get to it, shouldn’t be that hard (fingers crossed ;) ).
Another big surprise is that our context manager is all the way at the bottom of the tree while our actual contexts are at the top. The layout I propose is a dependency graph. Things are lower because they don’t depend on as many things. In the case of the context manager, it does not depend on the contexts because it does not call any of the contexts. As far as it is concerned, a context is an abstract idea and its job is to make sure that everyone knows of the state of these contexts, irregardless of what they actually are.
The design I've laid out isn't by all means set in stone. I encourage you to experiment with what I have and try to come up with something new. To that extent, I've posted the word document that I used to generate the design:
http://www.eng.uwaterloo.ca/~rramraj/Gamedev/chess_tutorial/design_doc.docI'm curious to see what you can come up with :)
The context management subsystem:
So how does this system work? If you have ever played StarCraft(tm) or Halo(tm) then you've seen this type of system at work. This is the system that loads the startup menu (or main context), then switches to the game context, then lets you bring up the menu over the game, ect... We have identified four contexts so far. They are the main screen, the game screen, the menu and the console. The relationship between the contexts is a stack (first in, last out). When the game starts, we push the main context onto the stack. If the user decides to play a new game we push the game context onto the stack. If the user decides to quit, then they must first de-initialize the game context to pop it off the stack and they are then returned to the main screen. If they decide to quit from the main screen, they de-initialize it to pop it off the stack. Since there is nothing left on the stack, the game can quit.
Here are the requirements of this subsystem:
- it must tell you if the game is still running (this is its primary job)
- it should let you push a context onto its stack
- it should let you know if your current context has focus (is on the top of the stack)
- it should let you pop a context off the stack
Some implementation notes (considering the assumptions I made with this design):
- the first thing a given context is going to want to do is push a context onto the stack and the last thing is pop one off
- weather we want to be able to pop a context that is not in focus is a matter of discussion
- Since the values that reference a given context are sensitive, you are going to want some control over what has the authority to use them. For example, if you let anyone, globally, pop the context in focus without some sort of check, then you are in for some trouble; your game might end abruptly for no apparent reason (or something similar might happen). Here’s what I suggest you do to stop this from happening. When you push a context onto the stack, the pushing function returns an index for the created context. At no other time can you query the context manager for an index. You can then do several things with this index. You can check the context manager to see if the index has focus. You can use the index to pop the context off the stack. This way, the only thing that can control the context is the creator of the context. Unfortunately, you now have an issue where other systems (beside the creating system) may need to know if a given context is in focus. How do you do this without exposing the index? Simple; wrap the index. When you push the context, store the index in a local variable. Lets call it con_index for the game context. Then you write a wrapper function (exposed globally) called bool game_is_game_in_focus(void); and implement it like so:
bool game_is_game_in_focus(void)
{
return con_man_is_con_in_focus(con_index);
}
"Wait a tic", how do you implement this if you don’t even have a window for your project yet? Simple; implement the manager in your dos basecode. Your basecode will never make it into the final project...but that does not mean it can’t be used and thrown away ;)
And I'm done for now...wow that was a long post...If you have any questions, comments, concerns, or queries feel free to post :). Now that we have a ton of preamble out of the way, we can tackle this project, subsystem by subsystem. Next we take a look at the clock...then the world!!!...of chess...
Will post later,
- llvllatrix