用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)
2019-12-21 21:03:36
2.18MB
DDraw
1