Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

C# managed DirectX HexEngine - Part I

0.00/5 (No votes)
2 Sep 2003 1  
A simple Hex engine using DirectX

Sample Image - hexengine.jpg

Introduction

I wanted to write a simple strategy game and went looking for a 2D isometric engine , but most were very slow (mainly due to using GDI+) so I thought I would write my own using managed DirectX. The only one I found on line was a VB isometric engine but I wanted to use hexes. The results are pretty good so far , by using DirectX the program gives excellent performance and allows high speed scrolling, zooming etc.

Background

Why use hexagons? For a start board games have been using them for a long time yet the printer can deliver perfect quality. The reason is gameplay - hexes have the advantage of 6 directions without having to deal with the diagonal issues you can get from isometric tiles, in addition, when compared to pure 3D landscapes hexes have the advantage that they easily convey ownership allowing you to give areas properties etc.

The main disadvantage of hexes is performance but games that emphasise gameplay and strategy dont usually have high performance . Still the perfomance is quite good using my crappy TNT32 card which only supports software vertex processing.

In Part II I will post how to use heights with hexes , I have not seen this done but it is certainly possible and we can put in vertical cliffs which many 3D terrain engines can not handle.

Features

The engine supports the following features.

  • Picking - by clicking on a hex the program can locate which hex was clicked on.
  • Lighting - the L key toggles a direct light on the scene.
  • Hex Borders - the G key toggles showing hex outlines
  • Centre - The C key centres the display to the middle of the map
  • Zoom in and out with bounds checking to ensure the user does not go through the map or too far away.
  • Scrolling with repeat key and bounds checking to quickly move around the map. The repeat key has a build in acceleraror.
  • Multiple textures
  • Wireframe Solid toggle - Use W to toggle between these modes.
  • Support for dynamic resizeing , alt -tab , full screen ( alt - enter) , windowed mode and reduced CPU in background .

Using the code

Before we start I need to give a disclaimer I am no DIrectX guru, so any tips would be appreciated. Second this is all pretty rushed so go easy on the grammar, spelling and poor comments in the code.

The code is pretty easy to use but will require some work to bolt into your progam and the textures are hard coded at present. Basically you create the hexes and pass them to the HexGrid which keeps track of them. You will need to add aditional rendering phases for clouds, moving light sources (sun), roads, villages etc . I may add some of these in later parts.

The code requires Managed DirectX 9 and the .NET framework installed.

The classes used:

HexEngine Controls the rendering. It based on Microsofts DirectX samples so is bolted to the form. It does have the advantage of handling things like Alt-Tab , Full screen - windowed mode, resizing and global exception handling etc . It also controls the keyboard and mouse movements.

Hexagon These are the base units , you can modify this class directly or use inheritance to create your game hexes. At present the only functionality the engine uses is the texture ID.

HexGrid This keeps track of the Hexagons as an array. The class calculates the vertex and index buffers as well as storing some convenient properties of hexagons.

Points of Interest

When I started I first wrote HexGrid based on the excellent article "Coordinates in Hexagon-Based Tile Maps" by Thomas Jahn 28 Feb ,2002 on Gamedev.net. This was good starting point for the basic mathematics and coordinates.

The first big 3D issue was how to handle Vertexes , Texture coordinates and Indices. Since each hex corner was going to border 2 other hexes each of which could have a different texture. By default a vertex can only hold one texture coordinate. Looking futher into things multiple texture coordinates are possible but these have some limitations.

  • My card which is the most common card only supports 2. With 2 vertexes the extra complexity of the calculating which vertex needs to render is not worth the benefit.
  • Adding 3 texture coordinates to each vertex would add 6 floats more than doubling their size anyway.

So I had a rough choice to make but mainly due to the vid card I decided that each hex would have its own vertices. This meant there were nearly 3 times as many! This vertex structure though made it simpler to render, saved me buying a new vid card (since my HD died and I needed a new HD..), and resulted in smaller vertices (5 or 6 floats instead of 11 -12) partially offsetting the additional numbers. I could then easily calculate index buffers for each terrain and leave the vertes buffer untouched. There was also the minor benefit of having vertical cliffs and hexes could be raised if I wanted to do some terrain.

Calculating the vertex was pretty simple as can be seen below, for each hex there are 6 vertexes. The texture coordinates uv and uw matched the xy of the vertex. yvalue represents the height and is left as a plane at present.

public void GenerateVertexBufferData(VertexBuffer source)
{
    const float yValue = 0f;
    VertexBuffer vb = source;

    // Create a vertex buffer (100 customervertex) and lock it

    CustomVertex.PositionNormalTextured[] verts = 
        (CustomVertex.PositionNormalTextured[])vb.Lock(0,0);

    for (int y = 0; y < this.Y; y++)
        for (int x = 0; x < this.X; x++)
        {
            //Hexagon hex = this.hexagons[x,y];

            PointF topLeft= this.GetTopLeftPixelBoundingRectangle(x, y);

            // vertex 0

            verts[6*(x+ y*this.X)].SetPosition(new Vector3(topLeft.X, yValue,
                topLeft.Y + this.H));
            verts[6*(x+ y*this.X)].Tu = 0.0f;
            verts[6*(x+ y*this.X)].Tv = (float) this.H / this.height;

            // vertex 1 top 

            verts[6*(x+ y*this.X)+1].SetPosition(new Vector3(topLeft.X + 
                this.Radius, yValue, topLeft.Y));
            verts[6*(x+ y*this.X)+1].Tu = 0.5f;
            verts[6*(x+ y*this.X)+1].Tv = 0.0f; 

            // vertex 2

            verts[6*(x+ y*this.X)+2].SetPosition(new Vector3( topLeft.X +
                this.width, yValue, topLeft.Y + this.H ));
            verts[6*(x+ y*this.X)+2].Tu = 1.0f;
            verts[6*(x+ y*this.X)+2].Tv = (float) this.H/ this.height; 

            // vertex 3

            verts[6*(x+ y*this.X)+3].SetPosition(new Vector3( topLeft.X +
                this.width, yValue, topLeft.Y + this.H + this.side ));
            verts[6*(x+ y*this.X)+3].Tu = 1.0f;
            verts[6*(x+ y*this.X)+3].Tv = (float) (this.H + this.side) /
                this.height; 

            // vertex 4 bottom 

            verts[6*(x+ y*this.X)+4].SetPosition(new Vector3( topLeft.X +
                this.Radius, yValue, topLeft.Y + this.height ));
            verts[6*(x+ y*this.X)+4].Tu = 0.5f;
            verts[6*(x+ y*this.X)+4].Tv = 1.0f ; 

            // vertex 5

            verts[6*(x+ y*this.X)+5].SetPosition(new Vector3( topLeft.X,
                yValue, topLeft.Y + this.H + this.side ));
            verts[6*(x+ y*this.X)+5].Tu = 0.0f;
            verts[6*(x+ y*this.X)+5].Tv = (float) (this.H + this.side) /
                this.height; 
        }

    vb.Unlock();
}

I then had to consider how to index the vertexes. At first I used triangle fans as most doco says these are for things like HEXAGONS... That was a big mistake. Triangle fans are good for one hexagon. If I was to render 2500 (50*50) it would result in 2500 Draw calls to the device.. Ouch . Also a single fan was not possible as they were all drawn from one point. To change the structure all I had to do was update the indexes so I benchmarked all the options and at low hex counts it was all pretty even. At high hex counts triangle strips were the best by 20-50%, the second best was triangle lists. So I decided on triangle strips. The next issue was how to combine multiple hexes into one strip. The answer was to use a degenerative triangle at the end of each hex; this was done by repeating the last vertex. This means the last triangle of the current hexagon had 2 vertexes the same (eg had no area, but you can see them with the default wireframe) and the first triangle of the next hexagon. This allowed me to send all hexes of one terrain type in one big index and making big chunky calls has to be good. The second issue was culling; working out the order of the strip so it complied with culling and which allowed an efficient strip , I ended up with a strip which was 7 vertexes per hex which was pretty good I think.

I then decided on a project space and decided on perspective so I could zoom in and out and I wanted a top down view. This proved a problem since whenever I aligned the camera and the viewing point exactly the image would not display at all! I left it as a little offset and hoped the math rounding of such a small difference would not get me into trouble.

After much fiddling the display seemed to come together . The next hurdle was picking, eg clicking on part of the screen and showing the coordinates. I tried playing around with unproject but did not have a lot of luck (mainly due to a bug in another part but the complexity hid it) . So I went for a simpler method , I used project to calculate where the top left and bottom right corners of HexGrid would project and then scaled.

The last bit to get going was scrolling . I wanted to use repeat keys and the only way to get this happening was ugly if anyone else knows a cleaner way I would appreciate it . The repeatkey method usies a char conversion but I can't find a char for the arrows so I hook the call before and trap the arrow key which then sends a rarely used ascii sequence. The second issue was with zooms. I thought an accelerator would be useful which caused a slightly more complex Scroll method but which works quite well now its finished.

Outstanding Issues

  • Size limit to a little above 50*50 hexes. This will not change as you are looking at 20,000 vertices in an index buffer. Since index buffer are shorts you are very limited here. To fix this try these
      • one way is to render multiple hexgrids
      • Merge hexes as the user zooms out.
      • The HexGrid can be recalculate (fairly cheap) as the user scrolls every 10 hexes to a new centre.
  • Comments - These are few and not very well done. At least C# is pretty easy to read.
  • Code structure is pretty poor. keyboard events should be moved to a different class. Textures should be passed in etc
  • Code checking - there is very little..
  • Hex borders - we are using a trick to render the hex borders but this is apperent at the edges.
  • Lighting shows the hexagon edges as white . I tried a few combos and they all caused the edge lines to dissapear until I found one which highlighted the edges.
  • Mouse scrolling - The scroll method already exists so it is a no brainer. Just short of time....

History

v00 posted 22/8/2003

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here