<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>