Home       About Me       Contact       Downloads       Part 2    Part 4   

Part 3: Good Old 2D Graphics

December 8, 2010

Fig 1: Needs more junk
needs more junk

If you've been downloading the demo, you know the current view is just the world, looking like Figure 1. It's obviously different from a commercial game somehow... I know! There's no health, mana or weapons status, no target selected, no mini-map, no quest text, no chat text, no "entering combat" messages, no open bags or spell palettes or help windows ... no junk all over the screen! (see Figure 2) To add that stuff, we need 2D graphics.

Fig 2: There's a world under there somewhere
Wow screen

We'll also be using 2D graphics later for things like name tags on the avatars, word balloons when they speak, and signs in the world.

The DirectX (or OpenGL) graphics support wants to texture images onto triangles to make up the 3D scene. I don't see any obvious interfaces in DirectX for drawing onto the screen buffer after the 3D image has been rendered. So instead, we create an overlay texture and draw a large rectangle covering the entire screen with that texture. By turning off all the transform matricies, we can get a 1-1 mapping between screen pixels and texture pictures. This is the same way the cursor is implemented.

Back the first time I did any 3D programming, textures had to have dimensions which were a power of two. For this use of texture, that would be a problem. If your screen is 1920 by 1080, the next biggest power of two is 2048 by 2048. This is going to waste nearly half of the texture data vertically. Looking through the OpenGL documentation, it advises you to stick to powers of two, but does not require it. DirectX allows arbitrary texture sizes up to a device-defined limit (8192 on my machine), and doesn't say much of anything about performance other than that you should keep textures small (it says 256 by 256 is optimum.)

So perhaps we could create a 1920 by 1080 texture and it would work fine. I'm from the stone age though, when a megabyte was enough memory for a mainframe computer that had 100 terminals connected to it. I don't know if I can bring myself to allocate even 1920 by 1080 times 4 bytes per pixel = 8,294,400 bytes in a single chunk! Even if I did, it's kind of a waste. Most of the time, most of the overlay will be transparent, and we don't need a texture full of zeros. And even though nearly every graphics card out there has at least 256 megabytes of memory on it, I'm not sure I want to spend 8 megabytes of it in a single chunk for the overlay graphics. So I'm splitting it up into 256 by 256 tiles. As an optimization, I could only allocate a tile when I need it for graphics.

Fig 3: We need transparency
WoW transparent

There's another issue which is a real nuisance. We have to get the 2D graphics as a texture from somewhere. Up to now, we've been using images stored in JPG or GIF files. That would work for mostly-static elements, like health indicators, but to do the full range of overlays you see in Figure 2, you need to construct it in code from 2D graphics primitives. The chat text for example needs arbitrary text. You can't do that with any combination of canned images.

Windows obviously has the capability to do this. We can create a bitmap, create a device context over it, and draw lines, text, images, etc. on the context. When we're done, we pull all the bytes out of the bitmap and there's our texture image.

Except that this will be a 24-bit image, with red, green and blue planes. It won't have the fourth "alpha" plane which indicates transparency. Looking at Figure 3, you can see we want transparency all over the place -- between and around the spell icons, and in the chat text. We can't just fill in the alpha plane with a fixed value either. A 50% of black would show as a translucent black rectangle. We need alpha values to run from 0 (completely transparent) to 255 (completely opaque) around the graphics we are drawing. Standard Windows GDI graphics isn't going to do this.

There's a newer GDI+ interface, which does understand alpha blending, but not the way we want. It will blend lines and text and images together to make a nicer RGB image, not set the alpha plane we need for the 3D texture.

There's an even newer Direct2D interface in Windows 7 that would do everything we want, but I'm not going to use it. For one, it would restrict the demo to Windows 7 users only. For another, it's.... disgusting. This is from their "Getting Started with Direct2D" document:

Fig 4: My fingers hurt just from reading this
// Create a gray brush.
HRESULT hr = m_pRenderTarget->CreateSolidColorBrush(
               D2D1::ColorF(D2D1::ColorF::LightSlateGray),
               &m_pLightSlateGrayBrush
             );

D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();

// Draw a grid background.
int width = static_cast<int>(rtSize.width);
int height = static_cast<int>(rtSize.height);

for (int x = 0; x < width; x += 10)
{
  m_pRenderTarget->DrawLine(
    D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
    D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
    m_pLightSlateGrayBrush,
    0.5f
   );
}

Microsoft, are you really forcing all of your programmers to write "static_cast<int>(A)" instead of "(int) A" now? Did you really have to name the point class "D2D1::Point2F"? Reading through the documentation, I'm seeing names like "ID2D1RoundedRectangleGeometry" Are you trying to drive us all insane? We have to type these names a hundred times, you know!

Fig 5: Kerning
kerning
Another problem with Direct2D is that they seem to have taken yet another whack at a text-drawing engine. It has its own name now, "DirectWrite". I think what has happened here is that Microsoft hired some typographers and asked them how to draw the most beautiful text possible. It starts with simple things like "kerning" (see Fig 5), where you should draw the "e" in "Testing" under the bar of the "T", which you would not do with the "h" in "Thing." It gets more and more complicated from there. With newer text engines, they are rendering the letters with "subpixel resolution", so that all kinds of antialiasing is done to make text prettier.

The thing is, when you have finished implementing all of this, you have a very complicated text rendering engine. I just want to draw a string on the screen. Before I do that, I want the interface to tell me how much space that string will take up. When the user clicks on that string, I want to place a text cursor where he clicked. To do that, I need to know where to draw that cursor between characters. Back in the old days when a character had a width and that was all you needed to know, these were simple questions. In the new world, where the text engine is fussing over character placement, these are complex questions.

So Microsoft has built this text engine and allowed you to customize it in various ways. The engine is in control though, and if there's no option to do what you want to do, you are stuck. I haven't read far enough into it to tell if you can even write something simple like a text label which sets its own size, or a text input area that manages its own cursor. The entire DirectWrite system will be useless to me if you can't. It might seem unlikely that Microsoft would do this to programmers, but I had similar problems with the GDI+ interface that I could never figure out.

The Bottom Line

The bottom line here is that I implemented my own RGB+alpha drawing engine, by allocating two bitmaps and doing all the graphics twice. The first time, the opaque graphics are drawn in the RGB bitmap. Then the color is set to a gray corresponding to the alpha value we want (from black for transparent to white for opaque) and the graphics are drawn again in the alpha bitmap. I pull the bytes from both bitmaps, combine them into a single 4-plane texture, and we're ready to use it on the screen.

This works, but it's slow and uses twice the memory it should, and I hate it all. One of these days, I'll reimplement it. For now, it's good enough to do the overlay graphics we need.

New Features

Now that we have 2D graphics, it's time to add some new features. In order of implementation, they are 1-line alert messages that pop up and then fade away (Figure 6), a help window (Figure 7), and a palette of block types (Figure 8).

Fig 6: Useful information!
alert messages
Fig 7: More help than Minecraft
help window
Fig 8: Pick a block, any block
block palette

At the end of the last part, we had a selection rectangle on the block you were pointing at. The only thing I needed to allow you to add and delete blocks was this palette. So in the new demo, you can change the landscape. Whee...!

Select the type of block you want to add with number keys 1-9, then click the right button. Press and hold the left button to delete a block.

You can also add new block types (see "File Formats" below), so you should be able to have some fun with this demo. Take a screenshot (F2) and post a link to it in the comments if you do something you'd like to share.

You can save the world with F4. There are no "world slots" as in Minecraft. Just save your docs/world.txt file if you get a version you want to keep. There's a defaultWorld.txt file there if you need to restore the original version.

A User Interface Issue

The demo is complicated enough to have an actual UI decision to make now. In MMO mode, where you look from behind the avatar, we are already using the mouse for two things. We use it to move (both buttons down), and to turn the camera (drag with either button down.)

In Minecraft, the add and delete block functions are also on mouse buttons, but it feels a bit overloaded in the demo. You can start deleting a block, then shift the camera by moving the mouse. That shifts the selected block and you could start deleting something else.

In WoW, my reference MMO, the mouse is not used to act on things, only to select them. Once you've selected your target, you use spells off the bars on the screen or key presses to act on the target. I've done the same in the demo. When in MMO mode, you add blocks with the insert key, and delete blocks by holding the delete key. Let me know what you think.

In "shooter" mode (no avatar), it works like Minecraft, off the mouse buttons.

File Formats

As you write your huge game or other program, eventually you are going to have to decide how to store things. You have two questions to answer: 1) how performance critical is this? And 2) Who is going to use this file?

The answer to question #1 depends on the use of the file, not what's in it. A piece of data we load only once at the beginning is not performance critical (unless it's huge.) Something we continually read and write is critical. There's nothing much I can say about this in general, except that since systems are so much faster now, this question is a bit less important. Something like Microsoft Word, which writes all of its documents in binary formats, would probably not be written that way today.

The answer to question #2 might seem trivial -- my program uses this file. My program is the only thing that will ever read it or ever write it, end of story. But that is rarely true.

For one thing, your program will change. Is the format of this data going to be the same for all future versions of your program? If not, better put some kind of version indicator in the file, so that you can read old versions with the newer code. (I always forget to do this!)

Second, you might write some little utility to check your file or generate the initial version, etc. As soon as you do that, you've lost a bit of control of the format. Changing it means changing all the utilities.

Third, once useful utilities are out there, other people will take those and not want them to break when you suddenly decide to add a new byte to the file format. They might write their own utilities, even reverse-engineering the format of your files in order to do that. When that happens, you've completely lost control of the file format. It is now a public standard and you have to support it, or really annoy all of your users.

Lastly, there are options and things you expect the user to read and change. Those are public from the start and can't be constantly changing.

Back in the old days when every instruction and every byte counted, we'd use binary formats for a lot of files. It's faster to just read a chunk of bytes right into your data structures than to parse some text file. Nowadays, systems are a lot faster and you can afford to use text files for more things. And if you are going to use text files, you might as well use XML.

I personally have a number of little issues with XML, and for my previous attempts at this project, I implemented my own format. With all of you watching this, I don't have the nerve to introduce a new format just to address a few pet peeves. So I've written a simple XML parser and added it to my Util directory. I'm using it for several odds and ends:

  • Options. There's now an options.xml file in the root directory that sets all kinds of run-time options for the demo. That lets me pull out all the hard coded file names that were in the code previously. If you download the latest demo, you can create new images for the selection border on blocks, the animation sequence when blocks are destroyed, etc.

  • Cursors. The cursor patterns are images, but I also need a "hot spot" (x,y) indicating where the cursor points. Without a file format, I had to hard code that. Now in docs/cursors there are two files, arrowCursor.xml and crossCursor.xml that define the cursor patterns.

  • Blocks. Instead of hard coding the various block texture images into the demo, there is now a file that specifies them. In docs/blocks, find blocks.xml which lists the images are used to make up a set of blocks. You can create your own block images, describe them in a similar XML file and point to the file in options.xml.

Note on images: The JPEG library I'm using is ancient and does not read any file format with an "alpha" (transparency) plane. So I'm specifying all transparent images as a pair of files. In the options and other xml files, you'll see a value like "texture.jpg;textureAlpha.gif" The first image specifies the RGB values, and the second image the Alpha values. Images can be in JPEG, GIF or BMP formats. If the second, alpha image, is an RGB format, the red plane is used. The sizes of the two images have to match. Since most of these are textures, they have to be a power of two in size. Practical sizes for block textures or cursors are 32 by 32, 64 by 64, 128 by 128 or 256 by 256.

About This Series

My original plan was to work on this project "in public", by doing a piece of code and then writing up the interesting parts of it. I didn't want a conventional "what I did today" blog. Releasing the code, both source and a demo, keeps me focused. The fact that some of you download the code and actually try it keeps me motivated.

The downside of this approach is that it takes time. I was shooting for one part a week, but I'm averaging more like 10 days per part. That's only going to get worse as I use up my older framework code, and the projects get larger. For example, doing the server will probably take more than 10 days...

I could take smaller steps. For example, I could have released the "File Formats" section by itself, with a demo that didn't do anything new visually, but had options for you to play with. If you had downloaded that demo, you could have changed the block face patterns or other textures. I didn't think that was interesting enough to make a complete part of the series, so I went on to doing the 2D graphics, which results in more useful features.

I could also write parts that are only description, with no demo or source code. New demos would only appear when I reach a milestone and have code I'm happy with. But that works against the original reason for this series -- to keep me focused on writing code.

I'm open to suggestions. Let me know what you think in the comments.

The Demo

The new demo is at The Part 3 Demo. It has been tested on Windows 7, 32-bit and 64-bit, and Windows XP, 32-bit.

Since we now have a help screen, just hit F1 for help. Hit ESC to exit the demo.

We're not doing anything ambitious with the graphics here, so if you can run any 3D game, you should be able to run this. If you get an error message about needing "d3dx9_42.dll", you need to update your DirectX version. Go to Microsoft DirectX Update for a page that does that.

The Source Code

Download The Part 3 Source for the C++ code, a roadmap to the source, and a build directory. This includes the executable demo and the files it needs. If you download this, you don't need to also download the demo zip above.

If you want to compile the code, the project is built with Microsoft Visual C++ 2010 Express. You will also need the DirectX Software Development Kit. It's possible you'll need the Windows 7 Software Development Kit too. These are all free from Microsoft.

Unfortunately, all three of these downloads are huge. Hopefully, if you are interested in game development, you already have them or an equivalent development environment. If not, I hope you have a fast internet connection! Download and install them in that order - Visual C++, then the DirectX SDK, then the Windows SDK.

Statistics

The XML parser is new code, added to Utilities. The 2D graphics support was old code that I rearranged substantially. It's been added to Framework. The uses of the XML and 2D graphics are all new, and are part of TheGame. Here are the new totals:

  Full Project   New for Part 3
TheGame lines 5,210 1,402
Framework lines   5,637 2,191
Utilities lines 5,195 870
Total 16,042 4,463

Coding hours 111.2 26.7
Writeup hours 23.0 4.4

Home       About Me       Contact       Downloads       Part 2    Part 4   

blog comments powered by Disqus