Monday, February 14, 2005

Using DirectX and C Sharp To Create A Triangle


Screenward Ho!


The first step to creating a game is making the computer show us something. So our first goal is to have the screen output us some graphics.

Introduction


This tutorial is about creating an RPG style game using C#, DirectX and VS.net IDE. You can probably use it even if you're developing with something else but you will have to do a little work yourself.

Getting started


If you are already familiar with the VS.net IDE then skip or skim this this part below.

First steps first, we need to get images from the files on our hard drive into the computer memory and then onto the screen. We must do this the way DirectX and C# want us to do it. So the first challenge is learning what must be done to achieve this goal.

An application must be created, this application must be linked to DirectX. Because we are using the 3D API we must then create some polygons and then load our image as a texture onto them. Let the hoop jumping begin!

Your first C# Application


VS.net is assumed to be already installed, all we need to do is load it up.

Visual Studio Icon.

! The latest versions of DirectX come with a Framework, this includes support for GUI stuff. You may also wish to investigate the framework. The last time I looked at it - it seemed overly cluttered to me but if I was starting a new project I'd check it out again.


We will create a brand new empty project. This way everything is very clear and clean. It will allow us to see exactly what code is needed to make the game. The default projects tend to add in a lot of clutter. Starting a new project can be done by selecting File from the menu, then going to New and selecting Project from the list.

Creating a brand new project.

This brings up a small box of predefined projects. As we are using C# we select the Visual C# Projects folder on the left of the window, under the label Project Types. Now from the list of project templates on the right of the window we want to select Empty Project, it's at the bottom of the window so it may mean scrolling down a little. Once New Project is highlighted by clicking on it, we may optionally choose a name for the project.

If we do not choose a name then a default name will be assigned; along the lines of "New Project 1". Anyway here I've chosen to call it "Simple Window". Once you have decided on a name click OK and a new project will be created.

Choosing an empty project.

The project is totally empty, so there isn't much to see. In Class View, you will be able to see the root of a tree named after your new project. In this case called "Simple Window". The tree can be expanded but won't show anything because currently there's nothing to show - it's an empty project! The 'Class View' window on my VS.net setup is to the left side of the screen. Class view isn't too important for now though, first let us add a new file. Once again up to the File label on the menu bar, but this time select Add New Item.

Adding a new item.

There are a number of files to choose from. We want a Code File. In the Name field it will have the extension .cs. Once highlighted you can optionally name the file you want to add, here we have called it "AForm.cs". We've chosen the word "Form" because that's how windows are referred to in C#. Once named click Open, this will create a new file "AForm.cs" and open it in the main code area. The active tab, the tab that is highlighted, should be labelled AForm.cs and below that in the blank window is were we will type the code!

Choosing what type of item to add.

The code we intend to write will be communicating directly to the Windows Operating System. C# comes with lots of libraries (handy pieces of code already written by other people) and we need to referencesome of them in this project so we can use them in our code. Not referencing the required libraries is an easy mistake to make. The most common reference is to System. The System library has lots of useful functions to interact with the operating system. (We can write string of texts to a console window for example).

Therefore we should include a reference to System and as we are going to be doing Windows programming we should also include System.Windows.Forms. All the libraries are in namespaces, namespaces can be thought of like bags, in the System bag there can be a Windows bag which can contain a Form bag. Also in each of the bags are nuggests of code that we can use. The form code allows us to make Window Applications.

Namespaces are literally spaces for names. So we have a 3 layered namespace with System.Windows.Form. Namespaces have some restrictions the Windows Namespace could not contain say, two sub-Namespaces both called Form. This would cause a clash, the compiler wouldn't know which Namespace you were refering to when you used the word Form. But if you're doing lots of programming the same names are going to come up again and again. We may need to use the word 'Form' again in ASP programming or something similar - as long as the name 'Form' is in a different namespace - such as Systen.Form or System.ASP.Form, then the same name can be used and there's no conflict. This goes for names of classes, enumerations and all that stuff too. You should use namespaces to group related code in your project - this will make it more managable.

Before we begin programming we need to add the resources we're going to use. This can be done through the Solution Explorer. This should be over on the left of the screen, there are some tabs including class view, resources and search. If you cannot see it or it's not there, simply go to the file menu, select View and then chose Solution Explorer which should be the first option.

Remember, Windows in C# are refered to as forms. So to create a windows based program we will usually have our application inheriting from the form class. In our application's constructor we can set the Form's title bar and various properties. The in the Main function of our application we can write some code that will display the form. Setting all this up in C# is different than doing so in C++, for one thing it's a lot simpler and less messy.

Opening the Solution Explorer.

This should bring up a small window. In this window there is an application icon and in bold next to it is our project name. This is the root of our "Solutions" tree. VS.net calls projects and programs solutions, I assume because they like buzz words and feel they haven't got enough. Beneath that is a folder icon and the word references. Below that is our code file - AForm.cs.

Adding a Reference.

Right-Click on the icon next to the word References in the Solution Explorer. This will bring up a menu with two options - choose Add Reference not Add Web Reference or whatever the other one's called. This brings up a new window with a few tabs at the top. The current tab is .NET which is the tab we want. Below that there is a big list of Components( or References or Libraries - call them what you wiil).

Choosing the References.

We need to scroll down to System, a quick way to do this is to click on the list of components and then press 'S' on the keyboard. This gets us to the 'S' section, from here scroll down a little until you find a component called System.dll (or you can keep pressing 'S', same thing), click the select button over on the right of the window. System.dll will appear below in the window named Selected Components.

Scroll down a bit more until you see System.Windows.Forms.dll, select this as well, and then hit the okay button at the bottom. We are now ready to begin programming!

Some Code!

Okay we should now focus our attention to the big code window. It should already be active and the AForm.cs tab should be in focus. If not go to the solution explorer we saw earlier and double click on the Aform.cs file. Good.

We have added the references to the project but we also need to tell the code file that we'll be using them. This is done using the using keyword. This is the first thing we'll type in. Lets have look at the first chunk of code then I'll explain whats going on.


using System;
using System.Windows.Forms;

namespace Tutorials.UsingAForm
{
      public class AForm
      {
      }
}



Now if you try to compile this you will find it creates a compiler error about the program not having an entry point defined. This error refers to the fact we don't have a main function (you may be familiar with this from C++). Whenever a program runs, the code in the main function is the code that it executes first. So far we haven't added this function but we will shortly.

Let's review what's been written. First we have two using statements, these are the libraries we imported earlier. Generally you are always going to need 'System'. The details of the various namespaces we're including can be explored using MSDN. One of the simplest ways to have a quick look at what's in the references / namespaces is to let the VS.net interface show you the options:

Showing you whats inside the System namespace.

You can scroll up and down this list and look deeper into the namespace. Its one of the more helpful tools avaliable to you.

So we have added System and System.Windows.Form. These allow us to access the Operating System and tell it that we want to create a window. The next line creates a namespace which we will use to keep our code in. This is not necessary but its considered good coding practice and is useful for splitting up code. Generally the suggested format is YourCompanyName.YourProject name, this way if you are using code from seperate companies the naming system is never going to clash.

In this case we have created a two level namespace first Tutorials then Using a form. Try typing Tutorials. in the code window, and it will list UsingAForm in a helpful pop up window. You have to type the '.' though. Instead of typing UsingAForm you can then just press enter and it will write it for you. If you put a dot after UsingAForm it will show you the class we have created.

Showing you whats inside the namespaces we created.

The namespace contains everything inbetween its brackets ({}), this means you may have several namespaces per file if you so wished. I would keep it to one namespace per file myself, that way it's nice and clean. You may also go the other way and have the namespace spread over serveral files. Just redefine the namespace at the top of a new code file and the contents of both files will be lumped together. To access stuff in other namespaces you have to include it and declare the class or whatever with its namespace path. There are others ways to do this and we'll see them later when we're adding DirectX

Inside the namespace we've declared a new class. The keyword 'public' means you can access the class from anywhere else in the code. The name of this class is AForm. Everything between its brackets is included inside this class. You may have classes nested inside classes and then you can access them much the same way as namespaces.

So that is the code so far, we'll do a quick "Hello World" program and then create the form. Inside the class we will add the main function. When you execute the program the code in the Main function will be executed first in all simple programs. As we should all know from Object Orientated Programming, classes are like moulds while objects are like instances of the class. So to use any functions from the class declaration we must have to have an object (a class instance) representing this class.

(Say a Mage is a class and Bob the Mighty Mage of Woo is a class instance, or object, so is "Jeff the Mage who wears a nice green hat". A class is like a template - covering all possible Mages, say. An object is like an instance - a particular mage. I hope this clears up the difference.)

This seems a bit Catch-22, we need the main function which will be run first. Yet to be able to exectute a function in a class we must have some code to create a class object (instantiate the class). So how do we break out of this? The static keyword comes into play. Static functions can be thought of as belonging to the class itself and not the objects created from it, so it can also be thought of to belong to all objects. Variables can be static too, infact a common use for such static variables is to count how many objects are created by the class. Each time an object is created it adds one to a static int belonging to the class and this holds the number of how many objects are created. Most useful for writing messy C++ code with those nasty pointers :)

(So may the class mage would have static variable static bool CanCastMagic = true; we know all mages can cast magic, so this belongs more to the mage class. Maybe particular mages might have a local bool currentlyUnableToCastMagic for when people cast silence on them or they lose their memory. Mage class could also have static int numberOfMagesAliveInTheWorld and each time a particular mage is born then this static value is bumped up by one.)

So if we have a static function main, then no object has to be created and the code can be executed right away. Write out the code below and you'll get the idea.


using System;
using System.Windows.Forms;

namespace Tutorials.UsingAForm
{
      public class AForm
      {
         static void Main()
         {
         }
      }
}


Now the code will compile without any trouble. The keyword 'void' is the type of variable that the Main function returns after it has completed. As the main function only completes when the our program finishes we can tell it that it should not bother to return anything - hence the void.

The brackets are just like a normal function in C or most other programming languages, we're saying the function takes no arguments. So how about that hello world? Well we'll use the System library and this is how it's done:

static void Main()
{
      System.Console.WriteLine("Hello World");
}


We're using System; therefore in this new line of code we could have left the System. part out. Instead writing "Console.WriteLine("Hello World");. I chose to make the call explicity so we can see which namespace Console is in.

As we added this line of code we could look into the System namespace because of VS.net's groovy GUI stuff. Inside System was a Console class and we called its static member WriteLine. Console is the dos box, a terminal window. Try running the program now and it will pop up and dissapear immediately - probably too fast to see! To fix this you could add a ReadLine() that waits for a carrige return (the enter key), try to work that one out on your own. Now you're programming!

Note that the terminal window can be used at any time, even if you already have a form so its great for debugging.

Let's create a form! The class we're using called Aform, is going to become the form. This class will represent the Window that we'll see on the screen. To do this we inherit from the form class. How do we do that? Simple enough.

public class AForm : Form
{


Now if you run this, you might notice the notable abscene of a form. Don't worry, we haven't told the operating system about it yet. Now in C++ and C, this is a lot of code, far too much code! Most of the code you don't care about but have to fill it out anyway. In C# its far easier as lots of things are filled in by defaults. Lets tell the operating system about our cool new form.

public class AForm : Form
{
      static void Main()
      {
         Application.Run(
new AForm());
      }
}


Bam that's all there is to it. One window. We tell the Application to Run our class. First we have to create a new object to instanitate our class, therefore we use the new keyword. The console window also loads up with the form. We can output to console at any time, using the Console's WriteLine function!

Our First Window.

Okay so it's pretty dull at the moment and this isn't exactly how it would work in a game. Currently we're in the Event based paradiagm of programming. The program waits around for events to happen, when an event happens such as a key being pressed, a mouse being moved, a form button being pressed, a monster attacking - a certain function in the code is called. If your familiar with C++ or C programming this is handled through the event queue. What we want is for the program not to be waiting on events doing nothing but instead to be continously chugging along and doing thingst. We could infact use the event queue to do this - there is a special type of message used to tell us when the queue is checked and it is sent as often as possible, whenever it's sent we could tell our game to do a frame. This is more friendly to other applications but we're a game and we want to grab all the resources we can.

We want a game loop. At the end or start of a frame (a single loop) we will check if any events have happened that we care about. Events such as a user shutting down the window our game is running in, or the gamepad the user was playing with becoming unplugged, or the window being resized and all sorts of other troublesome things.

For now though let's spruce up the Window we've made. Another "Hello World" before we move on to the next stage. First though lets name it. The form has a string called text that is the name of the window. So we want to change it, we want to change it as soon as possible. Let's look at one way of doing this. When we create a new object from a class the first function called is the Constructor. So we'll write the constructor for our class. In the constructor we'll set some of the Form's properties.

public class AForm : Form
{
      static void Main()
      {
         Application.Run(
new AForm());
      }

      public AForm()
      {
         Text = "A Brand New Window";
      }
}



Constructors have to use the same name as the class they are a part of and they cannot have a return type. They should usually be public, the only reason you would have them private is to stop an object being created. You might not want an object to be created if it was to be a static only class (you can call functions directly from the class rather than using an object instance of the class). So when the computer executes the code new AForm(). Then the constructor is called because a new class is being constructed.

Our Window with a title.

So thats good but how do we get things into the windows-box? Well we use a Graphics object. We're going to write "Hello World". To make sure the text we write stays in the box we have to write it everytime the window is redrawn. To do this we must override the function that handles the "The Window has to be redrawn" message. These functions exist in the form that we have inherited already, so all we need do is find the correct one and override it. To find the function we can use the MSDN, the object browser, or a handy reference book. The function we intend to override is called OnPaint Makes sense yeah? Good. Eveytime there is a message saying "Redraw the Window", then the OnPaint function is called.

We are going to be Drawing to the window. This involves quite a lot of work that the Operating System has to do. Fonts, sizes, colours it has to know it all. For this complexity we need to add another library reference, so we pop over to references and add a new one. This time select and add System.Drawing. Now lets tell our code file that we intend to use this new reference! We add another line at the top : using System.Drawing;. Okay lets have a look at the code.

using System;
using System.Windows.Forms;
using System.Drawing;

namespace Tutorials.UsingAForm
{
      public class AForm : Form
      {
         static void Main()
         {
            Application.Run(
new AForm());
         }

         public AForm()
         {
            Text = "A Brand New Window";
         }

         protected override void OnPaint(PaintEventArgs pea)
         {
            pea.Graphics.DrawString("Hello World",
this.Font,
                     Brushes.Black, 0,0);
         }
      }
}


Upon running this code, we will get a Window and in the top corner of that Window it will say "Hello World". It's that simple and its that little code to do. Pratically as simple as it was to do in the Console window. Lets have a closer look at the function that we overrode.

      protected override void OnPaint(PaintEventArgs pea)
      {
         pea.Graphics.DrawString("Hello World",
this.Font,
            Brushes.Black, 0,0);
      }


The keyword 'protected' means only those classes inheriting from the Form are allow access to it. We have access so we may override it, everytime Painting is done our function gets called. Override means that the function already exists in Form but we want to replace it with our own code. The next part of the line is the return type and name of the function we are overriding: void OnPaint. Pretty simple. The function takes in a PaintEventArgs object, so we have to include that in the parameters for convience we call it pea.

From this PaintEventArgs parameter we get access to a Graphics object and from this we can call DrawString that lets us draw on our Window. Graphic lets you draw lots of other things too, and you can get an idea of what by browsing through its members. For DrawString we need the string we want to draw, in this case simply "Hello World". Then the font we wish to draw in, the form has a default font called Font, the this. is unneccessary and is only present for clarity. Next we need a brush. Brushes can draw dashes and draw in different colours but we just want the default brush. We get this using the Enumeration Brushes avaliable in the 'System.Drawing' namespace. We chose plain old black. Then there are the X,Y co-ordinates, we have chosen 0,0 so its in the top corner. Feel free to play with all these things. There are also many other things hiding in the form class that are worth checking out. For now though let us move on.

Download icon.[Hello World Source Code]

Moving On

Hooking Up Direct3D

The DirectX SDK usually has to be downloaded from the MSDN library, we're looking for Managed DirectX. Then we need to add Direct X as a resource. (As a side note the installer I used was broken and I had to manually extract dxnt.cab and then select the required references via the browse button in add references. Hopefully this is fixed now) As before select the Solution Explorer, right click on references and choose Add Reference. Then select Microsoft.DirectX and Microsoft.DirectX3d. With this done all the power of DirectX is available and kick-ass games can proceed to be made.

Co-ordinates Of The Window

Each point on the plane of a window can be addressed using an X, Y co-ordinate. (0,0) is the very top corner (this.height, this.width) would be the very bottom corner. These points are considered to be inside the form. The co-ordinates do not include the border around the window or the title bar. A diagram of coordinate system is displayed below (the +5 numbers are arbitary and don't reflect the actual units used).

Windows coordinates.

The system could be considered to be slightly strange as increasing the value of Y causes the point to descend. It's important to make a note of this. Especially since you will be using a graphics library with it's own, different, coordinate system. If you don't bare in mind which you wish to use at a given moment then lots of headaches can ensue.

Windows coordinates.

In the above image the window would be drawn starting at the origin 0,0 and the down the y axis and across the x axis.

Drawing A Triangle

Drawing a triangle is one of the basic steps to 3D graphics mastery and Microsoft has a tutorial that includes. The tutorials are generally meant for those who already know what they are doing, to get up to speed therefore I'll be going over it again here.

A triangle is made up of three points (generally called Vertices in the computer graphics world) joined together with three lines. Before this a degree of preperation and setup must be performed (doesn't it always?). We need to head through the following topics:

  • How games work - the general architecture of most games

  • Application Programming vs. Game Programming

  • Setting up Direct3D and learning about your 'device' :)

  • Hooking Direct3D up to a form

  • Drawing in 3D space

Preparation For A Triangle Drawing Adventure!

The first thing required is to create a new form. Just a normal blank form, with the references for DirectX goodness (Microsoft.DirectX and Microsoft.DirectX3d).

using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace VerticesTutorial
{
      public class example : Form
      {

         static void Main()
         {
               example form =
new example();
               Application.Run(form);
         }
      }
}


This will produce a form like we've seen before but there is a problem here and if you've not really programmed with windows before you may not see it. There's a question that needs asking How do games work? .

Aside: How Do Games Work?

When I first started I was quite confused about how games worked. First there is a screen - it can be abstractly viewed as a big square divided into pixels, and you can tell the computer which pixels should be which colour. All games that use a screen boil down to telling the screen what values it's many many pixels should have at a given moment. But how do we make these pixels represent monsters and players all moving in seemingly real time? -> we need to update the screen as often as possible!

A computer game is easiest to think about when it's code works as a continual loop.

Main()
{

      //Do setup
      while(the_game_is_not_over) do
      {
            //update game states
            //output results to screen
      }
}


This is basically how most games work. They are resource hogs and don't like to think about other programs. Most Windows programs are applications, they are programs that run together and may often be used at the same time, therefore they shouldn't take up all system resources possible - they should do the opposite and try to use as little as possible. Therefore windows adopts event based programming.

All applications could run in a loop, just like our game, each iteration of the loop the application would want to know what the user was doing - had he pressed a key or clicked the mouse. So the application would query the opertating system and say "has the user done so and so" and then it would act upon the opertating systems answer and the loop would wrap around again.

Event based programming works differently, the program is written like "On the A key being pressed do this ...". Pressing the A key would be classified as an 'event'. As events occur they are put in a big queue. All program look at the queue, see what is happening and then act on it. The event queue works like a big continous loop for all the programs. This is currently how our application works - to make our programming jobs easier and to give us more power we want to reimplement a big loop, so we can update the game state. Things will be happening in our game if the user gives us any input or not. On a word processor, if you enter nothing then nothing happens, for instance space invaders do not advance down the page. But in a game space invaders may very well advance down the page - therefore we need a loop to tell our program how what it should be doing at a given moment.

I have explained this badly :( But I hope you get the point!

Back To The Code, Getting It In A Nice Loop

Here's the reformed code, to break us out of the event based paradigm. The code creates the form, it then shows the form, i.e. makes it visible to the user. Then while the form is Created (that is, while the user doesn't shut it down) the operating system is told to handle any messages recieved. Upon running this code, you will find it works just like before! The difference is at the code level - now we have access to an inner loop that we can use to update our game with. The following paragraph will include some rehashed arguments of why this is needed and why this is good, because the reasons in the section above seemed a little confusing when I wrote them.

static void Main()
{
         example form =
new example();
         //Application.Run(form);
         form.Show();
         while (form.Created)
            {
                  //Update our game, and render to screen
                  Application.DoEvents(); //Let the OS handle what it needs to
            }
}


Notice now that a while loop has been inserted. As long as the form exists the program will keep running around the while loop. If we put something in the while loop like another while loop e.g.

While(true){}
Then the system will hang! If anything that takes too much time is put in there, then the system will noticably slow down. The system can do a lot - an amazingly large amount of calculations before we even notice this slow down. So we tell the system to do a load of stuff, and it does it continually faster than we can see, to us it appears it all happens in real time. Imagine Quake, each screen refresh, each model is updated, each vertex and line, all put on a virtual set, itself made out of lots of points and lines. Textures are rendered according to current view - masses of stuff is done and then it is blitted to the screen and this all happens so fast it seems like a really fast 3D jaunt.

We might have a ball in that continous loop for instance and each iteration of the loop we may say "Where is the ball?" okay "move the ball two points to the left". Then each time the loop was called the ball would move left and because computers tend to be so zippy fast it would seem as though the ball was travelling smoothly across the screen.

Okay I think I've talked about that quite enough now. So all we wanted to do was make a triangle - let's see what we need to do.

The 'Device'

To create wonderful 3D graphics in DirectX you need to use a Device, this will usually be a software representation of the Video card in your machine. Let's add a little code to cater for the device and then we'll jump through the various hoops required to hook it up to our window.

We're added the references Microsoft.DirectX and Microsoft.DirectX.Direct3D but we haven't added the using statements to our code yet. So at the top of the window we add:

using System;
using System.Windows.Forms;
using System.Drawing;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;


Then we can can't start using some of the classes and objects within those namespace. Such as our trusty device.


public class example : Form
{
      Device device =
null;

      static void Main()
      {
            example form =
new example();
            form.Show();

            while (form.Created)
            {
                           form.Render();
//Where we tell our 'device' what to do!
                     Application.DoEvents(); //Let the OS handle what it needs to
            }

      }

      private void Render()
      {
               if (device == null)
                     return;
      }

}


Now to create the device - not overly simple because there are many people in the world, all with graphics cards which in turn may all differ in various ways. We must do a basic setup that creates a 'device' object that will allow us to do what we want, if this is impossible then it should exit gracefully (put an error message up saying "No." rather than just crash). We'll do a quick run through of setting up a device object that will suit most of our everyday needs, and then we'll see how the code as a whole is shaping up.

To set the device up we're going to create a function called InitializeGraphics, this will be called before the game loop starts and setup the Device object. We can later expand on this function to add any other graphics setup stuff.


public Device (
   System.Int32 adapter ,
   Microsoft.DirectX.Direct3D.DeviceType deviceType ,
   System.Windows.Forms.Control renderWindow ,
   Microsoft.DirectX.Direct3D.CreateFlags behaviorFlags,
   Microsoft.DirectX.Direct3D.PresentParameters presentationParameters )


Let's have a quick run through of what the device constructor is all about and then we'll create one with some suggested variables. First argument is an integer, this allows you to choose which adapter you wish to use - for instance if you have a number of graphics cards. To make this simple it can just be set to 0 and the default device will be chosen. DeviceType is whether you're going to use software or hardware, and is set with a flag. The renderWindow is the form you wish to render to - this will just be the form we've created. The behaviour flags is a big bag of different options - we'll keep this bit simple and just choose what we need.

The last parament is how the data will be presented to the screen, to configure this correctly a PresentParameters object is first created, then set to our particular needs and then passed in. So currently our code looks a little like this:


device =
new
               Device(0,
               DeviceType.Hardware,
               
this,
               CreateFlags.HardwareVertexProcessing,
               PresentObject);


We need to create the PresentObject! We're gonna keep this as simple and brief as possible so we can get to that glorious triangle-based action. The only thing we need to tell the PresentObject is that we're going to be creating a windowed application (did I forget to tell you this? Well we are! :P) and that we're going to discard the SwapEffect buffer. The swap buffer is like a virtual screen in memory so you update that little by little as you update your game state and then whack it to the screen. If you didn't have a full done image to give the screen you might get nasty artifacts and tearing and flickering as the screen is updated. SwapEffect.Discard discards the contents of the buffer if it isn't ready to be presented.

Let's see how the code is shaping up.

public class example : Form
{
      Device device =
null;

      static void Main()
      {
               example form =
new example();
                  form.InitializeGraphics();
               form.Show();

               while (form.Created)
                  {
                           form.Render();
                           Application.DoEvents();
//Let the OS handle what it needs to
                  }
      }

      public void InitializeGraphics()
      {
            try
            {
                     PresentParameters presentParams =
new PresentParameters();
                     presentParams.Windowed=
true;
                     presentParams.SwapEffect = SwapEffect.Discard;

                     device =
new Device(0,
                           DeviceType.Hardware,
                           
this,
                           CreateFlags.HardwareVertexProcessing,
                           presentParams);
            }
            catch (DirectXException e)
            {

                  MessageBox.Show(
null,
                  "Error intializing graphics: "
                  + e.Message, "Error");
                  Close();
            }
      }


      private void Render()
      {
               if (device == null)
                  return;

                     device.Clear(ClearFlags.Target,
                        System.Drawing.Color.Blue,
                        1.0f, 0);

                     device.Present();

      }

}


Notice the try and catch blocks in the InitializeGraphics() procedure. This means that when a device is attempted to be created but for some reason has an error (such as no hardware support) - then the error is thrown - this is caught by the 'catch' block and an error message is produced. It is worth noting that this error message would be decidably unhelpful to an end user and should be expanded upon in a full project!

There are also changes to the render method - which is called each loop iteration (which from now on I'll start calling an iteration a frame - this is where frames per second etc comes from, there's a little more to it - timing and such but from now on they're frames!) so each frame the Device is being cleared to a blue colour then the device is being presented to the screen. ClearFlags.Target means we're clearing the target window (rather than the Stencil Buffer or Z Buffer, which we will be trying our best not to worry about). Then we grab a predefined colour - I chose blue. The second to last parameter is the Z-buffer and the last parameter is the value of each stencil, for us in this place and this time - they do not matter - just some unfortunate magic words we have to say :(.

Once this is all done it can be run and the window is a blue colour and the window, DirectX and the graphics card are all fuzed together in a big ball of game making potential.

A Window with a blue background - what wonders await us next?

Creating The Actual Triangle



In a finished game, people might want to fiddle with the settings of the graphics. We should program in a way that allows us to incorparate this easily. For instance if we changed a load of options we'd like just to be able to call InitializeGraphics() with some new arguments. I'm a little worried we're going down the road of complicated here, but I think we can handle it if we stick to our guns.

Delegates, delegates are like variables that store functions, and you can add lots of functions to the variables. Then when you call the variable all the functions are run - good heh? Yes. So Device has a few of these special delegate variables that we can hook functions to. Dry those eyes! Why is this useful - because it saves us work, let's pan and zoom and see why!

device.DeviceReset += new System.EventHandler(this.OnResetDevice);

DeviceReset is one of those delegates here we add an event handler to it - what does this mean? An event handler, handles an event! , i.e. when the event occurs, the handler is called to deal with it. If the cat being hungry was an event, then the then function of the owner feeding it could be thought to be the event handler (yeah ...). So every time the Device is reset we call a function called 'OnResetDevice', a function we're going to write. (OnResetDevice arguments are defined by System.EventHandler - we can't just thow anything in there.)

Let's add the above code to our application. It will say everytime the device is created then call the function 'OnResetDevice'. Easy!

public void InitializeGraphics()
{
      try
      {
               PresentParameters presentParams =
                     
new PresentParameters();
               presentParams.Windowed=
true;
               presentParams.SwapEffect = SwapEffect.Discard;

               device =
new Device(0,
                     DeviceType.Hardware,
                     
this,
                     CreateFlags.HardwareVertexProcessing,
                     presentParams);

               device.DeviceReset +=
                     
new System.EventHandler(this.OnResetDevice);


Delegates and event handlers get used quite often in Windows programming and they're useful to you (the games programmer) as well - if you don't know about them, then I suggest you make some test programs and get to understand why they're useful.

What do we want to do when the Device has been reset? - Reload all the triangle data so we can get some triangle action.

public void OnResetDevice(object sender, EventArgs e)
{
}



The object that being sent (object sender) in this case is the Device. Device stores the delegate and when a delegate is activated it sends a reference of itself to the function. All that is required is that we cast Sender from an Object to a Device object and then we're good to go. The arguments above are standard for delegates and are defined by that newSystem.EventHandler bit of code where we link up our function to the delegate at the end of IntializeGraphics.

Remember how triangles where made out of vertices (also known as points, but in Graphic-speak always vertices) and lines? Good! Well we need a VertexBuffer to store those vertices. A buffer is, simply, a portion of memory set aside to store data. So this is where we store our vertices which will make up our triangle.

3D objects are store as collections of vertices and the lines are automatically drawn by DirectX to create polygons. So all that is required is a data structure to store our vertices.

DirectX gives us the datastructure we need - VertexBuffer. We must to learn the constructors in order to discover how to use it. We will define a global VertexBuffer that we'll call vertexBuffer - original heh?

public class AForm : Form
{
      Device device =
null;       //Think of this as your graphics card
      VertexBuffer vertexBuffer = null; //To store 3D objects!

Then construct a vertex buffer using the below code. We're placing this code into the OnResetDevice function. So after we create a device, then we set up a vertex buffer. Then the device will draw what's in the vertex buffer and we can all do a small dance.

vertexBuffer = new VertexBuffer(typeof(CustomVertex.TransformedColored),
      3,
      dev,
      0,
      CustomVertex.TransformedColored.Format,
      Pool.Default);



So we put this code here:

public void OnResetDevice(object sender, EventArgs e)
{
      Device dev = (Device)sender;
      vertexBuffer =
            
new VertexBuffer(typeof(CustomVertex.TransformedColored),
            3,
            dev,
            0,
            CustomVertex.TransformedColored.Format,
            Pool.Default);

}


What!? More arguments, more things to remember? Yes, but we're oh so nearly over and for a large amount of this stuff you can cover your ears, close yours eyes and say "aalalalalalalal" and you should still be okay. So arguments, hmmm, let's see - there's the number '3' that's the number of vertices we're going to use. A triangle is made up of three points. 'dev' is a reference to the device we're using but the other arguments are a little more cryptic.

The first arguement is the type of vertices we're going to use (i.e. the vertex buffer will only be storing vertices of this type), there are a number to choose from. Here we choose CustomVertex.TransformedColored which means the triangle need not be moved or rotated, it is to be specified using screen co-ordinates (screen co-ordinates are 2D form coordinates which are Transformed co-ordinates). It also includes a colour component for each point (vertex) in the triangle, which allows polygons to be coloured! The next two arguments are Vertex Numbers and the Device, so on to the third which is how the Vertex Buffer is going to be used, for now we just stick in 0 here and ignore it. Next argument is what the format of the vertices will be. As we're using only CustomVertex.TransformedColored we can get it's predefined formatting information and stick that in. The last argument is the memory pool - in the computer memory or in graphics card memory, by going default we tell the computer to do whatever it believes is appropiate.

Let's review the code at this point:

public class example : Form
{
      Device device =
null;
      VertexBuffer vertexBuffer =
null;

      static void Main()
      {
            example form =
new example();
            form.InitializeGraphics();
            form.Show();
            
while (form.Created)
            {
                  //Update our game, and render to screen
                  form.Render();
                  Application.DoEvents();
//Let the OS handle what it needs to
            }
      }

      private void Render()
      {
            if (device == null)
                  return;
            device.Clear(ClearFlags.Target,
                  System.Drawing.Color.Blue,
                  1.0f, 0);
            device.Present();
      }



      public void InitializeGraphics()
      {
            try
            {
                  // Now let's setup our D3D stuff
               PresentParameters presentParams = new PresentParameters();
               presentParams.Windowed=
true;
               presentParams.SwapEffect = SwapEffect.Discard;
               device =
new Device(0,
                  DeviceType.Hardware,
                  this,
                     CreateFlags.HardwareVertexProcessing,
                     presentParams);

                  device.DeviceCreated +=
                        new System.EventHandler(this.OnResetDevice);
                  OnResetDevice(device,
null);
            }
            catch (DirectXException e)
            {

                  MessageBox.Show(
null, "Error intializing graphics: "
                  + e.Message, "Error");
                  Close();
            }
      }

      public void OnResetDevice(object sender, EventArgs e)
      {
            Device dev = (Device)sender;
            vertexBuffer =
               
new VertexBuffer(typeof(CustomVertex.TransformedColored),
               3, dev, 0, CustomVertex.TransformedColored.Format,
               Pool.Default);
      }
}


The code will still just display a window with a blue background but behind the scenes there is a data structure just waiting to contain 3 lucky vertices. Lets go back to render first though. We want to use render to display whatever is in VertexBuffer.

public void Render()
{

      if (device == null)
            return;

      //Clear the backbuffer to a blue color (ARGB = 000000ff)
      device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);

       device.BeginScene();

      device.SetStreamSource( 0, vertexBuffer, 0);
      device.VertexFormat = CustomVertex.TransformedColored.Format;
      device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);


      //End the scene
      device.EndScene();

      device.Present();
}


We need to call device.BeginScene(), then we draw everything we want for the current frame, then we call device.EndScene(). SetStream source is where the vertices are going to be coming from. The first argument is the stream number, as we're only dealing with one stream this number is set to zero. vertexBuffer is the data that is to be streamed. The third parameter is the number of bytes of offset, we're not too concerned with this so we set it to zero too.

VertexFormat tells the device what to expect, the same as we set in the VertexBuffer. It needs to know what kind of vertices it's dealing with so it can draw then correctly. Then the last function is actually drawing the vertices. We are going to draw a triangle the most suitable PrimitiveType for this is triangle list. Second parameter is the starting vertex from which we draw. So we have one triangle of three points, therefore we want draw from the first vertex at position 0. The last parameter is the number of primatives - '1'. We we're drawing a single triangle. This number helps DirectX connect the vertices with lines (edges). Now all that needs doing is filling the vertexbuffer!

We can insert the following code at the end of the OnResetDevice function. This is called when we know we have a device to use. Then the vertex buffer is set up so we know we have some where to put the vertices. Then we actually but the vertices in with the below code.

GraphicsStream stm = vertexBuffer.Lock(0, 0, 0);
CustomVertex.TransformedColored[] verts =
new CustomVertex.TransformedColored[3];

verts[0].X=150;
verts[0].Y=50;
verts[0].Z=0.5f;
verts[0].Rhw=1;
verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
verts[1].X=250;
verts[1].Y=250;
verts[1].Z=0.5f;
verts[1].Rhw=1;
verts[1].Color = System.Drawing.Color.Brown.ToArgb();
verts[2].X=50;
verts[2].Y=250;
verts[2].Z=0.5f;
verts[2].Rhw=1;
verts[2].Color = System.Drawing.Color.LightPink.ToArgb();
stm.Write(verts);
vertexBuffer.Unlock();


Here we get a Graphic stream to the VertexBuffer, returned by Lock - lock means while this functions fiddles with the vertexBuffer nothing else does. Each vertex is assumed to be a point in 3D space, and can be address by X,Y,Z values. So each of our three vertices has one of these X,Y,Z values as well as a colour. Here we create an array of three vertices, then assign values to the vertices so that they form a triangle. Finally we write the array to the stream and unlock the VertexBuffer - it now has all the vertices in it and we're no longer need to use it! Okay lets look at the code and the program it produces!

using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

public class example : Form
{
      Device device =
null;
      VertexBuffer vertexBuffer =
null;

      
static void Main()
      {
            example form =
new example();
            form.InitializeGraphics();
            form.Show();
            
while (form.Created)
            {
                  
//Update our game, and render to screen
                  form.Render();
                  Application.DoEvents();
//Let the OS handle what it needs to
            }
      }

      private void Render()
      {
            if (device == null)
                  return;
            device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);

            device.BeginScene();

            device.SetStreamSource( 0, vertexBuffer, 0);
            device.VertexFormat = CustomVertex.TransformedColored.Format;
            device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1);

            device.EndScene();
            device.Present();
      }



      public void InitializeGraphics()
      {
            try
            {
                     // Now let's setup our D3D stuff
                     PresentParameters presentParams = new PresentParameters();
                     presentParams.Windowed=
true;
                     presentParams.SwapEffect = SwapEffect.Discard;

                     device =
new Device(0,
                              DeviceType.Hardware,
                           
this,
                           CreateFlags.HardwareVertexProcessing,
                           presentParams);

                     device.DeviceReset +=
                           
new System.EventHandler(this.OnReseDevice);
                     
this.OnResetDevice(device, null);
               }
               
catch (DirectXException e)
               {

                     MessageBox.Show(
null, "Error intializing graphics: "
                     + e.Message, "Error");
                     Close();
               }
      }

      public voidOnResetDevice(object sender, EventArgs e)
      {
            Device dev = (Device)sender;
               vertexBuffer
               =
new VertexBuffer(typeof(CustomVertex.TransformedColored),
               3,
               dev,
               0,
               CustomVertex.TransformedColored.Format,
               Pool.Default);

            GraphicsStream stm = vertexBuffer.Lock(0, 0, 0);
            CustomVertex.TransformedColored[] verts =
                  
new CustomVertex.TransformedColored[3];

            verts[0].X=150;
            verts[0].Y=50;
            verts[0].Z=0.5f;
            verts[0].Rhw=1;
            verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
            verts[1].X=250;
            verts[1].Y=250;
            verts[1].Z=0.5f;
            verts[1].Rhw=1;
            verts[1].Color = System.Drawing.Color.Brown.ToArgb();
            verts[2].X=50;
            verts[2].Y=250;
            verts[2].Z=0.5f;
            verts[2].Rhw=1;
            verts[2].Color = System.Drawing.Color.LightPink.ToArgb();
            stm.Write(verts);
            vertexBuffer.Unlock();
      }
}



What Is Rhw?



RHW (reciprocal of homogeneous W), X,Y,Z are pretty obvious but W - what is that used for? Vertices are often represented by Vectors, which are multiplied by matrices and W makes this smoother. It's used when you take the vertices from 3D space and then flatten them against the screen to create a 2D image.

TRIANGLE!

We're using Transformed Coloured Vertices, so the co-ordinates used are in screen co-ordinates. The vertices are joined in a clockwise fashion. The coordinates are the same as before. Let's have quick look at the triangle again.

TRIANGLE AGAIN!

Feel free to mess around with the colours, if you want to add more vertices remember to increase the number expected by the vertex buffer and if necessary the number of primatives expected in device.DrawPrimative(...). (Hint: To draw a square using triangles requires two triangles - so six vertices, not four!)


Wonderful, we're done - now go show your Mum.

Download icon.[Displaying A Triangle Source Code]

Review




To get to triangle production took two stages - intializing the 'device'. Once the device was created then we needed to create the triangle data, feed it to the device and get the device to display it.

Part 1 : Intializing Direct X



  • Create Global Device object

  • Set device using it constructor and PresentParameters

  • Catch any errors and if found exit gracefully

  • In Render- Clear scene, Begin Scene, Draw Scene, End Scene, Present Scene.



Notes: Currently this setup code is not roboust - it doesn't check what hardware the user is using. If you are only aiming to develop for one set of criteria then the current code is fine. But say if you wish to take advantage of special features on some cards (and still support cards that don't have these features). Then this code may need some tweaking.

Part 2: The triangle



  • Create a VertexBuffer - knowing Vertex type and format and number of vertices

  • Create a stream of the Vertex buffer

  • Create suitably sized array of vertices

  • Send vertices into VertexBuffer and unlock

  • Ensure DrawPrimatives code is compatible when it comes to the time to draw the buffer



This is obviously a 'hard-coded' solution to get something to the screen. A more complicated project needs a more elegant solution and more general datastructures defined around our currently hard coded concept.

Refining The Code




A few bits and pieces that might make our code look a little nicer or work a little better.

Resizing




If you attempted to stretch the window, or resize it - the triangle dissappears. Each time the window is resized the device must be reset(this is performed automatically)! The vertex buffer must be recreated on each device reset. So how do we do this? Well we must add all the Vertex info into the delegate when the vertexBuffer is recreated.

public void OnResetDevice(object sender, EventArgs e)
{
         Device dev = (Device)sender;

         vertexBuffer =
            
new VertexBuffer(typeof(CustomVertex.TransformedColored),
            3,
            dev,
            0,
            CustomVertex.TransformedColored.Format,
            Pool.Default);



         VertexBuffer vb = vertexBuffer;
         GraphicsStream stm = vb.Lock(0, 0, 0);
         CustomVertex.TransformedColored[] verts =
                        new CustomVertex.TransformedColored[3];

         verts[0].X=150;verts[0].Y=50;verts[0].Z=0.5f;
         verts[0].Rhw=1;
         verts[0].Color = System.Drawing.Color.Aqua.ToArgb();

         verts[1].X=250;verts[1].Y=250;verts[1].Z=0.5f;
         verts[1].Rhw=1;
         verts[1].Color = System.Drawing.Color.Brown.ToArgb();

         verts[2].X=50;verts[2].Y=250;verts[2].Z=0.5f;
         verts[2].Rhw=1;
         verts[2].Color = System.Drawing.Color.LightPink.ToArgb();

         stm.Write(verts);
         vb.Unlock()
}



Now the window can be resized without the triangle dissapearing.

Download icon.[Displaying A Triangle Source Code II]

Alternative way of assigning vertices



This is an alternative function. It is more concise and there is less occurring between locking and unlocking. I'm not sure which is best or if any difference at all.

public void OnResetDevice(object sender, EventArgs e)
{
      Device dev = (Device)sender;

      vertexBuffer =
         new VertexBuffer(typeof(CustomVertex.TransformedColored),
         3,
         dev,
         0,
         CustomVertex.TransformedColored.Format,
         Pool.Default);


      CustomVertex.TransformedColored[] verts =
         new CustomVertex.TransformedColored[3];

      verts[0].Position =
new Vector4(150,50,0.5f,1);
      verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
      verts[1].Position =
new Vector4(250,250,0.5f,1);
      verts[1].Color = System.Drawing.Color.Black.ToArgb();
      verts[2].Position =
new Vector4(50,250,0.5f,1);
      verts[2].Color = System.Drawing.Color.Purple.ToArgb();

      vertexBuffer.SetData(verts, 0, LockFlags.None);

}


Download icon.[Displaying A Triangle Source Code III]

References used

Post a Comment