C项目实践--贪吃蛇(1)
1.功能需求分析
1.1主要功能
i.游戏欢迎界面
ii.游戏执行功能,包括计算得分
iii.游戏结束界面
1.2游戏基本规则
游戏开始时蛇的长度是4个单位,并且按照当前方向不停地移动。移动范围是COLUMNS*ROWS个格子。食物随机出现在屏幕上,但不能紧靠边缘,保存屏幕上有3个事物。如果蛇碰到边缘或自己,则游戏结束。游戏中可以暂停以及恢复游戏。
基本操作
i.游戏进行中玩家可以按键盘上的上、下、左、右键改变蛇的当前行进方向。蛇是根据当前方向自动移动的。
ii.游戏中按下回车键,暂停游戏。再次按下回车键则继续游戏。
2.总体设计
游戏以windows窗口的形式运行。窗口的左边作为游戏的桌面,右边是计分等提示信息显示区域,桌面的大小是COLUMNS*ROWS个单位。蛇出现的位置是桌面的中心,蛇的颜色为绿色,长度是4个单位。开始以后蛇向上移动。
食物为随机出现的原形图案,分为三种类型,用红、蓝和黄三种颜色区分,吃到红色食物的1分,蓝色食物得2分,黄色食物得3分。在桌面的右上方显示得分。得分下面显示帮助信息。当蛇的头部(蛇身体的第一个单位)碰到食物时,碰到的食物变为蛇的身体的一部分。如果蛇的头部碰到窗口的边缘或自己的身体时,结束游戏。当键盘的方向键按下时改变蛇的当前运动方向为方向键所指向的方向,但不能回头。例如,假设蛇当前的移动方向是向左移动,这个时候按下右方向键不起作用。
3.处理流程
贪吃蛇游戏开始以后首先创建连续的四个坐标作为贪吃蛇的身体,然后随机在桌面上的三个坐标点生成食物;默认设置贪吃蛇的移动方向为向上移动;贪吃蛇移动时根据方向计算蛇头部移动方向的坐标,将头部坐标设置为新的坐标,依次改变坐标为其前一节的坐标。移动之前要检查是否碰到边缘、食物或自己。主要的处理流程如图:
4.详细设计与系统实现
作为游戏编程,绘图是程序主要实现的功能,玩家的操作改变数据的值以及程序中自动改变的数据的值通过每隔一小段时间重新绘制,体现到界面上就构成了游戏的主体。由此可见,数据是游戏的核心部分。下面首先设计游戏的主要数据结构和变量,然后设计如何将数据绘制到界面上以及如何操作这些数据。
数据结构
1.游戏桌面数据结构和食物设计
根据总体设计,在游戏窗口的左边部分,用ROWS*COLUMNS的单位大小表现一个表格,作为蛇移动的空间。那么需要定义一个整型二维数组:
table[ROWS][COLUMNS]
这个二维数组的第一维ROWS表示行即y轴坐标,第二维COLUMNS表示列即x轴坐标。每个坐标点值的含义是:0表示空白,1到3表示分值分别为1到3的食物。如图:
图1.游戏桌面数据table的示例
2.贪吃蛇数据结构的设计
贪吃蛇身体的每一部分用其在桌面上的坐标表示,用单链表的形式将每部分联系起来。设计全局变量head存储头节点的指针。身体每部分的数据结构如下:
typedef struct _snake_body
{
int x;
int y;
struct _snake_body* next;
}snake_body;
所以蛇在桌面上的表示形式以及每一节之间的联系,如图:
3.系统实现
首先打开vs,新建Win32 Application, 选择Empty Proj, 单击Finished 完成SnakeWin项目创建,然后在Header Files 和Source Files 中分别新建Snake.h和Snake.c文件。首先打开Snake.h文件把系统要用到的函数声明到这个文件中,具体声明如下:
//Header Info
#ifndef WIN_H_H
#define WIN_H_H
#include <Windows.h>
#endif
//Function Definitions
void clear_snake(); //清空贪吃蛇链表
int out_of_table(int x ,int y); //检查某一个坐标点(x,y)是否在游戏桌面外
int eat_self(int x , int y); //检查坐标点(x,y)是否碰到贪吃蛇的身体部分
void eat_food(int x , int y); //处理贪吃蛇吃掉(x,y)坐标点食物的函数
//贪吃蛇头部移动到(x,y)坐标点的函数,头部移动到(x,y)点,
//其它部分依次移动到前一个节点的坐标
void move(int x , int y);
void create_food(); //在游戏桌面上创建食物
int random(int seed); //取得一个随机数
void new_game(); //创建一个新游戏
void run_game(); //执行游戏的处理
void draw_table(); //绘制游戏桌面
void paint(); //WM_PAINT消息调用的函数,将内存位图绘制到窗口上
void key_down(WPARAM wParam); //按键处理函数
void resize(); //位图随窗口大小改变而改变
void intialize(); //初始化游戏
void finalize(); //释放初始化游戏时创建的资源
//响应windows消息的回调函数,用来处理windows消息
LRESULT CALLBACK WndProc(HWND hwnd, UINT message,WPARAM wParam,LPARAM lParam);
在本系统中要用到的函数大概就是上面这些了, 那么在Snake.h中的函数声明也就结束了,下面回到Snake.c中来实现这些功能模块,打开Snake.c, 首先包含相关的头文件,具体如下:
//header Info
#include <time.h>
#include <stdio.h>
#include "Snake.h"
游戏中要设定游戏桌面的长宽,食物数量以及食物颜色等诸多常量,为了便于维护我们决定把它们声明为常量宏,这样需求有变,只需修改这些常量宏即可了。相关常量定义如下:
//constant definition
#define APP_NAME "SNAKE" //游戏窗口标题
#define APP_TITLE "Snake Game" //游戏开始标题
#define GAMEOVER "GAME OVER" //游戏结束标题
#define COLUMS 40 //游戏桌面可分成COLUMS列
#define ROWS 40 //游戏桌面可分成ROWS行
#define FOOD_COUNT 3 //游戏食物数量
//以下是游戏中可能要用到的颜色变量定义
#define RED RGB(255,0,0)
#define GREEN RGB(0,255,0)
#define BLUE RGB(0,0,255)
#define YELLOW RGB(255,255,0)
#define GRAY RGB(128,128,128)
#define BLACK RGB(0,0,0)
#define WHITE RGB(255,255,255)
#define STONE RGB(192,192,192)
#define CHARS_IN_LINE 14 //显示分数字符串的长度
#define SCORE "SCORE %4d" //显示分数的格式化字符串
为了实现系统功能,还需要定义相应的数据类型,包括一些结构体和枚举类型
1.定义一个贪吃蛇节点链表的结构体类型snake_body,用来表示蛇,其中x,y用来表示蛇身体的相对坐标,next表示指向下一个节点的指针,即蛇身体的下一块。具体定义如下:
typedef struct _snake_body
{
int x;
int y;
struct _snake_body* next;
}snake_body;
2.蛇移动的时候需要一个方向,而方向就只有上下左右这么四种值可选,没有别,所以合适定义为一个枚举类型direction,用来表示贪吃蛇的移动方向,0表示向上,1表示向下,2表示向左,3表示向右。具体定义如下:
typedef enum _direction //蛇移动方向枚举类型
{
UP = 0,
DOWN,
LEFT,
RIGHT
}direction;
3.游戏中还需要一个变量来表示游戏当前的状态, 而游戏的状态也只有四种情况,即游戏开始、运行、暂停或结束。所以合适定义为一个枚举类型state, 来表示游戏的当前状态,具体实现如下:
enum game_state //游戏状态
{
game_start,
game_run,
game_pause,
game_over
}state = game_start;
接下来还需要声明一些变量,这些变量是跟随系统的生命周期而存在的,所以需要声明为全局变量才行,它们的具体声明如下:
//global variable Definition
COLORREF food_color[] = // 食物颜色
{
RED,
BLUE,
YELLOW
};
snake_body* head = NULL; //定义一个蛇头节点,将其初始化为NULL
direction dirt; //贪吃蛇当前移动的方向
char score_char[CHARS_IN_LINE] = {0}; //显示分数的字符串
char* press_enter = "Press Enter key..."; //游戏结束时的提示信息
char* help[] = //字符串数组help中存储的是帮助信息
{
"Press direction key to change snake's direction.",
"Press enter key to pause/resume game.",
"Enjoy it. :-)",
0
};
int score = 0; //得分记录的变量
//int table[ROWS][COLUMS] = {0};
int table[ROWS][COLUMS] = {0}; //游戏桌面
clock_t start = 0; //游戏每一帧开始时间
clock_t finish = 0; //游戏每一帧结束时间
//用于绘图的GDI全局变量
HWND gameWND; //Window窗口句柄
HBITMAP memBM; //内存位图
HBITMAP memBMOld; //内存原始位图
HDC memDC; //内存DC
RECT clientRC; //游戏界面显示区域
HBRUSH blackBrush; //黑色画笔
HBRUSH snakeBrush; //贪吃蛇画笔
HBRUSH foodBrush[FOOD_COUNT]; //食物画笔数组,三种食物,每种一个
HPEN grayPen; //灰色画笔,用来绘制帮助信息
HFONT bigFont; //大字体,用来显示游戏名字和字符串"GAME OVER"
HFONT smallFont; //小字体,用来显示帮助信息等字体
至此,已经把系统中需要使用到的数据结构即变量等都声明定义完毕,下面来实现相应的功能模块,具体实现如下:
1.清空链表
函数名称:clear_snake
函数功能:清空贪吃蛇链表。具体实现如下:
//Functions
void clear_snake()
{
snake_body* p = head; //定义一个蛇链表节点p 指向头节点
while(head != NULL)
{//从头节点开始往后清空链表
p = head->next;
free(head);
head = p;
}
}
2.检查坐标越界
函数名称:out_of_table
函数功能:检查某一个坐标点(x,y)是否在游戏桌面外。具体实现如下:
int out_of_table(int x ,int y)
{
//如果坐标点不在游戏桌面范围内,则返回真否则返回假
if(x < 0 || y < 0 || x >= COLUMS || y >= ROWS)
{
return 1;
}
return 0;
}
3.检测坐标是否碰到蛇身
函数名称:eat_self
函数功能:检查坐标点(x,y)是否碰到贪吃蛇的身体部分,用来判断贪吃蛇在运行过程中,头部是否碰到了自己的身体。具体实现如下:
int eat_self(int x , int y)
{
snake_body* p = head;
while(p != NULL)
{ //将当前坐标值与蛇身上的每个坐标值进行对比
//如果有坐标值相等的情况则返回1,表示碰到蛇身了
//否则返回0
if(x == p->x && y == p->y)
{
return 1;
}
p = p->next;
}
return 0;
}
4.蛇吃食物
函数名称:eat_food
函数功能:蛇吃到食物,身体变长
处理流程:处理贪吃蛇吃掉(x,y)坐标点食物的函数,蛇在顺利吃食物时,用食物所在的坐标值创建一个新的节点,作为蛇的头部坐标,将新节点的next指向蛇的原头部节点。所以蛇的身体变长,当移动方向不变。具体实现如下:
void eat_food(int x , int y)
{
snake_body* p = head;//定义一个蛇链表指针变量,指向头节点
//为head重新分配内存
head = (snake_body*)malloc(sizeof(snake_body));
//将当前食物坐标点赋值给头节点head,然后将p赋值给head的next域
head->x = x;
head->y = y;
head->next = p;
}
5.蛇身移动
函数名称:move
函数功能:蛇移动的处理
处理流程:蛇的移动主要靠头部坐标的移动,函数的功能是将贪吃蛇头部移动到(x,y)坐标点,其它部分依次移动到前一个节点的坐标位置。具体实现如下:
void move(int x , int y)
{
int tmpx, tmpy;
snake_body* p = head;
while(p != NULL)
{
//tmpx tmpy 记录当前p节点的坐标值
//将当前传入的x,y赋值给p节点
//然后将tmpx,tmpy记录的之前的p节点的值赋值给变量x,y
//然后循环将所有值都向前移动一个单位位置。
tmpx = p->x;
tmpy = p->y;
p->x = x;
p->y = y;
x = tmpx;
y = tmpy;
p = p->next;
}
}
6.创建食物
函数名称:create_food
函数功能:在游戏桌面上创建食物,调用create_food()函数在桌面上随机创建3个不同颜色的食物。具体实现如下:
void create_food()
{
//声明x,y变量获取食物生成的坐标值
int x ,y;
x = random(COLUMS - 2);
y = random(ROWS - 2);
//如果随机生成的x,y构成的坐标点的值不是0,就表示上已经有食物了
//或者说这个点是蛇身的一节,则
//重新调用create_food()生成新的食物坐标点
if(table[y+1][x+1] > 0)
{
create_food();
Sleep(1);
return ;
}else{
//否则,为该坐标点赋值
//FOOD_COUNT 为常量3 通过random(FOOD_COUNT)+1 最终为选定的
//食物坐标点赋值1,2或3 代表该食物的得分。
table[y+1][x+1] = random(FOOD_COUNT) + 1;
}
}
7.取随机数
函数名称:random
函数功能:取得一个随机数。具体实现如下:
int random(int seed)
{
if(seed == 0)
{
return 0;
}
//注意这里返回的rand()%seed值 肯定是 0,1,……,到seed-1之间的自然数
return (rand() % seed);
}
8.创建游戏
函数名称:new_game
函数功能:创建一个新游戏
处理流程:创建新游戏的过程是首先创建贪吃蛇,然后调用create_food()函数随机创建3个食物。具体实现如下:
void new_game()
{
int i = 0;
snake_body* p; //声明一个蛇链表节点
//因为要用到rand()函数来为随机生成食物坐标点
//但是在使用rand()函数之前,需要先调用srand()函数来生成相应的
//种子才行,反正就是说要调用rand()函数,就要先调用srand()函数,
//但是srand()函数只要调用一次就可以了
srand((unsigned)time(NULL));
//将表格所有值置0,即去除所有食物
//因为不是0就代表那里是个食物的坐标点
memset(table, 0, sizeof(int)*COLUMS*ROWS);
//将上一局中的贪吃蛇链表清空
clear_snake();
//蛇头节点是全局变量,下面是定义一个蛇头节点,赋值给p
head = (snake_body*)malloc(sizeof(snake_body));
head->x = COLUMS /2;
head->y = (ROWS - 4) / 2;
head->next = NULL;
p = head;
//蛇身初始化为4个节点
for( i = 1; i <4; i++)
{
p->next = (snake_body*)malloc(sizeof(snake_body));
p->next->x = head->x;
p->next->y = head->y + i;
p->next->next = NULL;
p = p->next;
}
//随机创建3个食物
for(i = 0; i < 3; i++)
{
create_food();
}
start = clock(); //初始化当前帧的开始时钟
score = 0; //分数清零处理
}
9.运行游戏
函数名称:run_game
函数功能:运行游戏的处理,首先移动蛇的坐标,移动过程中要判断蛇头坐标是否出界,是否碰到蛇的身体,是否吃到食物等状态,如果蛇头出界或碰到蛇的身体,则结束游戏,如果碰到食物,调用eat_food()函数完成蛇吃食物的操作。具体实现如下:
void run_game()
{
int x , y;
finish = clock();
//clock()函数返回从"开启这个程序进程"到
//"程序中调用clock()函数"时之间的CPU时钟计时单元数目
//linux下1秒钟大概有1000000个CPU时钟计时单元
//如果从开始到结束用时超过300个CPU计时单元,
//则响应一下蛇身的移动情况
if((finish - start) > 300)
{
//取得贪吃蛇的头节点将要移动到的坐标点
x = head->x;
y = head->y;
//方向键位判定
switch(dirt)
{
case UP:
y--;
break;
case DOWN:
y++;
break;
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
}
//判断是否出界
if(out_of_table(x,y))
{
state = game_over;
}else if(eat_self(x,y)) //判断是否碰到贪吃蛇
{
state = game_over;
}else if(table[y][x]) //判断是否碰到食物
{
score += table[y][x]; //加分
table[y][x] = 0; //清除当前食物
create_food();//生成新食物
eat_food(x,y);//吃掉食物
}else //正常移动贪吃蛇
{
move(x,y);
}
//下面的start可以控制蛇移动的速度
start = clock();
//重绘窗口区域
InvalidateRect(gameWND,NULL,TRUE);
}
}
10.绘制游戏桌面函数
函数名称:draw_table
函数功能:绘制游戏桌面。为了能看到蛇身体上的每一方块,绘制桌面时要按照先绘制蛇,再绘制食物,最后绘制表格的顺序来进行。这里还要根据蛇吃食物的情况绘制得分及相应的帮助信息。具体实现如下:
void draw_table()
{
//声明三个变量hBrushOld
//hPenOld, hFontOld记录系统原有的画刷,画笔,字体属性
HBRUSH hBrushOld;
HPEN hPenOld;
HFONT hFontOld;
//声明一个RECT结构的rc变量,记录客户区
RECT rc;
//声明变量x0,y0表示记录游戏桌面左上角的起始坐标点
//w表示游戏桌面块的大小
int x0, y0, w;
//x,y表示坐标点的坐标值,i,j表示循环中需要用的变量
int x ,y , i ,j;
//字符指针变量str用来接收相应的说明字符串
char* str;
// int x1 ;
// int y1 ;
// int row ;
// int column ;
snake_body* snake = NULL; //定义蛇身节点,初始化为NULL
w = clientRC.bottom / (ROWS + 2);//游戏桌面块的大小
//w = clientRC.right /(ROWS*2);
//w = 8;
x0 = y0 = w;//赋值,游戏桌面的左上角坐标值
//用黑色绘制背景
FillRect(memDC,&clientRC,blackBrush);
//绘制开始和结束画面
if(state == game_start || state == game_over)
{
memcpy(&rc,&clientRC,sizeof(RECT));
rc.bottom = rc.bottom /2;
hFontOld = (HFONT)SelectObject(memDC,bigFont);//选择字体
SetBkColor(memDC,BLACK);//设置背景颜色
if(state == game_start)
{
str = APP_TITLE;
SetTextColor(memDC,YELLOW);
}else{
str = GAMEOVER;
SetTextColor(memDC,RED);
}
DrawText(memDC,str,strlen(str),&rc,DT_SINGLELINE | DT_CENTER | DT_BOTTOM);
SelectObject(memDC,hFontOld);//恢复系统原有的字体属性
//重新选择字体属性
hFontOld = (HFONT)SelectObject(memDC,smallFont);
rc.top = rc.bottom;
rc.bottom = rc.bottom*2;
//如果是游戏结束,则需要显示最终得分
if(state == game_over)
{
SetTextColor(memDC,YELLOW);
//将score以SCORE格式赋值给score_char
sprintf(score_char, SCORE,score);
//绘制最终得分
DrawText(memDC,score_char, strlen(score_char),&rc,DT_SINGLELINE|DT_CENTER|DT_TOP);
}
SetTextColor(memDC,STONE);
DrawText(memDC,press_enter,strlen(press_enter),&rc,DT_SINGLELINE|DT_CENTER|DT_VCENTER);
SelectObject(memDC,hFontOld);
return ;
}
//绘制食物,扫描整个表格,只要
//坐标值不是0,则表示有食物,则需要
//根据坐标值来绘制相应颜色的食物
for(i = 0; i < ROWS ; i++)
{
for(j = 0; j <COLUMS; j++)
{
if(table[i][j] > 0)
{
x = x0 +j*w;
y = y0 + i*w;
//根据坐标值来选择相应的颜色
hBrushOld = (HBRUSH)SelectObject(memDC,foodBrush[table[i][j]-1]);
//绘制椭圆
Ellipse(memDC,x,y,x+w+1,y+w+1);
}
}
}
//选入新的画刷,准备绘制贪吃蛇
//将贪吃蛇链表中的所有点所在的方块都绘制成带颜色的矩形
//整体表示一条蛇
hBrushOld = (HBRUSH)SelectObject(memDC,snakeBrush);
snake = head;
while(snake != NULL)
{
x = x0 + snake->x*w;
y = y0 + snake->y*w;
Rectangle(memDC,x,y,x+w+1,y+w+1);
snake = snake->next;
}
//选择新的画笔
hPenOld = (HPEN)SelectObject(memDC,grayPen);
//下面绘制表格,此表格指定了绘制的行列数,且其中每一块
//会随着窗口的缩放而改变大小
for(i = 0; i <= ROWS; i++)
//for(i = 0; i <= clientRC.bottom; i++)
{
MoveToEx(memDC,x0,y0+i*w,NULL);
LineTo(memDC,x0+COLUMS*w,y0+i*w);
}
for(i = 0; i<= COLUMS; i++)
//for(i = 0; i<= clientRC.right; i++)
{
MoveToEx(memDC,x0+i*w,y0,NULL);
LineTo(memDC,x0+i*w,y0+ROWS*w);
}
//下面这种绘制表格的方法是随着窗口的大小改变其绘制的行列数目,但是块的大小不改变
//Start /////第二种绘制表格的方法///////////////////////////////////////
//w = 16;
// x1 = w;
// y1 = w;
// column = clientRC.bottom/w -w;
// row = clientRC.right/w*2;
////先绘制行
//for(i = 0; i<= row;i++)
//{
// MoveToEx(memDC,x1,y1+i*w,NULL);
// LineTo(memDC,x1+column*w,y1+i*w);
//}
////绘制列
//for(i = 0; i<= column;i++)
//{
// MoveToEx(memDC,x1+i*w,y1,NULL);
// LineTo(memDC,x1+i*w,y1+row*w);
//}
//End /////第二种绘制表格的方法///////////////////////////////////////
SelectObject(memDC,hPenOld);
//重新定位左上角的坐标点,用于绘制得分情况
x0 = x0 + COLUMS*w + 3*w;
y0 = y0 + w;
hFontOld = (HFONT)SelectObject(memDC,smallFont);
SetTextColor(memDC,YELLOW);
sprintf(score_char, SCORE,score);
//绘制得分情况
TextOut(memDC,x0,y0,score_char,strlen(score_char));
//重新定位左上角的坐标点,用于绘制提示信息
x0 = (COLUMS + 2)*w;
y0 += 4*w;
SetTextColor(memDC,GRAY);
i = 0;
while(help[i])
{
TextOut(memDC,x0,y0,help[i],strlen(help[i]));
y0 += clientRC.bottom / (ROWS + 2);
i++;
}
SelectObject(memDC,hFontOld);
}
11.绘图
函数名称:paint
函数功能:WM_PAINT消息调用的函数,在窗口上绘制内存位图。具体实现如下:
void paint()
{
//声明一个绘图结构信息变量ps
PAINTSTRUCT ps;
HDC hdc;
draw_table();
//在调用BeginPaint函数时,会把要绘制的图像信息自动填入到
//ps中的各个字段中
//如果BeginPaint执行成功,则返回指向特定显示设备的句柄
//否则返回NULL
hdc = BeginPaint(gameWND,&ps);
//将内存DC位图贴到显示器上
BitBlt(hdc,clientRC.left,clientRC.top,clientRC.right,clientRC.bottom,memDC,0,0,SRCCOPY);
EndPaint(gameWND, &ps);
}
接下来的实现部分请参看:C项目实践--贪吃蛇(2)