结束了上一节即时战略游戏的讲解,我们继续来到第一人称射击游戏的介绍,其实我感觉,真正要论游戏的复杂性,第一人称射击游戏绝对是最复杂的,尤其是室内场景的搭建和渲染,不过自己水平有限,而且也没人帮我建模,所以我们就已一个简单的室外场景为例说明下。
首先,室外场景怎么搭建,我们需要一个地图,这个地图的来源有很多种,我这里介绍很简单的一种,就是利用数组来构建地图,形式如下:
int MapTemp[20][20]={
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
};
其中0代表可以通行的路径,1代表墙壁,那么怎么去应用这个地图数组呢,首先,我们要构造墙壁和地面的Mesh对象,然后通过D3DXMatrixTranslation来建立一个位移矩阵,讲Mesh移动到相应的位置上去。构建地图的代码如下:
for(int i=0;i<20;i++)
{
for(int j=0;j<20;j++)
{
D3DXMATRIX mat;
D3DXMatrixTranslation(&mat,j*20,0,400-(i+1)*20);
CString strId;
strId.Format(L"%d %d",j*20,400-(i+1)*20);
if(m_MapInfo[i][j]==0)
{
CDXEntity *pPath=new CDXEntity(&m_PathMesh,&mat);
m_Root.AddEntity(pPath,strId);
}else if(m_MapInfo[i][j]==1)
{
CDXEntity *pWall=new CDXEntity(&m_WallMesh,&mat);
m_Root.AddEntity(pWall,strId);
}
}
}
代码还是很简单的吧,好了,构建好地图以后,我们怎么来渲染呢,因为场景不大,所以我们可以直接渲染,但是我这里没有这样做,而是用了八叉树来做管理,这里我就简单介绍下室外的场景管理。
场景管理来说我了解的大致分为三者:四叉树,八叉树,和bsp。四叉树的应用非常广,即可用于2d游戏,也可用于3d游戏,在室外简单场景中,四叉树是一个相对来说比较简单,方便的管理,因为他将空间分为四个组成部分,室外场景往往没有那么复杂的构成,所以四个空间组成就已经够用了。而八叉树,则是可以将任意复杂的空间分割开来的一个场景管理结构,不仅可以应用与渲染,也可以很好的适用于物体的碰撞检测,关于他,我推荐下面一篇文章:http://blog.csdn.net/zhanxinhang/article/details/6706217,这篇文章关于四叉树和八叉树的介绍写的很好,大家可以看看,最后一个是bsp树,它虽然是一个二叉树,但是却可以适用于任意维度的空间,而且有了它,我们可以进行多边形裁剪甚至可以忽略z缓冲来做渲染的位置前后排序。而且这个也是在《3d游戏大师编程技巧》里做详细介绍的,有兴趣的读者可以去翻一翻。
好了,回归主题,我们在这个主要使用了八叉树,也就是DXLib里的CDXOctTree,关于八叉树的构建如下所示:
void CDXOctNode::_Sort( CDXOctNode* pNode,int iDepth)
{
if(pNode->m_EntityList.size()<=1 || iDepth==iDepthNum)
{
return;
}
pNode->m_bSort=true;
float x1=pNode->m_MinVec.x,y1=pNode->m_MinVec.y,z1=pNode->m_MinVec.z;
float x2=pNode->m_MaxVec.x,y2=pNode->m_MaxVec.y,z2=pNode->m_MaxVec.z;
pNode->m_pChildNode[0]=new CDXOctNode();
pNode->m_pChildNode[0]->m_pParNode=pNode;
pNode->m_pChildNode[0]->m_MinVec=D3DXVECTOR3(x1,y1+(y2-y1)/2,z1+(z2-z1)/2);
pNode->m_pChildNode[0]->m_MaxVec=D3DXVECTOR3(x1+(x2-x1)/2,y2,z2);
pNode->m_pChildNode[0]->m_strId.Format(_T("%d0"),iDepth);
pNode->m_pChildNode[1]=new CDXOctNode();
pNode->m_pChildNode[1]->m_pParNode=pNode;
pNode->m_pChildNode[1]->m_MinVec=D3DXVECTOR3(x1+(x2-x1)/2,y1+(y2-y1)/2,z1+(z2-z1)/2);
pNode->m_pChildNode[1]->m_MaxVec=D3DXVECTOR3(x2,y2,z2);
pNode->m_pChildNode[1]->m_strId.Format(_T("%d1"),iDepth);
pNode->m_pChildNode[2]=new CDXOctNode();
pNode->m_pChildNode[2]->m_pParNode=pNode;
pNode->m_pChildNode[2]->m_MinVec=D3DXVECTOR3(x1,y1+(y2-y1)/2,z1);
pNode->m_pChildNode[2]->m_MaxVec=D3DXVECTOR3(x1+(x2-x1)/2,y2,z1+(z2-z1)/2);
pNode->m_pChildNode[2]->m_strId.Format(_T("%d2"),iDepth);
pNode->m_pChildNode[3]=new CDXOctNode();
pNode->m_pChildNode[3]->m_pParNode=pNode;
pNode->m_pChildNode[3]->m_MinVec=D3DXVECTOR3(x1+(x2-x1)/2,y1+(y2-y1)/2,z1);
pNode->m_pChildNode[3]->m_MaxVec=D3DXVECTOR3(x2,y2,z1+(z2-z1)/2);
pNode->m_pChildNode[3]->m_strId.Format(_T("%d3"),iDepth);
pNode->m_pChildNode[4]=new CDXOctNode();
pNode->m_pChildNode[4]->m_pParNode=pNode;
pNode->m_pChildNode[4]->m_MinVec=D3DXVECTOR3(x1,y1,z1+(z2-z1)/2);
pNode->m_pChildNode[4]->m_MaxVec=D3DXVECTOR3(x1+(x2-x1)/2,y1+(y2-y1)/2,z2);
pNode->m_pChildNode[4]->m_strId.Format(_T("%d4"),iDepth);
pNode->m_pChildNode[5]=new CDXOctNode();
pNode->m_pChildNode[5]->m_pParNode=pNode;
pNode->m_pChildNode[5]->m_MinVec=D3DXVECTOR3(x1+(x2-x1)/2,y1,z1+(z2-z1)/2);
pNode->m_pChildNode[5]->m_MaxVec=D3DXVECTOR3(x2,y1+(y2-y1)/2,z2);
pNode->m_pChildNode[5]->m_strId.Format(_T("%d5"),iDepth);
pNode->m_pChildNode[6]=new CDXOctNode();
pNode->m_pChildNode[6]->m_pParNode=pNode;
pNode->m_pChildNode[6]->m_MinVec=D3DXVECTOR3(x1,y1,z1);
pNode->m_pChildNode[6]->m_MaxVec=D3DXVECTOR3(x1+(x2-x1)/2,y1+(y2-y1)/2,z1+(z2-z1)/2);
pNode->m_pChildNode[6]->m_strId.Format(_T("%d6"),iDepth);
pNode->m_pChildNode[7]=new CDXOctNode();
pNode->m_pChildNode[7]->m_pParNode=pNode;
pNode->m_pChildNode[7]->m_MinVec=D3DXVECTOR3(x1+(x2-x1)/2,y1,z1);
pNode->m_pChildNode[7]->m_MaxVec=D3DXVECTOR3(x2,y1+(y2-y1)/2,z1+(z2-z1)/2);
pNode->m_pChildNode[7]->m_strId.Format(_T("%d7"),iDepth);
list<CDXEntity*>::iterator it;
for(it=pNode->m_EntityList.begin();it!=pNode->m_EntityList.end();)
{
for(int i=0;i<8;i++)
{
D3DXVECTOR3 min=pNode->m_pChildNode[i]->m_MinVec,max=pNode->m_pChildNode[i]->m_MaxVec;
if(CDXHelper::CheckBoxCollide(min,max,(*it)->m_BoundMin,(*it)->m_BoundMax))
{
pNode->m_pChildNode[i]->m_EntityList.push_back((*it));
//(*it)->m_NodeList.insert(pNode->m_pChildNode[i]);
}
}
it=pNode->m_EntityList.erase(it);
}
iDepth++;
for(int i=0;i<8;i++)
{
_Sort(pNode->m_pChildNode[i],iDepth);
}
}
可以看出八叉树的构建是一个深度递归的过程,递归结束的条件比较多,我这里是当节点里的物体为0或者为1的时候停止往下分割或者是当递归的深度等于最大递归深度的时候停止划分,应该来说代码还是比较容易理解的,我相信大家也能明白。值得一提的是八叉树对空间的规划还是比较费时的,所以我们应当在初始化的时候来做空间的分割动作。
划分好以后,我们该如何去渲染呢,这里不得不提一下视锥这个概念,什么是视锥,我们知道,3d空间里的坐标结果世界变换,视图变换和投影变换以后会被变换到一个x【-1,1】,y【-1,1】,z【0,1】这样的齐次裁剪空间里,这里面的点才是有效点,会经过视口变换到屏幕上去,那么我们怎么剔选出这些有效点呢,于是我们就需要将这个齐次裁剪空间进行逆变换,把他逆变换到世界坐标系中,然后我们就可以从成百上千的物体中剔选出我们真正可以观察到的物体来做渲染,这样可以极大的提高渲染效率。
D3DXPLANE Planes[6];
D3DXMATRIX Matrix,matView,matProj;
pDevice->GetTransform(D3DTS_PROJECTION,&matProj);
pDevice->GetTransform(D3DTS_VIEW,&matView);
Matrix=matView*matProj;
Planes[0].a=Matrix._14+Matrix._13;
Planes[0].b=Matrix._24+Matrix._23;
Planes[0].c=Matrix._34+Matrix._33;
Planes[0].d=Matrix._44+Matrix._43;
D3DXPlaneNormalize(&Planes[0],&Planes[0]);
Planes[1].a=Matrix._14-Matrix._13;
Planes[1].b=Matrix._24-Matrix._23;
Planes[1].c=Matrix._34-Matrix._33;
Planes[1].d=Matrix._44-Matrix._43;
D3DXPlaneNormalize(&Planes[1],&Planes[1]);
Planes[2].a=Matrix._14+Matrix._11;
Planes[2].b=Matrix._24+Matrix._21;
Planes[2].c=Matrix._34+Matrix._31;
Planes[2].d=Matrix._44+Matrix._41;
D3DXPlaneNormalize(&Planes[2],&Planes[2]);
Planes[3].a=Matrix._14-Matrix._11;
Planes[3].b=Matrix._24-Matrix._21;
Planes[3].c=Matrix._34-Matrix._31;
Planes[3].d=Matrix._44-Matrix._41;
D3DXPlaneNormalize(&Planes[3],&Planes[3]);
Planes[4].a=Matrix._14-Matrix._12;
Planes[4].b=Matrix._24-Matrix._22;
Planes[4].c=Matrix._34-Matrix._32;
Planes[4].d=Matrix._44-Matrix._42;
D3DXPlaneNormalize(&Planes[4],&Planes[4]);
Planes[5].a=Matrix._14+Matrix._12;
Planes[5].b=Matrix._24+Matrix._22;
Planes[5].c=Matrix._34+Matrix._32;
Planes[5].d=Matrix._44+Matrix._42;
D3DXPlaneNormalize(&Planes[5],&Planes[5]);
为什么视锥的构建是上面这样的呢,这里面有一定的数学推导过程,http://blog.sina.com.cn/s/blog_4db3fe550100kyc5.html,这篇文章我相信可以解决各位的疑惑,我在此就不做累述。
好了,视锥已经构建完成,存在于世界坐标系里,下面便是判断物体是否在视锥里,因为我们的物体都是从局部坐标系经过世界变换到世界坐标系里的,难道要我们还要依次去计算每个顶点变换后的位置吗,这样显然效率很低,我在这里用了碰撞盒的技术,我们只要预先计算好一个物体的碰撞盒,就算这个物体经过了怎么样的变换,我们只要相应的去变换这个碰撞盒,然后去计算碰撞盒的顶点位置并重新调整碰撞盒,便可以得出这个物体的大概实际位置以及所占空间的大小,虽然这个方法不能说很精确,但是确实是一种很快捷,效率也很高的方法。我在这里给出判断物体是否在视锥里的代码,同样是一个深度递归,和八叉树的构建有些类似。
void CDXOctNode::_Render( CDXOctNode* pNode,D3DXPLANE* Planes,bool bCheck )
{
if(bCheck)
{
bool bFullContained;
if(_CheckInFrustum(pNode->m_MinVec,pNode->m_MaxVec,Planes,&bFullContained))
{
if(pNode->m_bSort==true)
{
for(int i=0;i<8;i++)
{
_Render(pNode->m_pChildNode[i],Planes,!bFullContained);
}
}else
{
if(pNode->m_EntityList.size()>0)
{
list<CDXEntity*>::iterator it;
for(it=pNode->m_EntityList.begin();it!=pNode->m_EntityList.end();it++)
{
CDXEntity *pEntity=*it;
bool bFull;
if(_CheckInFrustum(pEntity->m_BoundMin,pEntity->m_BoundMax,Planes,&bFull))
{
pEntity->Render();
}
}
}
}
}
}else
{
if(pNode->m_bSort==true)
{
for(int i=0;i<8;i++)
{
_Render(pNode->m_pChildNode[i],Planes,false);
}
}else
{
if(pNode->m_EntityList.size()>0)
{
list<CDXEntity*>::iterator it;
for(it=pNode->m_EntityList.begin();it!=pNode->m_EntityList.end();it++)
{
CDXEntity *pEntity=*it;
pEntity->Render();
}
}
}
}
}
最后所有需要渲染的物体会保存在一个渲染列表里,然后做相应的变换并渲染。当然我这个八叉树渲染做的也有很多不足的地方,比如说材质的切换,这个是一个非常消耗资源的操作,如何可以把相同的材质的顶点统一渲染,那么渲染的效率必将提升一个档次。
文章有不足之处,还望各位多多指正。