目录
一、目的:
1、记录案例中有价值的东西
2、按照教程一步步操作
二、参考
1、
三、注意:
1、
四、操作
1、无
五、文档内容: 具体实现
3.1 代码分析
当模拟好了游戏玩法和分析透彻游戏后,再分析下利用代码该如何实现游戏,也就是分析如何敲代码,怎么敲,在上面的分析思路中,如果我们想利用C++的面向对象的思想来完成该项目,就应该把每个模块划分开,让各个功能之间相互依赖又分离实现。
那么总结一下我们应该具体来完成哪些模块
首先可以创建项目时候,有个程序入口,这个文件可以写为Game.cpp,游戏中的主要部分,在这个模块中主要用户接受用户的输入,调度其他模块来完成一个总指挥的作用!其次我们可以从最简单的模块入手,墙是最简单的,内部只要能维护一个二维的数组即可,然后是蛇模块,负责管理蛇的一切行为,最后还有个食物模块。
总结:游戏中模块分为:主的程序入口、墙、蛇、食物。
3.2 创建项目
分析好了所有需求后开始实现游戏,第一步创建项目,打开VisualStudio,点击新建项目
创建控制台应用程序,名称为Snake,点击确定,之后点击下一步,创建一个空项目
点击完成。
添加新建项
添加主模块 game.cpp
然后添加程序入口函数,也就是main函数如下图
3.3 墙模块
下面我们开始制作游戏中墙模块的开发,首先经过分析,我们可以得出在墙模块中,我们需要维护一个二维数组,对整个游戏中的元素进行设置,所以我们可以声明一个二维数组:char gameArray[][],具体的行数和列数可以定义出一个枚举,比如本游戏中设置的是26行,26列,enum{ ROW = 26, COL = 26};
那么墙模块开发阶段,需要提供的主要接口是 初始化墙initWall, 以及打印墙,也就是将二维数组中的内容打印到控制台中,draw方法。当然对外还要提供出一个可以修改二维数组元素的方法以及根据索引获取二维数组元素的方法:getWall ,setWall;
3.3.1创建墙类 wall.h和wall.cpp
3.3.2 .h中声明墙模块中需要声明的成员方法和成员属性
#ifndef _WALL_HEAD
#define _WALL_HEAD
#include <iostream>
using namespace std;
class Wall
{
public:
enum
{
ROW = 26, //行数
COL = 26 //列数
};
Wall(); //构造
~Wall(); //析构
void initWall();//初始化数组
void draw(); //将数组画到控制台中
void setWall(int x, int y, char key); //根据索引设置二维数组中的元素
char getWall(int x, int y); //根据索引获取二维数组中的元素
private:
char gameArray[ROW][COL]; //维护的二维数组
};
#endif
3.3.3 .cpp中实现方法
Wall::Wall(){}
Wall::~Wall(){}
void Wall::initWall()
{
for (int i = 0; i < ROW;i++)
{
for (int j = 0; j < COL;j++)
{
//墙的条件
if (i == 0 || i == ROW - 1 || j == 0 || j == COL -1)
{
gameArray[i][j] = '*';
}
else
{
gameArray[i][j] = ' ';
}
}
}
}
void Wall::draw()
{
for (int i = 0; i < ROW;i++)
{
for (int j = 0; j < COL;j++)
{
cout << gameArray[i][j] << ' '; //加一个空格间隙,打印出来比较好看
}
//版本信息以及其他提示信息
if (i == 5)
{
cout << " Snake Game V1.0" << " ";
}
if (i == 6)
{
cout << " Create by zt, 2016-09-10" << " ";
}
if (i == 7)
{
cout << " up : w" << " ";
}
if (i == 8)
{
cout << " down : s" << " ";
}
if (i == 9)
{
cout << " left : a" << " ";
}
if (i == 10)
{
cout << " right : d" << " ";
}
cout << endl; //打印换行
}
}
void Wall::setWall(int x, int y, char key)
{
gameArray[x][y] = key;
}
char Wall::getWall(int x, int y)
{
return gameArray[x][y];
}
3.3.4 game.cpp中引用并测试墙模块
main函数
int main(){
Wall wall; //墙对象
wall.initWall(); // 初始化墙
//测试
wall.setWall(5, 5, '@');
wall.draw(); //打印墙
cout << wall.getWall(0, 0) <<endl;
cout << wall.getWall(1, 1) << endl;
cout << wall.getWall(5, 5) << endl;
system("pause");
return EXIT_SUCCESS;
}
最后打印的 ‘*’ ‘ ’ ‘@’是测试的对外接口,在开发中实时的测试十分重要
3.4 蛇模块
3.4.1 创建蛇类 snake.h 和snake.cpp
3.4.2 .h中声明成员方法和属性
首先分析下蛇模块中需要做的事情,蛇其实是一个链式结构,在本案例中,如果我们只学过C的链表,还未学习C++中的List容器,那么我们可以先从最基本的结构体来模拟蛇这个链表。
声明一个结构体 struct point ,每一个蛇的身段(节点)我们都称为一个point,在结构体中我们分为指针域(指向下一个节点)和数据域(保存具体数据)
其次,我们还需要提供一些方法将蛇初始化,initSnake;在初始化蛇中起始需要做两步操作,第一步销毁蛇所有节点,destoryPoint这一步是为了以后有可能会有重玩游戏的功能,所以我们初始化时候先清空所有蛇,第二步添加蛇节点 addPoint,这个方法就是给蛇增加蛇身的方法
当然我们还需要一个私有成员属性 Point * pHead来记录蛇头,还一个私有成员保存墙的引用,因为我们需要将设置的内容保存到维护二维数组的墙中
#ifndef _SNAKE_HEAD
#define _SNAKE_HEAD
#include <iostream>
using namespace std;
#include "wall.h"
struct Point
{
//数据域
int x;
int y;
//指针域
Point * next;
};
class Snake
{
public:
Snake(Wall &tmpWall);
~Snake();
// 初始化蛇
void initSnake();
// 销毁蛇
void destoryPoint();
// 添加蛇节点
void addPoint(int x, int y);
private:
//蛇头结点
Point * pHead;
Wall & wall;
};
#endif
3.4.3 实现方法
这里我们利用初始化列表的方式,将私有成员属性墙进行赋值。
#include "snake.h"
Snake::Snake(Wall &tmpWall) :wall(tmpWall) //初始化列表获取墙对象
{
pHead = NULL; //构造时候头节点为空
}
Snake::~Snake()
{
}
void Snake::initSnake()
{
//销毁原来的节点
destoryPoint();
//初始化蛇,蛇头和2段蛇身
addPoint(5, 3);
addPoint(5, 4);
addPoint(5, 5);
}
void Snake::destoryPoint()
{
Point * pCur = pHead;
while (pHead != NULL)
{
pCur = pHead->next;
delete pHead;
pHead = pCur;
}
}
void Snake::addPoint(int x,int y)
{
// 创建新节点
Point * newP = new Point;
if (newP == NULL)
{
return;
}
// 新节点赋值
newP->x = x;
newP->y = y;
newP->next = NULL;
//修改原始的头为身子
if (pHead != NULL)
{
//设置当前的头结点为身子
wall.setWall(pHead->x, pHead->y, '=');
}
//新的节点添加到链表头部,不管有没有头结点,新节点都会是头节点
newP->next = pHead;
pHead = newP;
wall.setWall(pHead->x, pHead->y, '@');
}
3.4.4 测试蛇模块
在main函数中添加代码如下
int main(){
Wall wall; //墙对象
wall.initWall(); // 初始化墙
Snake snake(wall); //蛇对象
snake.initSnake(); //初始化蛇
wall.draw(); //打印墙
system("pause");
return EXIT_SUCCESS;
}
运行程序,最终可以看到一条为两个蛇身一个蛇头的蛇在屏幕中
3.5 食物模块
3.5.1 创建食物类 food.h 和 food.cpp
3.5.2 .h中声明食物模块中成员方法和属性
食物模块比较简单,无非是在屏幕中设置一个随机的位置做食物,但是这个位置有个要求就是不能在蛇上。
我们可以把设置食物的方法称为 setFood,当然我们可以记录这个随机的位置,称为FoodX、FoodY,由于需要设置到墙的二维数组里就需要引用墙的模块wall,因此.h中如下
#ifndef _FOOD_HEAD
#define _FOOD_HEAD
#include <iostream>
using namespace std;
#include "wall.h"
class Food
{
public:
Food(Wall & tmpWall);
void setFood(); //设置食物
private:
int foodX; //食物的X
int foodY; //食物的Y
Wall & wall; //墙模块引用
};
#endif
3.5.3 .cpp中实现方法
#include "food.h"
Food::Food(Wall & tmpWall) :wall(tmpWall)
{
}
void Food::setFood()
{
while (1)
{
//食物的随机位置
foodX = rand() % (wall.ROW - 2) + 1;
foodY = rand() % (wall.COL - 2) + 1;
//如果当前位置是可行区域,可以设置为食物并且退出循环,否则继续循环
if (wall.getWall(foodX, foodY) == ' ')
{
wall.setWall(foodX, foodY, '#');
break;
}
}
}
3.5.4 食物模块测试
int main(){
srand((unsigned int)time(NULL)); //随机数种子
Wall wall; //墙对象
wall.initWall(); // 初始化墙
Snake snake(wall); //蛇对象
snake.initSnake(); //初始化蛇
Food food(wall); //食物
food.setFood(); //设置食物
wall.draw(); //打印墙
system("pause");
return EXIT_SUCCESS;
}
在main函数中引用食物模块,并且设置随机数种子,运行代码,可以看到食物呈现在屏幕中
3.6 主模块
3.6.1 分析
当我们完成了所有的元素的模块后,我们已经将游戏静止状态下的内容做好,下面就是让蛇运动起来,那么此时我们发现,蛇目前还没有移动能力,也就是对外没有提供一个移动的接口。
下面给snake蛇模块扩展出移动接口,move
移动时经过上面分析,分为两种,一种吃到食物,另一种正常移动,而正常移动时候需要我们把蛇的尾节点删除掉,这就需要我们再提出一个接口,删除尾节点,delPoint
3.6.2 蛇模块添加方法以及实现
snake.h中添加
代表上下左右的枚举
enum{ UP = 'w', DOWN = 's', LEFT = 'a', RIGHT = 'd' };
//移动蛇方法
bool move(char key);
//删除尾节点
void delPoint();
由于移动有成功移动,和失败移动,也就是GameOver时候,所有我们定义一个返回值为bool类型,参数为key,这个key就是用户输入的方向键,我们也可以定义出一个枚举,代表上下左右
.cpp中
bool Snake::move(char key)
{
int x = pHead->x;
int y = pHead->y;
switch (key)
{
case UP:
x--;
break;
case DOWN:
x++;
break;
case LEFT:
y--;
break;
case RIGHT:
y++;
break;
default:
return true;
}
if (wall.getWall(x, y) == '=' || wall.getWall(x, y) == '*')
{
cout << "GameOver!" << endl;
return false;
}
if (wall.getWall(x, y) == '#')
{
addPoint(x, y);
//重新设置食物
food.setFood();
}
else
{
//正常移动
addPoint(x, y);
delPoint();
}
return true;
}
void Snake::delPoint()
{
//两个节点以上删除
if (pHead == NULL || pHead->next == NULL)
{
return;
}
//用两个临时节点,一个是前一个节点pre,一个是当前节点cur
Point * pre = pHead;
Point * cur = pHead->next;
while (cur->next != NULL) //指向的下一个不为空,就循环
{
pre = pre->next;
cur = pre->next;
}
//尾节点修改内容
wall.setWall(cur->x, cur->y, ' ');
delete cur;
cur = NULL;
pre->next = NULL;
}
delPoint,需要两个节点来寻找出尾节点,并且删除掉尾节点,将原有的倒数第二个节点的指向空,作为当前的尾节点;
move,需要判断出移动后的碰到的是否是墙、蛇、或者是食物,这几种可能性
3.6.3 测试新添加的蛇模块
我们在main函数中添加测试代码
int main(){
srand((unsigned int)time(NULL)); //随机数种子
Wall wall; //墙对象
wall.initWall(); // 初始化墙
Food food(wall); //食物
food.setFood(); //设置食物
Snake snake(wall,food); //蛇对象
snake.initSnake(); //初始化蛇
//测试
snake.move(snake.UP);
snake.move(snake.UP);
snake.move(snake.LEFT);
wall.draw(); //打印墙
system("pause");
return EXIT_SUCCESS;
}
然后看没有测试代码和添加了测试代码后的效果,在这里我们写了3行测试代码,也就是将蛇进行了 上移、上移、左移,实现效果后可以看出蛇头已经朝左,证明移动方法成功
3.6.4 运行游戏,接受用户输入
接下来先将测试代码删除,然后我们准备接受用户的输入,根据用户输入来让蛇移动,首先,获取到用户的按键,然后判断是否是w、s、a、d,如果是再进行下一步操作
添加如下代码
char key = _getch(); //conio头文件
if (key == snake.UP || key == snake.DOWN || key == snake.LEFT || key == snake.RIGHT)
{
//移动蛇
snake.move(key);
system("cls"); //清屏
wall.draw(); //重新绘制蛇
}
但是我们发现,蛇此时只能移动一次,再次按键就会退出程序,这时候我们需要通过循环来让蛇进行一直移动,而且我们还需要一个标示,这个标示表示蛇是否已经死亡,死亡的话我们就可以退出当前这个循环了,因此程序改为
//死亡标示
bool isDead = false;
while (isDead != true)
{
char key = _getch();
if (key == snake.UP || key == snake.DOWN || key == snake.LEFT || key == snake.RIGHT)
{
//移动蛇
if (snake.move(key) == true)
{
system("cls"); //清屏
wall.draw(); //重新绘制蛇
}
}
}
运行代码,我们此时可以测试游戏了,只不过每走一步都需要玩家控制,也就是这个蛇只能按一次按键走一步,这并不是游戏应该的样子,正常游戏应该激活后,如果没有新的按键,就应该以之前的方向进行移动,因此我们也应该记录一下之前的方向,char preKey
再次修改代码
//死亡标示
bool isDead = false;
//上一次的运行方向
char preKey = NULL;
while (isDead != true)
{
char key = _getch();
do
{
if (key == snake.UP || key == snake.DOWN || key == snake.LEFT || key == snake.RIGHT)
{
//移动蛇
if (snake.move(key) == true)
{
system("cls"); //清屏
wall.draw(); //重新绘制蛇
Sleep(300); //睡眠
}
}
} while (!_kbhit()); //当没有键盘输入时返回0
}
此时运行游戏,蛇就可以以一个方向进行移动了,我们还加了个睡眠时间控制蛇的移动速度。
但是此时代码中存在许多bug,需要我们解决,首先我们的bug有这么几个
第一:目前我们只处理了移动正常的时候,蛇一旦死亡,就会一直显示GameOver,也就是没有处理死亡状态跳出循环,效果如图
第二:我们没有处理反向移动,如何一上来我用a键激活游戏,那么游戏开始就会进入死亡,也就是上来就吃到了蛇身。但是游戏规则蛇不可以进行180°移动
效果如图,上来就GameOver
第三:蛇在死亡时候,只是死在了当前位置,并没有移动最后一步,这种显示并不友好,虽然我们自己可能看得出来是为什么死亡,但是玩家会觉得比较恶心,效果如图
其实已经碰到墙壁,但是没有走这步,就进入了GameOver状态。
第四:当蛇有4个身段或更多时候,蛇如果碰到蛇尾,应该进行循环,而游戏中正常移动是先加节点,后删除尾节点,所以会导致循环蛇时候出现死亡,而正常的蛇其实是头尾同时移动,并不会直接死掉,所以我们应该判断一下蛇如果下一步到的位置是蛇尾,就不要进入死亡状态,bug效果
第五: 如果我们在游戏中,按了其他案件,也就是除了w a s d的按键,游戏就会停止。这也算个bug。
3.6.5 解决bug
3.6.5.1处理死亡状态
3.6.5.2 180°移动
3.6.5.3 显示蛇死亡的最后一步
可以在蛇模块的move方法中添加如下代码
3.6.5.4 解决循环问题
判断尾节点,是尾节点不进入死亡判断
如果是循环状态,还需要修改一下代码,否则显示会出现错误
3.6.5.5 其他按键的屏蔽
if (key == snake.UP || key == snake.DOWN || key == snake.LEFT || key == snake.RIGHT)
之后我们可以加一个else
else
{
key = preKey;
}
这样当我们输入其他按键时候,就会保持上一个方向。
3.6.6 测试代码
解决所有问题后,玩游戏 测试代码。基本和贪食蛇一样。