基于C++开发的射击游戏

时间:2016-05-20 10:39:56
【文件属性】:

文件名称:基于C++开发的射击游戏

文件大小:2.18MB

文件格式:RAR

更新时间:2016-05-20 10:39:56

DDraw

用DDraw实现射击游戏说明文档 要点一:画图自动切割 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; 只需将上述代码加在CGraphic::BltBBuffer() 中的m_bRect = m_rect; 前即可。 要点二:背景的滚轴实现 画背景可以分为以下三种情况: 情况一:背景图片与窗口等高 情况二:背景图片高度小于窗口高度 情况三:背景图片高度大于窗口高度 上述讲解图与代码相对应地看,有助于容易理解。 另外,要点一实现之后,由于已经可以自动切割,画背景可以用其它方法。 要点三:精灵图的实现 在游戏中,如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::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::iterator itei; //vector迭代器 for (itei = m_BulletList.begin(); itei != m_BulletList.end(); itei ++) //遍历所有子弹 { itei->pos.y -= itei->speed; //子弹飞行 } 要点五:碰撞检测 使用Windows API函数RectInRegion: vector::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)


【文件预览】:
射击游戏
----Rator.rar(701KB)
----Rator(src).rar(1.5MB)

网友评论

  • 代码很详细,又有游戏说明书,学习中
  • 很详细,学习中
  • 代码很好,很详细,学习中
  • 我下载了你上传的这个设计小游戏,想自己学习一下。这个游戏的编译环境是?我知道有vc++6.0,那个.bmp格式的图是用什么啊?还有能推荐比较实用的书吗?谢谢你了。