项目源码:meng痴痴的github
1 案例介绍
1.1 贪吃蛇游戏简介
贪吃蛇是一款经典的小游戏。玩家使用方向键操控一条长长的蛇不断吞下食物,同时蛇身随着吞下的食物不断变长,当蛇头撞到蛇身或者墙时,游戏结束。
1.2项目截图
1.3 游戏元素
游戏元素分为:墙、蛇、食物以及蛇的可行区域和版本号和游戏玩法提升
- 墙:#井号表示,代表一个区域范围,也就是蛇的可移动区域,蛇如果碰到墙壁视为死亡,游戏结束。
- 蛇:分为蛇头和蛇身,蛇头用@表示,蛇身用=表示,当蛇吃到食物时,蛇身+1.贪吃蛇可以通过不断地吃食物来增长自身的身体。
- 食物:*星号表示,蛇碰到食物会将食物吃掉
- 可移动区域: 空格表示,代表蛇可以移动的区域
- 提示信息:右侧展示,可以显示当前贪吃蛇版本号、游戏玩法等提示信息
1.4游戏规则
当游戏刚开始运行时,游戏画面静止不动,可以默认如上图所示,蛇头朝右,游戏中设置了“w” “s” “a” “d” 4个案件分别代表上、下、左、右。当用户输入 w 或者 s 或者 d时激活游戏,注意输入a不可以激活,因为蛇不可以180°转弯,因此蛇的移动方向只可以一直向前或者90°旋转
当蛇吃掉食物的时候,此时蛇会增加一个身段,另外食物需要随机的生成在屏幕上。
游戏的结束方式:1:蛇碰到墙视为死亡;2:蛇碰到自己的身子。
1.5 游戏模块
如果我们利用C++的面向对象的思想来完成该游戏,就应该把每个模块划分开,让各个功能之间相互依赖又分离实现。
首先,创建项目的时候,需要有个程序入口,这个文件可写为Game.cpp作为游戏中的主要部分,在这个模块中主要用户接受用户的输入,调度其他模块,起到一个总指挥的作用。其次我们可以从最简单的模块入手,墙是最简单的,内部只需要维护一个二维的数组即可。然后是蛇模块,负责蛇的一切行为,最后还有一个食物模块。
2 游戏静态模式代码实现
2.1 墙模块
下面我们开始游戏中墙模块的实现。首先经过分析,我们可以得出在墙模块中,我们需要维护一个二维数组,对整个游戏的元素进行设置,所以我们可以声明一个二维数组:
char gameArray[][]
具体的行数和列数可以定义出个枚举,比如本游戏设置的是26行,26列
enum{ROW=26,COL=26}
那么墙模块的开发,需要提供的主要接口是 初始化墙initwall,以及打印墙,也就是将二维数组中的内容打印到控制台中,draw方法。当然对外还有提供出一个可以修改二维数组元素的方法以及根据索引获取二维数组元素的方法:getwall、setwall。
Wall.h:
#pragma once
#ifndef _WALL_HEAD
#define _WALL_HEAD
#include<iostream>
using namespace std;
class Wall {
public:
enum { ROW = 26, COL = 26};
void initWall();
void drawWall();
void setWall(int x,int y, char c);
char getWall(int x, int y);
private:
char gameArray[ROW][COL];
};
#endif
Wall.cpp:
#include "Wall.h"
void Wall::initWall() {
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++)
{
//set wall
if (i == 0 || j == 0 || i == ROW - 1 || j == COL - 1) {
gameArray[i][j] = '#';
}
else {
gameArray[i][j] = ' ';
}
}
}
}
void Wall::drawWall() {
for (int i = 0; i < ROW; i++) {
for (int j = 0; j < COL; j++) {
cout << gameArray[i][j] << " ";
}
if (i == 5) { cout << "create by Alex"; }
if (i == 6) { cout << "W: move up"; }
if (i == 7) { cout << "S: move down"; }
if (i == 8) { cout << "A: move left"; }
if (i == 9) { cout << "D: move right"; }
cout << endl;
}
}
void Wall::setWall(int x, int y, char c) {
if (0 <= x < ROW && 0 <= y < COL) {
gameArray[x][y] = c;
}
else {
cout << "index over" << endl;
}
}
char Wall::getWall(int x, int y) {
if (0 <= x < ROW && 0 <= y < COL) {
return gameArray[x][y];
}
else {
cout << "index over" << endl;
}
}
2.2 蛇模块
蛇模块的实现:首先蛇我们采用stack数据结构储存数据,同时,我们的蛇是需要打印在wall中的,所以在构造函数需要传一个wall 的对象。最后,我们需要提供蛇的初始化函数initSnake(),数据清理函数destoryPoint,以及蛇身增长函数addPoint
snake.h
#pragma once
#include<iostream>
#include "wall.h"
using namespace std;
class Snake
{
public:
Snake(Wall & tempWall);
struct Point {
//value
int x;
int y;
//pointer
Point* next;
};
void initSnake();
void destoryPoint();
void addPoint(int x, int y);
//head point
Point* hPoint;
Wall & wall;
};
snake.cpp
#include "snake.h"
Snake::Snake(Wall & tempWall) :wall(tempWall)
{
hPoint = NULL;
}
void Snake::initSnake() {
destoryPoint();
addPoint(5, 3);
addPoint(5, 4);
addPoint(5, 5);
}
//Destory all node
void Snake::destoryPoint() {
Point *cPoint = hPoint;
while (hPoint != NULL) {
cPoint = hPoint->next;
delete hPoint;
hPoint = cPoint;
}
}
void Snake::addPoint(int x,int y) {
//create new node
Point* newPoint = new Point;
newPoint->x = x;
newPoint->y = y;
newPoint->next = NULL;
if (hPoint != NULL) {
wall.setWall(hPoint->x, hPoint->y, '=');
}
newPoint->next = hPoint;
hPoint = newPoint;
wall.setWall(hPoint->x, hPoint->y, '@');
}
2.3 食物模块
食物模块的实现: 食物首先具有坐标属性(food_x,food_y),其为随机值,同时还要有一个对外的生成接口setFood
food.h
#pragma once
#include<iostream>
#include"wall.h"
using namespace std;
class Food {
public:
Food(Wall & tempwall);
void setFood();
int foodx;
int foody;
Wall& wall;
};
food.cpp
#include "food.h"
Food::Food(Wall &tempwall) :wall(tempwall) {
}
void Food::setFood() {
while (true) {
foodx = rand() % (Wall::ROW - 2) + 1;
foody = rand() % (Wall::COL - 2) + 1;
if (wall.getWall(foodx, foody) == ' ') {
wall.setWall(foodx, foody,'*');
break;
}
}
}
3 游戏动态模式代码实现
3.1 删除结点和蛇的移动
在snake模块中添加了两个接口
void delPoint();
bool moveSnake(char key);
更新之后的Code
snake.h:
#pragma once
#include<iostream>
#include "wall.h"
#include "food.h"
using namespace std;
class Snake
{
public:
Snake(Wall & tempWall, Food & tempFood);
enum { UP = 'w',DOWN = 's',LEFT = 'a',RIGHT = 'd' ,QUIT = 'q'};
struct Point {
//value
int x;
int y;
//pointer
Point* next;
};
void initSnake();
void destoryPoint();
void addPoint(int x, int y);
void delPoint();
bool moveSnake(char key);
//head point
Point* hPoint;
Wall & wall;
Food & food;
bool isRool;
};
snake.cpp
#include "snake.h"
Snake::Snake(Wall & tempWall,Food & tempFood) :wall(tempWall),food(tempFood)
{
hPoint = NULL;
isRool = false;
}
void Snake::initSnake() {
destoryPoint();
addPoint(5, 3);
addPoint(5, 4);
addPoint(5, 5);
addPoint(5, 6);
}
//Destory all node
void Snake::destoryPoint() {
Point *cPoint = hPoint;
while (hPoint != NULL) {
cPoint = hPoint->next;
delete hPoint;
hPoint = cPoint;
}
}
void Snake::addPoint(int x,int y) {
//create new node
Point* newPoint = new Point;
newPoint->x = x;
newPoint->y = y;
newPoint->next = NULL;
if (hPoint != NULL) {
wall.setWall(hPoint->x, hPoint->y, '=');
}
newPoint->next = hPoint;
hPoint = newPoint;
wall.setWall(hPoint->x, hPoint->y, '@');
}
void Snake::delPoint() {
if (hPoint == NULL || hPoint-> next == NULL) {
return;
}
Point* curP = hPoint->next;
Point* preP = hPoint;
while (curP->next != NULL) {
preP = preP->next;
curP = curP->next;
}
wall.setWall(curP->x, curP->y, ' ');
delete curP;
curP = NULL;
preP->next = NULL;
}
bool Snake::moveSnake(char key) {
int x = hPoint->x;
int y = hPoint->y;
switch (key) {
case UP:
x--;
break;
case DOWN:
x++;
break;
case LEFT:
y--;
break;
case RIGHT:
y++;
break;
}
Point* curP = hPoint->next;
Point* preP = hPoint;
while (curP->next != NULL) {
preP = preP->next;
curP = curP->next;
}
if (curP->x == x && curP->y == y) {
//crash the end of sanke
isRool = true;
}
else {
if (wall.getWall(x, y) == '#' || wall.getWall(x, y) == '=') {
addPoint(x, y);
delPoint();
system("cls");
wall.drawWall();
cout << "GAME OVER" << endl;
return false;
}
}
if (wall.getWall(x, y) == '*') {
addPoint(x, y);
food.setFood();
return true;
}
else {
addPoint(x, y);
delPoint();
if (isRool == true) {
wall.setWall(x, y, '@');
}
return true;
}
}
3.2 game.cpp完成动态输入
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"wall.h"
#include"snake.h"
#include "food.h"
#include<ctime>
#include<conio.h>
#include<windows.h>
using namespace std;
int main() {
srand((unsigned int)time(NULL));
bool isDead = false;
char preKey = NULL;
Wall wall;
wall.initWall();
Food food(wall);
food.setFood();
Snake snake(wall,food);
snake.initSnake();
wall.drawWall();
//customer input
while (!isDead) {
char key = _getch();
if (preKey == NULL && key == snake.LEFT) {
continue;
}
if ( key == snake.QUIT) {
isDead = true;
break;
}
do {
if (key == snake.LEFT || key == snake.RIGHT || key == snake.UP || key == snake.DOWN) {
if (key == snake.LEFT && preKey == snake.RIGHT ||
key == snake.DOWN && preKey == snake.UP ||
key == snake.RIGHT && preKey == snake.LEFT ||
key == snake.UP && preKey == snake.DOWN
) {
key = preKey;
}
else {
preKey = key;
if (snake.moveSnake(key) == true) {
system("cls");
wall.drawWall();
Sleep(300);
}
else {
isDead = true;
break;
}
}
}
else {
key = preKey;
}
} while (!_kbhit());
}
system("pause");
return EXIT_SUCCESS;
}
4 游戏优化
4.1 游戏难度设定
随着蛇的不停增长,我们可以根据蛇的长度设定不同的游戏难度。比如我们可以分为3个难度,不同难度对应不同的移动速度:
- 蛇长在20以内,难度等级为1
- 蛇长为20-50,难度等级为2
- 蛇长超过50,难度等级为3
根据这个需求,我们可以在snake模块中加入新的接口,获取蛇的长度:countList 和 获取游戏界面刷新时间:getSleepTime
int Snake::getSleepTime() {
int sleepTime = 0;
int size = countList();
if (size < 5) {
sleepTime = 300;
}
else if (size >= 5 && size <= 8) {
sleepTime = 100;
}
else {
sleepTime = 50;
}
return sleepTime;
}
int Snake::countList() {
int size = 0;
Point *curPoint = hPoint;
while (curPoint != NULL) {
size++;
curPoint = curPoint->next;
}
return size;
}
4.2 游戏得分显示
我们可以在snake模块中加入新的接口:** getScore**
int Snake::getScore() {
int size = countList();
int score = (size-4) * 100;
return score;
}
4.3 更新显示刷新机制
我们现在做的游戏是依靠刷新来重新绘制图片,给人一种是否卡顿的体验。如果我们不希望对整体进行刷新二实现相同的游戏效果,可以利用定位光标来修改游戏内容
#include<windows.h>
...
void gotoxy(HANDLE hOut, int x, int y) {
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(hOut, pos);
}
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);