用DirectDraw实现射击游戏技术要点

 

用DirectDraw实现射击游戏技术要点


  游戏Demo版下载: Rator.rar

  游戏源码下载:Rator(src).rar

要点一:画图自动切割

  IDirectDrawSurface7::BltFast()方法中没有自动切割功能,即当画图元素超出窗口以外时不会自动切割,DDraw选择自动忽略不画,造成一旦超出窗口,画图元素会突然消失。

  解决这一问题的方法是手动切割,代码如下:

    //自动切割
     RECT scRect;     //存放当前窗口大小区域
     ZeroMemory( &scRect, sizeof( scRect ) );
     GetWindowRect( GetActiveWindow(), &scRect );

      //防止图片左上角超过窗口左上角
     if ( x < 0 )
     {
         m_rect.left -= x;
         x = 0;
     }
     if ( y < 0 )
     {
         m_rect.top -= y;
         y = 0;
     }


    //防止图片右下角超过窗口右下角
     x = x > scRect.right ? scRect.right : x;
     y = y > scRect.bottom ? scRect.bottom : y;
     m_rect.right = x + m_rect.right - m_rect.left > scRect.right ? scRect.right - x + m_rect.left : m_rect.right;
     m_rect.bottom = y + m_rect.bottom - m_rect.top > scRect.bottom ? scRect.bottom - y + m_rect.top : m_rect.bottom;

    只需将上述代码加在画图( IDirectDrawSurface7::BltFast() )前即可。

 

要点二:背景的滚轴实现

         画背景可以分为以下三种情况:

         情况一:背景图片与窗口等高
 

         情况二:背景图片高度小于窗口高度


         情况三:背景图片高度大于窗口高度

  

   上述讲解图与代码相对应地看,有助于容易理解。

  另外,要点一实现之后,由于已经可以自动切割,画背景可以用其它方法。

 

要点三:精灵图的实现

  在游戏中,如RPG游戏中的人物图、射击类游戏的飞机、爆炸等,叫做精灵图。

  精灵图实际上是将所有帧的图片放在一个文件中,游戏时靠一个RECT来控制画图像文件中的哪一部分,进而控制游戏显示哪一帧图,只需控制好RECT的位置即可。如下图:

     
    

 

  控制RECT的四个角的坐标的移动,有以下代码:

    if (m_timeEnd – m_timeStart > 100)                        //只有到了100ms之后才绘图
    {
        m_ImageID++;
        if(m_ImageID - m_beginID >= num)
        {
            m_ImageID = m_beginID;     //最后一帧的下一帧是第一帧
        }
        m_timeStart = timeGetTime();
    }
    int id = m_ImageID++;
    SetRect(&m_rect, 41 * id, 0, 41 * (id + 1), 41);                  //飞机精灵图大小是41×41
    m_pGraph->BltBBuffer(m_pImageBuffer, true, m_Pos.x, m_Pos.y, m_rect);

  这样就实现了精灵动画的效果。

 

要点四:拿STL进行子弹的实现

  子弹的实现可以使用STL中的vector,当按下开火键时发出一颗子弹,就往vector中添加一个结点;当子弹飞出窗口或击中敌机时,再将结点从vector中删除。每帧游戏画面中子弹飞行时只需将vector中的所有子弹进行处理、绘画即可。

  参考代码如下:

  1.添加子弹

    if (g_ctrlDown)                               //当ctrl键按下时开炮!
    {
        m_BulletEnd = m_Gtime->GetTime();
        if ((m_BulletEnd - m_BulletStart) * 1000 > 120)               //如果连续按着开火键不放,这里控制不会发出太多子弹
        {
            m_BulletStart = m_BulletEnd;
            MBULLET tmpBullet;
            tmpBullet.pos.x = m_SPos.x - 1;              //记录开火时的子弹位置
            tmpBullet.pos.y = m_SPos.y - 26;
            tmpBullet.speed = 5;                               //该子弹的飞行速度
            m_BulletList.push_back(tmpBullet);        //将子弹添加到vector中
        }
    }

  2.删除子弹

    vector<MBULLET>::iterator itei;              //vector迭代器
    for (itei = m_BulletList.begin(); itei != m_BulletList.end(); itei ++)      //遍历所有子弹
    {
        m_BulletList.erase(itei);       //删除这个子弹
        itei = m_BulletList.begin();   //删除一个结点后,为避免出错下次就从头检查
        if (m_BulletList.empty())
            break;                            //若删除结点后子弹vector已空则跳出循环
    }

  3.子弹遍历处理

    vector<MBULLET>::iterator itei;              //vector迭代器
    for (itei = m_BulletList.begin(); itei != m_BulletList.end(); itei ++)      //遍历所有子弹
    {
        itei->pos.y -= itei->speed;     //子弹飞行
    }

 

要点五:碰撞检测

  使用Windows API函数RectInRegion:

    vector<CEnimy>::iterator itei;                   //vector迭代器
    for (itei = m_EnimyList.begin(); itei != m_EnimyList.end(); itei ++)      //遍历所有敌机
    {
        HRGN hrgn = ::CreateRectRgn(m_player->pos.x, m_player->pos.y,
                                                         m_player->pos.x + 41, m_player->pos.y + 41);                   //得到飞机Region,图宽41高41
        SetRect(&m_rect, itej->getPosition().x, itej->getPosition().y,
                      itej->getPosition().x + 50, itej->getPosition().y + 50)              //得到敌机rect,敌机宽50高50
        if ( RectInRegion(hrgn, &m_rect) )                  //两机相撞
        {
            …………………….      //碰撞之后的各种处理
        }
    }

  让碰撞更加精确:

  使用Windows API函数PtInRegion()和CreatePolygonRgn(),选取主角飞机的三个关键点的坐标放在POINT数组中,并将其作为参数代入 CreatePolygonRgn()中生成HRGN,在子弹与主角飞机做碰撞检测时只需判断子弹的中心点是否在这个Region中即可(PtInRegion())。

  注意:CreateRectRgn()与CreatePolygonRgn()等创建Region的函数会占用系统资源,由于游戏的主渲染函数Render()是不断执行的,这样会造成资源浪费,因此在用完之后一定要释放:DeleteObject(region)

 

要点六:敌机直线飞行

  最初想这个问题的时候,以为很好实现,脑子里马上想到  和  了。其实这样实现有问题,当起点和终点的连线斜率不是1或-1时就会出现意想不到的事情了,飞机并没有直接飞向终点,而是以斜率绝对值为1的路径飞过去,再水平或垂直飞向终点。

  解决这个问题有几个方法,其中有一个方法是利用计算机图形学上的Bresenhem直线算法。该算法用于计算机画平面上的直线,算法如下:

    |m|<1的情况
    1、输入线段的两个端点,并将左端点存储在(x0,y0)中;
    2、将(x0,y0)装入帧缓冲器,画出第一个点;
    3、计算常量dx,dy,2dy和2dy-2dx,并得到决策参数的第一个值:
                                d0 = 2dy-dx
    4、从k=0开始,在沿线路径的每个xk处,进行下列检测:
             如果dk<0,下一个要绘制的点是(xk+1,yk),并且
                                dk+1 = dk+2dy
             否则,下一个要绘制的点是(xk+1,yk+1),并且
                                 dk+1 = dk +2dy –2dx
    5、重复步骤4,共dx次。

  利用此原理,实践在敌机直线飞行中的代码如下:

    void  CEnimy::Move()
    {
        int deltaX = m_targetPos.x - m_pos.x;
        int deltaY = m_targetPos.y - m_pos.y;

        // 轨迹斜率 = 0
        if ( !deltaX )
        {
            if ( deltaY < 0 )
                m_pos.y -= m_speed;
            else
                m_pos.y += m_speed;
            return;
        }

        // 轨迹斜率无穷大
        if ( !deltaY )
        {
            if ( deltaX < 0 )
                m_pos.x -= m_speed;
            else
                m_pos.x += m_speed;
            return;
        }

        // 以下是用计算机图形学 Bresenham 算法计算两点间的直线轨迹
        if ( abs(deltaX) > abs(deltaY) )     // 轨迹斜率 < 1
        {
            if ( m_bFirstCalculate )
            {
                m_Delta = 2 * abs(deltaY) - abs(deltaX);     // d0 = 2 × dy - dx
                m_bFirstCalculate = false;
            }

            // 根据轨迹斜率判断是否要移动 Y 坐标
            if ( m_Delta > 0 )     // < 0 时只改变 X 坐标,否则 X、Y 坐标都要变
            {
                if ( deltaY < 0 )
                    m_pos.y -= m_speed;
                else
                    m_pos.y += m_speed;
                m_Delta += 2 * abs(deltaY) - 2 * abs(deltaX);     // 计算下一个 dn

                }
            else
            {
                m_Delta += 2 * abs(deltaY);     // 计算下一个dn
                }

            // X 坐标每一帧都要向目标移动
            if ( deltaX < 0 )
                m_pos.x -= m_speed;
            else
                m_pos.x += m_speed;
        }
        else     // 轨迹斜率 > 1
        {
            if ( m_bFirstCalculate )
            {
                m_Delta = 2 * abs(deltaX) - abs(deltaY);     // d0 = 2 × dx - dy
                m_bFirstCalculate = false;
            }

            // 根据轨迹斜率判断是否要移动 X 坐标
            if ( m_Delta > 0 )    // < 0 时只改变 Y 坐标,否则 X、Y 坐标都要变
            {
                if ( deltaX < 0 )
                    m_pos.x -= m_speed;
                else
                    m_pos.x += m_speed;
                m_Delta += 2 * abs(deltaX) - 2 * abs(deltaY);    // 计算下一个dn
            }
            else
            {
                m_Delta += 2 * abs(deltaX);     // 计算下一个dn
                }

            // Y 坐标每一帧都要向目标移动
            if ( deltaY < 0 )
                m_pos.y -= m_speed;
            else
                m_pos.y += m_speed;
        }
    }

 

要点七:通过读取配置文件实现敌机的飞行轨迹

  不同敌机以不同的轨迹飞行,实现的方法有很多,只要把轨迹上的几个关键点作为敌机的目标点,当到达这个目标点时,把目标列表中的下一个点作为下一个目标点,敌机继续向其飞行,这样就实现了敌机的不同轨迹飞行。但是要想把游戏中所有的敌机都写在代码中会很乱,不容易维护。VC++开发平台提供了两个函数:GetPrivateProfileSectionNames()和GetPrivateProfileString(),用来读取硬盘上的配置文件(.cfg),这样,每一架飞机的初始化信息可以写在.cfg文件中,通过一个循环算法来读取。

  1. 函数说明:

  这是将.cfg文件中所有的section names读取到一字符数组中:

    DWORD GetPrivateProfileSectionNames(
        LPTSTR lpszReturnBuffer,   // 用来存放section names 的字符串指针
        DWORD nSize,                   // 字符串的长度
        LPCTSTR lpFileName         // .cfg文件的路径
    );

  这是读取某一section name中的某个字段的值:

    DWORD GetPrivateProfileString(
        LPCTSTR lpAppName,   // 在这个section name中查找
        LPCTSTR lpKeyName,   // 要查找的字段名
        LPCTSTR lpDefault,   // 若查找失败的默认返回值
        LPTSTR lpReturnedString, // 存放指定字段名所对应的值
        DWORD nSize,   // 存放返回值的字符串长度
        LPCTSTR lpFileName  // 在这个.cfg文件中查找
    );

  2. 文件要求:

  .cfg文件的内容格式如下:

            [section name]
            key1=string
            key2=string

  例如,在敌机的配置文件enimy.cfg中可以这么写:

    [ENIMY01]
    //这是进度号,不同进度加载不同敌机
    tempoid=1
    //这是图片号,根据需要加载不同的敌机图片
    imageid=0
    //这是图片的总帧数
    imageframenum=2
    //这是图片的宽度
    imagewidth=100
    //这是图片的高度
    imageheight=50
    //这是敌机生命值
    hp=3
    //这是敌机移动速度
    speed=1
    //这是敌机的初始位置
    pos.x=512
    pos.y=-50
    //有两个目标点,即由两个点决定其轨迹
    targetnum=2
    //以下是目标点的坐标
    targetpos0.x=512
    targetpos0.y=192
    targetpos1.x=240
    targetpos1.y=600

  其中,注释可以写入文件中,但不能与即将要读取的数据在同一行。

  3. 代码例子:

    // 读取 CFG 文件中所有的敌机名称
    // 读取完后m_sEnimyName中的字符串是每个section name的连接,两两之间用”\0”字符分开,如:
    // “enimy01.enimy02.enimy03”其中的点就是空字符

    GetPrivateProfileSectionNames(m_sEnimyName, sizeof(m_sEnimyName), "data/enimy.cfg"); 
    char *pStr = m_sEnimyName;   // 用来保存当前的section name
    char returnedString[64];
    m_iTempo++;   // 每发动一波敌机,游戏进度加1

    // 从 cfg 文件中找到进度等于 m_iTempo 的敌机
    GetPrivateProfileString( pStr, "tempoid", "1", returnedString, sizeof( returnedString ), "data/enimy.cfg" );


    // 跳过以前已经加载过的敌机
    while ( *pStr && atol( returnedString ) < m_iTempo )
    {
        pStr += strlen( pStr ) + 1;   // 这样处理,就能使pStr指向下一个section name
        GetPrivateProfileString( pStr, "tempoid", "1", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
    }

    // 开始加载敌机
    while ( *pStr )
    {
        // 读取敌机的图片ID号
        GetPrivateProfileString( pStr, "imageid", "0", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        int imageID = atol( returnedString );

        // 读取敌机图片的总帧数
        GetPrivateProfileString( pStr, "imageframenum", "2", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        int imageFrameNum = atol( returnedString );

        // 读取敌机图片的宽度
        GetPrivateProfileString( pStr, "imagewidth", "50", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        int imageWidth = atol( returnedString );

        // 读取敌机图片的高度
        GetPrivateProfileString( pStr, "imageheight", "50", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        int imageHeight = atol( returnedString );

        // 读取敌机移动速度
        GetPrivateProfileString( pStr, "speed", "1", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        int speed = atol( returnedString );

        // 读取敌机的初始位置
        POINT initPos;
        GetPrivateProfileString( pStr, "pos.x", "50", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        initPos.x = atol( returnedString );
        GetPrivateProfileString( pStr, "pos.y", "0", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        initPos.y = atol( returnedString );

        // 读取敌机运动轨迹上的各个目标点
        int targetNum; // 目标点总数
        GetPrivateProfileString( pStr, "targetnum", "1", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
        targetNum = atol( returnedString );
        POINT *targetArray; // 存放各目标点坐标
        targetArray = new POINT[ targetNum ];   // 根据读取的目标点总数分配多少个坐标点

        // 读取每一个目标点坐标
        for ( int i = 0; i < targetNum; i++ )
        {
            char buf[32];
            sprintf( buf, "targetpos%d.x", i );
            GetPrivateProfileString( pStr, buf, "0", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
            targetArray[i].x = atol( returnedString );
            sprintf( buf, "targetpos%d.y", i );
            GetPrivateProfileString( pStr, buf, "0", returnedString, sizeof( returnedString ), "data/enimy.cfg" );
            targetArray[i].y = atol( returnedString );
        }

        // 根据读取的敌机数据,创建敌机,并放入容器当中
        CEnimy tmpEnimy( m_pGraph, m_pEnimyImageBuffer[imageID], 0x00000000, imageFrameNum, imageWidth, imageHeight );
        tmpEnimy.Init( initPos.x, initPos.y, speed, targetArray, targetNum );
        m_EnimyList.push_back( tmpEnimy ); //发射一架敌机
        pStr += strlen( pStr ) + 1;   // 取下一个字符串
        GetPrivateProfileString( pStr, "tempoid", "1", returnedString, sizeof( returnedString ), "data/enimy.cfg" );

        // 属于当前进度的敌机加载完后跳出while循环
        if ( atol( returnedString ) > m_iTempo )
            break;
    }   // end of while (*pStr)

posted @ 2006-08-21 20:21  cxun  阅读(1438)  评论(4编辑  收藏  举报