白驹过隙,这篇文章距今已有一年以上的历史。技术发展日新月异,文中的观点或代码很可能过时或失效,请自行甄别:)

寒假啃完了P先生的《Windows Programing 5th》,接下来准备鼓捣下网络的知识以及数据结构,正逢此游戏正火,便拿它来作为暂时告别win32skd的纪念吧。当初知道这个游戏的时候就觉得是一个非常简单的游戏,结构简单,基本没有什么算法。然而正式开始写的时候却一波三折。以下是我的相关情况

游戏成品:http://pjf.name/post-106.html

游戏进度:

2月27日:

google相关图片和声音资源,抠图,ps等,同时下载了该手机游戏反复玩来查看游戏的玩法和规则以及相应的处理方法。

2月28日:

编写相关结构体,函数,晚上进行第一次测试,发现效果奇差,闪屏严重,动画卡顿,且逻辑不规范,修改代码工作量过大,忍痛重写。

3月1日-3月2日:

从安卓版flappy bird的apk包中提取出图片和声音资源,进行相关PS,并重写游戏逻辑思路和相关结构体以及函数结构。

3月3日:

相关函数进行测试,发现仍然有第一次的闪屏问题,同时操作很不方便,经过相关调试和分析,修改动画函数。

3月4日:

上午对重新修改后的代码进行测试,并进行游戏的封装和逻辑的处理,发现闪屏问题解决,同时进行大量测试,对游戏难度进行调整。下午对游戏细节进行调整和优化,晚上进行最后的测试,V1.0版发布。

游戏流程图:

Untitled.png

平台:VS2010

结构体:

游戏中采用了两个结构体,一个用来保存游戏相关变量,另外一个保存小鸟情况

游戏相关结构体:

struct{
UINT Score;
UINT BestScore;
UINT GameState;//the game is login,get ready or playing
UINT GameGrade;
int NewPipeTime;
BOOL isDayOrNight;
BOOL Amination;
}GameInfo;//the structure information about the game

小鸟相关结构体:

struct{
UINT BirdHeight;
int isBirdDie;
UINT WhichBird;
}BirdInfo;//the structure information about the bird

木箱子:

采用一二维数组:BOOL PipeInformation21分别代表行和列,游戏中只存在20行20列,第21行为了和小鸟触碰地面进行死亡判断,第21列用来生成新的木箱子。

宏:

为了操作和维护性,定义了以下的宏

#define WM_GAMEGETREADY WM_USER+1//define the message when the game get ready will send

//define the game state
#define LOGINGAME    1
#define GETREADYGAME 2
#define PLAYINGGAME  3

//define the five kind of medals
#define NOMEDAL      1
#define COPPERMEDAL  2
#define SLIVERMEDAL  3
#define GOLDMEDAL    4
#define PLATIUMMEDAL 5

//define the pipe information on the screen
#define HAVEBOX 1
#define NOBOX 0

//define the bird is die or cross the pipe
#define DIE 0
#define LIVE 2
#define CROSS 1

//define the space time(milliseconds) the pipe move and meanwhile it is the timer ID
#define PIPEMOVE 40

//define the id about the play and quit buttons
#define IDPLAYBUTTON 1
#define IDQUITBUTTON 2

//define which bird be chose
#define YELLOWBIRD 0
#define BLUEBIRD   1
#define REDBIRD    2

//define choose the day or night picture
#define DAY 1
#define NIGHT 0

相关函数:

BOOL NewPipe(void);//create new pipe
int WhichGrade(void);//calculate the grade of the player
void PipeMove(void);//pipes move to left
int isBirdDie(void);//detect if the bird is die,cross or live
BOOL DayorNight(void);//random choose the bkground pic is day or night
int ChooseBirdtoFly(void);//rand choose what kind of bird to play
BOOL DrawPlayingPic(HDC hdcWindow);//draw the Playing amination
BOOL DrawGetReadyPic(HDC hdcWindow);//draw the Get ready amination
BOOL DrawLoginPic(HDC hdcWindow);//draw the pic when login
BOOL DrawGameOver(HDC hdcWindow);//draw the game over amination and score board

游戏中的操作,在定时器内通过GetAsyncKeyState来检测空格是否按下,小鸟是否死亡,木箱子的移动和生成等

对于游戏中草地的动画和小鸟的动画的处理,通过游戏第一个第二次的bug情况,通过游戏结构体的isAnimation检测来处理动画,并使用双缓冲技术避免闪屏,当isAnimation为TRUE和FALSE的时候分别绘制不同的图画,以此达到动画效果。同时设置了一个定时器来改变isAnimation的值并刷屏屏幕,并在WM_PAINT消息中通过对游戏结构体的GameState情况来绘制不同的动画。

具体代码如下:

case WM_TIMER:
if(TRUE==GameInfo.Amination)
GameInfo.Amination=FALSE;
else
GameInfo.Amination=TRUE;
switch(GameInfo.GameState)
{
case LOGINGAME:
case GETREADYGAME:
if(GETREADYGAME==GameInfo.GameState)
if(GetAsyncKeyState(VK_SPACE)&0x8000)
{
GameInfo.GameState=PLAYINGGAME;
}
InvalidateRect(hwnd,NULL,FALSE);
return 0;
case PLAYINGGAME:
BirdInfo.isBirdDie=isBirdDie();
if(DIE==BirdInfo.isBirdDie)
{
KillTimer(hwnd,PIPEMOVE);
PlaySound(TEXT("sfx_hit.wav"),NULL,SND_ASYNC|SND_FILENAME|SND_NODEFAULT);
}
else
{
NewPipe();
PipeMove();
if(GetAsyncKeyState(VK_SPACE)&0x8000)
{
if(0==BirdInfo.BirdHeight)
{
InvalidateRect(hwnd,NULL,FALSE);
break;
}
BirdInfo.BirdHeight;
}
else
BirdInfo.BirdHeight++;
}
InvalidateRect(hwnd,NULL,FALSE);
break;
}
break;
case WM_PAINT:
hdc=BeginPaint(hwnd,&ps);
switch(GameInfo.GameState)
{
case LOGINGAME:
DrawLoginPic(hdc);
break;
case GETREADYGAME:
DrawGetReadyPic(hdc);
break;
case PLAYINGGAME:
DrawPlayingPic(hdc);
if(DIE==BirdInfo.isBirdDie)
{
DrawGameOver(hdc);
EnableWindow(hwndPlayButton,TRUE);
EnableWindow(hwndQuitButton,TRUE);
ShowWindow(hwndPlayButton,SW_SHOW);
ShowWindow(hwndQuitButton,SW_SHOW);
}
break;
}
EndPaint(hwnd,&ps);
break;

判断小鸟是否结束:通过对小鸟的所在高度和该列的值进行比较,如果小鸟所处位置有箱子,则小鸟死亡,如果该处没箱子并且最高处有箱子,表示小鸟顺利穿过该排木箱子,得分加一,如果小鸟所在列都无箱子,则还没遇到箱子或者箱子已经穿过,代码如下:

int isBirdDie(void)//detect if the bird is die,cross or live
{
if(20==BirdInfo.BirdHeight||HAVEBOX==PipeInformation[BirdInfo.BirdHeight][4])
return DIE;
if(NOBOX==PipeInformation[BirdInfo.BirdHeight][4]&&HAVEBOX==PipeInformation[19][4])
{
GameInfo.Score++;
PlaySound(TEXT("sfx_point.wav"),NULL,SND_ASYNC|SND_FILENAME|SND_NODEFAULT);
return CROSS;
}
else
return LIVE;
}

生成及移动木箱子

生成:整个屏幕一个20列木箱子,但定义了21列木箱子,最后一列用来生成木箱子,而生成木箱子的时间石油gameinfo结构体重NewPipeTime决定,而NewPipeTime值则由GameInfo.NewPipeTime=(int)(1000/PIPEMOVE)-2*GameInfo.GameGrade算出,如果时间不为零,则第21列都没箱子,并把时间减一,如果时间为0,则由C语言的rand函数随机指定第21列的某连续3行无箱子,其余有箱子。为了兼容游戏动画,第一行和最后一行指定有箱子。

移动:很简单,从最左边的列开始,先把最左边的列全都改为没有箱子,第二列开始到第21列全部把值左移一列并把本列全部设为无箱子;

具体代码:

BOOL NewPipe(void)//create new pipe
{
int i,NoPipeHeight;
time_t t;
if(0!=GameInfo.NewPipeTime)//it is not time to create the new pipe
{
for(i=0;i<20;i++)
PipeInformation[i][20]=NOBOX;
GameInfo.NewPipeTime;
return FALSE;
}
else//create new pipe
{
srand((unsigned)time(&t));
NoPipeHeight=rand()%15+1;
for(i=1;i<19;i++)
{
if(NoPipeHeight==i||NoPipeHeight+1==i||NoPipeHeight+2==i||NoPipeHeight+3==i)
PipeInformation[i][20]=NOBOX;
else
PipeInformation[i][20]=HAVEBOX;
}
PipeInformation[0][20]=PipeInformation[19][20]=HAVEBOX;//insure the top and bottom have box
GameInfo.NewPipeTime=(int)(1000/PIPEMOVE)-2*GameInfo.GameGrade;
return TRUE;
}
}
void PipeMove(void)//pipes move to left
{
int i,j;
for(j=0;j<21;j++)
for(i=0;i<20;i++)
{
if(0==j)
{
PipeInformation[i][j]=NOBOX;
}
else
{
PipeInformation[i][j-1]=PipeInformation[i][j];
PipeInformation[i][j]=NOBOX;
}
}
}

奖牌及小鸟飞行速度设置:

小鸟速度:通过GameInfo.NewPipeTime=(int)(1000/PIPEMOVE)-2*GameInfo.GameGrade得出,因此得分越高,小鸟速度越快;

奖牌:通过得分除以30来计算相应奖牌,具体代码如下:

int WhichGrade(void)//calculate the grade of the player
{
switch((int)(GameInfo.Score/20))
{
case 0:
GameInfo.GameGrade=NOMEDAL;
break;
case 1:
GameInfo.GameGrade=COPPERMEDAL;
break;
case 2:
GameInfo.GameGrade=SLIVERMEDAL;
break;
case 3:
GameInfo.GameGrade=GOLDMEDAL;
break;
case 4:
default:
GameInfo.GameGrade=PLATIUMMEDAL;
break;
}
return GameInfo.GameGrade;
}

白天黑夜背景以及小鸟选择:

采用随机生成法,在游戏最初先初始化为白天和蓝色小鸟,具体代码如下:

BOOL DayorNight(void)
{
time_t t;
srand((unsigned)time(&t));
return rand()%2;
};
int ChooseBirdtoFly(void)
{
time_t t;
srand((unsigned)time(&t));
return rand()%3;
}

总结:

原计划两天完成的东西最后却花了3倍的时间来完成,造成此问题的最大原因就是前期的规划没有做好,游戏逻辑,流程未落实为文档而是只存与脑中,由此照成了很多bug和流程处理的错误。为此而付出的2倍的时间来重写,得不偿失。因此在以后的实践中要注意前期的规划,流程等的处理,也只有这样才能提高效率。

基本情况就是这样,完整代码见附件:FlappyBird_V1.0.zip