www.gameinstitute.com Week 4 Game Institute Introduction to C and C++ by Stan Trujillo Introduction to C and C++ : Week 4: Page 1 of 34 www.gameinstitute.com © 2001, eInstitute, Inc. You may print one copy of this document for your own personal use. You agree to destroy any worn copy prior to printing another. You may not distribute this document in paper, fax, magnetic, electronic or other telecommunications format to anyone else. This is the companion text to the www.gameinstitute.com course of the same title. With minor modifications made for print formatting, it is identical to the viewable text, but without the audio. Introduction to C and C++ : Week 4: Page 2 of 34 www.gameinstitute.com Table of Contents Introduction to C++ 4 Lesson 4 – Application Architecture 4 The typedef Keyword 5 Handles 7 Macros 8 The HelloWin32 Sample 8 Windows Messaging 11 The Message Pump 12 Callback Functions 13 Window Creation 15 The SimpleWindow Sample 16 Class Frameworks 21 The GameApplication Class 21 The WinMain function 27 The WndProc Function 28 The SimpleWindowClass Sample 31 Message Pump Modifications 32 What’s next? 33 Introduction to C and C++ : Week 4: Page 3 of 34 www.gameinstitute.com Introduction to C++ A GameInstitute course by Stan Trujillo The console applications that we’ve been written so far are useful for game-related tools and utilities (and instructional samples), but console applications are too limited for the game application itself. What we need is a windowed application. This will let us create a window in which to display graphics, and provide access to some important Windows-specific features. But this isn’t the only thing that a game requires. Although games have a lot in common with most windowed applications, there are some important differences. In this lesson we learn about windowed applications and high-level game application design, and a few C++ topics that we still need to cover. This lesson is different than the previous lessons. It introduces some real-world programming issues that we haven’t had to use thus far, and it focuses on building a body of code that we can use for the remaining lessons. It also moves more briskly than the previous lessons—meaning that not all of the code presented is discussed in detail. The reason for the increased pace is that this lesson is designed to bridge the gap between the simple and educational samples from the previous lessons and the game that we’re building up to. This bridge takes the form of a small class framework that encapsulates some of the more mundane elements of Windows programming. The advantage is that once we’ve written this framework, we don’t have to worry about how it works. This lesson moves quickly because there is a lot to cover, and because it is not necessary for you to master the Windows programming details required to implement this framework. On the other hand, it’s not enough to be given a class framework and being told not to worry how it works. That’s why we’ll spend this lesson building the framework from the bottom up. What is important to take from this lesson is the basics of Windows programming, the high-level design of a game application, and some techniques that go into good class design. Lesson 4 – Application Architecture The console applications used in the previous lessons each had two portions: those that we provided, and those and were provided in the form of functions from the standard libraries. The standard library provided us with helpful features such as user input support, screen output support, and string manipulation functions. All of these features are still available to us now that we’re going to be writing windowed applications, but we’ll need more than just the standard libraries, which are largely platform independent. What we need is Windows-specific features. Microsoft provides access to Windows-specific features through Win32, which is essentially a huge set of functions. Win32 is the Application Programming Interface (API) that is used to write Windows applications. (The ‘32’ referrers to the fact that Win32 is specific to 32 bit versions of Windows.) There are other APIs for writing Windows applications, but Win32 is the most direct way to access Windows features. Win32 is the product of an evolution that began with the first version of Windows. At the time, C was the language of choice, so Microsoft designed Win32 based on C programming techniques. Win32 is still a C-style, function based API, partly because there are so many existing applications that Microsoft can’t change the underlying API without upsetting thousands of developers, and partly because Microsoft has been relatively slow to adopt C++ in general. Introduction to C and C++ : Week 4: Page 4 of 34 www.gameinstitute.com For C++ programmers, Microsoft provides other APIs such as MFC (Microsoft Foundation Classes), and ATL (Active Template Library). Instead of a collection of functions, these packages provide a collection of classes that can be used for Windows application development. MFC in particular provides classes that are designed to be used as a generic class framework from which any type of application can be written. Both MFC and ATL are built “on top of” Win32, meaning that although they provide an alternative interface, they are in fact written using Win32. As a result, most game programmers feel that using these APIs for games is foolish because better performance can be gained by using Win32 directly. This is a debatable issue, but for this course we’ll go with the status quo, and opt not to use MFC or ATL. This doesn’t mean that we won’t be using classes to build the foundation required for games. It means that we’ll be using Win32 to write a small and efficient class framework that is designed specifically with games in mind. This framework can be small because, although Win32 is a massive API, most games require the use of only a tiny fraction of the complete Win32 API. Before we can do any of this, however, there are some topics that need to be covered first. For starters, using Win32 requires knowledge of some language features that we haven’t discussed yet. The typedef Keyword C and C++ both support the typedef keyword, which is short for type definition. A type definition is essentially a type alias. The typedef keyword can be used to define an alternative name for any data type. typedef is used extensively in Win32. For example, Win32 defines aliases for most of the intrinsic types. Consider these variable declarations: INT i; SHORT j;; LONG l; FLOAT f; CHAR ch; Each of these variables uses an intrinsic type, but through an alias that Win32 provides. The code in any Windows application is free to use these types instead of the genuine intrinsic types. There’s no difference between the two, because the genuine types are used with typedef to create these aliases, like this: typedef int INT; typedef short SHORT; typedef long LONG; typedef float FLOAT; typedef char CHAR; The typedef keyword is followed by the name of an existing type, and ends with the desired alias and a semicolon. typedef doesn’t create new data types—it creates new names for existing data types. The examples shown above are of dubious value. They don’t provide any advantage over the native types unless you happen to prefer upper-case data type names. But these are just a few of the type aliases that Win32 provides. Some are useful merely because they are shorter than the alternatives. For example: typedef unsigned short USHORT; typedef unsigned char UCHAR; typedef unsigned int UINT; Introduction to C and C++ : Week 4: Page 5 of 34 www.gameinstitute.com These type definitions provide shorthand syntax for the unsigned variation of the intrinsic types: USHORT index; Instead of unsigned short index; But Win32 doesn’t stop there—some new terms are used to describe familiar types. One example is the term word. For a 32-bit operating system like Windows, a word is a 16-bit unsigned integer, and a double word is a 32-bit unsigned integer. Hence these typedefs: typedef unsigned short WORD; typedef unsigned long DWORD; Win32 also uses typedef to define names for some data types that are used for specific purposes. For example, Windows is a message-based operating system. Messages are used to communicate with the application code. Each message has two optional parameters, which are represented by the WPARAM and LPARAM types, as defined here: typedef UINT WPARAM; typedef LONG LPARAM; As we’ll see later, Windows messages are delivered by functions that provide the message itself, which is represented by the UINT type alias, and is accompanied by parameters, like this: int ProcessMessage(UINT msg, WPARAM wParam, LPARAM lParam); At first glance, this function appears to use three exotic parameter types, but each is actually just a typedef for an integer type. Pointer types are also frequently assigned alternative type names by Win32. These typedefs have the prefix “LP”, which is short for long pointer. The “long” refers to a designation made obsolete by 32-bit operating systems, but is nevertheless still used. For example, LPDWORD is a pointer to the DWORD type, and can be used like this: DWORD value; LPDWORD pValue = &value; Despite the fact that no asterisk appears in the declaration of pValue, it is a pointer because the LPDWORD type definition includes the asterisk. Win32 defines LPDWORD like this: typedef DWORD* LPDWORD; A number of similar type aliases are provided for char pointers: typedef CHAR* LPSTR; typedef const CHAR* LPCSTR; These definitions make the following declarations possible: LPSTR str = “Initial Text”; Introduction to C and C++ : Week 4: Page 6 of 34 www.gameinstitute.com LPCSTR const_str = “Const Text”; Win32 uses these alternative type names extensively, so many of the function prototypes and structures that Win32 provides for use in Windows programming look as though they are based on exotic data types, but in fact most of the types used are simply intrinsic types. Nevertheless, the frequency at which these types are used makes using Win32 a bit more difficult at first. Handles Win32 is designed around constructs that C programmers developed long before C++ was invented. One of these constructs uses handles to represent entities that, had C++ been used, would be classes instead. A handle is a data type that represents a specific entity such as a window, a bitmap, or an icon. When one of these items is created, Win32 provides a handle that represents the new item. This handle can then be used with functions that Win32 provides to manipulate the item. A prominent example is the HWND data type, which serves as a handle for a window. (The “H” prefix stands for handle, and WND is short for window.) When a new window is created, an instance of the HWND type is used to store the handle value for that window. This handle can be used with a number of functions what Win32 provides to manipulate that window. The ShowWindow function, for example, has this prototype: BOOL ShowWindow( HWND hWnd, int nCmdShow ); The first argument is the handle to the window that should be affected by the function call, and the second argument is used to indicate the effect desired. The SW_SHOW symbol, for example, is provided to indicate to ShowWindow that the window that the handle represents should be displayed if it is currently hidden. Notice that the ShowWindow return type uses BOOL instead of the intrinsic type bool—yet another type alias used by Win32. Another important handle type is HINSTANCE, which essentially represents the application itself. As we’ll see soon, Win32 provides an HINSTANCE handle to the application on startup. The idea behind a handle is that it, together with the functions that accept handles as arguments, encapsulate the functionality required for dealing with windows, applications, or any other programmatic entity. Without classes, that’s the best that can be done to hide how a system works from the programmer. If Win32 had been written using C++, then handles wouldn’t have been necessary. Most of the time, handles are actually type aliases created with typedef that use either an integer or a pointer as an underlying type. The HINSTANCE handle type, for example, is defined this way: typedef void* HINSTANCE; HINSTANCE is actually a void pointer. Pointers to the void type are generic pointers they are capable of pointing to any type of data. The fact that Win32 defines HINSTANCE this way tells us that each HINSTANCE is a pointer to a data type that is probably used by Win32 internally, but that we’re not supposed to know about. The details of how Windows handles HWND and HINSTANCE are therefore hidden from us—providing the C version of encapsulation. Introduction to C and C++ : Week 4: Page 7 of 34 www.gameinstitute.com Macros We’ve intentionally avoided macros after mentioning them briefly in Lesson 1. In C++, macros can almost always be avoided with superior features. For example, when a literal value has been required in our programs, we’ve been using const, like this: const int MaxPlayers = 10; This is a better solution than the macro equivalent: #define MaxPlayers 10 Both can be used in the same way: SetMaxPlayers( MaxPlayers ); But the macro is a preprocessor command, whereas a constant is a C++ language construct. This means that C++ understands constants in a way that it doesn’t understand macros. In the example above, if the SetMaxPlayers function was modified to take a float instead of an int, the macro would cause a cryptic compiler error, but the constant would result in type-mismatched error, more correctly reporting the problem. Nevertheless, Win32 uses macros liberally. Virtually every symbol used in conjunction with Win32 functions is a macro and not a constant. The SW_SHOW symbol used with the previously mentioned ShowWindow function, for example, is defined like this: #define SW_SHOW 5 These symbols would best be constants instead, but Win32 still uses macros. Luckily, programmers are unaffected most of the time. The HelloWin32 Sample Unlike console applications, Windows applications don’t use main as a universal entry-point. Instead, they use a similar function called WinMain. Each Win32 application must provide a version of this function. Like main, WinMain is the first function called, and the last to exit. It also provides command- line information to the application. As we mentioned in Lesson 1, a windowed application that actually displays a window requires at least 50 lines of code or so. However, an application that doesn’t display a window can be much simpler. All you need is the WinMain function. But Win32 applications don’t support the same output options available to console applications, so, although you can send data to cout, there’s no place to display it. With no output, there is very little point in running the resulting executable, but the code for the simplest possible Win32 application looks like this: #include <windows.h> int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { // all processing occurs here return 0; Introduction to C and C++ : Week 4: Page 8 of 34 www.gameinstitute.com } Using Win32 requires that the windows.h header file be included. This header file declares and defines the core data types and functions necessary for writing Windows applications. The WinMain function returns an int, but another symbol appears after the return type: APIENTRY. This is a macro that Win32 defines to distinguish between the two types of function calling conventions available in C and C++. These conventions define the exact manner in which a function call is performed at the machine-language level. The standard method is used by default, and the pascal style is activated by the pascal keyword. The APIENTRY macro resolves to the pascal keyword, which causes the compiler to implement the WinMain function with the Pascal calling convention. For our purposes, all we need to know is that APIENTRY or pascal must appear before the WinMain function name. WinMain has four parameters. The first is the HINSTANCE that represents the application. This value is required for some Win32 function calls, and is often stored in a global variable so that it is accessible to all of the functions in the program. The second parameter is also an HINSTANCE, but is always zero for Win32 applications (it was used for 16-bit versions of Windows), so it can be ignored. The third argument is a string containing the command-line arguments used to launch the executable. Unlike the argv parameter provided to the main function, this is not an array of strings, but a single string containing all of the arguments as provided on the command-line. Windowed applications use command- line arguments less frequently than console applications, so this parameter is often ignored. The fourth and final WinMain argument is an integer that contains a value indicating the desired initial state for the application’s window. Normally this value is equal to the SW_SHOWNORMAL symbol. This initial version of WinMain doesn’t do anything except return zero. For the WinMain function, a return value of zero indicates that the application is terminating without processing any messages, which—in this case—is true. This is the first point in this course in which we’ve had to implement a function that provides parameters that we are unlikely to use. The second WinMain parameter in particular is useless, and we won’t need the command-line arguments either so we can rewrite the definition of this function so that no parameter name is given, like this: int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) { // all processing occurs here return 0; } The parameter type is mandatory, so we’ve left the type for the 2 nd and 3 rd parameters, but the parameter name is unnecessary if it is unused, so we can safely omit the names for these two parameters. Clearly, this is a pretty boring Win32 application, even if it is our first, because there is no output. We can’t use cout, but we can use another form of Window output: a message box. Win32 provides the MessageBox function for situations in which an application must display a message—usually an error message. The MessageBox function prototype looks like this: int MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType); Introduction to C and C++ : Week 4: Page 9 of 34 www.gameinstitute.com The first argument that MessageBox requires is the HWND of the application window. Since we don’t have a window yet, we’ll use zero, which instructs Win32 to use the Windows desktop as the parent window for the message box. The second argument is the text that is to appear inside the message box and the third argument is the text to appear on the message box title bar. The fourth argument is an integer that is used to control which icon (if any) appears on the message box, and which buttons should appear. For example, using the MB_OK symbol causes the message box to display just an “OK” button. We can display a message box by adding a call to MessageBox to WinMain, like this: int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) { MessageBox( 0, "Hello Win32!", "WinMain", MB_OK ); return 0; } This version of WinMain is used in the HelloWin32 sample, which displays this output: Before we move on, we can use the MessageBox function to discuss the bit-wise OR operator (|). This operator combines two integer values into a single value by setting the individual bits of the resulting value according to the bits of the two source values. The OR operator manipulates values at the binary level—something that we won’t get into in this course, but is an operation that is often used within an API to allow one or more options to be enabled through a single parameter. The MessageBox function uses this technique for the fourth parameter. Previously, we used the MB_OK symbol to indicate that an “OK” button should be displayed, but Win32 provides more symbols for use with the MessageBox function. The MB_ICONEXCLAMATION symbol, for example, indicates that an icon containing an exclamation point should be displayed. We can modify our call to MessageBox to use both of these options, like this: MessageBox( 0, "Hello Win32!", "WinMain", MB_OK | MB_ICONEXCLAMATION ); The OR operator is used to separate the MB_OK and MB_ICONEXCLAMATION symbols. The result is that the value of each symbol is combined into a single integer, which is provided to the MessageBox function. Both symbols have an effect on the results, which look like this: Introduction to C and C++ : Week 4: Page 10 of 34 [...]... two different uses in C++, depending on whether it appears inside a class definition For our purposes we’ll concentrate on how static affects data members and member functions when used with a class Consider this modification: class GameApplication { public: GameApplication() { assert( objectPresent == false ); objectPresent = true; } www.gameinstitute.com Introduction to C and C++ : Week 4: Page 22... TranslateMessage and then to DispatchMessage The while loop continues to pump messages until GetMessage returns false, which happens when the WM_QUIT message is posted www.gameinstitute.com Introduction to C and C++ : Week 4: Page 12 of 34 This message pump can be placed inside the WinMain function, like this: int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow) { MSG msg; while (GetMessage(... call without using the function name, let’s take a closer look at how function calls work Consider this function: void Func(int val) { cout . been relatively slow to adopt C++ in general. Introduction to C and C++ : Week 4: Page 4 of 34 www.gameinstitute.com For C++ programmers, Microsoft. Introduction to C and C++ : Week 4: Page 2 of 34 www.gameinstitute.com Table of Contents Introduction to C++ 4 Lesson 4 – Application