Thursday, February 17, 2005

Stage I - II: Tiles!

The last section introduced some C# windows programming and some Direct3D. Now you might think this section would be all about building boxes etc.(unless of course you read the title - observance bonus points for you!) But no, I want a tile based game because I want to create a tile based role playing game just like everyone else.

We're going to be basing this off the code we wrote previously - here's a quick link. Either continue the project that you created for the first tutorial or create a new blank C# solution. Then do add existing item and add the code from the quick link, then add all the relevant references - after that it should look something like this:

Image Hosted by ImageShack.us

Introduction - what are tiles?


Tiles are small squares that are given a texture and lined up in a grid-like fashion to make a pretty map. So we need to make squares instead of triangles, which are all we're manage to create up until now. We also need to load textures from the disk and then we need to apply the textures to the tiles. The code here will create two tiles each with a different texture - working from the assumption that once this is done it will be a simple matter to create more as necessary.

Drawing a tile, creating a square


A square needs 4 vertices - not the 3 we used for the triangle. There are a number of places we had '3' before that we now need to knock up to '4'. Note that this is the kind of hackish coding that is prime bug territory, really we should have some kind of variable that stores the number of vertices, rather than using a 'magic number' each time (that is a number that is not predefined anywhere instead it is merely added in, as is, throughout the code.)

In the OnResetDevice the line:

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


Should be changed to:

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


The buffer will now hold 4 vertices! A triangle has 3 corners, a square 4 - pretty elementary!

We have increased our buffer size to four elements. Currently though there is no code that says what the fourth element will be. No problem! Let's wander over to OnCreateVertexBuffer. First we must bump our array up to 4, so the code should now look like:

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

Unless, of course, you've been very sensible and created some kind of variable to store all this 4 business in.

The fourth vertex needs to be positioned in space so that, combined with the other 3 vertices, a square is formed (in a join-the-dots fashion). Let's position it now!.

public void OnCreateVertexBuffer(object sender, EventArgs e)
{
         VertexBuffer buffer = (VertexBuffer)sender;

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


         verts[0].Position =
new Vector4(150,50,0.5f,1);
         verts[0].Color = System.Drawing.Color.AntiqueWhite.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();

         verts[3].Position =
new Vector4(50,50,0.5f,1);
         verts[3].Color = System.Drawing.Color.Purple.ToArgb();


         buffer.SetData(verts, 0, LockFlags.None);
}


Try running it now! Oh no! It's the same old, boring triangle and not the expected exciting new square. But before you dash off into some heavy traffic, we must remember that we haven't fiddled with the render function! So lets have a look at this and recap.

Recap of the Render Function


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();
}


The problem is at the DrawPrimatives call. We are saying we have a list of triangles, each defined as three points. The we telll it that starting from vertex 0 it should draw exactly one triangle from the list. Let's have a quick mooch at the old diagram down here to see what's going on.

TriangleList

As you can see a Triangle List creates triangles from three points. So if we wanted to create a tile we could combine two triangles and make them look like a square - but this would take six vertices and we should only need 4! If we only have four points we cannot use TriangleList to draw two triangles. It requires 3 points per triangle so it would draw the first triangle okay but when it comes to draw the second triangle it will only have one vertex left! How can we solve this? By using PrimitiveType.TriangleFan instead.

Use this code:

device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);

This says draw two triangles from four points. When we run the code we get something like this:

Nearer...

All we have to do to make the square is alter the position of the vertices.

verts[0].SetPosition(new Vector4(250,50,0,1));

That change should do it! But how does it work?

Fans of triangles

Well once one triangle is draw another can be created by introducing another vertex. In this case we place the next vertex in such a way, that the two triangles that are created, when viewed together form a nice square - perfect starting place for a tile. In 3D graphics for games nearly everything comes down to triangles, as this is what graphics cards have been made to process.

Beautification



Of course very few games have a single, big, off center, purplish triangle as the basis of a game, generally the tiles line up from the top corner and you cannot even tell that they are tiles. So just to makes sure we're not fooling ourselves let's try that.

First let's work on a uniform colour, it's very nice to colour each vertex differently but the mixing of colours is currently a bit distracting. How about making our tile green? Like grass - genius! So this is a pretty simple change, all that is required is to go to the OnCreateVertexBuffer function and change all the colour assignments so they read:

Color = System.Drawing.Color.Green.ToArgb();

Vertex[3], the forth vertex, is the top left vertex of the tile. We wish to move it so that it's in the top-left corner of the window. In the OnCreateVertexBuffer we set it's X and Y values to 0 - this puts it in the top corner. Then vertex[2] should have it's x value set to 0 and vertex[0] should have it's y value set to zero. Now the green tile is hugging the sides of our application window, this is good but it makes a rather massive tile. On many old style rpg's tiles where of pixel size 32 by 32 (powers of two are faster). That sounds good so lets change create a tile of 32x32 position in the op left corner of our window. This code is the result:

public void OnCreateVertexBuffer(object sender, EventArgs e)
{
            VertexBuffer buffer = (VertexBuffer)sender;

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

         verts[0].Position =
new Vector4(32,0,0,1);
         verts[0].Color = System.Drawing.Color.Green.ToArgb();

         verts[1].Position =
new Vector4(32,32,0.5f,1);
         verts[1].Color = System.Drawing.Color.Green.ToArgb();

         verts[2].Position =
new Vector4(0,32,0.5f,1);
         verts[2].Color = System.Drawing.Color.Green.ToArgb();

         verts[3].Position =
new Vector4(0,0,0.5f,1);
         verts[3].Color = System.Drawing.Color.Green.ToArgb();

         buffer.SetData(verts, 0, LockFlags.None);
}

This looks vaguely game like..

So imagine we wanted to put a tile next to that. We'd have to call DrawPrimatives for the first tile, then for the second we'd need to unlock the vertex buffer and then move the tile over to the left by 32 pixels. If we had a great number of tiles, this would mean a lot of locking and unlocking. These calls will mount up quickly and bog down our code.

Trouble in Paradise (or is it?)


The current coordinate system isn't going to hold up, for the following reasons.

  • The vertex buffer isn't just defining a tile's size it's defining it's position.

  • Each tile would needs it's own Vertex Buffer -> Waste and Memory Explosion

  • Or we would have to continually lock and unlock the VertexBuffer and fiddle with the insides (making our program very slow)

For a small game these problems aren't too bad but they are going to become a problem as more stuff is added. We want to be able to place tiles anywhere without altering the vertices each time. This introduces us to the wonderful world of Transformations and Viewports.

Remember vertices?

Vertices are a data structure that represent the points from which meshes are made. Now if you recall we chose a specific type that let us use screen co-ordinates for X,Y and Z (not really for Z :)). There where other vertices we could have used and we're going to have a look at these one's as alternatives.

Transformations and Viewports


A scene can be thought to be viewed through a virtual video camera. To push the cameras buttons - transfomations are used. There are three transformations :- world, view and projection. The view port is the two dimensional screen, the 3D world needs to be mapped to that screen.

  • World Transformation
    Allows an object to move in 3D, so we can place tiles without fiddling with the vertices! As many tiles as we want all in different places

  • View Transformation
    Controls the Position and Orientation of the viewer of the scene. For instance if we wanted to turn the world upside down, we could do it with this.

  • The Projection Transformation
    Allows control of field of view. (like controlling the cameras lens)

These are all very exciting but the one we're most interested in at the moment is world transformation. Let's set up our code so we can take advantage of it

Taking Advantage

We need to change the type of vertex we are using - we don't want to use transformed any more we want to use position vertices. That is vertices who have their position defined in 3D space rather than in windows coordinates. Now to do this we need to change where we define the coordinates.

In the procedure OnResetDevice(object sender, EventArgs e) we need to change the vertex type that we told DirectX we'd be using. We're going to choose PositionColored.

Changing Vertices


Okay now we need to change OnCreateVertexBuffer(object sender, EventArgs e) so the line:

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

reads:

CustomVertex.PositionColored[] verts =
            new CustomVertex.PositionColored[4];

Now if you attempt to compile you will get a number of errors along the lines of:
 Argument '1': cannot convert from 'Microsoft.DirectX.Vector4' to 'Microsoft.DirectX.Vector3'

The new Vertex format does not require us to enter a Rhw value, so we need to take all those values out and make sure we're creating a vector with 3 arguments rather than 4. That's no problem:

verts[0].Position = new Vector3(32,0,0.5f);
verts[1].Position =
new Vector3(32, 32, 0.5f);
verts[2].Position =
new Vector3(0, 32, 0.5f);
verts[3].Position =
new Vector3(0, 0, 0.5f);

Once that's done there's one last change in the render function. In the render function we change the line

device.VertexFormat = CustomVertex.TransformedColored.Format;

to

device.VertexFormat = CustomVertex.PositionColored.Format;

Then we run it ... but oh no! everything has dissapeared :(. Why? Why is the world mocking us again? Well we've just changed co-ordinate systems but we haven't changed co-ordinates.

Do you remember the windows coordinate system? Increasing Y means Y moves further down the screen? Well now we've changed this is no longer the case - we've doing it Cartesian style. Y needs to be decreased to move down the screen. Therefore we can fix our problem sharpish by putting all the Y's to minus.

verts[0].Position = new Vector3(32, 0, 0.5f);
verts[1].Position =
new Vector3(32, -32, 0.5f);
verts[2].Position =
new Vector3(0, -32, 0.5f);
verts[3].Position =
new Vector3(0,0,0.5f);

Very Black Green ...

There are a few things that spring out immediately. First that Green is a rather dark shade, in fact, it's black! Secondly our nicely placed and scaled tile is now rather big and in a different place. Let's consult an earlier diagram:

An earlier diagram

So what's the top corner? That would be X = -1, Y = 1. That's right! In the middle it's 0,0 and it's 1 unit to each edge. So let's scale our tile here and make all those 32's equal to 0.32. The screen itself is a 2 by 2 plane. (from X = -1 to 1 and from Y = -1 to 1). So we're got (0.32x0.32)% of that as the size of the tiles. This size is arbitary, it has no meaning and will change with the size of the window - try strectching and pulling the window to see what I mean.

verts[0].Position = new Vector3(0.32f, 0, 0.5f);
verts[1].Position =
new Vector3(0.32f, -0.32f, 0.5f);
verts[2].Position =
new Vector3(0, -0.32f, 0.5f);
verts[3].Position =
new Vector3(0, 0, 0.5f);

Note we need to type 0.32f to say it's a float rather than double.

Smaller

So like before we wish to get this tile up to the the top corner ... we don't need to change anything in the Vertex Buffer -> this was the whole point of changing coordinate systems. So let's look into the render function and see what we can do!

Moving dem there tiles

How do we move these tiles? We transform the world. So we move our 3D space draw our primatives there and then it's moved back and we can see our model in a different place. Okay so let's see this in action.

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

         Matrix QuadMatrix =
new Matrix();
         QuadMatrix = Matrix.Identity;
         QuadMatrix.Translate(-0.32f, 0.32f, 0f);

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

         device.BeginScene();

         device.SetStreamSource( 0, vertexBuffer, 0);
         device.VertexFormat = CustomVertex.PositionColored.Format;
         device.SetTransform(TransformType.World, QuadMatrix);
         device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);

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


Here we create a matrix then we set it to the identity matrix. The identity matrix is a bit like the number one for matrices. If you multiply by the identity then nothing happens.

Thee next line of code we translate the matrix. We tell it we want to go -32 on the X axis and +32 on the Y. So we have a matrix with these instructions (it is an encoding of the transformation that we wish to apply) - we then transform the world according to the matrix and then draw the primatives. That allows us to move the tile and draw it where we want. We can also draw more than one tile - let's see. Make these changes in the render function:

device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);
QuadMatrix.Translate(-0.64f, 0.64f, 0f);
device.SetTransform(TransformType.World, QuadMatrix);
device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);


Just by making a simple change to the code, doubling the commands and doubling some values - we can draw two tiles in a diagonal line, which should look like the image below.

Two Tiles

Drawing a tile in the top corner is very simple - we just translate the world to -1,1. So our first translation of Quad matrix looks like:

QuadMatrix.Translate(-1f, 1f, 0f);

Now we can plot the entire screen with tiles really easily. Just call translate, then draw, then increase X,Y as necessary by the length of width of the tiles (which is stored in the vertex buffer) and then repeat until we've covered the screen.

Light!

Time to address a question I'm sure you've been asking. Why is everything black? Because we're in 3D space now and everything is expected to be lit! Lighting is fun and I'm sure we could make some groovy effects with it but for your average flat-world-tile game it's hassle with a captial H. So lets get rid of it for now - it can always be switched back on if it's groovyness is required.

All we need do is alter OnResetDevice like so:

public void OnResetDevice(object sender, EventArgs e)
{
         Device dev = (Device)sender;
         dev.RenderState.Lighting =
false;

         vertexBuffer =
new VertexBuffer(
            typeof(CustomVertex.PositionColored),
            4,
            device,
            0,
             CustomVertex.TransformedColored.Format,
            Pool.Default);

         vertexBuffer.Created +=
new System.EventHandler(this.OnCreateVertexBuffer);
            this.OnCreateVertexBuffer(vertexBuffer, null);
}


And look! We have our green tiles and we can place them in the top corner - we surely, have now conquered 3D graphics for 2D games! We'll wiz through textures, a bit of tiling and then we'll be on to actual game architecture.


Green Tiles; correctly placed, well at least one of them is.

Throwing on a bit of texture

If you recall somewhere I blathered on a little about how Vertices may have T and U values. These are values associated with textures - "how?", you might ask. Well, we all know vertices are points in 3D space, well T,U represent a given vertices point on a texture map.

If you have three vertices and your texture map was a bitmap then using the T,U values - a triangle could be cut from the texture and mapped to the polygon. Clever! There's a little more to it - for instance to get a tiling texture your T,U values are general larger than the bounds of the texture but we need not worry about that for now.

Let's choose some textures

I made these textures in Photoshop a while back, I cannot remember if they tile but this does not matter because these are the tiles we are going to use! You are free to use these tiles in which ever ventures you wish.


Grass Texture
Stone Texture


Download icon.[Grass Bitmap]
Download icon.[Stone Bitmap]


So lets put these textures on the root of C: so we can reference them easily as C:\Stone.bmp and C:\Grass.bmp. Or if we put them in the same directory that our .exe file will be created in then they can just be Grass.bmp or Stone.bmp. (To do this put them in the bin\debug directory of your VS.net project. It's probably a good idea, keeps everything clean).

To hold these images we'll need some kind of texture datastructure, thankfully provided for us by DirectX - Yay!

public class attempt : Form
{
         Device device =
null;
         VertexBuffer vertexBuffer =
null;
         Texture GrassTexture;
         Texture StoneTexture;

...


The Texture data structure is defined in Direct3D and we'll be using it! Notice how Grass and Stone have the same number of characters for maximum layout pleasure. At this point we could create some kind of general purpose texture loader taking in a bitmap string and some numbers about what to cut out - but for now we're going to do a simple hack function that we can come back to later.

Before we create this function though, let's set the vertices up so that they are ready to handle textures. Let's do this the quick way this time and do a find and replace Edit>Find And Replace>Replace or Control-H for those keyboard fanciers out there. This of course gives us the old T,V coordinates so we should set them while we're at it and remove our colour assignments.


Change our vertex type


The vertex at the 0 position in our array will be the top right hand corner of our tile. Therefore we set the texture out with that in mind. Remember that U represents the X axis of the texture and V the Y. The code in our OnCreateVertexBuffer will look a little like this:

verts[0].Tu = 1.0f;
verts[0].Tv = 0.0f;
// Top Right Hand Corner
verts[3].Tu = 0.0f;
verts[3].Tv = 0.0f;
verts[1].Tu = 1.0f;
verts[1].Tv = 1.0f;
verts[2].Tu = 0.0f;
verts[2].Tv = 1.0f;

// Comment out or remove depending on how clean you like your code :)
//verts[0].Color = System.Drawing.Color.White.ToArgb();
//verts[1].Color = System.Drawing.Color.White.ToArgb();
//verts[2].Color = System.Drawing.Color.White.ToArgb();
//verts[3].Color = System.Drawing.Color.White.ToArgb();

From Bitmap to texture

Okay, now to our hackish function to get our bitmap into our texture structures.

public void LoadTextures()
{
         try
         {
            System.Drawing.Bitmap grass = (System.Drawing.Bitmap)
               System.Drawing.Bitmap.FromFile(@"C:\Grass.bmp");

            System.Drawing.Bitmap stone = (System.Drawing.Bitmap)
               System.Drawing.Bitmap.FromFile(@"C:\Stone.bmp");

            GrassTexture = Texture.FromBitmap(device, grass, 0, Pool.Managed);
            StoneTexture = Texture.FromBitmap(device, stone, 0, Pool.Managed);
         }
         catch(Exception e)
         {
            MessageBox.Show(
this,
            "There has been an error loading the textures:" +
            e.ToString());
        }
}


We do a try and catch so that errors are caught gracefully :). If your not familiar with use of '@' it just means write this next bit out with out picking up any formating information - write it out as is! So we load a bitmap - using prebuilt functions it's not hard at all. The we store them in the relevant texture - this requires the use of the global device variable. Then our textures are ready to use!

The remaining question is where we should call this function from. It is callable as soon as the device is created. But if we resize the window - we lose the textures! For now the code is entered in the InitializeGraphics() function. (we deal with losing the texture problems towards the end)

 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);

            LoadTextures();

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

            OnResetDevice(device,
null);


Using the textures



To use the textures we use the device.SetTexture() function. Let's have a look at the entire render function in it's full beauty.

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

         Matrix QuadMatrix =
new Matrix();
         QuadMatrix = Matrix.Identity;
         device.Clear(ClearFlags.Target, System.Drawing.Color.Blue, 1.0f, 0);

         device.BeginScene();

         device.SetStreamSource( 0, vertexBuffer, 0);
         device.VertexFormat = CustomVertex.PositionTextured.Format;

         QuadMatrix.Translate(-1f,1f, 0f);
         device.SetTransform(TransformType.World, QuadMatrix);

         device.SetTexture(0, GrassTexture);
         device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);

         QuadMatrix.Translate((-1 + 0.32f),1f, 0f);
         device.SetTransform(TransformType.World, QuadMatrix);

         device.SetTexture(0, StoneTexture);
         device.DrawPrimitives(PrimitiveType.TriangleFan, 0, 2);

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


And Voila


There we go, textured quads on the screen that we can move at will, and colour different colours! Yay! After this we don't have to do too much thinking about 3D graphics, we've brought it down to something simpler and more intuitive.

Download icon.[Textured Tiles Source Code]

(Note: To run this code both stone.bmp and grass.bmp must be in the root of your C: drive!)

Random Notes

Perspective

The current way we're set up is as one would expect - things get smaller as they move away from the camera (along the Z-axis). This may not be totally suitable for a tile based game we may want to use the Z axis to represent our layers of tiles but don't want the distortion - in this case we should investigate Orthagonal Projection.

What needs to be done

    I think we need to:
  • Load standard bitmap collections of tiles that go together of some decided size. (Like a collection of 32x32 grass type tiles in one big 256x256 bitmap)

  • Divide the bitmaps into textures equal to the number of tiles. (make sure the bitmaps have the tiles in them fitting perfectly so they can be easily extracted)

Why?

We wish to use reasonably large bitmaps because we don't want to keep accessing the disk. Then break up into many tiles so they can be applied during run time without needing to change U,V values and constantly locking, unlocking buffers. (There are other ways to appoach this which I tackle soonish)

References

Post a Comment