- 注册时间
- 2005-4-8
- 最后登录
- 1970-1-1
|
大家好,这篇文章是一个系列的开始,我将在这个系列中以游戏为线索,全面地介绍C++语言,同时给出几个游戏开发的实例,供大家参考。
本次的例子已经放出,本篇将对这个例子的一部分作详细的解释。
例子:(点我)贪食蛇
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
一、引言:
c++是什么?我看到版上很多人都有自己的见解。从统计结果来看,c++也是一门有些群众基础的语言,但是大家对c++的了解却程度不一。有些人认为c++就是个库很多的c,稍稍沾边一点的看法是c++就是带类的c,也有激进的看法认为c++彻底是一门新的语言,和c没关系。其实,这些观点在某些情况下都是正确的,但是我个人来看,更愿意把c++当成c精神的继承,肉体的再生。
灵活高效,相信程序员的宗旨一直是c/c++的灵魂,所以无论如何,在语言的设计中,效率都是第一条要考虑的内容。而语言的简洁,易用等等影响语言吸引力的特性确实被重视得不够。不过这并不影响c++被广泛的使用在各个领域,因为他实在太快了
其次,即使c++是c的一个超集(好吧,在c99出来以后不再是了),c++和c语言又有很大的不同。从语言本身,c++是混合范型的语言。OO(FAQ1),泛型,Functional等等主流的泛型都可以在c++中找到语言的支持。而c不同,c是典型的面向过程语言。有人说c也可以oo。当然可以,啥都可以oo。可是那个叫模拟,而不是支持。在语言层面上,c没有任何支持oo的语法机制以及背后的系统。要说模拟的话大家都是汇编写出来的,能说汇编oo么?从语言库的支持上,两者其实不相上下,不过c++的库大凡都是oo的库,还有最近出来的泛型库,而且c++基本都可以使用c的库。从这一点上来看,c++的库比c多一些。
好了,区别好c/c++,别的语言就无需多讲。语言的好坏不需要去争论,只要看看什么地方适合就成了。而游戏设计偏偏就是最适合c++的地方,高效快速可以保证游戏流畅的进行,而
强大的抽象机制又可以支持复杂的工程设计。好了,废话少讲,下面进入正题。
二、贪食蛇:
贪食蛇这个东西是个好例子。游戏逻辑简单,但是又不乏可玩性,也有相当的图像效果,用它来作讲解,可涵盖很大层面的东西,但是又不至于太复杂。这一节将详细介绍前面放出的demo中实现游戏的代码。
1、关于框架:
大家一定发现,我的工程中有很多文件,其实其中大部分是为了维持一个Windows应用程序正常运行的固定代码。Windows程序运行的机制和一般的像在文区星上或者Dos下运行的程序不一样。为了运行一个Windows程序,你需要注册一个窗口类,建立一个窗口,创建消息循环,监听消息,处理消息。好在这些代码可复用性很高,尤其对于游戏而言,他们统统都是一样的,所以我将他们封装起来,做成一个框架,大家写程序的时候就可以不用理会他们。这个框架其实是我和FantasyDR正在做的一个东西的一部分,我拿来做了些修改。下面列表表示各个文件中的代码表示的含义,有兴趣的人可以参照一下:
Application.h Application.cpp 包装消息循环
Window.h Window.cpp 包装窗口的注册和创建
Frame.h Frame.cpp 对于上面两层的一层修饰,滤掉面向对象的东西,留下简单的函数接口,另外一些自定义API也在其中
Util.h 不必关注,里面有框架需要的简单的定义,处于完整性考虑,没有作处理,有很多用不着的东西
Main.cpp
这个文件就是整个游戏的游戏逻辑部分了,游戏就是在这个文件中实现的,下面将详细分析这个文件 。
2、Main.cpp:
a. 结构:
对于我的框架,有两个函数需要实现:void Main(HWND hWnd),和void Render(HWND hWnd) (FAQ2)
不要被HWND吓倒,其实他就是一个整数,用来表示当前的窗口,有很多信息都可以从这个传入的参数中获得。Main这个函数就像一般c语言程序中的main一般,可以把所有的代码写在里面,它从头到尾地执行。
大家只要想象一开始你就有一块白白的屏幕,然后你的程序从这个Main的第一行开始执行就可以。不过由于游戏的特性,我们生成了Render这个函数。这个函数将在Main执行完毕后开始执行,而且他是一直不停的重复执行,直到你调用Exit函数。
只要做过游戏的人都知道,我们往往需要一个无限的循环来让游戏进行,每循环一次都刷新一桢并监测输入,处理屏幕上的元素,这里的Render函数就是这个作用。而Main这个名字回头来看反倒像是Init函数之类的,一般只用来进行初始化操作了。
b. 关于API:
在Windows下编程自然离不开WindowsAPI,不过很多API涉及比较麻烦的东西,刚开始见的话太头大,所以我自己定义了一些现在用的到的API,包括:
int GetKey();
int WaitKey();
void Exit();
三个函数。而对于绘图函数我则没有将他们封装起来,因为一来他们不是很难以理解,二来大家可以自由替换他们,实现自己的绘图功能,封起来的话可能有些麻烦。
c. 关于绘图:
Windows下绘图的工具叫做GDI,其实就是一大包函数,他们的核心是一个类型为HDC的变量。同那个HWND,它其实就是整数类型,这个变量现在大家可以当成是屏幕(它的学名是设备上下文),所有绘图函数都要有这么一个参数告诉他们往哪里画:
Rectangle(HDC hDC,int x,int y ,int cx,int cy);
这个画长方形的函数就是个很好的例子,第一个参数是个HDC,后面四个为长方形左上角和右下角的坐标,调用一下这个长方形就画好了。
那个HDC要从那里获得呢?这里有一个函数:
HDC GetDC(HWND hWnd);
所以只要有了HWND,就有HDC。而HWND是框架传入的参数,所以大家不用担心找不到他。
GDI函数大致可分类为:设备上下文函数(如GetDC、CreateDC、DeleteDC)、画线函数(如LineTo、Polyline、Arc)、填充画图函数(如Ellipse、FillRect、Pie)、画图属性函数(如SetBkColor、SetBkMode、SetTextColor)、文本、字体函数(如TextOut、GetFontData)、位图函数(如SetPixel、BitBlt、StretchBlt)、坐标函数(如DPtoLP、LPtoDP、ScreenToClient、ClientToScreen)、映射函数(如SetMapMode、SetWindowExtEx、SetViewportExtEx)、元文件函数(如PlayMetaFile、SetWinMetaFileBits)、区域函数(如FillRgn、FrameRgn、InvertRgn)、路径函数(如BeginPath、EndPath、StrokeAndFillPath)、裁剪函数(如SelectClipRgn、SelectClipPath)等。
其中有些我也没用过,常用的大概就是像LineTo,FillRect等等,这里需要注意的是画线,大家要先调用MoveTo函数,将某个神秘的点移动到一个位置,再调用LineTo指定从那个点画到哪里。对于参数,大可在IDE中(例如vc6,vc.net)输入函数名,再打一个“(”,它的参数列表自然会跳出来。
d. 代码详解:
注意:对于c/c++程序员,应该可以无障碍阅读。对于有指针概念的程序员,
阅读起来也不困难,对于没有指针概念的程序员,可以把指针理解为一个变量的位置,或者干脆就把指针理解为一个变量好了。
关于c/c++的基本语法,不妨自己买本书看看,最基本的部分其实不难的
- #include "Window.h"
- #include "time.h" //这个东西是设置随机数种子用的,不用管他
- #include "Frame.h"
- using namespace WinApp; //不要管他
- POINT food; //POINT是一个Windows预定义好的struct,有x,y两个分量
- struct Snake
- {
- POINT p;
- Snake* next;
- Snake* before;
- }; //表示蛇的一个结构,其实就是个双向的链表
- Snake* head;//链表头,可能c程序员习惯写 struct Snake* head。不过这里大可省略struct。
- Snake* end; //链表尾
- int dir; //表示蛇的方向,上右下左:
- 0 1 2 3 4
- int dx[]={0,1,0,-1};
- int dy[]={-1,0,1,0}; //用来控制移动的数组,四个元素分别表示四向移动的增量(比如向上,dx[0]=0,
- dy[0]=-1表示x方向不动,y方向减一
- int x=0,y=0; //蛇的位置
- int width=10,height=10; //蛇身每一块的大小
- RECT rect; //屏幕大小,RECT也是Windows预定义结构,有左上角坐标和宽,高分量
- int rx,ry; //那个“豆”的位置
- bool MoveSnake()
- {
- //将蛇向现在方向移动一步,大量的链表操作,大凡就是将尾部的节点移动到头部,并把它的位置修改为
- //下一步蛇的位置。
- int x=head->next->p.x;
- int y=head->next->p.y;
- end->p.x=x+dx[dir]*width;
- end->p.y=y+dy[dir]*height;
- if(end!=head->next)
- {
- Snake* s=end->before;
- end->next=head->next;
- end->before=head;
- head->next->before=end;
- head->next=end;
- end=s;
- s->next=0;
- }
- //下面判断是不是撞墙了,或者是不是撞倒了自己
- if(head->next->p.x > rect.right || head->next->p.x<0 || head->next->p.y > rect.bottom || head->next->p.y < 0)
- return false;
- Snake* sh=head->next;
- while(sh!=0 && sh->next!=0)
- {
- if(head->next->p.x==sh->next->p.x && head->next->p.y==sh->next->p.y)
- return false;
- sh=sh->next;
- }
- return true;
- }
- void DrawSnake(Snake* shead,HDC hDC)
- {//把蛇画出来
- /*下面是GDI函数,
- SelectObject选择一个刷子,返回值是一个HGDIOBJ(还是个整数),表示原来的刷子
- GetStockObject获得一个系统刷子,那个参数表示要一个灰色的刷子
- */
- HGDIOBJ hBr=::SelectObject(hDC,::GetStockObject(GRAY_BRUSH));
- while(shead!=NULL)
- { //循环遍历链表
- //画一节
- ::Rectangle(hDC,shead->p.x,shead->p.y,shead->p.x+width,shead->p.y+height);
- shead=shead->next;
- }
- ::SelectObject(hDC,hBr);//把原来那个刷子恢复回去
- }
- void DrawSeed(HDC hDC)
- { //画那个“豆”
- HGDIOBJ hBr=::SelectObject(hDC,::GetStockObject(GRAY_BRUSH));
- ::Rectangle(hDC, rx,ry,rx+width,ry+height);
- ::SelectObject(hDC,hBr);
- }
- HBRUSH brush;
- void Clear(HWND hWnd,HDC hDC,POINT p)
- { //清除掉某个位置的一个方块
- RECT re;
- re.left=p.x;
- re.top = p.y;
- re.right=p.x+width;
- re.bottom=p.y+height;
- //这里用FillRect,使用那个颜色为背景的brush,效果就是擦去
- ::FillRect(hDC,&re,brush);
-
- }
- void Main(HWND hWnd)
- { //我们的主函数
- srand(::time(NULL));//设置随机种子,如果要用随机数,这句必须先被调用一次,而且只需一次
- /*
- 下面这句是动态内存分配。对于c程序员,你们使用的是malloc,这里使用new。
- 对于其他没有指针和动态内存分配概念的人,可以假装这里就是为这个head变量找了个地方,让它能放下Snake这个结构。
- */
- head=new Snake();
- head->next=new Snake();//head是链表中为了方便而放置的头指针,它的next才是真正的蛇身
- end=head->next; //end一开始只有一个蛇身,只好指向他
- head->next->p.x=100;
- head->next->p.y=100; //设置那唯一一个蛇身,同时也是蛇头的位置
- head->next->next=0; //链表的下一个是空指针,就是0
- head->next->before=head; //蛇身的上一个指针是head
- dir=0; //初始方向向上
- /*
- 这个也是一个Windows API函数, 用来获得当前窗口的大小,就当屏幕大小好了
- 执行完这句,rect里面就是屏幕的大小了
- */
- ::GetWindowRect(hWnd,&rect);
- rx=rand()%(rect.right-rect.left) / width * width;
- ry=rand()%(rect.bottom-rect.top) / height * width;//产生一个屏幕内的随机数
- /*下面是几个GDI相关的函数
- CreateSolidBrush创建一个刷子,其中的参数是它的颜色,他返回一个HBRUSH(同样,是个整数)
- 代表了一个刷子
- GetBkColor是获得背景色的函数,它的参数就是那个HDC,我们用GetDC获得
-
- 刷子的作用是在绘制封闭图形时(圆,长方形),如果当前有个刷子,它会用这个刷子把图形填满
- */
- brush=::CreateSolidBrush(::GetBkColor(::GetDC(hWnd)));
-
- }
- void Render(HWND hWnd)
- { //这是我们的渲染函数,游戏逻辑和绘制将在这里进行
- int k=GetKey(); //获得当前的按键(注意,GetKey不会等待)
- switch(k) //switch关键字,表示对于k的值分类讨论
- {
- /*
- VK_UP是Windows预定义常数,表示按键“上”
- 这句意思是,如果k等于VK_UP,也就是刚才按了上,那么
- */
- case VK_UP:
- if(dir==1||dir==3)//如果现在是向左,或者向右
- dir=0; //变成向上
- break; //每个case必加
- case VK_RIGHT:
- if(dir==0||dir==2)
- dir=1;
- break;
- case VK_DOWN:
- if(dir==1||dir==3)
- dir = 2;
- break;
- case VK_LEFT:
- if(dir==0||dir==2)
- dir = 3;
- break;
- }
- HDC hDC=::GetDC(hWnd);//获得那个HDC,用来画图
- /*下面这句的意思是清除掉蛇尾的方块,详见这个函数的定义(就在上面)
- 设计思想是,由于蛇每次向前走一格,身体其他部分不动,只是蛇尾消失,蛇头长出来一块
- 所以这里先去掉蛇尾
- */
- Clear(hWnd,hDC,end->p);
- /*下面这句是现将蛇移动一步,然后判断这步移动是不是成功
- 成功的话没说的,否则表示撞了墙或者自己,就失败了
- */
- if(!MoveSnake())
- {
- ::TextOut(hDC,100,100,"Die",3);//GDI的函数,在指定位置写字,最后那个参数表示有几个字要写
- /*我写得API,表示等待一个按键
-
- 注意,虽然这个函数是有返回值的,不过在C/C++中,如果用不到,可以不写那个返回值
- */
- ::WaitKey();
- //结束程序
- ::Exit(hWnd);
- };
- if(head->next->p.x==rx && head->next->p.y==ry)//蛇头的位置和豆的位置相同
- { //那么,蛇会长一节
- Snake* snake=new Snake;//新建一节蛇
- snake->p.x=rx;
- snake->p.y=ry; //位置就在豆上
- snake->next=0;
- snake->before=end;
- end->next=snake;
- end=snake;//链表操作,把新的一节接在尾巴上
- rx=rand()%(rect.right-rect.left) / width * width;
- ry=rand()%(rect.bottom-rect.top) / height * width;//重新生成一个豆
- }
- //调用上面写好的函数画豆和蛇
- DrawSeed(hDC);
- DrawSnake(head->next,hDC);
- //停100毫秒,让蛇跑的不会太快
- ::Sleep(100);
- //注意,到了这里会返回函数头部继续执行
- }
复制代码
e. 呼,打完了。这段代码说长不长,不过对于初学者来说是有点大。但是要写一个游戏出来至少得这么多。
希望大家可以仔细研究研究这段不长的代码。当然,重点不是贪食蛇的实现,而是整体的架构实现,包括Main和Render的分工,包括绘图的时机和方法。
这段代码实现得并不好看,而且有经验的c++程序员会看出来,这其实就是一段c代码,和c++其实关系不大。我这里没有使用c++的特性来写是因为极力想使他看起来更贴近于大家平时所见的代码,包括c,lava甚至Basic程序员等等愿意接受的形式(不得不说这并不容易)。不过要注意,这种代码风格是不值得提倡的,我们既然有了c++,就要使用它更有力的特性,让我们能更方便,快捷的生成代码。
这次的目的就是让大家熟悉我使用的框架,并且也能使用它做做自己的东西。下一次我将重写这段代码,那时候才是c++正式登场的时候
3、作业:
确实应该练习一下,这里希望大家可以把所有代码清空,只留下include和Main(),Render()两个函数的外壳,自己可以试着调用:
GetDC,Rectangle,MoveTo,LineTo,Sleep
这些函数画画玩玩,也可以去找找别的GDI函数来画一画,稍稍练习一下就很有效果了
我所有的代码其实都是c++代码,不过这期的教程我尽量在用c的样子来写,因为怕把很多潜在读者吓跑了,不过确实还有些问题,我会修改一下。不过说起来c++使用的话在某些方面其实要比c容易一些,因为他有很多已经写好的,很好用的标准组件,方便得很,而c就要从头来做,下一讲将会引入他们,可以让这段代码变短不少,而且也易读很多。
三、FAQ:
1、oo是什么?
oo是Object-Oriented的缩写,也就是面向对象的意思,这是一种程序设计方法,他使用类和对象的概念试图将程序中的物体当作现实世界的物体来处理。不过这只是理想的想法,实际应用中其实是靠很多技巧和规则在支持(可能你现在看不懂,没关系,以后多写写代码,回头来看就是了,现在只要明白他和过程式程序设计是一个层次的概念就好了)。c++是一种面向对象的语言,也就是他从语法语意上支持面向对象的设计。
2、HWND hWnd ???
HDC hDC ???
其实c中也有类似的东西,HWND就是一个typedef,本质上还是个整数(可能不同版本的vc会把他解释成不同的类型)
是声明(定义)变量的语句,表示hWnd是HWND类型的变量,hDC是HDC类型的变量,就好像在C语言中:
int Hello;
表示Hello是int 型的变量一样。
用在参数列表中就表示这个参数类型是HWND/HDC
如果你看得C的书比较老(比如老到你看不懂我上面这句话),他会有一种奇怪而古老的函数形参定义语法,那么建议你换书。其实最好直接看C++的书,跳过C吧
至于HWND 和HDC是系统定义的两种类型,其实他们就是整数类型,用来表示一个系统的对象(就是系统分配给你一个东西,给了一个这个东西号码,以后你通过这个号码就能找到那个东西)。HWND是窗口句柄(号码),用来表示一个窗口。而HDC使设备上下文句柄(号码),用来表示……就表示一个可以画画的画布。 |
|