DIRECTX中独占模式与窗口模式的切换

最近在GAMEDEV上发现了这篇文章,觉得挺不错的,特此翻译过来,有不对的地方希望大家指正

DIRECTX中独占模式与窗口模式的切换
介绍
让你的游戏能够在独占(全屏)模式与窗口模式下运行应该很简单,但想要让它合理且优雅的运行就要多做些工作了.在这篇文章中,我将用业界十分常用的C++语言来讲解这方面的技术,如果你想,可以用类把这个例子封装起来以便于使用.
我假设你已熟悉独占模式下的DirectDraw的设置与使用,这里我不再赘述,让我们开始吧!
设计
 DirectDraw窗口模式下的初始化有好多与独占模式不同.最好的方法是在你程序的开始就创建DirectDraw对象,第二步再创建所有表面,设置协调层级和显示模式,初始化你需要的变量,等等.独占模式与窗口模式的不同都体现在这第二步中,所以,你的函数可以写成这样:

void CreateDirectDraw();
void DestroyDirectDraw();
和 
void CreateSurfaces(bool bExclusive, int nWidth, int nHeight, int nBPP);
void DestroySurfaces();
第一部分(CreateDirectDraw and DestroyDirectDraw)分别创建和销毁DirectDraw对象.你自己应该可以完成的.第二部分(CreateSurfaces and DestroySurfaces)解决所有除去创建和销毁DirectDraw对象以外的事.看看参数 bExclusive,它表明创建一个独占模式的表面或窗口模式的表面以及相关的各个对象. 参数 width, height,bpp用以描述显示模式(当bExclusive为true时)
我们需要稍微改变游戏循环以正确处理窗口模式.为了正常改变窗口模式我们增加了一个函数:
void SwitchMode(bool bExclusive, int nWidth, int nHeight, int nBPP);
继续来看如何实现这此些函数!

CreateSurfaces 
我们把这个函数分成两部分,分别实现独占模式和窗口模式下的DirectDraw的初始化,如下:
if( bExclusive )
{
    // exclusive code

    // save the mode
    g_bExclusive = bExclusive;
}
else
{
    // windowed code

    // save the mode
    g_bExclusive = bExclusive;
}
你还需要创建一个全局变量g_bExclusive用以标志当前的模式,在游戏循环中要用到它的.请明确g_bExclusive的重要性,我们借此追踪模式
你可以把你以前的代码放到exclusive部分,用参数nWidth, nHeight, 和 nBPP设置显示模式等.然后让我们一起来完成windowed部分的代码(我用了几个函数来实现,都将它们分成了两部分,就像你在这个函数里看到的一样)
就和我前面提到的一样,当这个函数被调用时, DirectDraw已经创建,所以初始化DirectDraw的下一步是通过lpDD->SetCooperativeLevel()来设置协调层级. 把主窗口的句柄和DDSCL_NORMAL作为参数传给它:
lpDD->SetCooperativeLevel(hMainWnd, DDSCL_NORMAL);
如果你想使用多线程,把标识DDSCL_MULTITHREADED与DDSCL_NORMA相与传给它就是了,但注意:在窗口模式下不能设置显示模式!所以下一步是创建主表面与后缓冲区.
在窗口模式下你需要一个完全不同的”缓冲系统”,你不能在创建主表面时连接一个后缓冲区然后调用flip()函数来翻转,为什么?因为你在窗口模式下不能独享显卡,而翻转是交换主表面和一个与其相连的后缓冲区的地址的过程,显然,你不能在窗口模式下这么做,因为此时你和其它应用程序共享主表面.
要使用窗口模式,应该用下面这个DDSURFACEDESC2结构去创建主表面:

DDSURFACEDESC2 ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));

ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;

这些语句创建的主表面将使用现在的屏幕格式,而且你不能修改(因为是窗口模式),也请注意我没有使用DDSD_BACKBUFFERCOUNT这个标识,这只能在独占模式下使用
然后再创建后缓冲区,代码如下:

DDSURFACEDESC2 ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));

ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = 6 4 0;  // whatever you want
ddsd.dwHeight = 4 8 0; // whatever you want

注意这里也没有使用DDSCAPS_BACKBUFFER标识,因为这也是特别为独占模式应用程序准备的
请记住,在DirectDraw中主表面往往表示整个屏幕.为了防止你在窗口模式下在整个屏幕里作图,你可以给主表面连接一个裁剪器,并将其与主窗口相连(这很简单!)
LPDIRECTDRAWCLIPPER lpddClipper;
lpDD->CreateClipper(...lpddClipper...);
lpddClipper->SetHWnd(...hMainWnd...);
lpddsPrimary->SetClipper(...lpddClipper...);
简便起见,我省略了这些函数的其它参数
好了,这就是CreateSurfaces函数,接下来我们要看看如何清除对象了!
DestroySurfaces
你结束程序所用的清理代码也将有所不同.原来你只需要释放DirectDraw对象和主表面,现在你还需释放后缓冲区和裁剪器.同样,用if 语句将处理独占模式的代码与处理窗口模式的代码分开:
if( bExclusive )
{
    // exclusive code
}
else
{
    // windowed code
}
把你的独占模式的代码放入exclusive  code部分,然后我们来添加窗口处理代码.
把下面的代码加入到windowed code部分:

if( lpddBack )
{
    // release the back buffer
    lpddBack->Release();
    lpddBack = NULL;
}

if( lpddPrimary )
{
    // release the clipper (indirectly)
    lpddPrimary->SetClipper(NULL);
    lpddClipper = NULL;

    // release the primary surface
    lpddPrimary->Release();
    lpddPrimary = NULL;
}

当你加入了上述代码后,让我们一起进入游戏循环!
由于论坛容量有限,分两部分贴出


作者:Null pointer



翻译 :炎炎
邮箱 :hbyzd@126.com


当你加入了上述代码后,让我们一起进入游戏循环!

游戏循环
你也许认为在游戏循环中唯一真正的不同是当你在窗口模式下时把后缓冲区的内容贴到主表面上而不是用翻转(fliping).好,我们就从那里开始.将你的渲染函数也用if 语句分成两部分:
if( g_bExclusive )
{
    // exclusive code
}
else
{
    // windowed code
}
g_bExclusive表示什么?这是我们用来追踪当前模式的全局变量!
把你原来独占模式下的渲染代码移植到exclusive code部分,然后在windowed code部分增加一个Blt函数,主表面作为目的表面,后缓冲区作为源表面,如下:

lpddPrimary->Blt(NULL, lpddBack, NULL, DDBLT_WAIT, NULL);
简直和随手拿来一样简单!等等,这语句对整个主表面作图,不只是窗口!我们怎样才能用DirectDraw把整个后缓冲区复制到窗口的窗户区呢?好,这样,我们先得到窗口的客户区,然后把客户区矩形的两个点坐标转换成相对于屏幕左上角的坐标(屏幕坐标),再把这个矩形作为Blt的第一个参数,这里是部分代码:
    // calculate the client rect in screen coordinates
    RECT rect;
    ZeroMemory(&rect, sizeof( rect ));

    // get the client area
    GetClientRect(hMainWnd, &rect);

    // copy the rect's data into two points
    POINT p1;
    POINT p2;

    p1.x = rect.left;
    p1.y = rect.top;
    p2.x = rect.right;
    p2.y = rect.bottom;

    // convert it to screen coordinates (like DirectDraw uses)
    ClientToScreen(hMainWnd, &p1);
    ClientToScreen(hMainWnd, &p2);

    // copy the two points' data back into the rect
    rect.left   = p1.x;
    rect.top    = p1.y;
    rect.right  = p2.x;
    rect.bottom = p2.y;

    // blit the back buffer to our window's position
    g_lpPrimary->Blt(&rect, g_lpBack, NULL, DDBLT_WAIT, NULL);


在游戏循环中你还有一些要做的是:你不能你像独占模式下一样占着系统不放.当WINDOWS其它应用程序需要资源时,你必需让给它们.因为这是由用户选择决定的,不仅仅取决于操作系统.一个好的窗口应用程序允许用户在窗口间切换,在同一时刻使用多个应用程序.这就是WINDOWS名字的由来吧!
那么,我们怎么做呢?好, 是什么消耗了大部分计算机资源?当然是游戏循环.所以当用户切换出去时我们应该使游戏暂停,当用户切换回来时再自动开始游戏(在像大型多人在线之类的游戏中,这不太可行,你必须想其它办法 提示:你至少可以暂停渲染或只是降低一点速度).不管怎样,想办法暂停游戏循环,我们添加一个变量来追踪游戏状态:是否在运行!

bool bRunGame;
然后,我们在主消息循环(WndProc)中处理一个特定的消息: WM_ACTIVATE.每当主窗口获得或失去焦点时,我们就收到一个WM_ACTIVATE消息.wParam参数表明是获得焦点或失去焦点.设置bRunGame标识如下:

if( LOWORD( wParam ) == WA_INACTIVE )
{
    // the user is now working with another app
    bRunGame = false;
}
else
{
    // the user has now switched back to our app
    bRunGame = true;
}
如何使用这个刚设置的变量呢?每次继续游戏循环前检查此变量,并用以下代码代替你程序中的主消息循环(一般在WinMain函数中);我们看到游戏循环已嵌入到消息循环中(在最后部分):
MSG msg;
ZeroMemory(&msg, sizeof(msg));

for( ;; )
{
    if( PeekMessage(&msg, NULL, NULL, NULL, PM_NOREMOVE) )
    {
        // retrieve a message
        GetMessage(&msg, NULL, NULL, NULL);
        
        if( msg.message == WM_QUIT )
            break;  // only way out of the for( ;; ) loop

        // dispatch the message to our WndProc
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
    {
        if( bRunGame )
        {
            // game code here
        }
    }
}

return msg.wParam;

然后把你的游戏代码放入到新增的if  语句中(标有// game code here处),这样就完成了游戏循环部分.哈!现在到了看看如何在运行中改变模式的时候了,相信我,这将是本文中最容易的部分!
在运行时切换模式

啊!真的终于到了切换模式的时候了吗?
好的,我们将使用一个简单的函数来切换模式,它可以在窗口模式和独占模式间自由切换,并设置显示模式(在独占模式下),看:
void ChangeDisplayMode(bool bExclusive, int nWidth, int nHeight, int nBPP);
有点简单,不是吗?不,让我来向你解释一下.当你要从独占模式切换到窗口模式时,你这样调用函数:

ChangeDisplayMode(false, 0, 0, 0);  // windowed

当你想从窗口模式切换独占模式,或在独占模式下改变分辨率与色彩深度,你这样调用:

ChangeDisplayMode(true, 6 4 0, 4 8 0, 16);  // 6 4 0x4 8 0x16 exclusive
ChangeDisplayMode(true, 8 0 0, 6 0 0, 32);  // 8 0 0x6 0 0x32 exclusive

不错吧!让我们来完善它吧,下面是完整的函数:

ChangeDisplayMode(bool bExclusive, int nWidth, int nHeight, int nBPP)
{
    // destroy any existing surfaces and clippers.
    DestroySurfaces();

    // create new surfaces and change the 
    // cooperative level and display mode
    CreateSurfaces(bExclusive, int nWidth, int nHeight, int nBPP);
}

这就是模式切换!
在DirectX窗口应用程序中有许多技巧来提升性能,也有不少方法使其易于使用.我们的目标是达到达到两者合一.我将在下篇文章中来阐述这方面的问题!
祝好运!
                                作者    Null pointer



翻译 :炎炎
邮箱 :hbyzd@126.com


介绍
这是关于DIRECTX中窗口模式系列文章的第二篇,这里我将向你展示如何加强以前那些函数的功能,以及提高游戏在两种模式下的响应与外观.我会用到之前创建的函数与变量,所以你最好先看看第一篇文章
本文将涉及:窗口,Windows坐标系统,改变主窗口大小以适应其相应模式,还有就是多线程理论与性能.当你读完本文后,你将有足够的知识把你的只有独占模式的游戏改装成可切换模式的游戏
窗口与WINDOWS坐标系统
之前我只是花了一点时间让我们的程序能运行起来,但是现在,我们深入的研究一下了,屏幕坐标以屏幕左上角为(0,0),当你向屏幕右下角移动时递增.x,y坐标的最大值分别为屏幕的宽度与高度.直观起见,我把我的桌面缩小后拷在下面,图中的黑色矩形框就是屏幕坐标区域(我在里面放了个X以便于辨认)
 
屏幕坐标
Screen.jpg
width: 512 px
size: -1 bytes
double click to view all

客户区坐标以客户区左上角为(0,0)点,向客户区右下角移动时递增.我同样也放了一张图,现在窗口客户区被包在了黑框里:
 
客户区坐标
Client.jpg
width: 512 px
size: -1 bytes
double click to view all

你可以看到,屏幕坐标可以标识整个屏幕,而客户区坐标只能访问某个窗口区域,因此,你用CreateWindow()参数设定一个窗口的位置时,用的是屏幕坐标,而当你响应WM_PAINT消息作图时,用的是客户区坐标,因为你只需要在窗口中画,而不用管它在屏幕中的位置.换句话说,屏幕坐标是绝对的,而客户区坐标是相对的(相对于窗口位置,
你可能要经常在屏幕坐标与客户区坐标间转换,WINDOWS为此提供了两个非常有用的函数: ScreenToClient() and ClientToScreen().,它们都以原坐标系的一个点坐标为参数,并返回这个点在相对坐标系中的坐标点.DIRECTDRAW总是使用屏幕坐标系(绝对坐标系),因为你可以对整个屏幕作图.
最后,窗口的窗户区并不总是一样大的,当你声明了窗口左上角在屏幕上的坐标及窗口宽度和高度后,用CreateWindow()函数创建这个窗口.但是标题栏, 菜单,工具栏,状态栏等也要占据一些空间.所以同样大小的窗口在窗口设置不同的电脑上客户区的大小不尽相同,例如标题栏的字体大小不同.要算出出所有的差异几乎是不可能的.幸好,在WINDOWS下要得到精确的客户区大小度不是很难.
首先,我们用CW_USEDEFAULT常量代替x,y坐标与窗口宽高来创建一个默认大小的窗口,然后,我们用我们需要的窗户区大小减去默认的客户区大小,再把原默认窗口的大小加上/减去这个差值,简单吧?为了方便使用,我们用一个函数把所有要做的封装起来,然后再调用它:

RECT CalculateWindowRect(HWND hWndToAdjust, SIZE szDesiredClient);

这个函数以我们要调整的窗口的句柄和预期的客户区大小(SIZE型)为参数,算出新的窗口矩形并返回之.现在让我们来实现这个函数吧(如果你一开始看不懂,不要担心,先跳过去,当我们讨论了它的细节后再回来看看):
RECT CalculateWindowRect(HWND hWndToAdjust, SIZE szDesiredClient)
{
  // declare a RECT to hold the results of our calculations
  RECT rcDesiredWindowRect;
  ZeroMemory(&rcDesiredWindowRect, sizeof( rcDesiredWindowRect ));

  // get the current window rect and its client rect
  RECT rcCurrentWindowRect;
  RECT rcCurrentClientRect;

  GetWindowRect(hWindow, &rcCurrentWindowRect);
  GetClientRect(hWindow, &rcCurrentClientRect);

  // get the difference between the current and desired client areas
  SIZE szClientDifference;

  szClientDifference.cx = (rcCurrentClientRect.right  - szDesiredClient.cx);
  szClientDifference.cy = (rcCurrentClientRect.bottom - szDesiredClient.cy);

  // get the difference between the current window rect and the desired
  // desired window rect
  rcDesiredWindowRect.left   = rcCurrentWindowRect.left;
  rcDesiredWindowRect.top    = rcCurrentWindowRect.top;
  rcDesiredWindowRect.right  = (rcCurrentWindowRect.right  - szClientDifference.cx);
  rcDesiredWindowRect.bottom = (rcCurrentWindowRect.bottom - szClientDifference.cy);

  return rcDesiredWindowRect;
}
好多代码啊!理解它的最好方法莫过于在调试器中单步执行来观察各个变量值的变化,以便了解这些计算和真正意义.现在我们有了个相对简单的函数,在那里使用它呢?当然是在我们可信的SwitchMode函数中了当我想在切换模式前改变窗口大小时,这个函数就起作用了.但最好再加一个改变窗口大小的函数,那样的话,. SwitchMode函数就更具可读性了,声明如下:
void AdjustMainWindow(bool bExclusive, int nScreenWidth, int nScreenHeight);
这年起来就像一个WINDOWS API函数,但之后我会给这个函数添加一些独特的功能.现在我们来看看这个函数:
void AdjustMainWindow(bool bExclusive, int nScreenWidth, int nScreenHeight)
{
  // hide the window while we're working...
  ShowWindow(hMainWnd, SW_HIDE);

  if( bExclusive )
  {
    // exclusive mode code
  }
  else
  {
    // windowed mode code

    // calculate the new window rect
    SIZE szDesiredClient;

    szDesiredClient.cx = nScreenWidth;
    szDesiredClient.cy = nScreenHeight;

    RECT rcNewWindowRect = CalculateWindowRect(hMainWnd, szDesiredClient);

    // resize the window according to the new rect
    MoveWindow(hMainWnd, rcNewWindowRect.left, rcNewWindowRect.top, 
      (rcNewWindowRect.right ?rcNewWindowRect.left),
      (rcNewWindowRect.bottom ?rcNewWindowRect.top), true);
  }

  // unhide the window now that we're done with it
  ShowWindow(hMainWnd, SW_SHOW);
}
我们一开始就隐藏了窗口以防止在完成所有设置前不断重绘而引起混乱.(这些变化是完全可见的,不信你去掉ShowWindow函数看看). if()语句用来判断不同的模式,在独占模式下,我们不用调整窗口大小;而对于窗口模式,我们必须改变窗口大小.首先,创建一个SIZE型的临时变量来存放新的客户区的宽与高,然后把它和主窗口句柄一起传给CalculateWindowRect以得到新窗口的矩形.然后把它传给MoveWindow函数以重置窗口大小,再调用ShowWindow重绘窗口(因为现在我们已完成所有设置了).(在这里显示窗口会自动重绘标题栏及其它窗口元素)
现在我们来测试一下,把下面代码加到SwitchMode函数中, 放在DestroySurfaces a和CreateSurfaces之间

// resize the main window抯 client area
AdjustMainWindow(bool bExclusive, nWidth, nHeight);

变量nWidth和 nHeight 是SwitchMode的第二,三个参数,所以你可以这样:

// DX changes the window size to *0x480 automatically here
SwitchMode(true, *0, 480, 16);
// and this adjusts the windows?client area (where we’ll draw!) to *0x480, also
SwitchMode(false, *0, 480, 16);

我们传给SwitchMode的width 和height只表明了我们所需的绘图页的大小.与全屏(独占)或窗口模式无关.它们现在完成一样的功能!(当然,不包括位深(bit-depth),我们不能,也不应该在窗口模式下改变它)


让主窗口更加完美
还记得我说过要在AdjustMainWindow函数中加些优雅的技巧?好,我们现在就做.你会发现在第一篇的例子中, 即使在独占模式下我们也不拥有整个屏幕,它还有标题栏,关闭按钮等一些会把你的游戏搞的一团糟的东西(虽然我们是可以在整个屏幕中绘图,但当你点击在屏幕顶端的怪物时,游戏突然退出了,你肯定很不爽,好了,我懂我意思了吧)
你可以用一些消息技巧来避免这些情况的出现,但按常规做会好些(也更简单),要得到一个没有标题栏,边框,系统菜单的窗口,你只要在调用CreateWindow时把style设为WS_POPUP即可.等等!我要在每次切换模式时销毁并重建窗口吗?不,有一个方法可以在程序运行中改变其窗口风格,现在我们就来看看:
这是两个 Windows API 函数:

LONG GetWindowLong(HWND hWnd, int nIndex);
LONG SetWindowLong(HWND hWnd, int nIndex, LONG dwNewLong);

有点少见,对吧?谁能说出他们的名字(别撒谎,我知道你说不上来)基本上, hWnd 表示你要操作的窗口句柄,nIndex表示你所需的信息的类型.如果你要为窗口设置新的特性,你把它存在dwNewLong中,你能用这两个函数做许多美妙的事情,但在这里,我们只用到nIndex的GWL_STYLE
所以,我们这样得到主窗口风格:

LONG nMainWindowStyle = GetWindowLong(hMainWnd, GWL_STYLE);

可是记住,这可是直接访问窗口风格,不像CreateWindow函数,提供了合适的默认参数,还会在你设置不当时退出.我们在使用它时要特别小心,现在AdjustWindow函数变成这样了:

void AdjustMainWindow(bool bExclusive, int nScreenWidth, int nScreenHeight)
{
  static HMENU hOldMenu = NULL;

  // hide the window while we're working...
  ShowWindow(hMainWnd, SW_HIDE);

  if( bExclusive )
  {
    // exclusive mode code

    if( !g_bExclusive )
    {
      // change the style of the window
      SetWindowLong(hMainWnd, GWL_STYLE, WS_POPUP);

      // remove the menu and save the old menu
      hOldMenu = GetMenu(hMainWnd);
      SetMenu(hMainWnd, NULL);
    }
  }
  else
  {
    // windowed mode code

    if( g_bExclusive )
    {
      // change the style of the window
      SetWindowLong(hMainWnd, GWL_STYLE, /* windowed mode style */);

      // re-attach the old menu
      if( hOldMenu )
        SetMenu(hMainWnd, hOldMenu);
    }

    // calculate the new window rect
    SIZE szDesiredClient;

    szDesiredClient.cx = nScreenWidth;
    szDesiredClient.cy = nScreenHeight;

    RECT rcNewWindowRect = CalculateWindowRect(hMainWnd, szDesiredClient);

    // resize the window according to the new rect
    MoveWindow(hMainWnd, rcNewWindowRect.left, rcNewWindowRect.top, 
      (rcNewWindowRect.right ?rcNewWindowRect.left),
      (rcNewWindowRect.bottom ?rcNewWindowRect.top), true);
  }

  // unhide the window now that we're done with it
  ShowWindow(hMainWnd, SW_SHOW);
}
在SetWindowLong函数的/* windowed mode style */部分加上你在窗口模式下正常使用的窗口风格,独占模式的话就设为WS_POPUP.当然,我们还得在独占模式时卸掉菜单,并在转向窗口模式时加上菜单.但是注意了,我们那样简单的改变窗口风格并没有考虑到状态栏,工具栏,以及其它窗口一些组成.它只解决可以由窗口风格指定而创建的窗口组成(如标题栏)
坏消息!DIRECTDRAW不仅仅在独占模式下掌控了消息处理并处于顶端,在窗口模式下也是如此!所以在SwitchMode函数中必须先销毁窗口并重建一个普通窗口以确保WINDOW工作正常:


if( IsWindow(hMainWnd) )
  DestroyWindow(hMainWnd);

以上代码销毁一个窗口(如果存在的话),所以我们可以在不丢失资源的情况下重新创建它, IsWindow 和DestroyWindow是WINDOWS API 函数,注意DestroyWindow将发送一个WM_DESTROY消息至WndProc,所以一定要把你的PostQuitMessage(0)(结束程序)函数移到WM_CLOSE或其它地方,不然,你将在每次切换模式时退出程序
好了,我们所做的这些做到了什么呢?对了,我们创建了一个可在独占与窗口模式间相互切换的系统.它相对于你的绘图代码是透明的(绘制精灵,渲染3D等等),所以任何你可在后缓冲区的画面都可在屏幕上显示.就像以前提到的,你甚至可以在此系统下做3D,只要
合适的修改CreateSurfaces 和 DestroySurfaces这 两个函数.
这有什么好处吗?我不太愿说这些,但我们的电脑的发展趋向是越来越强大的,如果摩尔定律是正确的,CPU的速度能在几个月个加倍,而3D显卡的确速度至少能快5%(好像有点讽刺意味),之外其它方面也有很多的进步.我有一些有趣的DIRECTX的老游戏,是别人花时间写的, 在窗口模式下可以和独占模式运行的一样快.现在,我可以在网上查找游戏代码,然后让它们在我机子里运行,因为我的电脑已经升级了,升级电脑可以让你得到老游戏的新特性,让它们重现价值.(所以这种切换技术会随着电脑硬件性能的加强运行的越来越流畅:译者注)
对于开发者而言,窗口模式在现在仍然有着不凡的作用.但不是每个人都是”性能nut”(一种哪怕已运行在83帧速也还想加快的人),你的游戏一般在发行3个月后肯定会过时.此外,既然你已知道哪些事是比较棘手的,你就可以写一些游戏编辑器(简单的复制你的绘图代码,想想地图编辑器),接下来要讲的是多线程与性能问题.
DIRECTX中独占模式与窗口模式的切换(2.3)
多线程理论及性能分析
好,终于到了讨论性能问题的时候了.怎样才能让WINDOWS下的程序运行的更加高效呢?我们需要在原有代码的基础上进行改进
你可能发现从游戏向其它窗口切换在概需要1/3到1/2秒左右的的时间(很慢是吧!).所以我们有必要讲讲多线程以找到正确的方法来改进之
在WINDOWS下一个线程就像一个进程.因为虽然它们共享进程的地址空间,但调度程序却把它们当作独立的进程来处理.什么是调度程序?它是WINDOWS操作系统中十分重要的部分,总管着所有进程的运行.下面我来讲讲调度程序的工作原理吧!
举个例子,如果你的电脑的主频是600MHZ,并且你同时打开了浏览器资源管理器和控制面板3个程序.它们看起来同时运行着,但其实它们不可能真正的同时运行.每个程序在运行时必须独占的访问CPU,CACHE等系统资源.(否则你的程序肯定慢的跟爬一样,J)那么WINDOWS是怎么做的呢?它分给每个程序200MHZ(一个时间片)来运行.就是把一大块可用的时间(600MHZ)分成相等的几个时间片,各个程序在时间片中运行,当一个时间片结束了,就切换到另一个程序,如此循环往复.此时WINDOWS监视着所有打开的程序,所以当你再打开一个程序时(比如说我们的窗口切换程序),就有4个程序打开着了,此时WINDOWS就会重新分配这600MHZ处理器时间,把它分成相等的4份,即每个程序拥有150MHZ的时间片.只要WINDOWS不停止它们,它们就会按照时间片轮流执行.听起来很简单是吧,你懂了就好J总之,WINDOWS中负责安排与调度的这部分就叫做”调度程序”.
尽管如此,调度程序其实很容易被误用.WINDOWS并不知道什么时候你的程序运行,而什么时候你的程序应该暂停.对于WINDOWS来说,你的程序只不过是一个个的字节而已,根本无法说明你的程序要做什么.唯一的解决方案是让你的程序自己来决定它的执行或挂起.所以,问题出现了,因为我们共享着一个处理器,如果你的程序过了时间片还不主动挂起,就会占用其它程序的时间片,从而使它们变的相当地慢.如果你要的话,你可以占着CPU不放,但这会使WINDOWS出问题,并且让用户感觉极为不爽J.我永远也不会忘记那次经历,那次我上网看Diablo游戏策略然后想亲手试试,我只好同时打开着Diablo和那个网页,尽管Diablo是最小化的,我的450MHZ的电脑跑起来还是慢得和我以前那台老爷386一样J我甚至可以用秒来计算它重绘桌面壁纸的时间.游戏程序员们认为在DIRCTX的独占模式下他们可以像对待DOS一样来对待WINDOWS.但是不幸的是,用户们经受着痛苦(慢),而且从其独占模式转到窗口模式又引起很多问题,就像本文介绍的一样.
    太多的责备与否定了.现在我们来讨论一下解决此类问题的好的方法!好,让我们先把游戏循环改写成下面这样子:

void GameLoop()
{
  ProcessInput();
  ProcessLogic();
  ProcessGraphics();
  ProcessSound();
}

我们可以做得更好些.因为这里当引擎的一部分非常慢的时候,整个游戏就慢下来了,包括声音,输入等.我们甚至会掉线因为我们用了太多的时间去处理图像而没有很好的检查网络连接.而其它机器也会因为滞后而慢下来.如果数据操作不当或程序设计不好, 玩家都有可能受到影响.(此句原文为: gameplay will suffer because of improper handling of data/poor programming,若译的有错望大家指正)
对此我们有一个简单的方法.我们已经知道有些处理必须实时运行,否则游戏就没有意义了. ProcessInput()就是这样一个处理,它处理键盘,鼠标的输入(包括多人输入),但是它和又其它功能紧紧的联系在一起(见以上GameLoop).怎么把它分离出来呢?对,我们把它放到另外一个线程中.那么什么是线程呢?它是程序在运行中产生的新的处理单位(你可以把它看成下蛋J),WINDOWS调度程序像处理独立的程序一样来处理它.但线程却又独立于产生它的进程运行.线程可以访问该程序的地址空间,当游戏循环在执行时,线程在后台运行.
那么怎样创建一个线程呢?关于这个话题另外已有一篇文章<< Separating Input from the Game Loop >>详细的介绍了这些内容.在这里作为补充来说说如何给你创建的线程分配优先级.
优先级使我们的问题复杂化,但在我看来,它所带来的好处超过它带来的复杂性.首先,这不再是一个普通的时间片,每个程序(线程)都有自己的优先级.不同的程序具有不同的优先级,举个例子,设备驱动程序可能会创建一个高优先级的线程,它首先把自己的时间让给其它线程,当它具备了所需的条件时,它就可以完全的控制系统了.当然,这也只不过是几毫秒或更少的时间,所以不会怎么影响系统.但它必须实时执行,所以当它运行时,它必须独占系统;对于像WORD这样的程序,可能会创建一个低优先级的线程来做后台打印工作.这样,只有在系统中没有其它程序运行时才会执行后台打印线程.这并不会影响你,因为当你运行其它一般的或高优先级的程序时,它就会立即挂起.所以线程优先级是一种灵活使用CPU的方法.你告诉WINDOWS你要做什么,WINDOWS就相应的为你安排好一切J
我们如何告诉WINDOWS何时要切换到另一个程序?你可以调用Sleep(0)函数.但是为什么我们的消息循环中没有这个语句,至少游戏循环中就没有?记住了, GetMessage() 其实就是SleepUntilIHaveAMessage()
但是如果你的游戏在窗口模式下,你就必须用到线程了.如果你对我之前所讲的有点头绪的话,你就可能会问:我怎样才能使用一个线程呢?好的,你首先要做的就是编写
ThreadProcedure函数,它看来有点像WinMain函数,其一般形式如下:

#define WM_THREADSTOP (WM_USER+1)

DWORD WINAPI ThreadProcedure(LPVOID lpStartupParam)
{
  // get the data we passed to the thread. Note that we don't have to use this
  // at all if we don't want
  MYDATA* pMyData = (MYDATA*) lpStartupParam;

  // access some imaginary members of MYDATA, which you can define on
  // your own later
  pMyData->nTime = GetCurrentTime(); // imaginary function I created
  pMyData->nNumber = 5;


  // here's the thread's main loop ?kind of like the main loop in WinMain
  MSG msg;

  for( ;; )
  {
    if( PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)
    {
      GetMessage(&msg, NULL, 0, 0);

      if( msg.message == WM_THREADSTOP )
        break; // only way out of the for( ;; ) loop

      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
    else
    {
      // do the task ?add in your own stuff here

      // yield to other threads, because we almost never get messages
      // (note that we may be yielding to WinMain too)
      Sleep(0);
    }
  }
}

注意两个要点1)线程几乎不接收消息,除非它创建了自身的窗口,对,线程可以创建并处理其自身的窗口.(想想为什么WORD2000会用起来这么爽J).但是,如果一个线程不具有自身的窗口,它不太可能会接收消息,所以我们用Sleep(0)来通知WINDOWS现在是时候把时间片让给其它线程了.如果我们不这么做, 我们和程序和主窗口就会变得特别慢,而这是不必要的.2)因为我们几乎不可能执行到PeekMessage()-GetMessage()-DispatchMessage()这段程序,所以消息处理的代码并不会降低我们程序的执行速度.
那么我为什么要加入消息循环呢(既然不会执行到它),如果我不创建线程的窗口,这不是多余吗!不,我这样做是因为这样我们就可以向线程发送我们自定义的消息了.在主程序退出前,我们必须通知线程停止执行并释放资源,这就是我为什么要自定义一个消息了,你可以把它用PostThreadMessage()把它发送给线程.注意你也必须等到线程结束(再结束主程序),所以你还要一个全局变量来判断线程是否结束:

int g_nThreadExitCount = 0;

只有当线程从主消息循环中退出时,才自增此变量并返回0;当此变量增加到你运行的线程数时,说明你所有的线程都已停止,你就可以安全的退出WinMain了.千万别用
TerminateThread函数(它不能很好的释放资源).
DIRECTX中独占模式与窗口模式的切换(2.4)

你现在可能注意到,我还没告诉你怎样创建一个新的线程呢,你要用到CreateThread函数.

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
    DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,

    LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);

啊!好多参数!幸运的是,多数参数有默认值,而且对于几乎所有的程序与游戏都能工作的很好. lpThreadAttributes ,dwStackSize, dwCreationFlags.应该设为NULL(WINDOWS会给它们默认的值的,它们能工作的非常棒,就像WINDOWS自动配置栈大小一样J). LpStartAddress是你ThreadProcedure的函数地址, pParameter是传给ThreadProcedure的参数. LpThreadId是一个指向DWORD的指针,作为线程ID(和DX有点相象),下面是一个具体使用的例子,其中用到了前面的ThreadProcedure函数 :

// define a data structure to hold our data ?optional
struct MYDATA {
  int nTime;
  int nNumber;
};

// declare a variable of our data structure to pass to the ThreadProcedure
MYDATA MyThreadData;
MyThreadData.nTime = 7509843;
MyThreadData.nNumber = 39;

// declare a DWORD to hold our thread's new generated ID
DWORD dwThreadID = 0;

// actually create and start the thread now!
// the thread will run until we send it our own WM_THREADSTOP message
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE) ThreadProcedure,
  (LPVOID) &MyThreadData, NULL, &dwThreadID);


我们应该怎样设计一个线程函数呢?线程适用于简单且需要高频率执行的任务(然后它就Sleep(0)了J).,或者适用于复杂但实时性要求不高的任务,适于在后台运行.(也就是它们不在意什么时候能执行,只要给你能给它足够的时间就行了).想想这种情况:一个线程只做一件简单的事(就像lpDIKeyboard->GetData()一样)然后就Sleep(0),这是个简单的线程;但是如果线程做很多工作呢?就像每次在Sleep(0)之前打印一篇文档,这当然是个复杂的线程. 
最大的区别在哪里?复杂的线程趋向于”分散”的执行程序,它们的大多数时间用来把时间让给其它线程(没办法,优先级没人家高J);而简单的线程趋向于集中的执行程序(最好每次执行就完成相应的任务).区别就在于在于它们执行任务所用的时间与它们用Sleep(0) 放弃的时间的比值.离散执行的线程(一般为复杂的线程)适合于低优先级,而简单的的线程适合于高优先级.你可以用SetThreadPriority()来改变线程优先级,它返回线程原来的优先级.
你要想通过调整线程优先级来提高性能,就必须对线程和高调度程序十分了解.当然,千万,千万,千万,千万记住,在每个ThreadProcedure函数中加上Sleep(0)语句.否则.低优先级的线程就永远没机会执行!实际上这可能会使WINDOWS停止运转,如果使用不当,甚至可能使系统崩溃.但是如果使用得当,你就可以大大提高你游戏 的效率.这是一个很大的话题,我建议你找关于多线程方面的书或一些编程指南.
如果你对多线程真的很有兴趣,我建议你把我前面介绍的程序中的游戏循环部分用一个独立的线程来实现..对,就是对整个游戏循环!只是试试,刺激一下,对多线程编程有个大概的了解.如果你使你得程序运行了.那么试试优先级,或者改变一下消息循环J
WINDOWS就像共产主义,如果每个人都愿意把自己的东西拿出来共享,我们就有许许多多的资源可以利用了.但不幸的是,一个不好的程序会毁掉所有的一切(这就是为什么,恕我直言,共产主义很难实现了J)尽管如此,共享仍然是在WINDOWS(或 MAC 或Linux 或者,尽管在这里加是你喜欢的操作系统)等操作系统中解决多进程,多线程问题的最高原则.当你完成时,记得释放资源,那样,WINDOWS也就会工作的很好的.
祝好运!----在WINDOWS游戏编程的道路上!!!
如果你有任何问题,评论或更正,请与我联系:
Ratt96963@aol.com
我的网站是:
http://www.freeyellow.com/members8/nullpointer
I enjoyed writing this article, and I hope it encourages you to explore the world of Windows game programming for yourself.
很高兴写这篇文章,我希望这能够激励你去探索WINDOWS游戏编程的世界!

译者的话:
至此.,<<DIRECTX下独占模式与窗口模式间的切换>>( Moving from Exclusive Mode to Windowed Mode in DirectX)系列文章已全部翻译完毕,虽然只是翻译这么一些,可我觉得已经很累了(至少现在我的手是J).在此向沙鹰致敬,你翻译了<<WINDOWS游戏编程大师技巧2>>这么厚,这么好的书,然后,最关键的,真的翻译的很不错(我虽然没有买,但我年看了我同学的,不肯放手了J,只是现在想看看第二卷,不知哪里有?)
本文的两个的例子我已发给管理员,估计这两天可以出来.另外,大家可能会使用
<<WINDOWS游戏编程大师技巧2>>提供的引擎,可能与本文的例子提供的函数无法兼容使用,我已经完成了该代码.如果大家有兴趣,或者有需要,请与我联系,我可以把代码发给你.如果有必要的话(看大家的需要,不然我岂不是做无用功J)我可以另写篇文章并提供代码下载.
最后,感谢原作者,是他写下了如此美妙的文章(希望没被我搞砸J);感谢OGDEVR管理员,是他给我鼓励以完成此文;感谢所有OGDEV社区的成员,因为我们一起学习,一起成长!
完了,我会继续努力的.我更希望我们大家,所有OGDEV的成员,能够积极和加入到论坛中来,能够牺牲一点自己的时间来发发帖子,回回帖子(不光是看看,不然怎么算是会员呢,每个会员都这样,我们看什么呢?呵呵!我们有了论坛的权限,就应当负起相应的责任吧),其实这和此文恰好相一致,”如果每个人都愿意把自己的东西拿出来共享,我们就有许许多多的资源可以利用了”(见译文文最后一段),虽然这很难,但我们能做到,因为我们是程序员,不是吗?我并不是OGDEV的宣传者,我只是希望我们共创OGDEV论坛的辉煌!
我的联系方式为:  hbyzd@126.com
注:译得不好,请多指教;说得不爽,请多原谅!

2004-8-4
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值