Development Environment - An Implementation

Well, well, well. It's good to see you back here. 'Twould be a shame if I lost readers already :D. Last article we discussed an abstract of our development environment. In this article we will start getting our hands dirty and dive directly into actually setting up our environment. Overall, we have three different tools to setup to get the ball rolling: Visual Studio, NAnt, and Subversion. So, enough of this discourse protocol and on to the fun.

Our Directories

Before we open up Visual Studio, we need to setup our directory structure that we discussed last article. First you need to create a root directory in which to house your project. My root directories are located in T:\, yours may differ of course. Make any changes necessary to house in the directory you wish. The following is a quick example of how I prime my directory structure for a new project (all this actually resides in a script, but we will ignore that for now). I'm calling the entire project "Dynamo" because the main aspect of the project is going to be developing a 2d side scrolling game engine (called the "Dynamo" engine, no less (hehehe, stupid I know)). The actual game will simply be a sample game that uses our engine.

> mkdir Dynamo
> cd Dynamo
> mkdir branches
> mkdir tags
> mkdir trunk
> cd trunk
> mkdir doc
> mkdir src
> mkdir tools

If you haven't been able to tell yet, I'm a command-line junkie. I can't help myself. You can also do the equivalent of the above in Windows Explorer. I won't think any less of you if you do (I promise (I swear, I promise)). Well, that's all we really need to do to prime our directory structure. So, let's move on to a developer's best friend, Visual Studio.

Visual Studio

Don't jump the gun, tiger! We're not opening Visual Studio yet. First we are going to create our blank solution. I personally prefer to do this via Notepad instead of Visual Studio since Visual Studio desires to create its own directory for the solution which we don't want to do (since it would conflict with our directory naming scheme).

So open up Notepad. Now on the first line, write "Microsoft Visual Studio Solution File, Format Version 8.00" (this assumes you are using Visual Studio 2003). That's it! Now save that file as "Dynamo.sln" in your newly created "src" directory from above. Now we're really ready to move forward. Now that you have your blank solution created, go ahead and double click on your new solution file to open it in Visual Studio.

Add a new C# Windows Application project to your solution (File >> New >> Project). Name this project "IncredibleJourney" and make sure to choose the option "Add to current solution". Incredible Journey is the sample game we will develop that uses our Harvest engine. Now, create a new C# Class Library project, call it "Dynamo", and add it to the current solution (would you be surprised at all if I told you that this would be our main "engine" ;-) ). One more step. Create another C# Class Library project, call it "DynamoTests", and add it to the current solution. DynamoTests will contain all our automated testing suites for the Dynamo engine. Now save the solution and go ahead and close Visual Studio.

Pretty easy so far. We're just setting up the bare bones of our project. Not much difficulty so far (hopefully (if this has been mentally taxing, perhaps you need some more practice with the tools :P)). Now that we have our Visual Studio projects setup, let's move on to NAnt.

<ExtraStep value="Optional">

As an optional step, you can configure Visual Studio to build with NAnt if you wish. If you do, simply add a C++ Make Project (yes, that's right, a C++ Make Project (you'll see)) to your solution. Follow the steps described in this article to setup the C++ Make project and you should be good to go. Of course, you need to substitute the values and expected targets of our script for the ones provided in the article.

</ExtraStep>

NAnt

NOTE: You will need to download one of the latest nightly builds from the NAnt site in order to properly build with the following script.

More copying and pasting, I'm afraid. Here we are going to setup our build script. In the last article, we discussed the interface that we wanted for our build script. And the sample script posted is basically the script we are going to build here (with a couple of tweaks of course). The first step is to create a blank text file called "default.build" and save it in our root trunk directory (i.e. T:\Dynamo\trunk\). I'm not going to go into much detail here so if you have any questions about the syntax or structure of the scripts and what they are doing, study up on the NAnt wiki and do some smaller experiments to help you understand better. Remember, small experiments are "A Good Thing" when you are trying to understand a technology that is new to you.

For our build script, we need several variables. The number of script variables may simply be showing a personal taste on my side. I like to have the least number of hard-coded values in the build targets as possible. To this end, any parameters or paths that may occur I like to put into variables defined at the beginning of the script. If you look at the following variables, you will notice a good number of them are blank. This is because they will be initialized in our "Init" target. Let's take a look, shall we?

<?xml version="1.0" ?>
<project name="Dynamo" default="build">

         <!-- default configuration -->
         <property name="debug" value="true" unless="${property::exists('debug')}" />
         <property name="project.baseDir" value="${nant.project.basedir}\src" />
         <property name="build.config" value="debug" />
         <property name="build.baseDir" value="" />
         <property name="build.assetsDir" value="" />
         <property name="tools.FxCop.params" value="" />

         <!-- common paths -->
         <property name="build.tempDir" value="${nant.project.basedir}\_temp" />
         <property name="build.testingDir" value="${nant.project.basedir}\_unitTests" />
         <property name="build.logDir" value="${nant.project.basedir}\log" />
         <property name="tools.FxCop"
                  value="${nant.project.basedir}\src\tools\FXCop\fxcopcmd.exe" />
         <property name="tools.NUnit"
                  value="${nant.project.basedir}\src\tools\NUnit\nunit-console.exe" />

Here is a description of the various values:

  • "debug" - A simple boolean which specifies whether this is the debug build or not
  • "project.baseDir" - The directory which contains all the source code for the contained projects
  • "build.config" - Describes the build of the project. There are only two possible values for this option: "debug" or "release"
  • "build.baseDir" - The directory which the assemblies will reside ("test" for debug build, and "bin" for release builds)
  • "build.assetsDir" - The directory which game assets (i.e. images, maps, etc.) will be moved to
  • "tools.FxCop.params" - The relative paths to assemblies that will be analyzed with FxCop
  • "build.tempDir" - A temporary directory in which assemblies will be built to until they are distributed into their final resting places
  • "build.testingDir" - A temporary directory in which assemblies will be placed in order to undergo unit testing
  • "build.logDir" - The directory in which output logs of various tools will be placed (and will ultimately be integrated into our Continuous Integration environment)
  • "tools.FxCop" - The path to FxCop
  • "tools.NUnit" - The path to NUnit

Pretty clear, I hope. Next we need to create our actual build targets. The first target we need is more of a "setup" build target that will setup our properties depending on the project configuration we are working with. The setup build target will configure non-build-specific properties and use a dynamic target name to call a target that will actually set our build-specific properties.

         <target name="init" unless="${target::has-executed('init')}">
                  <call target="debug" if="${debug}" />
                  <call target="release" unless="${debug}" />

                  <property name="build.assetsDir" value="${build.baseDir}\data" />
         </target>

         <target name="debug">
                  <property name="build.config" value="debug" />
                  <property name="build.baseDir" value="${nant.project.basedir}\test" />
                  <property name="tools.FxCop.params"
                           value="/f:test\Dynamo.dll
                                    /f:test\IncredibleJourney.exe" />
         </target>

         <target name="release">
                  <property name="build.config" value="release" />
                  <property name="build.baseDir" value="${nant.project.basedir}\bin" />
                  <property name="tools.FxCop.params"
                           value="/f:bin\Dynamo.dll
                                    /f:bin\IncredibleJourney.exe" />
         </target>

You can see, as stated, we are simply setting the properties to values we will use later on in our script. Again, if you don't understand what a certain property is for, refer to the list above.

Now for the longest and most important target of our script. Without this target, our application won't even exist. So, without further ado, I am pleased to announce, the infamous, the incredible, "build" target! (::insert loud noises from a gi-normous, e-gantic crowd here::)

         <target name="build" depends="init">

                  <echo>Preparing Build Directories</echo>
                  <mkdir dir="${build.tempDir}" />
                  <mkdir dir="${build.baseDir}" />
                  <mkdir dir="${build.assetsDir}" />
                           
                  <echo>Building Projects</echo>
                  <solution configuration="${build.config}" outputdir="${build.tempDir}">
                           <projects>
                                    <include name="src\Dynamo\Dynamo.csproj" />
                           </projects>
                  </solution>
                  <solution configuration="${build.config}" outputdir="${build.tempDir}">
                           <projects>
                                    <include name="src\IncredibleJourney\IncredibleJourney.csproj" />
                           </projects>
                           <referenceprojects>
                                    <include name="src\Dynamo\Dynamo.csproj" />
                           </referenceprojects>
                  </solution>

                  <echo>Distributing Assemblies</echo>
                  <copy todir="${build.baseDir}">
                           <fileset basedir="${build.tempDir}">
                                    <include name="Dynamo.*"/>
                                    <include name="IncredibleJourney.*"/>
                           </fileset>
                  </copy>
                  <copy todir="${build.assetsDir}">
                           <fileset basedir="${nant.project.basedir}\assets">
                                    <include name="*.*"/>
                           </fileset>
                  </copy>

                  <echo>Cleaning Temporary Directories</echo>
                  <delete dir="${build.tempDir}" failonerror="false" />
         </target>

Not too complicated, eh? First we prepare the directories in which files will be placed. Once the directories are prepared, we build our two projects ("Dynamo" - the engine, and "Incredible Journey" - the sample game that will use the engine). If you are wondering where we build the unit testing project we created earlier, that will be done later on.

So, you'll notice that the projects are actually built into a temporary directory. Why not build the projects directly into their final destination folders? Well, that is largely personal taste. I find that as the solution becomes more complicated (more projects as well as adding new concepts like "modules" (which we might get to in a far later article)), that by taking this approach, the script is much more maintainable and is easier to understand and "keep in check". It is easy for several things to get out of hand if you're not careful (and perhaps a little too gung-ho as well), and that's the last thing we want to happen. With how chaotic software development can inherently be, we want to keep chaos in check as much as reasonably possible.

When the projects are built into the temporary directory, then we finally distribute into their final resting place. At the same time, we distribute all our art assets to where they should be as well. Once that is finished, the bulk of the work is done. At this point, all that there is left is to remove the temporary directory and call the job "finished."

Continuous Integration

In this article (and series), I will only merely touch on the subject of continuous integration. It is a pretty large topic in of itself. I'm touching on it now largely because the final target in our build script is aimed at my personal continuous integration environment. To get you all up to speed, I will briefly describe the setup just enough so you can understand the final target (this final build target is of course optional; if you don't have a continuous integration environment setup, feel free to leave this final target out of your build script).

In my personal development environment, I achieve continuous integration via a freely available tool called Cruise Control .NET. Essentially, every time I (or another friend who is developing with me) commits a change to the source repository for the project, Cruise Control will fire up and fire this build target. This build target will then get the latest sources from the source repository, call the "build" target we discussed above, build the unit testing project, run all the unit tests with NUnit, and then fire up FxCop to analyze the projects against best practice development rules.

The results from NUnit and FxCop, as well as the status of the build, will then be logged and made available on my build machine's website (also provided by Cruise Control .NET). So, at any time, I can visit the build machine's website and see the status of all builds that are in the integration environment. For each project, I can see the final status of the build, the results from all the unit tests, and see all the rule violations from FxCop.

Pretty convenient, I must say. If any project's build fails, Cruise Control fires off an email to me notifying me that a build failed and the reason why the build failed. With how easy it is to setup this environment (and not to mention that it is totally free of charge), there really isn't a reason not to do it. However, if you decide not to do it (I know, it can seem like a lot of unnecessary overhead for a sole hobby developer), please at least get all of your source code into a source control system. That much I beg of you :).

With all that said, here is the final target in the build script. I won't describe the specifics at all since they should be pretty clear. If they aren't clear, don't worry, they're probably not that important anyways. The important thing is not to set yourself up for failure. If setting up this entire environment is going to cause you to fail on the project (or simply give you the desire not to work on the project because the tools cause too much red tape), don't do it. We won't you to succeed here and have fun, after all :).

NOTE: At the time this article is being written, the svn-update NAnt task is part of the NAnt-contrib project. So, if you wish to have the svn-update task, you will need to download and install that project on top of NAnt. Hooking into Subversion can also be done using normal executable tasks if you wish instead.

         <target name="integrate" depends="init">

                  <echo>Get latest sources and build projects</echo>
                  <svn-update uri="svn://BuildMachine/Dynamo/trunk/"
                           destination="${project.baseDir}" />
                  <call target="build" />

                  <echo>Preparing Integration Directories</echo>
                  <mkdir dir="${build.testingDir}" />
                  <mkdir dir="${build.logDir}" />

                  <echo>Distributing Assemblies</echo>
                  <copy todir="${build.testingDir}">
                           <fileset basedir="${build.baseDir}">
                                    <include name="Dynamo.*"/>
                           </fileset>
                  </copy>

                  <echo>Building Unit Tests</echo>
                  <solution configuration="${build.config}" outputdir="${build.testingDir}">
                           <projects>
                                    <include name="src\DynamoTests\DynamoTests.csproj" />
                           </projects>
                           <referenceprojects>
                                    <include name="src\Dynamo\Dynamo.csproj" />
                           </referenceprojects>
                  </solution>

                  <echo>Generating Reports</echo>
                  <exec program="${tools.FxCop}"
                           commandline="${tools.FxCop.params} /o:log\fxcop-results.xml"
                           failonerror="false"/>
                  <exec program="${tools.NUnit}" workingdir="${build.testingDir}"
                           commandline="DynamoTests.dll /xml:../log/nunit-results.xml /nologo"/>
         
                  <echo>Cleaning Temporary Directories</echo>
                  <delete dir="${build.testingDir}" failonerror="false" />
         </target>
</project>

We will talk more about the actual architecture of our projects next article. For now, let's discuss the basic ideas behind the build targets above. We have two primary projects that we are bringing together into the same place. As you can see, we leverage the robustness of the Visual Studio Solution and Project files here. With the latest builds of NAnt, there is the "Solution" task that we can simple pass in and have the task parse those files out and run the compiler itself. This makes the script a lot easier to maintain in the long run. This way, we can also set the compiler options in Visual Studio and have the build script automatically pick them up. It is important to avoid as much duplicate work as possible. Take it from me, if you are having to do things in multiple places, eventually one of them (or more) stop being done altogether.

After we build the projects, we actually distribute the resulting files into their proper directories of our projects. As discussed before, the two primary projects are the actual "engine" and the sample game that will be built on top of that engine. It's not really that difficult. Just remember to keep your eye on the big picture. If you dive too far into the details without keeping an eye on the entire picture, chaos will creep into your system and it won't be nearly as organized as it could be. I can't stress it enough, but chaos is something that needs to be kept in check every step along the way. It may sound boring, but you will be much happier in the long run, trust me!

The last thing we do is remove the temporary directory that we originally compiled everything into. And that's all there is to our build script. If this doesn't make sense now, I promise that it will start making sense when we move forward. Once you start understanding the actual architecture of our game, it will all start to settle in. If you don't quite feel comfortable with the NAnt code above, please mosey on over to the NAnt site and read up and learn about the wonderful product that is NAnt :D.

Source Control

I won't dwell on source control too much here, because many of you might be using a different source control system than I am. What I will do is provide you with some initial setup tips that will get you off on the right foot.

From above, you might have noticed that under our root project folder, we created three folders called "branches", "tags", and "trunk", respectively. The true purpose of these folders are to encourage good development practices when it comes to the use of source control. If you follow this structure, your life will be much easier as time progresses and you work with more developers on a single project.

The branches directory will hold any "branching" projects that you do to work on your project. The concept of branching is well beyond the purpose of these articles, so I will leave the research of that as an exercise to the reader. Just know that the branches directory exists in order to make the practice of branching not only feasible, but quite a bit easier to control than it might otherwise be.

The tags directory is exactly like it sounds. This will hold major tags of our projects we make. When you think "tags", you might also think about "versions" or a concept similar. At any time, we could "tag" the current code base of our project in order to come back later or to simply "file away" for other purposes. Think about a tag as a "system check point" in Windows XP System Restore. At any point in the future, we could go back to any tag we wanted to. The difference is that a tag doesn't roll back the current code base. It just allows us to have a historical view of our projects, which can be a great thing, especially if you are releasing versions of your software.

The final directory, "trunk", is simply where the most current, live, version of the source code is. If you wanted to get the most up-to-date version of the source code for a project, you would checkout the trunk repository. Usually on a development machine, you would check out the root repository project (the root path containing the branches, tags, and trunk directories) if the project was small enough. If the project is large, and you are simply developing on the trunk of that project, you could simply checkout the trunk of the project. For instance, if I wanted to checkout the entire project for a small project (branches, tags, trunk, and all), I would most likely check out the repository "svn://BuildMachine/ProjectName". On the other hand, if I just wanted to do development on the trunk of the project, I would checkout from "svn://BuildMachine/ProjectName/trunk". Well, I would if the project was configured how we have configured our project.

In Closing

Whew! That was a little more length than I thought it was going to be. You should now have your development environment all ready and prepped for us to actually start developing our game. Yay! The next article we will discuss the high-level overview of our game engine architecture and how the engine fits into our game.

Until then, take it easy! If you have any questions, feel free to contact me :D.