《C++游戏开发》笔记十二 战争迷雾:初步实现
本系列文章由七十一雾央编写,转载请注明出处。
http://blog.csdn.net/u011371356/article/details/9475979
作者:七十一雾央 新浪微博:http://weibo.com/1689160943/profile?rightmod=1&wvr=5&mod=personinfo
一、前言
在这里雾央先解释一下战争迷雾的概念,以下内容引自维基百科:
战争迷雾(Fogof War),在传统意义上是指战争中由于对敌人情报不清楚而无法确认除友军所在以外的大部分地区,敌人的分布及活动情况。而目前在游戏范围内,尤其是即时战略类游戏中,这个词语出现的频率更高一些也更被多数人所熟悉。
从最初的即时战略《沙丘2》开始,战争迷雾的概念开始被引入和正式提出。在沙丘中每一次新开始游戏时,玩家只能观察到自己基地及单位周围极小的范围,而绝大多数地图区域均被黑色遮盖。当他命令单位向黑暗区移动后,经过的区域会被自动打开,地图变得可见,包括该区域的地形/敌人活动情况等等。这一经典模式也被绝大多数后来的即时策略游戏继承。
最初出现战争迷雾的沙丘2中,这些迷雾是一次性的,当玩家移动并探索一次后,绝大多数情况下将不再需要探索即可永久享有该处的情报。
在随后出现的下一个著名即时战略游戏《命令与征服》中,开始引进重复探测的概念,在非剧情模式的单人游戏下,可以设置迷雾是否再生,一旦选项生效,所有探测过的地区,其周边的黑暗区域每过一段时间即会膨胀再次积压掉一部分已探索的区域。同时游戏中也首次出现了反情报的设施,用于产生永久性的迷雾,进入该区域的对手,只要单位离开,几乎立刻就会重新被战争迷雾所遮住。第3个关于战争迷雾的新创意则是通过一些手段来确保永久性全地图全开。
《命令与征服》之后,游戏中的战争迷雾逐渐被普遍分割定义为地图层和单位层两种,地图层所包括的地形,由于很难改变或者根本不可能变化,在单位移开后仍然能保证其情报有效性,而单位层主要指该区域的活动单位之情况,由于不可能确保对方仍然停留,在我方情报源消失(如侦查单位移动开)之后,即会再次被遮盖。对两种层次的迷雾约定俗成,使用不同程度的黑色来区分,地图层的黑色更深,而地图层打开之后,遗留下的单位层迷雾相对更淡。以上这种战争迷雾形式相对更经典和受到普遍采用。
在即时战略的发展中,另外出现了更多关于战争迷雾的变化及设计,比如将不同单位的打开迷雾的能力区分以体现不同价值,将战争迷雾的获得能力与地形结合,站在高处能获得更多视野,低处不能观察高处,一些游戏开始取消地图层迷雾的设计,地形图从一开始就对所有玩家开放,等等。
另外很多回合策略中也吸收了战争迷雾的概念,尤其出现于4X概念体系的作品中。比如文明中需要单位移动才能打开地图,单位站在高处能额外获得多1格的视野(3代),从4X之一元素--探索的角度来说,战争迷雾是4X游戏所必不可少的概念。
如下面这张图
在这个截图中,我们可以看到两种战争迷雾:颜色较浅的部分为已探开区域,颜色较深的部分为单位层迷雾,全黑色部分为地图层迷雾。(游戏来源:《命令与征服:将军绝命时刻》)
像上面那样精致的战争迷雾的实现是比较困难的一件事,雾央肯定是没有能力实现的,而且在百度和谷歌中关于游戏中战争迷雾的资料非常少,达到了稀有的程度,所以这方面的知识学习起来很不容易,但这并不妨碍我们自己实现一些简陋的战争迷雾的效果。
在这一节笔记里,雾央将实现一种最简单的战争迷雾的效果。在接下来的几节笔记里,雾央将利用地图拼接算法实现过渡比较*滑一点的战争迷雾效果,地图拼接算法参考了网上一篇文章,雾央也将原作者的地址贴出来,供大家参考,雾央也会自己用C++带着大家实现一遍,在此先向原作者致以崇高的敬意。
地图拼接算法:地图拼接与战争迷雾,使用AS3实现
二、效果
雾央这一节里实现的效果如下,大家将鼠标想象成人物,鼠标的移动大家自行脑补成人物在移动,哈哈,人物周围的空间就是被照亮的。
明亮处看到的仍然是大雪纷飞的场景,呵呵
最上面看到一小部分
话说雾央开始打算将鼠标指针换成蜡烛,这样看起来就有感觉多了,但是可惜没有找到蜡烛图案的cur文件,不过大家可以试着去实现人物端着蜡烛在房间里行走的demo,呵呵。
三、原理
下面雾央就来详细解释一下实现原理。
大家看着效果图,就知道了雾央是把地图分成了一个个网格,地图背景是800*600,雾央采用的每个小网格是20*20,这样整个地图就是40*30个网格了。这就是之前我们提到过的TileMap,只不过我们的地图分成了两层,背景层和前景迷雾层,背景层是一张整图,而迷雾是TileMap。
我们使用的黑色迷雾就是下面这个小图了
大家还记得之前提到过的TileMap吗?前面说过,我们可以使用一个二维数组来保存地图,在这里我们可以使用1表示可见,0表示迷雾区域。
那么原理就很简单了,我们将鼠标所在位置的周围区域的Tile设置为可见,其他设置为不可见即可了。
大家可以看到,这样实现起来非常的简单,但是效果非常的差。迷雾的散开是以一个个方块进行的,迷雾的边缘处看起来就是方方正正的,离游戏中的光滑边缘差别很远,毕竟实现原理过于简陋了。但是在采用聪明或复杂的算法之前,我们仍然可以对这种方法进行改进,比如,我们减小方块的大小,由20*20变为5*5,那么效果看起来就会好很多,如果方块可以尽可能小,那么效果就越来越接*于*滑,如果方块可以只有一个像素大,每次展开一个圆,那么和圆的大小就几乎差不多了吧,当然这样带来的贴图次数也多了很多。
四、实现
雾央封装了一个场景类:
class CScene { private: CImage m_bg; //背景图片 CImage m_black; //每块迷雾大小为20*20,对于800*600的窗口即有40*30个小迷雾块组成 int m_fogArray[40][30]; public: CScene(char *bg); ~CScene(); public: //绘制背景 void DrawBG(CDC &cDC); //绘制迷雾 void DrawFog(CDC &cDC); //更新迷雾区域 void UpdateFogArea(int x,int y); };
实现为:
#include"stdafx.h" #include"scene.h" CScene::CScene(char *bg) { m_bg.Load(bg); m_black.Load("black.png"); //将数组清0,0表示为黑色迷雾状态 memset(m_fogArray,0,sizeof(m_fogArray)); } //绘制背景 void CScene::DrawBG(CDC &cDC) { m_bg.Draw(cDC,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,0,0,WINDOW_WIDTH,WINDOW_HEIGHT); } //绘制战争迷雾 void CScene::DrawFog(CDC &cDC) { for(int i=0;i<40;i++) for(int j=0;j<30;j++) { if(m_fogArray[i][j]==0) m_black.Draw(cDC,i*20,j*20,20,20); } } bool CheckFog(int xBox,int yBox,int xMouse,int yMouse) { //出界了返回false if(xBox<0 || xBox>=40 || yBox<0 || yBox>=30) return false; //未出界,则距离鼠标点击中心小于一定的范围内可见 if( (xBox-xMouse)*(xBox-xMouse) + (yBox-yMouse)*(yBox-yMouse) <=16) return true; else return false; } //更新迷雾区域 void CScene::UpdateFogArea(int x,int y) { //首先计算出鼠标所在的格子 int xPosBox=x/20; int yPosBox=y/20; //将迷雾区域复原 memset(m_fogArray,0,sizeof(m_fogArray)); //设置可见区域 for(int xBox=xPosBox-8;xBox<xPosBox+8;xBox++) { for(int yBox=yPosBox-8;yBox<yPosBox+8;yBox++) { if(CheckFog(xBox,yBox,xPosBox,yPosBox)) m_fogArray[xBox][yBox]=1; } } }
接着是CChildView.h
// ChildView.h : CChildView 类的接口 // #pragma once #include "particle.h" #include "scene.h" // CChildView 窗口 class CChildView : public CWnd { // 构造 public: CChildView(); // 特性 public: //保存客户区大小 CRect m_client; //雪花 CParticle *m_snow; //场景 CScene *m_scene; //缓冲DC CDC m_cacheDC; //缓冲位图 CBitmap m_cacheCBitmap; // 操作 public: // 重写 protected: virtual BOOL PreCreateWindow(CREATESTRUCT& cs); // 实现 public: virtual ~CChildView(); // 生成的消息映射函数 protected: afx_msg void OnPaint(); DECLARE_MESSAGE_MAP() public: afx_msg void OnTimer(UINT_PTR nIDEvent); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnMouseMove(UINT nFlags, CPoint point); };
最后是CPP
//-----------------------------------【程序说明】---------------------------------------------- // 【MFC游戏开发】笔记十二 战争迷雾初步 配套源代码 // VS2010环境 // 更多内容请访问雾央CSDN博客 http://blog.csdn.net/u011371356/article/category/1497651 // 雾央的新浪微博: @七十一雾央 //------------------------------------------------------------------------------------------------ // ChildView.cpp : CChildView 类的实现 // #include "stdafx.h" #include "GameMFC.h" #include "ChildView.h" #include "mmsystem.h" #pragma comment(lib,"winmm.lib")//导入声音头文件库 #ifdef _DEBUG #define new DEBUG_NEW #endif // CChildView CChildView::CChildView() { } CChildView::~CChildView() { mciSendString("stop bgMusic ",NULL,0,NULL); delete m_snow; } BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_TIMER() ON_WM_CREATE() ON_WM_MOUSEMOVE() END_MESSAGE_MAP() // CChildView 消息处理程序 BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) { if (!CWnd::PreCreateWindow(cs)) return FALSE; cs.dwExStyle |= WS_EX_CLIENTEDGE; cs.style &= ~WS_BORDER; cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), NULL); //-----------------------------------游戏数据初始化部分------------------------- //打开音乐文件 mciSendString("open background.mp3 alias bgMusic ", NULL, 0, NULL); mciSendString("play bgMusic repeat", NULL, 0, NULL); //雪花 m_snow=new CParticle(100); m_snow->Init(); //场景 m_scene=new CScene("bg.png"); return TRUE; } void CChildView::OnPaint() { static float lastTime=timeGetTime(); static float currentTime=timeGetTime(); //获取窗口DC指针 CDC *cDC=this->GetDC(); //获取窗口大小 GetClientRect(&m_client); //创建缓冲DC m_cacheDC.CreateCompatibleDC(NULL); m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height()); m_cacheDC.SelectObject(&m_cacheCBitmap); //————————————————————开始绘制—————————————————————— //贴背景,现在贴图就是贴在缓冲DC:m_cache中了 m_scene->DrawBG(m_cacheDC); //贴雪花 m_snow->Draw(m_cacheDC); //更新雪花 currentTime=timeGetTime(); m_snow->Update(currentTime-lastTime); lastTime=currentTime; //画出战争迷雾 m_scene->DrawFog(m_cacheDC); //最后将缓冲DC内容输出到窗口DC中 cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY); //————————————————————绘制结束————————————————————— //在绘制完图后,使窗口区有效 ValidateRect(&m_client); //释放缓冲DC m_cacheDC.DeleteDC(); //释放对象 m_cacheCBitmap.DeleteObject(); //释放窗口DC ReleaseDC(cDC); } //定时器响应函数 void CChildView::OnTimer(UINT_PTR nIDEvent) { OnPaint(); } int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) return -1; // TODO: 在此添加您专用的创建代码 //创建一个10毫秒产生一次消息的定时器 SetTimer(TIMER_PAINT,10,NULL); return 0; } //鼠标移动改变迷雾区域 void CChildView::OnMouseMove(UINT nFlags, CPoint point) { m_scene->UpdateFogArea(point.x,point.y); }
《C++游戏开发》笔记十二到这里就结束了,更多精彩请关注下一篇。如果您觉得文章对您有帮助的话,请留下您的评论,点个赞,能看到你们的留言是我最高兴的事情,因为这让我知道我正在帮助曾和我一样迷茫的少年,你们的支持就是我继续写下去的动力,愿我们一起学习,共同努力,复兴国产游戏。
对于文章的疏漏或错误,欢迎大家的指出。