Giáo trình C++ P5

Chia sẻ: Hoang Nguyen | Ngày: | Loại File: PDF | Số trang:0

lượt xem

Giáo trình C++ P5

Mô tả tài liệu
  Download Vui lòng tải xuống để xem tài liệu đầy đủ

Nội dung Text: Giáo trình C++ P5

  1. Week 5 Game Institute Introduction to C and C++ by Stan Trujillo Introduction to C and C++ : Week 5: Page 1 of 33
  2. © 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 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 5: Page 2 of 33
  3. Table of Contents Lesson 5 – Using DirectX......................................................................................................................... 4 Installing the DirectX SDK................................................................................................................... 5 DirectInput Basics................................................................................................................................. 6 The Input Classes .................................................................................................................................. 8 The DirectInputScroll Sample ............................................................................................................ 13 Direct3D Basics .................................................................................................................................. 16 The Direct3D Classes ......................................................................................................................... 18 The Circle Sample............................................................................................................................... 20 CircleApp Initialization ...................................................................................................................... 22 CircleApp Rendering .......................................................................................................................... 26 Casts.................................................................................................................................................... 30 The Interact Exercise .......................................................................................................................... 32 What’s Next? ...................................................................................................................................... 33 Introduction to C and C++ : Week 5: Page 3 of 33
  4. Introduction to C++ A GameInstitute course by Stan Trujillo Last week we created a foundation for windowed applications using Win32. These applications support graphics and user input, but these features are provided by Windows, and are not performance oriented. This week, we introduce DirectX to our programs, which allows for speed and versatility that the native Windows facilities can’t match. Lesson 5 – Using DirectX Windows wasn’t designed with games in mind. Originally, Windows was just an easy to use, graphical alternative to a command-line operating system like DOS. Windows was for spreadsheets, databases, and word processing. Games were written with DOS, which allowed the unhindered access to hardware devices such as video cards, sound cards, and input devices that games required. Once customers discovered the benefits of a graphical operating system, they adopted Windows, and sought alternatives to DOS-based applications. They wanted Windows games too, but Windows presented some obstacles to the development of anything other than parlor games. For game programmers, what made Windows a blessing also made it a curse. In contrast to DOS, Windows was device-independent. Applications could communicate with hardware devices through the operating system instead of with the device directly. With Windows, the operating system abstracted the functionality of all devices of a similar type into a single interface. DOS applications, including games, either supported just one or two devices, or shipped application-specific device drivers. Programming for DOS meant that you had to know everything about each device that your application might be expected to use. With Windows, you were prohibited from knowing any of these details. Device independence means just that—if a video controller offers a really nice and fast feature, it is Window’s responsibility to use it. If Windows doesn’t, then your game runs slowly, and there’s nothing you can do about it. Well, actually there were things you could do. You could circumvent Windows entirely. Programmers used tricks to force Windows out of the way. This gave them the access to devices that they wanted, but made for an unreliable gaming experience. It wasn’t until Windows 95 was released that Microsoft got serious about supporting gaming for Windows. The solution came in the form of an API called The Game SDK. (SDK is short for Software Development Kit. The SDK and API acronyms are generally used interchangeably.) The idea behind The Game SDK was that it was possible to provide very low-level and very direct access to devices, but still retain the benefits of device independence. The idea stuck, but the name didn’t. Microsoft renamed The Game SDK to “DirectX” for the second release. DirectX is currently in its eighth revision. It has undergone significant changes, and is generally faster, more reliable, and more intuitive than it’s ever been. DirectX is actually several separate APIs. Direct3D is the 3D graphics API, and DirectDraw provides 2D graphical support. DirectPlay is the networking API, and DirectInput provides support for using input devices. There’s also DirectSound, DirectMusic, and DirectShow. Although Direct3D and DirectDraw still separate APIs, Microsoft is in the process of phasing out DirectDraw and adding 2D support to Direct3D. The Direct3D name is being retired in favor of DirectGraphics. Introduction to C and C++ : Week 5: Page 4 of 33
  5. For this course we’ll use Direct3D and DirectInput, but each of these APIs is too big a subject to delve into in a six-week introductory C++ course. (GameInstitute offers a twelve-week course on Direct3D and a six-week course on DirectInput.) As a result, our use of these two APIs will largely be indirect. We’ll introduce and use some C++ classes that encapsulate not just the functionality of these APIs, but most of the interface as well. With Direct3D, although most of the required functionality will be hidden, we will use it for some operations. Direct3D is a math intensive API, so some of the code in this lesson uses mathematical constructs that you may not be familiar with. Bear in mind that this isn’t a DirectX or 3D course. Portions of Direct3D are exposed in this lesson to give you an idea of what working with DirectX is like, not because you are expected to know how it works. None of the questions in the final exam require knowledge of 3D math or any of the DirectX APIs. Installing the DirectX SDK The DirectX SDK must be installed on your computer before you can compile the samples for this lesson. The SDK is available at: Make sure you download the DirectX SDK, and not merely the DirectX runtime (which Microsoft calls “DirectX for home users”). The SDK includes both the header and library files required to program with DirectX, and the drivers required to execute DirectX applications. The “home user” version of DirectX does not include any programming tools. The DirectX SDK installation is very friendly. It asks for the installation directory (using the default is preferred), and whether you want to install the Release or Debug version of the DirectX runtime components. Select Debug, as it actually allows either Release or Debug version to be selected after installation, whereas selecting Release prohibits the use of the Debug version unless the SDK is re- installed. If you’re using Visual C++, the DirectX SDK installation will offer to configure Visual C++ to use the SDK automatically. This is convenient, but if for some reason it doesn’t work, here’s how to manually configure Visual C++ to use the DirectX 8 SDK: 1. In Visual C++, select Tools | Options 2. Select the Directories tab 3. Using the Show directories for pull-down, select Include files 4. Enter the include file directory (c:\mssdk\include by default) 5. Use the up arrow button near the upper right-hand corner to move the new entry to the top of the list. 6. Using the Show directories for pull-down, select Library files 7. Enter the lib directory (c:\mssdk\lib by default) 8. Use the up arrow button near the upper right-hand corner to move the new entry to the top of the list. The dialog should look like this when you’re done: Introduction to C and C++ : Week 5: Page 5 of 33
  6. The reason DirectX SDK directories must appear at the top of the directory list is that Visual C++ searches for files using the directories in this dialog starting from the top. Visual C++ comes packaged with some earlier versions of the DirectX SDK. If the DirectX directories aren’t at the top of these lists, therefore, older versions of the SDK will be used. We’re using the latest DirectX features, so we must compile with the DirectX 8 header files and libraries. DirectInput Basics Virtually every type of input device is supported through DirectInput. These devices are divided into three categories: Introduction to C and C++ : Week 5: Page 6 of 33
  7. • Keyboards • Pointing devices • Game Controllers The keyboard category includes any keyboard, regardless of layout or language. Keyboards with up to 256 keys are supported, which apparently covers all keyboards. The pointing devices category includes mice, trackballs, touch-pads, and tablets. Keyboards that include touch-pads or trackballs are treated as two devices: a keyboard and a pointing device. All other type of input devices fall into the game controller category. This includes joysticks, game-pads, flight yokes, driving wheels, and virtual reality (VR) gloves and helmets. In this lesson we’ll use keyboards and pointing devices. The purpose of DirectX is to allow access to a device that is as low as possible and still device independent. For DirectInput, this means that all keyboards and pointing devices are accessed the same way, but this access is raw—no special functionality or abstractions are provided. Normally, keyboard input is ASCII-based. If you press the ‘a’ key, a value of 97 (ASCII for ‘a’) is retrieved. If you hold one of the shift keys down and pressing the ‘a’ key, the result is a value of 63 (the ASCII value for ‘A’). Furthermore, if you hold the “a” key down for a moment, the keyboard auto-repeat feature will be activated, resulting in multiple inputs from the keyboard, despite the fact that only one key press actually occurred. With DirectInput, none of these features are supported. Keyboard activity is not reported using ASCII values, keys like SHIFT, CTRL, and ALT have no special meaning, and there is no auto-repeat support. Instead of ASCII, DirectInput represents each key with an integer that has no relation to the key that it represents. In fact, these integers have more to do with the order in which keys normally appear on the keyboard. The Escape key, for example, is represented by the value 1. DirectInput uses macros to define symbols that can be used to identify keyboard input values: #define DIK_ESCAPE 0x01 The string “DIK” is used as a prefix for these macro names (short for DirectInput Key), so these values are often called DIK codes. If your game uses DirectInput to allow users to enter text that is to be treated as ASCII, (a player name, for example) then you have to convert the DIK codes to ASCII. The state of either one of the shift keys must be checked separately for each keystroke to determine if the user intended a lower or upper case character. While all of this may seem unnecessarily painful, it makes sense because DirectX is designed for all-out speed. By providing raw keyboard codes, and no special treatment of the SHIFT, CTRL, and ALT keys, DirectInput can report keyboard activity faster. This works well for games because most of the time games are interested in the state of specific keys, not the symbol stamped on the key itself. In other words, games usually assign their own meanings to keys, so any effort spend by DirectInput in interpreting the traditional meaning would be wasted. DirectInput’s support for pointing devices, or mice is less draconian because the meaning of mouse input is straightforward. The inputs in this category are either buttons or axes. Up to 8 buttons are supported per device. The axes for devices in this category are reported in relative mode by default, meaning that the values reflect changes in position as opposed to absolute coordinates. This works fine for many games, but DirectInput can be instructed to report absolute coordinates if desired. Introduction to C and C++ : Week 5: Page 7 of 33
  8. Programming with DirectInput can be fairly involved; especially the devices in the game controller category, but the information above will suffice for our purposes. Now let’s look at the classes that we’ll be using to access DirectInput. The Input Classes The DirectInput based classes that we’ll use in this course are taken from the GameInstitute DirectInput course Exploring The DirectInput 8 API, and are part of a class hierarchy that includes support for each category of input device. The class names are prefixed with “ei”, short for eInstitute—the name of GameInstitute’s parent corporation. This class hierarchy looks like this: The eiInputDevice class is an abstract base class that provides common functionality. Each DirectInput device category is supported through a derived class. We’ll be using the eiKeyboard and eiPointer classes to support the standard keyboard and mouse. Notice that the term pointer is used to describe the pointing device class. This has no bearing on the pointer data type, but is used because DirectInput is currently undergoing a terminology shift. The term mouse was used for this category, but is being changed to pointer to more accurately describe the category, as trackballs and tablets are not considered mice. Nevertheless, with the exception of the eiPointer class name, we’ll use the term mouse, or mice. There is one additional class that is necessary to use these classes: eiInputManager. This class performs the actual DirectInput initialization steps, and provides an interface that can be used to detect the various devices currently attached to the computer. The use of game controllers in particular requires the each device be detected before it can be initialized. Since we’re not using game controllers, we can use this class to initialize DirectInput by simply creating an instance of this class. Its constructor requires the window handle of the application window: eiInputManager* inputMgr = new eiInputManager( GetAppWindow() ); (Although this, and the following code appears outside of the context of any particular application, the GetAppWindow function used above is a GameApplication member function.) The next step is to create instances of the eiKeyboard and eiPointer classes: eiKeyboard* keyboard = new eiKeyboard(); eIPointer* mouse = new eiPointer(); Next, the Attach member function must be called for each input device object. This function takes three arguments, but only the first two are necessary for our purposes. The first is an identifier that can be used to indicate the exact device to which this object is representative. DirectInput supports any number of devices, so this argument can be used to distinguish between multiple devices in the same category. Not Introduction to C and C++ : Week 5: Page 8 of 33
  9. long ago, a computer was virtually guaranteed to have only one keyboard and one mouse, but with the advent of USB devices, this is no longer the case. Nevertheless, providing zero for the first Attach argument indicates the system, or default device. The second argument is an integer that indicates the size of the input buffer that should be allocated for the device. An ideal value for this number is one that is large enough to store all of the input data generated by the device between the times when this data is retrieved, but not so large that excessive memory is wasted. Using a value of 10, for example, and checking the device for input once per second, means that if more than 10 changes to the input device are reported in a single 1 second interval, that some of the data will be lost because there’s no place for DirectInput to store it. Games must check for user input much more often than once per second, so a value of 50 is plenty—for keyboards at least. Pointing devices, however, typically generate much more data than keyboards, so using a larger buffer is advisable: keyboard->Attach( 0, 50 ); mouse->Attach( 0, 256 ); Now each device is ready for data collection, so each of these objects can be used to check for and retrieve user input. This involves the GetEvent function and the DIDEVICEOBJECTDATA structure. Checking the keyboard look like this: DIDEVICEOBJECTDATA event; while (keyboard->GetEvent( event )) { // check DIK code and key state } The DIDEVICEOBJECTDATA structure is defined by DirectInput, and is used to communicate any change in the state of an input device. This can be the pressing or releasing of a key or button, or a change in a device axis. The GetEvent member function retrieves the next event in the device buffer, or returns false if no data has been received. To determine the nature of a retrieved event, two DIDEVICEOBJECTDATA members must inspected: dwOfs and dwData. The “dw” prefix indicates that each of these data members is a DWORD. The dwOfs data member indicates the button or axis that has changed. (“Ofs” stands for offset, which is how DirectInput reports device elements.) For keyboards, this value is the DIK code of the affected key. dwData indicates the new state for the device element in question. For keys and buttons, the state of the eighth bit in this value indicates whether the key or button is up or down. Checking the value of this bit requires the bit-wise AND operator (&). In the previous lesson we learned that the bit-wise OR operator (|) can be used to combine two values at the binary level. The AND operator can be used to determine the state of specific bits, like this: if (event.dwData & 0x80) // the key has been pressed else // the key has been released Using the bit-wise AND operator and the hex value 80, the state of the eighth bit is determined. If this bit is set (if it is 1 as opposed to 0), then the key has been pressed, otherwise it has been released. Introduction to C and C++ : Week 5: Page 9 of 33
  10. The determination of which key has been affected is simply a matter of comparing the dwData member to one or more of the DIK codes, like this: if (event.dwOfs == DIK_F2) { if (event.dwData & 0x80) // the F2 key has been pressed else // the F2 key has been released } Retrieving input from a pointing device works much the same way. The only difference is that input events can contain button or axis data. Checking for the first button (the left button on a mouse) looks like this: DIDEVICEOBJECTDATA event; while (mouse->GetEvent( event )) { if (event.dwOfs == DIMOFS_BUTTON0) { if (event.dwData & 0x80) // mouse button 0 has been pressed else // mouse button 0 has been released } } Changes in pointing device axes can be detected using the DIMOFS_X, DIMOFS_Y, and DIMOFS_Z symbols to identify each axis: DIDEVICEOBJECTDATA event; while (mouse->GetEvent( event )) { if (event.dwOfs == DIMOFS_X) { // event.dwData contains axis data } } Strangely, although DirectInput defines the dwData member as a DWORD, negative values are valid for axis data. (Recall that a DWORD is a type definition for unsigned long—an unsigned data type.) In fact, C and C++ do very little to discourage integer types from being misused this way. Negative values can be assigned to unsigned integers, and no compiler or runtime errors will result. For example: DWORD dw = -1; This initialization compiles and runs without any trouble. The result is a value of 4,294,967,295. This is because the actual value contained by a signed or unsigned data type is open to interpretation. If the above value were an int instead of a long, then the new value would undisputedly be –1. But because a DWORD is used, the value is in fact the maximum value that an unsigned integer can store. Consider this code (which, because it uses cout, is intended for use in a console application): Introduction to C and C++ : Week 5: Page 10 of 33
  11. unsigned long ul = -1; cout
  13. 220 DIK _RWIN 221 DIK _APPS The DirectInputScroll Sample To demonstrate the use of the DirectInput classes, and to highlight an important difference between using Win32 and DirectInput for user input processing, let’s solve the Win32Scroll exercise from the previous lesson using DirectInput instead of Windows messages. We’ll call this sample DirectInputScroll. The first step is to add three data members to the application class: eiInputManager* inputMgr; eiKeyboard* keyboard; eiPointer* mouse; Next, the AppBegin member function must be modified to initialize and configure the keyboard and mouse: virtual bool AppBegin() { lastTime = timeGetTime(); showTimerTick = false; showMouseMove = false; Log("F2 = toggle timer display"); Log("F3 = toggle mouse display"); inputMgr = new eiInputManager( GetAppWindow() ); keyboard = new eiKeyboard(); mouse = new eiPointer(); keyboard->Attach( 0, 50 ); mouse->Attach( 0, 256 ); return true; } Next we’ll modify the AppUpdate function to check each device for new input events. But, instead of cluttering this function with the message event retrieval loops that we need, we’ll use two new member functions: virtual bool AppUpdate() { CheckKeyboard(); CheckMouse(); DWORD timeNow = timeGetTime(); if (timeNow > lastTime + TimerInterval) { if (showTimerTick) Log("tick"); lastTime = timeNow; } return true; } Introduction to C and C++ : Week 5: Page 13 of 33
  14. These two functions contain the input event retrieval loops that we looked at earlier. The difference is that now the F2 and F3 keys are used to toggle the timer and mouse display, and the Log function to display the status of each event: void CheckKeyboard() { DIDEVICEOBJECTDATA event; while (keyboard->GetEvent( event )) { if ( event.dwData & 0x80 ) { if (event.dwOfs == DIK_F2) { showTimerTick = !showTimerTick; if (showTimerTick) Log("timer tick display on"); else Log("timer tick display off"); } else if (event.dwOfs == DIK_F3) { showMouseMove = !showMouseMove; if (showMouseMove) Log("mouse move display on"); else Log("mouse move display off"); } else Log("Keydown %d", event.dwOfs); } } } Notice that the state of the key is checked before the key is identified. In this case we’re only interested in events describing a key press. Key releases are ignored. Here’s the CheckMouse function: void CheckMouse() { DIDEVICEOBJECTDATA event; while (mouse->GetEvent( event )) { if (showMouseMove) { if (event.dwOfs == DIMOFS_X) Log("mouse X axis = %d", event.dwData); if (event.dwOfs == DIMOFS_Y) Log("mouse Y axis = %d", event.dwData); } } } For the Win32Scroll exercise, it wasn’t necessary to implement the AppEnd function—there wasn’t anything to release. But now we need this function to release the DirectInput objects: Introduction to C and C++ : Week 5: Page 14 of 33
  15. virtual bool AppEnd() { delete keyboard; delete mouse; delete inputMgr; return true; } These additions are all that is required to add DirectInput support. But, if the code above is added to the Win32Scroll exercise solution, the output is very interesting: user input events are reported twice. Why? Because the Windows user input messages are still being delivered to our application. The use of DirectInput doesn’t prevent Windows from dispatching these messages, so the application responds to them in addition to the DirectInput messages. To make the migration from Win32 to DirectInput complete, the KeyUp, KeyDown, and MouseMove functions can simply be removed. The resulting executable looks like this: If you compare the Win32Scroll solution with the DirectInputScroll sample, you’ll notice a few differences. First, DirectInput reports changes to each axis separately, so each axis event displayed contains only part of the information about mouse position. Second, the values displayed by DirectInputScroll indicate motion, and not the position of the mouse. And finally, DirectInput allows device information to be gathered even if the mouse cursor is not currently inside the application window. Win32Scroll stops reporting mouse events as soon as the cursor leaves the window. Before we move on, there is one additional issue. By default, DirectInput will not allow user input to be collected if the application loses focus. If another application becomes active, requests for input device events fail. If the application regains focus, then DirectInput resumes normal behavior. Furthermore, DirectInput can act strangely when requests for input events are made when another application is active. The best solution is simply to stop making requests when the application is in the background. Introduction to C and C++ : Week 5: Page 15 of 33
  16. This can be accomplished by responding to the WM_ACTIVATE message. Windows delivers this message to each application any time an application gains or loses the focus. For the DirectInputScroll sample, the GameApplication class has been extended to support two virtual functions that get called when the WM_ACTIVATE message arrives: AppActivate and AppDeactivate. To avoid making DirectInput requests that are bound to fail, we’ll add a Boolean data member to the application class, and set it with these two functions: virtual bool AppActivate() { appActive = true; return true; } virtual bool AppDeactivate() { appActive = false; return true; } We’ll then modify the AppUpdate function so that user input is processed only if appActive is true: virtual bool AppUpdate() { if (appActive) { CheckKeyboard(); CheckMouse(); } DWORD timeNow = timeGetTime(); if (timeNow > lastTime + TimerInterval) { if (showTimerTick) Log("tick"); lastTime = timeNow; } return true; } Now that we know how to use DirectInput, we’re ready to introduce Direct3D into our applications. Direct3D Basics Of the APIs that comprise DirectX, Direct3D is the most complex. The API itself isn’t so terribly large, but using it requires an understanding of several subjects that we can’t possibly cover in one lesson. However, through the power of encapsulation, we can use Direct3D without fully understanding how it works. At the level at which we’ll be using Direct3D, four ingredients are necessary to produce 3D graphics: • The Direct3D device • A light source • At least one 3D model Introduction to C and C++ : Week 5: Page 16 of 33
  17. • A camera The first, and more important, is the Direct3D device. This construct essentially represents the graphical hardware. The device is created by selecting a number of settings, and instructing Direct3D to prepare the display hardware to produce output. With Direct3D, this step is notoriously gruesome, but it has gotten simpler with recent versions of DirectX. Regardless, this step will be performed for us by a class that will be introduced shortly. We won’t worry about the details, but we do need to discuss some conceptual topics. The actual drawing of 3D shapes and surfaces is called rendering. During initialization, at least two buffers must be created. One is for rendering the output that will make up the next scene, and the other is for the actual display of the scene. These two buffers are interchangeable. While Direct3D is drawing the next scene into one of the buffers, the other is being displayed. When rendering is complete, the two are swapped; the buffer containing the new scene becomes visible, and the visible buffer is made available for the rendering of the next scene. This is called double buffering, or page flipping, and is necessary in order to eliminate the flicking effect that is inevitable if only one buffer is used. The buffer that is currently displayed is often called the front buffer, and the buffer that is currently available for rendering is called the back buffer. Rendering graphics therefore involves drawing shapes onto the back buffer, requesting a page flip, and then repeating the process. Conceptually, the front and back buffers are part of the Direct3D device, and are both created for us during initialization, so we don’t need to know how that is done. The next ingredient is a light source. Without a light source, each scene would be completely black. There are several different kinds of light sources, but the two basic types (and the least computationally demanding), are the ambient and directional light sources. An ambient light source distributes light over the scene evenly. Ambient light sources are useful for adjusting the overall light level of a scene, but used by themselves, result in flat and lifeless images. Alternatively, a directional light source causes each 3D element to be lighted differently, depending on how it is oriented. If a surface is facing a directional light source, it will be brightly lit, but if it is facing away, it won’t be lit at all. Directional light sources highlight the curves and surfaces of 3D shapes, making them much more convincing. A 3D shape is called a model. The vast majority of 3D models are represented as a collection of polygons. In fact, Direct3D is essentially a polygon renderer. Complex shapes and curves are actually represented by hundreds or even thousands of polygons. Each polygon is planar, or flat—there are no curves. Representing curved surfaces effectively therefore requires many small polygons. Curved surfaces are an illusion--one that is helped with rendering algorithms that blend the sharp corners inherent in shapes composed of flat polygons. One of the disadvantages of 3D graphics, especially for programmers not working with a team of artists, is that the construction of 3D models is a science in itself. Some of the models that are used in modern games contain thousands of polygons, and are the result of months and even years of attention by professional 3D artists. For our 3D applications, we’ll be using simple, easily obtainable models. The final ingredient is the camera. This is a data structure that determines the location and angle from which the 3D objects will be viewed. Configuring a camera requires that its location, orientation, and field of view (FOV) be assigned during initialization. Rendering 3D graphics requires each of these ingredients. The Direct3D device must first be initialized. One or more light sources are then created, and a camera is initialized. The 3D models can then be drawn into the back buffer. Finally, a page flip make the scene visible. Animation is just a matter of repeatedly Introduction to C and C++ : Week 5: Page 17 of 33
  18. drawing to the back buffer and performing a page flip, each time with the 3D models in slightly different locations. The Direct3D Classes To use Direct3D in our applications, we’ll use two classes: one to represent the Direct3D device, and one for each model. As you’ll see, the Direct3D device is not entirely encapsulated. For example, we’ll use it to configure the camera and light sources, rather than creating classes to handle these tasks. Overall, however, the bulk of the required Direct3D functionality is provided for us by the D3DApplication class. The D3DApplication class represents the device, but moreover, it represents any application that uses Direct3D. This is possible because D3DApplication is derived from the GameApplication class. D3DApplication extends the GameApplication class to provide the initialization and maintenance steps required by applications that use Direct3D. Instead of deriving our application classes from GameApplication, we’ll derive from D3DApplication, inheriting the functionality and interfaces provided by both classes. The resulting class hierarchy looks like this: The interface that the D3DApplication class provides in order to support 3D applications consists of virtual and non-virtual member functions. These are the non-virtual functions: class D3DApplication : public GameApplication { protected: HRESULT InitializeDirect3D(); HRESULT ShutdownDirect3D(); HRESULT Reset3DEnvironment(); HRESULT Clear3DBuffer(D3DCOLOR); HRESULT Present3DBuffer(); LPDIRECT3DDEVICE8 Get3DDevice(); }; The InitializeDirect3D function performs the non-application specific Direct3D initialization steps. We’ll call this function in the AppBegin function. The ShutdownDirect3D function releases the resources initialized by InitializeDirect3D. We’ll call this function in AppEnd. It is occasionally necessary to perform a partial re-initialization of Direct3D. In particular, when the application loses focus, Windows forces Direct3D to release key resources, such as the front and back Introduction to C and C++ : Week 5: Page 18 of 33
  19. buffers. This is done so that other applications can use the display hardware. When the application regains focus, it’s necessary to restore these resources—a task performed by the Reset3DEnvironment function. The Clear3DBuffer function is used to erase the contents of the back buffer immediately before each new scene is rendered. If the application fails to call this function, the new scene will be rendered over the scene previously displayed. This function takes an argument of the D3DCOLOR data type, which Direct3D defines for the specification of colors. In this case the color provided will be used to fill the back buffer, overwriting the scene previously rendered into the back buffer. After a new scene has been rendered into the back buffer, calling Present3DBuffer performs the page flip required to make the new scene visible, and recycles the currently displayed front buffer for use as the new back buffer. Finally, the Get3DDevice function is a simple accessor function that can be used to retrieve the actual Direct3D device that the class represents. This is necessary in order to perform some of the required steps for rendering new scenes into the back buffer. The functions discussed above are non-virtual. They’re meant to be called, but not overridden. The D3DApplication class provides some virtual functions as well, to allow applications to perform application specific steps during initialization and the restoration required after the application regains focus: protected: virtual HRESULT InitDeviceObjects(); virtual HRESULT RestoreDeviceObjects(); virtual HRESULT DeleteDeviceObjects(); The D3DApplication class implementation calls the InitDeviceObjects function to allow the application to prepare the 3D models for use in rendering. Typically this means reading data from disk. For Direct3D, the standard file format for 3D models is the .x file. We’ll use files in this format to load the models for our samples. The RestoreDeviceObjects function is called once at startup, and again each time a reset procedure is necessary, as initiated by the Reset3DEnvironment function. Finally, the DeleteDeviceObjects should be overridden to release the application models, and is called during reset procedures and at shutdown. To represent each 3D model, we’ll use a class called D3DModel, whose public interface looks like this: class D3DModel { public: bool Load(LPCTSTR xFileName, LPDIRECT3DDEVICE8 device); void SetLocation(float x, float y, float z); void Render(); void RestoreModel(); void Release(); }; The Load member function, given the name of a .x file containing 3D model information and a pointer to the Direct3D device, loads a model from disk. If the file can’t be loaded, Load returns false. The D3DModel class looks for these files in the current working directory, so the provided file name must be located in this directory. Normally this is the same directory where the executable is located, but when Introduction to C and C++ : Week 5: Page 19 of 33
  20. projects are executed within Visual C++ IDE, the working directory is the project directory instead of the Debug or Release directory where the executable is actually located. This means that if you want to execute the following samples outside of Visual C++, you must either copy the executable into the project directory, or copy the necessary .x files into the Debug or Release directory. The SetLocation function assigns a position to the model in 3D space. The x, y, and z arguments are floating point values that indicate the new position. The x indicates the horizontal position, with increasing values moving objects to the right. The y parameter controls vertical position. Increasing values move the object up, (the opposite of most PC 2D coordinate systems), and the z parameter controls the third axis—the distance of the object from the camera. Increasing values move the object farther to the camera. The meaning of these values varies and is not universal. 3D programmers use a number of axis schemes; this is just one way of doing things. The model represented by D3DModel objects is drawn to the back buffer with the Render function. This operation requires access to the Direct3D device, but because the address of the device is provided to the object when a model is loaded, the Render function takes no arguments. The RestoreModel function is used during a device reset procedure to recover any resources lost by a loss of application focus, and the Release function is used at shutdown to release the resources that the D3DModel class uses. The Circle Sample Our first foray into Direct3D is a simple, non-interactive sample called Circle, which animates two spherical meshes along a circular path. Like the non-DirectX Win32 samples presented previously, the Circle sample is equipped with a logging feature that allows text to be displayed in a scrolling log. The application-specific code is provided by a class called simple CircleApp, and is derived from the D3DApplication class. We’ll build this class as we go along, starting with this definition: class CircleApp : public D3DApplication { public: CircleApp(); ~CircleApp(); }; This definition includes a constructor and a destructor, both declared as public. Interestingly, these are the only member functions that need to be public at the application level. The remainder of the member functions and data members can all be private because the entry point and callback functions necessary to execute the application code are provided by the base class. Remember, the D3DApplication class is derived from the GameApplication class, so CircleApp inherits the WinMain and WndProc functions necessary for Win32 applications, in addition to the Direct3D functions provided by D3DApplication. The constructor and destructor must be public because these are the only member functions that are called from outside the class. We’ll look at how these functions are implemented later. First, let’s add the necessary data members: class CircleApp : public D3DApplication { public: CircleApp(); ~CircleApp(); Introduction to C and C++ : Week 5: Page 20 of 33
Đồng bộ tài khoản