Anyhoo here's the docs for any one who cares (only first part, I don't have an image hosting I can use so all image links will be broken unless, mircales of mircales you have the same images as me stored on your harddrive in the same directories :)
The first of my retrospective documentation-come-tutorial, though not the first written merely the first pertaining to suggested order of reading.
Intro
I want to make an RPG, whatsmore I want to use C# and DirectX. So this is not going to be cross platform, maybe next time using something like Ruby and SDL. The game is tile based, but I want to use the DirectX 3D API. I will use DirectX 9. Reasons for all these choices are not necessary for this document and maybe be found elsewhere ... possibly.
I will also be using the Visual .net IDE 2002 (I've yet to upgrade).
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 files on my harddrive into the computer memory and onto the screen. We must do this the way DirectX and C# want us to do it. So the first challenge is understanding how to do this.
An application must be created, this application must be linked to DirectX. Because we are using the 3D api we must create some polygons and then load our image as a texture onto them.
Your first C# Application
First things first, we assume VS.net is already installed so lets get it running.
We will want to create a brand new project. This way its all very clear and clean allowing us to see exactly what is put into the project. 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.
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 clinking 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 decided on a name click OK and a new project will be created.
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 so in this case called "Simple Window". If you exapnd the tree though little will happen because it is empty. Class view 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 lable on the menu bar, but this time select Add New Item.
There are a number of files to choose from that can be added. We want a Code File it have the extension .cs. Once highlighted you can optionally name the file you want to add, here we have called it "AForm.cs"; because forms how we will refer to program windows in C#. Once named click Open, this will create a new file "AFrom.cs" and open it in the main code area. The active tab, the tab that is highlited, should be labelled AForm.cs below that in the blank window is were we will type the code!
The code we intend to write will be communicating directly to the Windows Operating System. C# comes with lots of libraries and we need to reference some of them in this project so we can use them in our code. Referencing necessary libraries is an easy mistake to make, so we will get it out of the way first. The most common reference is to System, this allows lots of useful calls such as outputting to the console and whatnot. We'll need to include this one 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.
Namespaces are literally spaces for names. So we have a 3 layered namespace with System.Windows.Form here. The namespace windows could not contain to namespaces both called form only one. Programming in general has a wide scope, 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 theres no conflict. This goes for names of classes, enumerations and all that stuff too.
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 its not there simply go to the file menu select View and then chose Solution Explorer which should be the first option.
Windows in C# are refered to as a form . To create a windows based program you will usually have your window class inheriting from the form class, in the constructor you'll modify a few things like the title bar and then in the Main function you set it to go. Its different from C++, for one thing its a lot simpler and less messy.
This should bring up a small window, there is an icon of an application and in bold next to it 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.
Right-Clicking on the icon next to the word References in the Solution Explorer will bring up a menu with two options - choose Add Reference. This brings up a new window with a few tabs at the top. The current tab is .NET, and this is the one we want. Below there is a big list of Components or References or Libraries whatever we are taking a fancy to calling them at the particular minute.
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' sections, from here scroll down a little until you find a component called System.dll, 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 intend to use them inside it. 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 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 compiles error about the program not having an entry point defined. This error refers to the fact we don't have a main
function that you may be familiar with from C++. Whenever a program runs, the code in the main function is the code it executes first. So far we haven't added this function but we will shortly.
First we have two using
statements, these are the libraries we imported earlier. Generally you are always going to need system. As we explained earlier they are namespaces and you can have look around them on the MSDN or in the ObjectBrowser in the .NET IDE. One of the simplest ways to have a quick look what is in them is to let the interface show you the options:
You can scroll up and down this list and look deeper into the namespaces. Its one of the more helpful tools avaliable to you.
So we have added System
and System.Windows.Form
. These allows to access the Opertaing System and tell it that we want to create a window. The next line we write creates our namespace for our code to lie in. This is not necessary but its considered good coding practice and is useful for splitting up your code. Generally the suggested format is YourCompanyName.YourProject name, this way if you are using code from seperate companies the code 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.
, and it will list UsingAForm in the 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.
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 ways its 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 the new code window 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 declare a new class. 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. Now the main
function has to be called first. As we should all now 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 have to have an object representing this class.
This seems a bit Catch-22, we need the main
function to be run first, yet before we can run it we have to create an object from the class and to this we need to run some code! So how do we break out of this? The static keyword comes into play. Static functions can be thought of as belong 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 if we have a static function main, then on objects have to be created from the class in order for it to be called. The system can call it right from the class itself and therefore it is legal to go to it staight away and start the program there.
using System;
using System.Windows.Forms;
namespace Tutorials.UsingAForm
{
public class AForm
{
static void Main()
{
}
}
}
Now the code will compile without any trouble. The 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 use the System library and this is how it's done:
static void Main()
{
System.Console.WriteLine("Hello World");
}
As we early said we are using System;
we could have left the System.
part out but I kept it in for clarity. So we in the added line we look in the System
namespace, then we enter the Console
class and call 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. To fix this you could add and ReadLine
that waits for a carrige return (the enter key), try to work that one out on your own.
Note that the terminal window can be used at any time, even if you already have a form so its great for debugging. Now lets create a form. The class we're using called Aform
, is going to become the form. 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 have to link it up to Windows. Now in C++ and C, this is a lot of code, far too much code that you don't care about and have to fill out in C# its far easier. Lets have a go!
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 create our class, therefore use the new
keyword. The console window also loads up and as I said before we can output to here at any time, using the WriteLine
function!
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 keybeing press, a mouse moved, a button pressed, a monster attacking - a certain function in the code it 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 wait around for events but to be continously chugging along and doing things while we wait for another event. We could infact use the event queue to do this - there is a special type of message used when the queue is checked and it is sent as often as possible, whenever its sent ew 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.
So we want to run a frame, a game loop and at the end or start 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, the window being resize and all sorts of other troublesome things.
For now though let's spruce up the Window we have - lets get it so say 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 lets 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 add a constructor to name the objects text field.
public class AForm : Form
{
static void Main()
{
Application.Run(new AForm());
}
public AForm()
{
Text = "A Brand New Window";
}
}
Constructors have to have the same name as the class 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 no parameters are given to the construtor for example. So where we have new AForm()
we are calling public AForm
and this sets the text of our form.
So thats good but how do we get things in the box? Well we use a Graphics object. To make sure the text we write stays in the box we have to write it again 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 corrent one and overright it. To find the function we can use the MSDN, the object browser of ahandy reference book. The function we intend to override is called OnPaint
Makes sense yeah? Good. Eveytime there is a message saying redraw the Window, OnPaint 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
. Then lets us tell our code file that we intend to use it! So 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 Windows", 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". Its 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 this function the we override.
protected override void OnPaint(PaintEventArgs pea)
{
pea.Graphics.DrawString("Hello Windows", this.Font, Brushes.Black, 0,0);
}
The protected
here 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 put our own code into it. Then the return type and name of the function we are overriding; void OnPaint
. Pretty simple. The function takes in a PaintEventArgs, so we have to include that in the parameters for convience we call it pea
.
For this Paint argument 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., then we need a brush this can be dashes and different colours and types but we can get a default brush using the Enumeration Brushes
avaliable in System.Drawing. We are going to choose plain old black here. 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.
Moving On
Hooking Up Direct3d
DirectX usually to be downloaded from the MSDN library, we're looking for Managed DirectX. Then we need to add Direct X as resources. (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) 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 of space on the plane of a window can be addressed using a 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).
The system could be considered to be slightly strange as increasing the value of Y causes the point to descend. So one must bare this in mind. Especially since you will be using a graphics library with it's own coordinate system, and if you do not bare in mind which you wish to use at a given moment then lots of headaches can ensue.
In the above image the window would be draw 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 gives some examples including the drawing of a triangle, but the explanation tends to be a bit lacking. The tutorials are generally meant for those who already know what they are doing, to get up to speed.
A triangle is made up of three points (generally called Vertices in the computer graphics world) joined together with three lines. Before this can be done though - a degree of preperation and setup must be done (doesn't is 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 examplet to use as little as possible. Therefore windows adopts event based programming.
All application 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 :(
Back to the Code, getting it in a nice loop
Here's the reformed code, to allow us to more easily write games. 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) then allow the operating system to handle any messages it recieves. Upon running this code, you will find it works just like before but 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 contiunally faster than we can see, so 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. Lets 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.
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 arguement is an integer, this allows you to choose which adapter
that 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 and a big bag of different options for how it will work - 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 disgard 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 a device is exampleted to be created, if this fails for whatever reason (such as no hardware supoprt) - then the an 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 their 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 blue colour and the window, DirectX and the graphics card are all fuzed togetther in a big ball of game making potential.
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 with 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.DeviceCreated += new System.EventHandler(this.OnCreateDevice);
DeviceCreated is one of those delegates and we add an event handler - 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 create we call a function callede OnCreateDevice, a function we write. (OnCreateDevice arguments are defined by System.EventHandler - we can't just thow anything in there.)
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 created? - Set it up so that is may provide us with triangle action!
public void OnCreateDevice(object sender, EventArgs e)
{
}
The object that is sending in this case is the Device, remember how we hooked the above function up to the Device object, well we get a reference to it. All that is required is that we case Sender to a Device object and then we're good to go. The arguments above are standard and defined by that new System.EventHandler
bit.
Remember how triangles where made out of 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. DirecTX gives us the object we need we just need to learn the constructors and it's use. We will define a global VertexBuffer
, here we call is vertexBuffer then construct a vertex buffer using the below code:
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 '3' that's how the number of vertices we're going to use. '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 TransformedColored.Transformed means the triangle need not be moved or rotated, it is to be specified using screen co-ordinates. It also includes a color component for each point (vertex) in the triangle, which will allow objects to be coloured! Next two arguments are Vertex Number 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 how 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 graphic 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.OnCreateDevice);
this.OnCreateDevice(device, null);
}
catch (DirectXException e)
{
MessageBox.Show(null, "Error intializing graphics: " + e.Message, "Error");
Close();
}
}
public void OnCreateDevice(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);
//Begin the scene
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. Then the last function is actually drawing the vertices. We are going to draw a triangle the most suitable type for this is triangle list. Second parameter is the starting vertex from which we draw and last parameter is the number of primatives - 1. Now all that needs doing is filling the vertexbuffer!
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 vertices is 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. Then we write the array to the stream and unlock the VertexBuffer - it now has all the vertices in 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.DeviceCreated += new System.EventHandler(this.OnCreateDevice);
this.OnCreateDevice(device, null);
}
catch (DirectXException e)
{
MessageBox.Show(null, "Error intializing graphics: " + e.Message, "Error");
Close();
}
}
public void OnCreateDevice(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 takes the vertices from 3D space and then flatten them against the screen to create a 2D image.
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.
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.
Review
To get to triangle production took two stages - intializing the 'device'. One the device was created then we needed to create the triangle data, feed it to the device and get the device to display.
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
Resizing
If you exampleted 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 is automatically reset as well and must be recreated. So how do we fix this? Well we must add all the Vertex info into the delegate for when the vertexBuffer is recreated. So the vertexBuffer has it a creation function assigned to it:
public void OnCreateDevice(object sender, EventArgs e)
{
Device dev = (Device)sender;
vertexBuffer = new VertexBuffer(typeof(CustomVertex.TransformedColored), 3, dev, 0, CustomVertex.TransformedColored.Format, Pool.Default);
vertexBuffer.Created += new System.EventHandler(this.OnCreateVertexBuffer);
this.OnCreateVertexBuffer(vertexBuffer, null);
}
In this function all the vertexBuffer intialization is set up, like so:
public void OnCreateVertexBuffer(object sender, EventArgs e)
{
VertexBuffer vb = (VertexBuffer)sender;
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.
Alternative way of assigning vertices
This is an alternative function. It is more consise and there is less occurring between locking and unlocking. I'm not sure which is best or if any difference at all.
public void OnCreateVertexBuffer(object sender, EventArgs e)
{
VertexBuffer buffer = (VertexBuffer)sender;
CustomVertex.TransformedColored[] verts = new CustomVertex.TransformedColored[3];
verts[0].SetPosition(new Vector4(150,50,0.5f,1));
verts[0].Color = System.Drawing.Color.Aqua.ToArgb();
verts[1].SetPosition(new Vector4(250,250,0.5f,1));
verts[1].Color = System.Drawing.Color.Black.ToArgb();
verts[2].SetPosition(new Vector4(50,250,0.5f,1));
verts[2].Color = System.Drawing.Color.Purple.ToArgb();
buffer.SetData(verts, 0, LockFlags.None);
}
No comments:
Post a Comment