DirectX 9.0c游戏开发手记之RPG编程自学日志之4: Preparing for the Book (准备工作)(下)

时间:2021-11-21 18:58:56

        本文由哈利_蜘蛛侠原创,转载请注明出处!有问题请联系2024958085@qq.com

 

        上一期我们讲了第一章的第4-6节。内容挺多。这次的内容也不少。

        为了方便,我把本章的各小节标题再次列在下面:

1、 DirectX

2、 Setting Up the Compiler (设置编译器)

3、 General Windows Programming (一般的Windows编程)

4、 Understanding the Program Flow (理解程序流)

5、 Modular Programming

6、 Statesand Processes

7、 Handling Application Data (处理应用程序的数据)

8、 Buildingan Application Framework (构建一个应用程序框架)

9Structuring a Project (结构化一个项目)

10Wrapping UpPreparations (总结准备工作)

 

 

7部分:Handling Application Data (处理应用程序的数据)

 

        下面仍旧是原文翻译部分:

 

---------------------------------------------------------------------------------------------------------------------------------

        所有的应用程序都以这样或那样的形式使用数据,尤其是游戏。你知道你过去三周里玩过的最喜欢的游戏里的游戏角色吧?那个角色的每一个比特的信息都是应用程序数据——他的名字、生命值(hit points)和经验等级,以及他携带的盔甲和武器。每当你退出游戏时,你的角色的数据就会被保存,等待着你以后玩游戏时进行载入。

 

1.7.1 使用数据打包

 

         处理应用程序数据的最简单的方法是建立一个可以操控保存和载入数据的数据打包系统。通过创建一个包含了数据缓冲区(a buffer of data)的对象,你可以添加一些函数来为你执行保存和载入的操作。为了看到我说的是什么意思,瞧一瞧下面的类:(注意:如果想将下面代码放在某个头文件中的话,要在开头加上#include <fstream>#include “windows.h”这两个语句。另外这里有一点小问题,详情参见我的修改版——稍后奉上。)

class cDataPackage
{
protected:
// Data buffer and size
void *m_Buf;
unsigned long m_Size;

public:
cDataPackage() { m_Buf = NULL; m_Size = 0; }
~cDataPackage() { Free(); }

void *Create(unsigned long Size)
{
// Free a previously created buffer
Free();
// Allocate some memory and return a pointer
return (m_Buf = (void*)new char[(m_Size = Size)]);
}

// Free the allocated memory
void Free() { delete m_Buf; m_Buf = NULL; m_Size = 0; }

BOOL Save(char *Filename)
{
FILE *fp;

// Make sure there’s something to write
if (m_Buf != NULL && m_Size) {
// Open file, write size and data
if ((fp = fopen(Filename, "wb")) != NULL) {
fwrite(&m_Size, 1, 4, fp);
fwrite(m_Buf, 1, m_Size, fp);
fclose(fp);
return TRUE;
}
}
return FALSE;
}

void *Load(char *Filename, unsigned long *Size)
{
FILE *fp;

// Free a prior buffer
Free();
if ((fp = fopen(Filename, "rb")) != NULL) {
// Read in size and data
fread(&m_Size, 1, 4, fp);
if ((m_Buf = (void*)new char[m_Size]) != NULL)
fread(m_Buf, 1, m_Size, fp);
fclose(fp);

// Store size to return
if (Size != NULL)
*Size = m_Size;
// return pointer
return m_Buf;
}
return NULL;
}
};


 

        cDataPackage类只包含4个你可以使用的函数(事实上有6个,如果包含构造函数和析构函数的话)。第一个你想要调用的函数是Create,它会根据你给它的大小而分配一块内存。而Free函数则释放这块内存。至于Save和Load函数,它们做的事情从它们的名字就可以看出来——保存数据块到硬盘和从硬盘中载入数据块,根据你提供的文件名。注意到Create函数和Load函数都返回一个指针。这个指针指向数据缓冲区,然后你可以把它转换为指向你自己的数据结构的指针。

 

 

1.7.2 测试数据打包系统

 

        想象你想创建一个数据打包系统来储存一列名字,然后你想要使用一个自定义的结构来处理名字。通过建立一个数据包并把返回的指针转换成指向一个结构的指针(原文是castingthe return pointer to a structure,应该有误,应为casting the returnpointer to a pointer to a structure,你可以快速地处理这个名字,如下所示:

 

// A structure to contain a name
typedef struct {
char Name[32];
} sName;


int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, \
LPSTR szCmdLine, int nCmdShow)
{
cDataPackage DP;
DWORD Size;

// Create the data package (w/64 bytes) and get the
// pointer, casting it to an sName structure type.
sName *Names = (sName*)DP.Create(64);

// Since there are 64 bytes total, and each name uses
// 32 bytes, then I can have 2 names stored.
strcpy(Names[0].Name, “Jim”);
strcpy(Names[1].Name, “Adams”);

// Save the names to disk and free the data buffer
DP.Save(“names.dat”);
DP.Free();

// Load the names from disk. Size will equal 64
// when the load function returns.
Names = (sName*)DP.Load(“names.dat”, &Size);

// Display the names
MessageBox(NULL, Names[0].Name, “1st Name”, MB_OK);
MessageBox(NULL, Names[1].Name, “2nd Name”, MB_OK);

// Free up the data package
DP.Free();

return 0;
}


        仔细观察正在使用中的数据缓冲区,你会发现在这被使用的64个字节中,两个32字节的块分别用来储存各自的名字,如下图所示:

DirectX 9.0c游戏开发手记之RPG编程自学日志之4: Preparing for the Book (准备工作)(下)

        使用数据打包能够做到的事情是相当多的。通过创建一些小的数据包对象,你可以把它们转换成你想要的所有指针,使得你的应用程序数据位于一个单一的、从容的可以自行进行保存和载入操作的对象之中。

 

---------------------------------------------------------------------------------------------------------------------------------

 

        好啦,这一节讲完了。其实并不难,主要就是文件的读写操作啦。不过很遗憾的是,书上给出的代码有些问题,不能直接拿来用。另外由于年代久远的关系,所以有一些函数现在被微软认为是“不安全的”。于是我把这一节的代码弄成了完整的程序,并且进行了适当的改动,现在应该没有任何问题了。代码的下载地址如下:

点击打开链接

 

        注意上述是一个Win32程序。

 

 

        下面我们来继续讲述第8节。讲完这一节后,这一章的内容就基本结束了。


8部分:Building an Application Framework(建立一个应用程序框架)

 

        下面仍旧是原文翻译部分:

 

---------------------------------------------------------------------------------------------------------------------------------

        我确定你会同意这一点的:每次都要一遍又一遍地重新输入同样的代码——创建窗口的代码,画图形的代码,播放声音的代码……你懂的——在你每次开始一个新项目的时候,这是一件烦人的事情。为什么不去创建一个包含这些函数的、你可以直接插入到新项目中的库文件,使得你有更多的时间去编写实际的应用程序呢?

        这正是应用程序框架背后的思想。在基本层面上,框架应该包含初始化应用程序和各种引擎(图形、输入、网络以及声音)、操纵初始化工作、每帧例程和关闭函数的代码。使用modular-coding技术也会有帮助,因为那些大的组件,比如引擎,可以被包含在单独的对象之中。

        我们现在的目标是建立一个简单的项目,你可以以它为基础编写你的应用程序。开始一个新项目并为之命名为framework(或者其他的描述性名称)。在这个项目里,你创建一个文件叫做WinMain.cpp。这个文件爱你代表你的应用程序的入口点。

        WinMain.cpp文件会尽可能地小,仅仅包含初始化窗口所需要的代码。瞧一瞧下面的WinMain.cpp文件的代码,这是我一般用于我的基础框架的:

 

// Include files
#include <windows.h>
#include <stdio.h>
#include <stdarg.h>

// Main application instances
HINSTANCE g_hInst; // Global instance handle
HWND g_hWnd;// Global window handle

// Application window dimensions, type, class and window name
#define WNDWIDTH400
#defineWNDHEIGHT400
#define WNDTYPEWS_OVERLAPPEDWINDOW
const char g_szClass[] = "FrameClass";
const char g_szCaption[] = "FrameCaption";

// Main application prototypes

// Entry point
int PASCAL WinMain(HINSTANCE hInst, HINSTANCE hPrev, \
LPSTR szCmdLine, int nCmdShow);

// Function to display an error message
void AppError(BOOL Fatal, char *Text, ...);

// Message procedure
long FAR PASCAL WindowProc(HWND hWnd, UINT uMsg, \
WPARAM wParam, LPARAM lParam);

// Functions to register and unregister windows' classes
BOOL RegisterWindowClasses(HINSTANCE hInst);
BOOL UnregisterWindowClasses(HINSTANCE hInst);

// Function to create the application window
HWND CreateMainWindow(HINSTANCE hInst);

// Functions to init, shutdown, and handle per-frame functions
BOOL DoInit();
BOOL DoShutdown();
BOOL DoPreFrame();
BOOL DoFrame();
BOOL DoPostFrame();

int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, \
LPSTR szCmdLine, int nCmdShow)
{
MSG Msg;

// Save application instance
g_hInst = hInst;

// Register window classes - return on FALSE
if (RegisterWindowClasses(hInst) == FALSE)
return FALSE;

// Create window - return on FALSE
if ((g_hWnd = CreateMainWindow(hInst)) == NULL)
return FALSE;

// Do application initialization - return on FALSE
if (DoInit() == TRUE)
{
// Enter the message pump
ZeroMemory(&Msg, sizeof(MSG));
while (Msg.message != WM_QUIT)
{
// Handle Windows messages (if any)
if (PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
else
{
// Do pre-frame processing, break on FALSE return value
if (DoPreFrame() == FALSE)
break;

// Do per-frame processing, break on FALSE return value
if (DoFrame() == FALSE)
break;

// Do post-frame processing, break on FALSE return value
if (DoPostFrame() == FALSE)
break;
}
}
}

// Do shutdown functions
DoShutdown();

// Unregister window
UnregisterWindowClasses(hInst);

return TRUE;
}

BOOL RegisterWindowClasses(HINSTANCE hInst)
{
WNDCLASSEX wcex;

 

    (下面两段的注意事项疑似顺序弄反了,所以我就把它们掉了个个儿。)

        注意:

        你使用RegisterWindowClasses函数来注册一个窗口类(通过RegisterClassEx函数)。使用你的应用程序的HINSTANCE值(在你的WinMain函数中传递给你)作为唯一的参数来调用RegisterWindowClasses函数。

// Create the window class here and register it
wcex.cbSize = sizeof(wcex);
wcex.style = CS_CLASSDC;
wcex.lpfnWndProc = WindowProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInst;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = NULL;
wcex.lpszMenuName = NULL;
wcex.lpszClassName = g_szClass;
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

if (!RegisterClassEx(&wcex))
return FALSE;

return TRUE;
}

BOOL UnregisterWindowClasses(HINSTANCE hInst)
{
// Unregister the window class
UnregisterClass(g_szClass, hInst);
return TRUE;
}

HWND CreateMainWindow(HINSTANCE hInst)
{
HWND hWnd;


        注意:

        你使用CreateMainWindow函数来创建及显示一个具有适当大小和类型(由WNDWIDTH, WNDHEIGHT和WNDTYPE宏所确定)的窗口。只需要把你的应用程序的HINSTANCE值作为唯一的参数传递给CreateMainWindow就OK了。

 

// Create the Main Window
hWnd = CreateWindow(g_szClass, g_szCaption, \
WNDTYPE, 200, 100, WNDWIDTH, WNDHEIGHT, \
NULL, NULL, hInst, NULL);

if (!hWnd)
return NULL;

// Show and update the window
ShowWindow(hWnd, SW_NORMAL);
UpdateWindow(hWnd);

// Return the window handle
return hWnd;
}

void AppError(BOOL Fatal, char *Text, ...)
{
char CaptionText[12];
char ErrorText[2048];
va_list valist;

// Build the message box caption based on fatal flag
if (Fatal == FALSE)
strcpy(CaptionText, "Error");
else
strcpy(CaptionText, "Fatal Error");

// Build variable text buffer
va_start(valist, Text);
vsprintf(ErrorText, Text, valist);
va_end(valist);

// Display the message box
MessageBox(NULL, ErrorText, CaptionText,
MB_OK | MB_ICONEXCLAMATION);

// Post a quit message if error was fatal
if (Fatal == TRUE)
PostQuitMessage(0);
}

// The message procedure - empty except for destroy message
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, \
WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}

return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

BOOL DoInit()
{
// Perform application initialization functions here
// such as those that set up the graphics, sound, network, etc.
// Return a value of TRUE of success, FALSE otherwise.
return TRUE;
}

BOOL DoShutdown()
{
// Perform application shutdown functions here
// such as those that shut down the graphics, sound, etc.
// Return a value of TRUE of success, FALSE otherwise.
return TRUE;
}

BOOL DoPreFrame()
{
// Perform pre-frame processing, such as setting up a timer.
// Return TRUE on success, FALSE otherwise.
return TRUE;
}

BOOL DoFrame()
{
// Perform per-frame processing, such as rendering.
// Return TRUE on success, FALSE otherwise.
return TRUE;
}

BOOL DoPostFrame()
{
// Perform post-frame processing, such as time synching, etc.
// Return TRUE on success, FALSE otherwise.
return TRUE;
}

 

        前面所给出的框架代码初始化了一个窗口并且进入一个等待应用程序摧毁指令的消息泵。所有的用于操控建立和关闭应用程序的方方面面的函数都各司其职。注意到创建的应用程序窗口没有背景;这对于使用DirectX图形——我们在第2章“用DirectX图形进行绘制”中会学习到——来说是合适的。

        为了改变窗口的设置,例如宽度、高度或者类型,你可以改变代码顶部的定义。至于窗口类和标题也是同理,它们定义在代码开头的两个const变量中。

        注意到我添加了一个似乎从没有被调用的函数。这个函数是AppError,我喜欢用来向用户显示错误信息。将TRUE值传递给Fatal参数会强制Windows来关闭应用程序窗口,而如果值为FALSE的话则会允许程序继续运行。

        在每个函数中,你会看到关于函数做什么的注释——插入代码来进行对象的初始化、图形载入、帧前处理等等就是你的事情了。

 

---------------------------------------------------------------------------------------------------------------------------------

 

 

        刚才的那一节虽然篇幅巨大,但是其实没什么难点。后面的几节内容就更简单了,我们赶紧把它们搞定吧!

 

 

9部分:Structuring a Project (结构化一个项目)

 

        原文翻译:

 

---------------------------------------------------------------------------------------------------------------------------------

 

        在每一个项目开始的时候,你面临着很多选择。组成你的应用程序的各种各样的函数既可以合并在一个源文件内,也可以根据它们各自的功能分散在几个文件中。例如,图形函数放在图形源文件中,声音函数放在声音源文件中,等等。只要你在你的项目中包含进了这些文件并且为每一个文件提供一个头文件,那么就没什么可担心的。(其实在C++中,因为包含头文件而需要注意的问题可不少哦!)

   

        我总是以WinMain.cpp文件开始我的程序。这个文件包含了应用程序的入口点。它初始化窗口并且调用所有必要的建立、每帧和关闭函数(而这些函数可能位于不同的源文件中)。事实上,我在整本书中都是用的这种方法。

        第6章“创建游戏核心”引入了一系列的类对象,我使用这些来加快游戏开发速度。你可以在你的项目中包含进来这些文件(以及它们各自的头文件),它们根据其各自的功能(图形、声音、网络等等)而分隔开来了。你所需要做的全部就是建立一个你想要使用的类实例然后一路披荆斩棘!

        最关键的一点就是:把你整个的项目组织成易于使用的组件,以免你焦头烂额。

 

---------------------------------------------------------------------------------------------------------------------------------

 

 

 

10部分:Wrapping Up Preparations (总结准备工作)

 

        原文翻译:

 

---------------------------------------------------------------------------------------------------------------------------------

 

        好了你已经搞定了所有事情!你安装了DirectX,设置好了你的编译器来使用它,并且给自己弄了一个完整的应用程序框架来便于今后的使用。不仅仅如此,你现在直到如何处理线程、临界区、状态、processes和数据包。我希望你对自己满意!我知道我是对自己满意的。

        有了你在这一章读到的知识,你应该准备好踏上RPG(以及任何其他类型的游戏)编程世界的入门之路了!祝贺你,并祝你好运!

 

---------------------------------------------------------------------------------------------------------------------------------

 

    好了,到这里为止,这第1章的内容可算是讲完了。下一期我们将开始将第2章,关于3D图形编程的。那一章的内容很多,应该要分好多期了。不过那一章的内容要比这一章简单,大家敬请期待!


最后送上之前的framework的源代码。由于作者的代码年代久远,所以我进行了适当的符合时代的更新:

点击打开链接