![]()
|
|
Smartphone Game Programming Tutorial 3: Events, Threads and Plotting Pixels Welcome to the third tutorial. In this part we're going to find out how our program can respond to menu clicks. We'll also consider (briefly) the difference between event-driven and timer-driven code, and create a new thread for our game. Finally we'll open a full-screen window and plot some pixels - the first step towards Graphics. This tutorial is longer than the previous ones, and it still doesn't cover as much as I'd expected. If you were hping to find out how to draw bitmaps, we'll cover that in the next tutorial. The Starting Point Start up eVC4 and create a new HelloWorld project called "tutorial3". As usual, make sure that your project settings are set to "SMARTPHONE 2003", not "STANDARDSDK" - which is for PocketPC use. Go straight to the ResourceView tab and open up IDR_HELLO_MENUBAR so you can edit it - see tutorial 2 if you don't know how to do this. You need to:
#include <windows.h> Leave this file in the root of your project. Otherwise you may get stdafx.h errors when you compile for real phones. I don't understand why (do you?) but you must have this. Events Now go and look at tutorial3.cpp, using the FileView. If you scroll down a little way from the top of the file, you'll see:
LRESULT CALLBACK WndProc(
HWND hwnd,
UINT msg,
WPARAM wp,
LPARAM lp
) Remember the messages from tutorial 2? Well, when anything Windows considers that something even a little relevant to one of our windows happens, a message is sent to our program - and this is where it arrives. Look at the code below the WndProc function declaration:
switch(msg)
{
case WM_CREATE:
...lots more case statements...
} This is a switch statement. I'm going to guess that you know what a switch statement is - a big if - then - else - if - then - else statement. If you don't, II suggest you look here. A little lower you'll find the following code:
case WM_COMMAND:
switch (wp)
{
case IDOK:
DestroyWindow(hwnd);
break;
default:
goto DoDefault;
}
break; Remember, of course, that the "Quit" button has an ID of "IDOK"? Well, when the user presses the Quit button, a WM_COMMAND message is generated, with an IDOK parameter. The net result, therefore, of pressing the "Quit" button is "DestroyWindow(hwnd);". Which, handily, quits the program. Change the code so that it looks like this:
case WM_COMMAND:
switch (wp)
{
case IDOK:
DestroyWindow(hwnd);
break;
case IDTEST:
MessageBox(hwnd,L"Test button pressed...!",L"tutorial3",MB_OK);
break;
default:
goto DoDefault;
}
break; Specifically, the change is that we've added a new CASE statement, for IDTEST, which displays a MessageBox. MessageBoxes are Good Things, but more about them in a bit. Note the extra "break;" statement as well - this is all-important. It's very easy to forget and leave it off - you will do this sooner or later - and it doesn't give a compiler error. Run the program under the emulator - see the previous tutorials if you don't know how to. If all turns out well, when you hit the "Play" menu and choose "Test", you'll get a pop-up box, with an OK button, a title bar of "tutorial3", and a message of "Test button pressed...!". That explains the last three of the MessageBox parameters we used (though it doesn't explain the strange L"..." notation - more later on that). The first parameter of our MessageBox call is the handle to the window we want to attach our MessageBox to. That is to say, the message box will appear "on top" of our main application window, because it will be a "child" of our main window. "hwnd" is one of the WndProc parameters. If that wasn't very clear, don't worry. For the first parameter of MessageBox, if you have a "hwnd" to use, use it. If you don't, then use NULL instead (note, it must be in capitals): MessageBox(NULL,L"Test button pressed...!",L"tutorial3",MB_OK); is allowed, and gets you out of needing to attach the messagebox to a window. MessageBoxes - a brief diversion I'm now going to take a brief diversion to give you some useful tips for using MessageBoxes. MessageBoxes are great because you can put them anywhere in your code. Your program will stop dead until you press the OK button, so they're very useful in debugging, reporting error messages and so on. It will therefore, at some point, be Very Useful Indeed if you can also get it to print out your program variables at the same time. This is a simple thing to want to do but it took me personally a long time to find out how to do it. Suppose you have an integer called "NumberOfTurtles", and you want to print it in a MessageBox. Here's how you do it.
int NumberOfTurtles = 5 ; // This is the number we want to print out.
TCHAR ScreenMessage[200]; // ScreenMessage is a variable name; 200 is the maximum length of our message
wsprintf(ScreenMessage,L"Number Of Turtles is %d",NumberOfTurtles);
// Note that "%d" will be replaced by the value of NumberOfTurtles
// Also note that strange L"..." notation again
MessageBox(hwnd,ScreenMessage,L"...",MB_OK);
If you replace the MessageBox we used before, with the above four lines of code, you'll find it doesn't work. You'll get the following error: D:\Projects\tutorial3\tutorial3.cpp(86) : error C2361: initialization of 'NumberOfTurtles' is skipped by 'default' label - which is not a bad description of the problem. The variable NumberOfTurtles (and, indeed, ScreenMessage) are only declared when the IDTEST code fires - any other route through WndProc means that they never get declared. This is Against The Rules. The fix is to declare the variables at the top of the function, as shown in the next code fragment. Make sure your WndProc looks like this, then build and execute the project.:
LRESULT CALLBACK WndProc(
HWND hwnd,
UINT msg,
WPARAM wp,
LPARAM lp
)
{
LRESULT lResult = TRUE;
int NumberOfTurtles;
TCHAR ScreenMessage[200];// ScreenMessage is a variable name; 200 is the maximum length of our message
switch(msg)
{
case WM_CREATE:
lResult = OnCreate(hwnd, (CREATESTRUCT*)lp);
break;
case WM_COMMAND:
switch (wp)
{
case IDOK:
DestroyWindow(hwnd);
break;
case IDTEST:
NumberOfTurtles = 5 ; // This is the number we want to print out.
wsprintf(ScreenMessage,L"Number Of Turtles is %d",NumberOfTurtles);
// Note that "%d" will be replaced by the value of NumberOfTurtles
// Also note that strange L"..." notation again
MessageBox(hwnd,ScreenMessage,L"...",MB_OK);
break;
default:
goto DoDefault;
}
break;
...continue with case WM_PAINT unchanged... If you run the program, and choose the "Test" menu option, you should get the message "Number Of Turtles is 5", which is of course the desired effect. Events versus Timers The Windows interface that we've created so far is event-driven. That is to say that when something happens, our program responds. When nothing is happening, our program does nothing. This is ideal for turn-based games like chess, but it's not what we're looking for. What we need is more like animation - we want to redraw the screen with new game graphics, n times a second. The way we're going to get that is to create a new thread. A thread is an application component, which runs on its own, independently from the main program flow. That's probably a good enough description for now; as ever, it will all become clearer. Creating a Thread It's important to understand that once we've created a new thread, we have *two* things running on our Smartphone, not one. The new thread is a child of the main application, so if the main application dies, our new thread should die with it. First, go to the ResourceView, and add a new menu option to the "Play" menu, called "New Game". Give it an ID of "IDNEWGAME". Now go back to tutorial3.cpp. In order to create the new thread, we have to write the code for the thread itself, as a "callback" function. We then, from the main program, issue a command to create a new thread based on the callback function we wrote. When we create the new thread, what we get back is another handle - this time, a handle to the thread. So the first thing we need to do is create a variable to hold this handle. We'll make it a global variable - you do know what a global variable is, don't you? Anyway, add the following line of code after "TCHAR g_szMessage[30];" at the top of tutorial3.cpp: HANDLE hGameThread = NULL; Now, go to just above the comments at the top of the "LRESULT OnCreate(" code, and add the following new function (note the HWND - we'll pass that as a parameter because we'll need it later...)
void NewGame(HWND hwnd)
{
hGameThread = CreateThread(NULL, 0, GameThreadProc, NULL, 0, 0);
SetThreadPriority(hGameThread, THREAD_PRIORITY_ABOVE_NORMAL);
}
The second line of code makes sure that our new thread takes priority over normal applications. This is good, of course, if you're writing a game. Now add the following case statement between the IDTEST case statement we added earlier, and the "default:" statement:
case IDNEWGAME:
NewGame(hwnd);
break; Now we have to write the GameThreadProc code, as a new function. We must put the function code above the call to the function. So go to the line above the "void NewGame()" line, and add:
DWORD WINAPI GameThreadProc(LPVOID lpParameter)
{
MessageBox(NULL,L"New thread has started...!",L"...",MB_OK);
return(0);
}
Run this new code, and you should find that you get the MessageBox as expected. We now have our new thread. At the moment it just runs then exits immediately - we'll change that later. The Game API (GAPI) There are two distinct ways of drawing graphics. The first is to use the standard Windows user interface tools to draw them. That's great for games like chess - again - but is slo-o-o-w - too slow for an arcade game. So we're going to use the Game API. The Game API provides us with a very small set of commands which we can use to draw bitmaps and plot pixels - and not a lot else. But since this is a game project, it's all we need. If you've played any Smartphone games at all, you'll recognise the Game API - it's when the game goes into full-screen mode. Having said that, the Game API is not pleasant to work with. We're going to use the excellent STGapiBuffer code, provided by SmartphoneDN.COM, and written by Yaroslav Goncharov. You can download it here; once you've downloaded it, extract the two files to your project directory (for instance, mine is D:\Projects\tutorial3). Go to the File View, and right-click on "Source Files". Choose "Add Files to Folder", then choose "STGapiBuffer.cpp" and click OK. Note that the file is added to the list of Source Files. Right-click on "Header Files", and add "STGapiBuffer.h" in the same way. The STGapiBuffer code is now available, but we have to specifically tell the compiler that we want to use STGapiBuffer instructions in tutorial3.cpp. So go to tutorial3.cpp and find the line at the top that reads #include "resource.h" We need to "include" STGapiBuffer.h as well, so add the following line of code below the resource.h line: #include "STGapiBuffer.h" The first things we have to do when a game starts are to open the graphics display and keyboard input systems. Similarly, the last things we do when the game ends are shut down the graphcis display and keyboard input. We can do these things at the top and bottom of the GameThread code, but we need to get hold of the main window HWND first. We'll do that by using another global variable at the top of the code. After the "HANDLE hGameThread = NULL;" line we added earlier, add HWND MainWindow; Add the following line to the top of the NewGame code: MainWindow = hwnd; Now, by the time our new thread starts, the handle of our main window will be available in the global MainWindow variable. Now replace GameThreadProc so it looks like this:
DWORD WINAPI GameThreadProc(LPVOID lpParameter)
{
// Open the graphics display and key input
if ( ! GXOpenDisplay(MainWindow, 1))
{ MessageBox(MainWindow, L"Cannot open display", L"fatal error...",MB_OK);
return 0;}
if ( ! GXOpenInput())
{ MessageBox(MainWindow, L"Cannot open input", L"fatal error...",MB_OK);
return 0;}
// Shut down the graphics display and key input
GXCloseDisplay();
GXCloseInput();
return 0;
}
GXOpenDisplay and GXOpenInput return true if they succeed, and false if there's a problem preventing them from working. Note the exclamation mark ("!") before GXOpenDisplay - this means NOT. So the first of these if statements says that if GXOpenDisplay did NOT return true, then show an error and end the function. Linking If you now try to build the application, you'll get a big nasty error:
...unresolved external symbol...GXOpenInput
...unresolved external symbol...GXOpenDisplay
...unresolved external symbol...GXDisplayProperties
emulatorDbg/tutorial3.exe : fatal error LNK1120: 3 unresolved externals This is because we're making calls to the GAPI system. The code for the GAPI functions is held in a separate library (.LIB) file. We must tell the compiler to "link" to GX.LIB. How do we know it's GX.LIB? Simple - use the Help system to look up GXOpenInput. If you do, you'll find that it mentions "Library: gx.lib" at the bottom. This means, if we don't link to gx.lib, then GXOpenInput isn't ever going to work for us. Go to the project menu, and choose "Settings". A dialog box appears showing, in the top-left, a drop-down list marked "Settings for:" - change this to "All configurations", because whether emulator or real phone, we will need to link to gx.lib. Now click the "Link" tab. You should see a field marked "Object/library modules" - add "gx.lib" to the end of this field, so it reads "commctrl.lib coredll.lib aygshell.lib gx.lib". Click OK to get rid of the dialog box. If you now build the project, it should compile fine. But if you try and run it on the emulator, you'll get a very terse eVC error: "(X) Cannot execute program.". This is a particular annoyance - you have to copy a file (GX.DLL) to the emulator first, by hand. You don't have to do this with real phones - only the emulator. Here's how to do it. First, you need to find GX.DLL - there are two versions of this file and you need the one in the X86 directory. On my machine, it's in "E:\Program Files\Windows CE Tools\wce420\SMARTPHONE 2003\Target\X86" - it'll be something similar on yours. Once you've found the file - using Windows' Find Files and Folders if necessary - and checked it's the X86 version, you need to copy the file to the emulator. To do this you need to start up the Remote File Viewer. OK the "Cannot execute program" error - if you didn't already - and choose Tools -> Remote File Viewer. This should load the Remote File Viewer. On my own PC, it actually just pops a DOS box which then vanishes. If you get this too, then here's a workaround - use the Remote File Viewer from eVC3 instead. Trust me, it works. If you need to do this, start up eVC3 (you needn't quit eVC4), then go straight to Tools -> Remote File Viewer. Once Remote File Viewer's loaded, you can shut eVC3 down again. The Remote File Viewer is a simple and slightly cumbersome program. Go to Connection -> Add Connection. Find "SMARTPHONE 2003 Emulator" and click OK. After a brief pause, you should see a standard tree view of the emulator's storage. Go to "Smartphone 2003 Emulator\Windows", and choose File -> Export. Use the file browser to locate GX.DLL, and export it. Once that's done, browse the Windows directory and you should be able to see that gx.dll is there. Hard work, and you need to do it on every hard boot. So if your code locks the emulator and you have to reset it, use the "Soft Reset" menu option and you won't have to copy GX.DLL across again - it'll already be there. Remember, you don't need to worry about GX.DLL on real phones, only on the emulator. You should now finally be able to run your program. If you select Play -> New Game, well, nothing happens. It's opening the screen, then closing it again. What we need it to do is draw something and wait for a moment, so we can see it's working OK. So let's add some colour. STGapiBuffer gives us buffering, which is a good thing. Instead of drawing straight to the screen, which is slow, we draw to a memory buffer, then draw the memory buffer to the screen buffer. Trust me, it works. So declare two new global variables underneath the hGameThread declaration:
CSTGapiBuffer ScreenBuffer;
CSTGapiBuffer MemoryBuffer; We need to allocate memory for the memory buffer - but we only do this once. So add this code just before the CreateThread call in NewGame(): MemoryBuffer.CreateMemoryBuffer(); Don't worry, this is all just stuff we have to do. Now, finally, we can add code to draw something. Don't bother trying to understand this code til after you've tried it out; immediately above the "// Shut down the graphics display and key input" comment, add the following code:
int VerticalPixel = 0;
int HorizontalPixel;
DWORD PixelColour;
while (VerticalPixel < 220) // the Smartphone screen is 220 pixels high
{
HorizontalPixel = 0;
MemoryBuffer.SetPos(0, VerticalPixel);
while (HorizontalPixel < 176) // the screen is 176 pixels wide
{
PixelColour = MemoryBuffer.GetNativeColor(RGB(VerticalPixel,0,HorizontalPixel));
MemoryBuffer.SetPixel(PixelColour);
MemoryBuffer.IncXPos() ;
HorizontalPixel++;
}
VerticalPixel++; // don't forget to increment the loop variable, else we get an infinite loop
// and have to pull the smartphone battery...!
}
void* MemPtr = GXBeginDraw(); // This is where we copy the Memory Buffer to the Screen Buffer...
ScreenBuffer.SetBuffer(MemPtr);
ScreenBuffer.BitBlt(&MemoryBuffer);
GXEndDraw();
Sleep(5000); // 5 seconds to look at what we made :-) If you build this and choose "Play Game", you should be rewarded with a five-second glimpse of this:
The white box is an annoying glitch - the user interface for the main window is interfering. We'll sort that out later. If you're scratching your head by this point because something's gone wrong, don't worry. Here's a zip file containing what you should have so far. Now go back and have a look at the code we added. You should be able to see:
And that's it! In the next tutorial we'll draw some bitmaps and move them around a bit, hopefully creating some sort of really, really simple game in the process. We might also look at dialog boxes if there's time... Help! Are you having problems with the above tutorial? First off, try "getting out of the car, then getting back in again" - which is to say, reboot your PC then try again (yeah, I know...). The eVC development tools (both versions 3 and 4) are buggy and prone to errors like "Platform Manager failed" for no (apparent) reason. Many of these problems can be cleared with the aid of a simple reboot. Having other problems? Why not email me and let me know what the problem is. I certainly can't guarantee I can help, but if I can, I will. If I can't help with a particular problem, I'll put a description of the problem here. Maybe some kind-hearted soul will mail me an answer or suggestion - if they do, I'll put it up alongside the original question. Questions and Answers No questions have yet been submitted on this tutorial. Source Code Tutorial 3 Source Code to Screenshot 1
all content copyright unless otherwise stated source code may be used freely for any purpose |