说起即时战略游戏,我第一时间想起魔兽争霸,这个不知道陪伴我多少个日日夜夜,让我哭让我笑的游戏,让我想起了sky,moon,grubby等人牵动心弦的战斗历程,让我想起了当年日日守在电脑前专注的欣赏着wcg的每一场比赛,想起了当年学校门口的网吧里我跟我哥在浩方上奋力的拼杀着,想起了很多年前和寝室室友打赌谁输谁请一天杂粮饼的承诺。哎,不说了,说起来都是泪啊。那么进入本文的正题吧,用D3D加mfc编写一个即时战略游戏。
其实这个游戏只是一个很简单的demo,各位千万不要把他想复杂了,但是我也实现了即时战略游戏的基本需求:地图编辑器,人物寻路,动态行走,网络同步等功能。在写这个游戏之前,我也在网上拼命的搜寻着相关资料,却发现这方面的资料十分残缺,许多都是只言片语,所以我觉得我需要把自己掌握的东西与大家一起分享,这样也能让跟我一样迷茫的朋友从中多少有点收获。
任何一个像样的游戏都离不开一个地图编辑器,我们可以在这个编辑器上面去创建修改我们想要的地图,本文里的地图编辑器就是用mfc编写,而地图文件用xml储存,这样查看的话也很方便。下面给出一个地图文件的样式:
<?xml version="1.0"?>
<root>
<class type="house" width="50" height="50">
<item left="780" top="460"/>
</class>
<class type="tree" width="50" height="50">
<item left="940" top="410"/>
</class>
</root>
这个文件很简单,class节点就是地图上元素的类型,目前支持house和tree,item则是每一个该类型元素所在的位置。下面我们来看一下如何用mfc来编写这个地图编辑器。
首先,在我们的程序界面上需要一个可以实时渲染地图场景的窗口,这个窗口用d3d来渲染,那么这个窗口怎么实现,大家应该还记得在d3d初始化的时候会指定一个窗口的句柄,于是我便定义一个名叫m_DrawWnd的CStatic类型的变量,然后在OnInitDialog函数里面创建该窗口即可:
m_DrawWnd.Create(0,0,CRect(10,10,810,610),this,0);
m_DrawWnd.ShowWindow(1);
然后在d3d初始化的时候指定该窗口的句柄:
m_d3dpp.hDeviceWindow = m_DrawWnd.m_hWnd;
m_d3dpp.BackBufferWidth = 800;
m_d3dpp.BackBufferHeight = 600;
是不是很简单,不过,等等,渲染那一块怎么办呢,mfc是消息驱动来重绘窗体的,很难做到实时渲染的啊,不要急,我仔细看了下mfc 的文档,发现WM_KICKIDLE这个消息,看下这个消息的官方解释:
So, how do you handle idle processing in a dialog-based app where the dialog has no parent window? Fortunately, it's trivial. The MFC developers provided a hook: WM_KICKIDLE. RunModalLoop sends this MFC-private message repeatedly when there are no messages in your dialog's queue just the way CWinThread::Run calls OnIdle. RunModalLoop even passes a counter and increments it for you. In effect, WM_KICKIDLE is the dialog equivalent of OnIdle. (Historical note: earlier versions of MFC did the modal/modeless swap and WM_KICKIDLE thing for property sheets. Apparently it worked so well they decided to make all modal dialogs modeless.)
其实就可以简单的看做是窗体的空闲消息,如果我们需要做实时渲染的话,那么这个消息的返回值应该为1,否则返回0就可以了。(我在看了mfc源码后发现,当我们的消息为wm_mousemove或者wm_ncmousemove的时候,会重置idle状态,如果当前没有接受到新的消息时,且idle为true的时候,会去发送 WM_KICKIDLE消息,以上只针对模式对话框)。当然了,实时渲染的代价便是cpu的上升,所以会有一个idlecount来记录WM_KICKIDLE被send 的次数,感兴趣的朋友可以自己去试试,看看其中究竟是怎么一回事。好了,渲染部分已经差不多了,下面我们来看下怎么去显示地图。我们的地图大小是1600*1200,但是我们设定的bufferSize只有800*600,所以我们需要去移动地图来显示地图的不同位置,怎么移动,按住鼠标左键拖动即可,代码实现如下:
POINT p;
GetCursorPos(&p);
if(m_bDown)
{
m_iScreenLeft-=(p.x-m_iDownX);
m_iScreenTop-=(p.y-m_iDownY);
m_iDownX=p.x;
m_iDownY=p.y;
if(m_iScreenLeft<0)
{
m_iScreenLeft=0;
}
if(m_iScreenLeft>800)
{
m_iScreenLeft=800;
}
if(m_iScreenTop<0)
{
m_iScreenTop=0;
}
if(m_iScreenTop>600)
{
m_iScreenTop=600;
}
CString strPoint;
strPoint.Format(L"Left:%d,Top:%d",m_iScreenLeft,m_iScreenTop);
SetWindowText(strPoint);
}
是不是很简单啊,那么下面我们该怎么往这个地图场景上添加元素呢,这个也不难,我创建一个imagelist,往里面添加我想要的图片,然后绑定listctrl就可以了啊。
m_TexListControl.MoveWindow(850,350,120,200);
m_TexListControl.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES);
m_TexListControl.SetIconSpacing(CSize(100, 90));
m_ImageList.Create(60,60,ILC_COLORDDB|ILC_COLOR32, 1,1);
WCHAR buf[255]={0};
GetCurrentDirectory(255,buf);
CString strDir=buf;
CBitmap bit;
bit.Attach(LoadPicture(strDir+L"\\img\\tree.jpg"));
m_ImageList.Add(&bit,RGB(0,0,0));
bit.Detach();
bit.Attach(LoadPicture(strDir+L"\\img\\house.jpg"));
m_ImageList.Add(&bit,RGB(0,0,0));
bit.Detach();
m_TexListControl.SetImageList(&m_ImageList,LVSIL_NORMAL);
m_TexListControl.InsertItem(0,L"tree",0);
m_TexListControl.InsertItem(1,L"house",1);
是不是也很简单啊,然后我们需要去记录当前地图上每一个位置的状态,比如说在地图的宽300,高200 的位置上有没有物体啊什么的,这里我们需要一个变量来记录这些,于是我定义:
byte m_MapInfo[600*2][800*2];
这里一定要注意,我们需要修改堆栈的大小,默认堆栈的大小为1MB,我们这里设置为4MB,否则会报错,属性->链接器->系统->堆栈保留大小 设置为4096000即可。
if(m_bNewBuildVaild)
{
for(int i=0;i<100;i++)
{
if(m_BuildingInfo[i]==0)
{
m_BuildingInfo[i]=new sBuildingInfo;
m_BuildingInfo[i]->type=m_strSelectTex;
CRect NewBuildRect(m_iScreenLeft+m_NewBuildRect.left,m_iScreenTop+m_NewBuildRect.top,m_iScreenLeft+m_NewBuildRect.right,m_iScreenTop+m_NewBuildRect.bottom);
m_BuildingInfo[i]->rect=NewBuildRect;
for(int row=0;row<NewBuildRect.Height();row++)
{
for(int col=0;col<NewBuildRect.Width();col++)
{
m_MapInfo[NewBuildRect.top+row][NewBuildRect.left+col]=i;
}
}
break;
}
}
}
m_strSelectTex.Empty();
当我们选中一个物体,放置在地图上的时候,便会调用上面的代码,在这里我说一下,m_MapInfo存储的元素为200的时候,表明该位置为空,否则为新建物体的id。值得注意的是,物体之间是不能重叠的,这个我在onmousemove里面做了相应的判断。
CRect r;
m_DrawWnd.GetWindowRect(&r);
if(!m_strSelectTex.IsEmpty() && PtInRect(&r,p))
{
D3DSURFACE_DESC desc;
m_TexList[m_strSelectTex]->GetLevelDesc(0,&desc);
int width=desc.Width;
int height=desc.Height;
int left=p.x-r.left-width/2;
int top=p.y-r.top-height/2;
left=floor(left*1.0/10)*10;
top=floor(top*1.0/10)*10;
m_NewBuildRect.SetRect(left,top,left+width,top+height);
CRect NewBuildRect(m_iScreenLeft+left,m_iScreenTop+top,m_iScreenLeft+left+width,m_iScreenTop+top+height);
m_bNewBuildVaild=true;
if(NewBuildRect.top<0)
{
m_bNewBuildVaild=false;
}else if(NewBuildRect.left<0)
{
m_bNewBuildVaild=false;
}else if(NewBuildRect.bottom>1199)
{
m_bNewBuildVaild=false;
}else if(NewBuildRect.right>1599)
{
m_bNewBuildVaild=false;
}else
{
for(int row=0;row<NewBuildRect.Height();row++)
{
for(int col=0;col<NewBuildRect.Width();col++)
{
if(m_MapInfo[NewBuildRect.top+row][NewBuildRect.left+col]!=200)
{
m_bNewBuildVaild=false;
break;
}
}
}
}
}
好了,这一部分的主体代码差不多就这些,其他的代码大家可以看下源码,有什么不懂的可以一起交流下,下一节我将给大家带来即时战略游戏中非常重要的一章:寻路。
本文有不足之处,还望大家多多指正。