<pre name="code" class="cpp">
本文为用C++写的一个贪吃蛇的小程序。旨在融入进面向对象编程的思想。希望巩固所学,这里有全部的源代码,也希望你不吝惜笔墨提出自己的看法。我将试着展示如何利用面向对象的思维从最初建模到代码的产生的整个过程,即本文的重点是尽力能够让你在阅读完后觉得,“恩,好像有些面向对象的味道”。
阅读本文之前,请先读读下面几点,这将有助于你阅读后面代码
1)类的成员变量都以“m_”开头,紧接着第一个小写字母是标志变量类型的一个字符,如i或者n表示int型,b表示bool型,pt表示指针变量等,后面的单词首字母大写,但若某个变量的类型是自定义或者是容器类型,则此变量只以“m_"开头,且第一个单词首字母小写(此为小程序,用不着如此讲究,但为了编程规范,所以……);
2)这里的变量名都比较长,这可能会使得在阅读代码时觉得臃肿,但正如以空间换时间一样,要想代码易于理解,就必须有所小牺牲,我的目的是你在看源代码有一种看伪代码的感觉,只要规范建立好了,你就能感觉到一马平川;
3)就展示面向对象思维而言,本应该去掉那些花哨,但为了程序的完整性,还是加入了一些面向过程的成分。但请将重点放在各个类和各个类之间的通讯方式上,即你的重点应该放在体会面向对象思想上,不要被过于看重main.cpp里面流程的控制,不过我还是会做一些必要的说明;
下面进入正题,面向对象编程,我的经验是问自己三个问题:
1)有哪些类(对象);
2)每次类的职责是什么;
3)这些类(对象)相互之间是怎样通讯的;
一)贪吃蛇这个游戏里面有哪些对象?我将之抽象出以下几个类:
蛇身节点类(BodyNode,其实是个结构体)、食物类(Food)、蛇类(Snake)、面板类(Interface)。
其UML类图如下(用visio2010所画):
这个抽象的过程因人而异,但很重要的一点是知道自己是如何将这些实体(如蛇、食物等)具体抽象成能用实实在在的数字表示的。比如我眼里的食物就只是一个坐标,蛇不过是一串有关联的坐标(Snake由一串BodyNode组成,而BodyNode只是一个坐标),所以蛇的移动,不过是这几个坐标的移动,这就将实体蛇抽象(具体化)成了计算机可以处理的对象了。心里有谱事情就好办。
二)每个类的职责是什么?
其实就是说类应该有哪些成员函数,或者说负责做什么事情,面向对象编程的一个原则是使类之间的关联性最小,这就是要求每个类要明白自己的职责范围,只做自己分内之事。这样,当其中一个类出故障时不会祸及池鱼。哦,原来是”谁出了问题就找谁“。如此程序就容易扩展和维护,因为很容易定位到bug处。就本文而言,各个类的职责见UML图。
三)这些类之间是如何通讯的?
类和类之间总共有三种关系,这里只用到类和类之间的依赖关系.说类A依赖于类B,指的是类A的某个成员函数的形参列表中有变量的类型为B.
1)Food类和Interface类之间的通讯:Food类只负责随机产生食物,并且食物一产生就通知Interface类;接着就是Interface类的职责,显示食物;
</pre><pre name="code" class="cpp">void Food::generateFood(<span style="color:#ff0000;">Interface*</span> ptface);//Food此成员函数依赖Interface
2)Snake类和Interface类之间的通讯:蛇只负责本身在每次发生变化后(移动或者吃掉食物)通知Interface;接着就是Interface类的职责,显示蛇;
void Snake::move(<span style="color:#ff0000;">Interface*</span> iface,int direction);//Snake此成员函数依赖Interface
3)Snake类和Food类之间的通讯:Snake只负责吃掉食物后通知Food类(该产生新的食物了);
void Snake::eatFood(Interface* ptface<span style="color:#ff0000;">,Food*</span> ptfood);//Snake此成员函数依赖Food
4)Snake类和Food类和Interface类之间的通讯:Snake只负责吃掉食物后通知Food类和Interface类;接着就是Food类和Interface类的职责( Food类产生新的食物(新的食物一产生,接着 ”1“又开始了),Interface类显示蛇的变化 )
void Snake::eatFood(<span style="color:#ff0000;">Interface*</span> ptface,<span style="color:#ff0000;">Food*</span> ptfood);//Snake此函数同时依赖Interface和Food
补充:
- 这里Snake类既依赖于Food类有依赖于Interface类(从UML类图中也可以看出),因为吃掉食物后要通知Food类(“3)”),但与此同时,蛇发生了变化,还要通知Interface类(”2)‘);
- 当Food类和Snake类的状态一旦发生改变,就都立马通知Interface类去更新。
关于代码中的几点说明:
1)main.cpp中_getch()用于在键盘上获取一个字符,但这里的字符指的只是普通的字符,并不包括方向键。要想获取方向键需要两次调用_getch(),第一次返回224,第二次才是返回键的键码值(up------72,down------80,left------75,right------77).所以如果只想获取方向键,可写成;
<span style="font-size:14px;">char ch;
while(_getch()!=224)
continue;
ch=_getch();//ch就是方向键之一了</span>
以下是全部源代码:
<span style="font-size:14px;">//BodyNode.h
#pragma once
struct BodyNode//组成蛇身的节点结构
{
int m_iBodyNodeX;
int m_iBodyNodeY;
BodyNode(int x,int y):m_iBodyNodeX(x),m_iBodyNodeY(y){}
};</span>
<span style="font-size:14px;">//Food.h
#pragma once
#include"Interface.h"
class Food//食物类
{
private:
int m_iFoodX;//食物的坐标
int m_iFoodY;
public:
Food():m_iFoodX(0),m_iFoodY(0){}
int getX()const{return m_iFoodX;}
int getY()const{return m_iFoodY;}
void generateFood(Interface* ptface);//Food类依赖Interface类
};</span>
<span style="font-size:14px;">//Food.cpp
#include"Food.h"
#include<time.h>//time()的头文件
#include<cstdlib>//srand()和rand()的头文件
void Food::generateFood(Interface* iface)
{
srand( (unsigned)time(NULL) );
do//使食物产生在面板里
{
m_iFoodX=rand()%20+1;
m_iFoodY=rand()%20+1;
}while(m_iFoodX==0 || m_iFoodX==21 || m_iFoodY==0 ||m_iFoodY==21);
iface->setPointYes(m_iFoodX,m_iFoodY,draw_food);
}</span>
<span style="font-size:14px;">//Interface.h
#pragma once
//方向键上下左右的键码值
#define up 72
#define down 80
#define left 75
#define right 77
//用以标记要在面板上点亮的点的类型
#define draw_food 1
#define draw_snake_head 2
#define draw_snake_body 3
class Interface//界面类
{
public:
char m_cSquare[22][22];
public:
Interface();
void show();
void setPointYes(int x,int y,int draw_kind);//点亮面板上某点(可能是food点或蛇点)
void setPointNo(int x,int y);
};</span>
<span style="font-size:14px;">//Interface.cpp
#include<iostream>
using namespace std;
#include"Interface.h"
Interface::Interface()
{
for(int i=1;i<=20;i++)//蛇可活动的区域的初始化
for(int j=1;j<=20;j++)
m_cSquare[i][j]=' ';
for(int j=0;j<22;j++)//上下墙壁初始化
m_cSquare[0][j]=m_cSquare[21][j]='-';
for(int i=1;i<22;i++)//左右墙壁初始化
m_cSquare[i][0]=m_cSquare[i][21]='|';
m_cSquare[1][1]=m_cSquare[1][2]='*';
m_cSquare[1][3]='#';//蛇头的初始位置
}
void Interface::setPointYes(int x,int y,int draw_kind)
{
if(draw_kind==draw_food)
m_cSquare[x][y]='$';
else if(draw_kind==draw_snake_head)
m_cSquare[x][y]='#';
else
m_cSquare[x][y]='*';
}
void Interface::setPointNo(int x,int y)
{
m_cSquare[x][y]=' ';
}
void Interface::show()
{
system("cls");
for(int i=0;i<22;i++)
{ for(int j=0;j<22;j++)
cout<<m_cSquare[i][j]<<' ';//这里的“ <<' ' ”很重要,这样才能使屏幕上22*22输出为方形
cout<<endl;
}
}</span>
<span style="font-size:14px;">//Snake.h
#pragma once
#include<iostream>
using namespace std;//这是为何?竟然不能有“include<iostream>”,放在.cpp中也不行??
#include<list>
#include"BodyNode.h"
#include"Food.h"
#include"Interface.h"
class Snake
{
private:
list<BodyNode> m_body;
public:
Snake()
{
m_body.push_front( BodyNode(1,1) );
m_body.push_front( BodyNode(1,2) );
m_body.push_front( BodyNode(1,3) );
}
void eatFood(Interface* ptface,Food* ptfood);
void move(Interface* ptface,int direction);
bool isMeetFood(Food* food,int direction);
bool isDie();
int getSize()const{return m_body.size();}
};</span>
<span style="font-size:14px;">//Snake.cpp
#include"Snake.h"
void Snake::eatFood(Interface* ptface,Food* ptfood)
{
ptface->setPointYes( m_body.front().m_iBodyNodeX,m_body.front().m_iBodyNodeY,draw_snake_body);//蛇头变蛇身
m_body.push_front( BodyNode(ptfood->getX(),ptfood->getY()) );
ptfood->generateFood(ptface);
}
void Snake::move(Interface* ptface,int direction)
{
list<BodyNode>::reverse_iterator ittemp=++m_body.rbegin();
ptface->setPointNo( m_body.back().m_iBodyNodeX,m_body.back().m_iBodyNodeY);//使蛇尾不可见
ptface->setPointYes( m_body.front().m_iBodyNodeX,m_body.front().m_iBodyNodeY,draw_snake_body);//旧蛇头变蛇身
for(list<BodyNode>::reverse_iterator it=m_body.rbegin();ittemp!=m_body.rend();it++,ittemp++)
{
it->m_iBodyNodeX=ittemp->m_iBodyNodeX;
it->m_iBodyNodeY=ittemp->m_iBodyNodeY;
}//蛇身移动
switch(direction)
{
case up:
m_body.front().m_iBodyNodeX--; //新蛇头
break;
case down:
m_body.front().m_iBodyNodeX++; //新蛇头
break;
case left:
m_body.front().m_iBodyNodeY--; //新蛇头
break;
case right:
m_body.front().m_iBodyNodeY++; //新蛇头
break;
default:
;
}
ptface->setPointYes( m_body.front().m_iBodyNodeX,m_body.front().m_iBodyNodeY,draw_snake_head);//点亮新蛇头
}
bool Snake::isMeetFood(Food* food,int direction)
{
int snakehead_x=m_body.front().m_iBodyNodeX;
int snakehead_y=m_body.front().m_iBodyNodeY;
switch(direction)
{
case up:
if(snakehead_x-1 == food->getX() && snakehead_y == food->getY())
return true;
break;
case down:
if(snakehead_x+1 == food->getX() && snakehead_y == food->getY())
return true;
break;
case left:
if(snakehead_x == food->getX() && snakehead_y-1 == food->getY())
return true;
break;
case right:
if(snakehead_x == food->getX() && snakehead_y+1 == food->getY())
return true;
break;
default:
return false;
}
return false;
}
bool Snake::isDie()
{
int snakehead_x=m_body.front().m_iBodyNodeX;
int snakehead_y=m_body.front().m_iBodyNodeY;
if(0==snakehead_x || 21==snakehead_x || 0==snakehead_y || 21==snakehead_y)//蛇头碰到墙壁
return true;
//蛇头和蛇身相碰(蛇身和蛇身是永远不会相碰的)
for(list<BodyNode>::iterator it=++m_body.begin();it!=m_body.end();it++)
if(snakehead_x==it->m_iBodyNodeX && snakehead_y==it->m_iBodyNodeY)
return true;
return false;
}</span>
<span style="font-size:14px;">//main.cpp
#include<iostream>
using namespace std;
#include<windows.h>
#include<conio.h>
#include<ctime>
#include"Snake.h"
#include"Interface.h"
#include"Food.h"
Interface face;
Food food;
Snake snake;//生成最初的有三个蛇身节点的小蛇
int getTimeElapse();
int direction=right;//默认方向为向右
bool canRun();
int main()
{
food.generateFood(&face);//在面板上生成食物
face.show();
while(!snake.isDie())
{
if(!_kbhit())
{
static int time_elapse=getTimeElapse();
Sleep(time_elapse);
if(!canRun())
break;
}
else
{
if(_getch()!=224)//屏蔽非法输入,这里正好借助了外层的while循环
continue;
direction=_getch();
if(!canRun())
break;
}
}
cout<<"\t o o game over"<<endl<<endl;
return 1;
}
bool canRun()
{
if(snake.isMeetFood(&food,direction))
snake.eatFood(&face,&food);
else
{
snake.move(&face,direction);
if(snake.isDie())//移动之后可能会die
return false;
}
face.show();
return true;
}
int getTimeElapse()
{
int snakeSize=snake.getSize();
if(snakeSize<=6)
return 400;
else if(snakeSize<=14)
return 200;
else if(snakeSize<=20)
return 100;
else if(snakeSize<=30)
return 50;
else
return 20;
}
</span>