C++windows控制台贪吃蛇

说实话,编程语言的学习挺枯燥的,对于已经过了尝鲜期,开始接触比较深层次的初学者来说更是如此。而作为耐得住寂寞的理工/IT男,“苦中作乐”已为常态。写点小游戏娱乐娱乐,满足创造欲的同时巩固、拓展一下所学知识,美哉美哉。写贪吃蛇是最好不过的选择了。
而这篇半吊子文章,教程?心得?破绽百出的胡扯?都算吧,各位随便看看就好,若有不同见识,不吝赐教。

贪吃蛇流程

  • 对于一个贪吃蛇这样一个单线程就能实现的小游戏,游戏框架应该是这样的:
int main() {
     

   init(); //初始化操作
   
   while(1) {
     
       input();    //输入
       update();   //数据更新
       repaint();    //画面更新
   }
   return 0;
}
  • 而对于贪吃蛇这个游戏来说:
    • 初始化操作应该绘制边框,蛇以及食物的初始化。
    • 输入则应检测是否按下上下左右(或者WSAD)按键。
    • 更新数据前应先判断死亡(撞墙和吃到蛇身)及是否吃到食物,若游戏继续则更新蛇和食物的数据。
    • 更新画面则是根据蛇和食物的坐标重绘画面。

关于蛇的实现

  • 我看过比较主流的有两种思路:
    • 一是使用一个大数组(整型数组或者自定义类的数组),分别存储地图的蛇、边框、食物和空白的数据,数据更新即对大数组进行更新。
    • 二则是我所采用的,用数组、链表这类的存储蛇的数据,配合部分win32函数对此进行更新。
      • 而数组较为固定,(个人认为)不利于管理。
      • c++有自带已写好的双向链表list,不必自己写,省去了一些功夫

list相关

若读者不熟悉list的用法,以下是本例所用到的。

  • 首先要包含头文件List

      #include 
    
  • 创建空链表

      listname;
    
  • 部分函数:

      front(), back():返回第一个、最后一个元素
      push_front(value), push_back(value):从首部、尾部追加值
      pop_back():删除最后一个元素
      begin(), end():返回第一个元素、最后一个元素下一个元素的迭代器(遍历相关)
    

`

  • 迭代器
    • 可以理解为遍历list的工具,遍历时相当于每个元素的指针,使用时应引入头文件Iterator

        #include
      
    • 一般遍历用法

        for(listlistName::iterator iter = listName.begin();iter != listName.end();++iter) {
            //to do
        }
      

类的建立

先定义了一个Entity类:

class Entity {
     
public:
	Entity(int, int);
	void setX(int);
	void setY(int);
	void setPosition(int, int);
	int getX();
	int getY();
	void virtual print() {
     } //根据子类绘制图案
	bool isHit(Entity); //判断两个Entity是否在同意位置

private:
	int x,y;    //x和y坐标
};

Entity类方法的实现:

Entity::Entity(int x, int y) {
     
	setX(x);
	setY(y);
}

void Entity::setX(int x) {
     
	this->x = x;
}

void Entity::setY(int y) {
     
	this->y = y;
}

void Entity::setPosition(int x, int y) {
     
	this->setX(x);
	this->setY(y);
}

int Entity::getX() {
     
	return x;
}

int Entity::getY() {
     
	return y;
}

bool Entity::isHit(Entity other) {
     
	if(x == other.getX() && y == other.getY())
		return true;
	else
		return false;
}

Entity类继而派生出Snake类(代表蛇身)和Food类(代表食物)

  • Snake类及其实现
#include 
#include 

#define BODY '*'    //以'*'作为蛇身

class Snake : public Entity {
     

public:
	Snake(int, int);
	static int getToilX();
	static void setToilX(int);
	static int getToilY();
	static void setToilY(int);
    static int getMoveOrder();
    static void setMoveOrder(int);
	void print();

private:
	static int toilX, toilY; //尾部坐标
    static int moveOrder; //当前移动命令
};

void gotoXy(int x, int y);

int Snake::toilX = -1;

int Snake::toilY = -1;

int Snake::keyOrder = VK_LEFT;  //初始为向左

Snake::Snake(int x, int y):Entity(x, y) {
     
}

int Snake::getToilX() {
     
	return toilX;
}

void Snake::setToilX(int toilX) {
     
	Snake::toilX = toilX;
}

int Snake::getToilY() {
     
	return toilY;
}

void Snake::setToilY(int toilY) {
     
	Snake::toilY = toilY;
}

int Snake::getMoveOrder() {
     
    return getMoveOrder;
}

void Snake::setMoveOrder(int moverOrder) {
     
    Snake::moverOrder = moveOrder;
}

void Snake::print() {
     
	gotoXy(getX(), getY()); //根据前往蛇身坐标指向的地方
	std::cout<<BODY;
}
  • Food类只重写了print()函数,即以’@'代表食物

游戏初始

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

#define SNAKE list
#define MAP_HEIGHT 25
#define MAP_LENGHT 25
SNAKE snake;
Food food(-1, -1);

void init() {
     

	initSnake();//初始化蛇
	hideCursor();//设置光标不可见
    paintMap();//绘制地图边框
	paintSnake();//绘制蛇
	createFood();//创建食物
    
}

初始化蛇

void initSnake() {
     

	for(int i = 0;i < 5;i++) {
     
		Snake node = Snake(MAP_LENGTH / 2 - 2 + i, MAP_HEIGTH / 2); //让它生成在中间位置
		snake.push_back(node);
	}
}

隐藏光标

用到了win32的函数,作用是隐藏光标,解决光标频闪问题

void hideCursor() {
     

    CONSOLE_CURSOR_INFO cci;    //定义光标结构体
    GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);    //获取当前光标
    cci.bVisible =  false;  //设置光标不可见
	SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);    //将当前光标设置为cci
}

边框绘制

  • 先定义好大小,再画出边框
    • 有个小细节就是不改变控制台的字体的话行距相差一个空格的大小,直接打印会显得不美观,因此建议打印时每行打印两倍长。
    • 用制表符打印边框本人觉得美观,推荐使用,一般输入法符号里有自带
#include 
using namespace std;

#define MAP_LENGTH 25
#define MAP_HEIGTH 25

void paintMap() {
     

	cout<<"┏";
	for(int i = 0;i < MAP_LENGTH * 2 + 2;i++)
		cout<<"━";
	cout<<"┓"<<endl;
	for(int i = 0;i < MAP_HEIGTH;i++) {
     
		cout<<"┃ ";
		for(int j = 0;j < MAP_LENGTH * 2;j++)
			cout<<" ";
		cout<<" ┃"<<endl;
	}
	cout<<"┗";
	for(int i = 0;i < MAP_LENGTH * 2 + 2;i++)
		cout<<"━";
	cout<<"┛";
}
  • 另外,在Snake类中print方法中出现的gotoXy()函数的定义(使用了win32函数):
void gotoXy(int x, int y) {
     
    
    COORD coord;    //定义坐标结构体
	coord.X = 2 * x + 2;
	coord.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);    //设置光标位置到coord
}
  • COORD 中(0, 0)表示左上角第一个字符位,X轴方向为右,Y轴为下
  • 本例Entity的坐标(x, y)中x的取值范围应为[1, MAP_LENGTH],y应为[1, MAP_HEIGTH]
  • 绘制边框时边框占了第一行和第一列,即

绘制蛇

void paintSnake() {
     

	for(SNAKE::iterator iter = snake.begin();iter != snake.end();++iter)
		iter->print();  //遍历list调用每个元素的print方法
}

创建食物

  • 所谓创建,其实只不过调用了setPosition方法,重置了food坐标而已。
  • 随机创建的同时要遍历检查snake,避免“食物出现在蛇身上”的尴尬。
#include 
#include 

Food food;

void createFood() {
     

	while(1) {
     
		
		SNAKE::iterator iter;

		srand((unsigned int)time(NULL));    //惯用套路,用时间作随机种子
		food.setPosition(rand() % MAP_LENGTH + 1, rand() % MAP_HEIGTH + 1);
        //生成食物应在地图范围内
        
		for(iter = snake.begin();iter != snake.end();++iter)
			if(food.isHit(*iter)) 
				break;

		if(iter == snake.end()) //遍历到最后,即没有重叠
			break;
	}
	food.print();
}

输入操作

在控制台输入中常用的诸如scanf()、getchar()、cin这类在这里并不适用,原因是使用这些用作方法会使程序停顿(想一下蛇走一步就要求你输入一次的情况有多蠢),getch()也有这个问题,好在它不用按下回车以作输入结束的标志,所以配合kbhit()使用也可以达到想要的效果。

kbhit():非阻塞地响应键盘输入事件,包含在conio.h文件,
        作用为检查是否有键盘输入,若有返回一个非0值,否则返回0
getch():包含在conio.h,从控制台读取一个字符,不显示在屏幕上,返回按键的ASCII码。

if(!kbhit()) ch = getch();实际上可以达到非阻塞输入的效果,但getch()并非标准C的函数,而且上下左右键无法被getch()正确,故本人建议使用win32的异步键盘输入函数GetAsyncKeyState()。

GetAsyncKeyState(int vkey):包含在Windows.h,在相应按键按下时,第15位设为1;若抬起,则设0。
利用判断该函数返还值 & 0x8000的值 是不是为真,来判断没有按下按键。
上下左右键分别对应定义好的宏VK_UP、VK_DOWN、VK_LEFT、VK_RIGHT。

将读到的键存储到Snake的静态变量中,方便后面处理蛇的数据。
注意加个判断当前命令方向与输入命令方向是否为相反反向,避免自杀情况出现。

void input() {
     

	if(GetAsyncKeyState(VK_UP) & 0x8000 && Snake::getMoveOrder() != VK_UP && Snake::getMoveOrder() != VK_DOWN)
		Snake::setMoveOrder(VK_UP);
		
	if(GetAsyncKeyState(VK_DOWN) & 0x8000 && Snake::getMoveOrder() != VK_UP && Snake::getMoveOrder() != VK_DOWN)
		Snake::setMoveOrder(VK_DOWN);

	if(GetAsyncKeyState(VK_LEFT) & 0x8000 && Snake::getMoveOrder() != VK_LEFT && Snake::getMoveOrder() != VK_RIGHT)
		Snake::setMoveOrder(VK_LEFT);

	if(GetAsyncKeyState(VK_RIGHT) & 0x8000 && Snake::getMoveOrder() != VK_LEFT && Snake::getMoveOrder() != VK_RIGHT)
		Snake::setMoveOrder(VK_RIGHT);
}

数据更新

void update() {
     

	moveTo();   //移动蛇
	judge();    //判断情况
}

移动

  • 移动即根据moveOrder命令对snake进行操作,这里活用双向链表的特性,使用了“斩了尾巴放在头上”的思路。
  • 这里先不要重置静态的尾部坐标,原因为移动后吃到食物无法正确补加尾部坐标
void moveTo() {
     

	gotoXy(snake.back().getX(), snake.back().getY());
	cout<<' ';  //用空格把尾巴覆盖,实现擦去的效果,后面输出时就不用因清屏而频闪
	snake.pop_back();
	Snake head(-1, -1);
	switch(Snake::getMoveOrder()) {
      //根据命令决定头的位置
	case VK_UP:
		head.setPosition(snake.front().getX(), snake.front().getY() - 1);
		break;
	case VK_DOWN:
		head.setPosition(snake.front().getX(), snake.front().getY() + 1);
		break;
	case VK_LEFT:
		head.setPosition(snake.front().getX() - 1, snake.front().getY());
		break;
	case VK_RIGHT:
		head.setPosition(snake.front().getX() + 1, snake.front().getY());
		break;
	}

	snake.push_front(head); //把“新头”安上
}

判断

  • 既然蛇发生了移动,则有可能发生3种情况:
    • 撞到墙,game over
    • 咬到自身,game over
    • 吃到食物,变长
  • 三种情况都只需要判断头的位置即可
void judge() {
     


	int x = snake.front().getX(), y = snake.front().getY();

	if(x == 0 || x == MAP_LENGTH + 1 || y == 0 || y == MAP_HEIGTH + 1)  //判断是否撞墙
		exit(1);

	int i = 1;
	for(SNAKE::iterator iter = snake.begin();iter != snake.end();++iter) {
     
    //判断是否咬到自己
		if(i) {
     
			i--;
			continue;
		}

		if(iter->isHit(snake.front()))
			exit(1);
	}
    
	if(snake.front().isHit(food)) {
     //判断是否吃到食物
		snake.push_back(Snake(Snake::getToilX(), Snake::getToilY()));
        //创建原尾部坐标的Snake节点,达到延长目的
		createFood();   //重置食物
	}
	Snake::setToilX(snake.back().getX());   //重置静态尾部坐标
	Snake::setToilY(snake.back().getY());
}

重绘

这里就重绘一下头部和食物坐标就行了

void repaint() {
     
    snake.front().print();
    food.print();
}

间隔

然后当你去运行的时候,发现这条蛇是条暴走的蛇皮怪,光速撞墙去世了
简单的加个Sleep()函数就行。

Sleep(DWORD dwMilliseconds):包含在Windows.h中,让系统中断,windows
                                            下单位是毫秒

但有个问题,Sleep()里的数字填太大的话,你会发现按一下键大部分时候都不灵,只有长按才能让它比较“听话”,而比较小的时候,游戏难度是在难以驾驭,两边不讨好,显然很蠢。
不过这里并不打算改,因为如果改的话我想到的方案工程量比较大,有时间的话我会尝试改一下。

  • 这里我想到的的方案是引入帧率机制,这样即使一秒30帧,也会有固定的高检查输入频率。
    • 但改起来的话就要引入平面速度、实数坐标等来调整数据,与上面的所写的有较大的不同

总结

  • 以上基本只实现了贪吃蛇的一般逻辑部分,事实上还有很多可以完善的地方:
    比如引入图形库(我是觉得引入了就一点也不初学者了),UI的完善、长按加速、随着时间变速、游戏结算、玩法创新等等,当然最牛的莫过于智能寻路算法了(恳请大神赐教)。
    总而言之,对像我一样的初学者来说,它算是游戏开发的启蒙项目了,
    而且最重要的是收获了无与伦比的码代码的快乐,不是吗?(滑稽)

整理后的代码,在vs上编译运行通过(单文件便于复制):

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

#define MAP_LENGTH 25
#define MAP_HEIGTH 25
#define SNAKE list

class Entity {
     
public:
   Entity(int x, int y){
     this->x = x;this->y = y;}
   void setX(int x){
     this->x = x;}
   void setY(int y){
     this->y = y;}
   void setPosition(int x, int y){
     this->x = x;this->y = y;}
   int getX(){
     return x;}
   int getY(){
     return y;}
   void virtual print() {
     } //根据子类绘制图案
   bool isHit(Entity); //判断两个Entity是否在同意位置

private:
   int x,y;    //x和y坐标
};

class Snake : public Entity{
     

public:
   Snake(int x, int y):Entity(x, y){
     }
   static int getToilX(){
     return toilX;}
   static void setToilX(int toilX){
     Snake::toilX = toilX;}
   static int getToilY(){
     return toilY;}
   static void setToilY(int toilY){
     Snake::toilY = toilY;}
   static int getMoveOrder(){
     return moveOrder;}
   static void setMoveOrder(int moveOrder){
     Snake::moveOrder = moveOrder;}
   void print();

private:
   static int toilX, toilY; //尾部坐标
   static int moveOrder; //当前移动命令
};

class Food : public Entity{
     

public:
   Food(int x, int y):Entity(x, y){
     }
   void print();
};

void init();
void initSnake();
void hideCursor();
void paintMap();
void paintSnake();
void createFood();
void gotoXy(int, int);
void input();
void update();
void moveTo();
void judge();
void repaint();

SNAKE snake;
Food food(-1, -1);

int main() {
     

   init();

   while(1) {
     

   	input();
   	update();
   	repaint();
   	Sleep(100);
   }

   return 0;
}

bool Entity::isHit(Entity other) {
     

   if(x == other.getX() && y == other.getY())
   	return true;
   else
   	return false;
}

int Snake::toilX = -1;

int Snake::toilY = -1;

int Snake::moveOrder = VK_LEFT;

void Snake::print() {
     

   gotoXy(getX(), getY());
   cout<<'*';
}

void Food::print() {
     

   gotoXy(getX(), getY());
   cout<<'&';
}

void init() {
     

   initSnake();//初始化蛇
   hideCursor();//设置光标不可见
   paintMap();//绘制地图边框
   paintSnake();//绘制蛇
   createFood();//创建食物
   
}

void initSnake() {
     

   for(int i = 0;i < 5;i++) {
     
   	Snake node = Snake(MAP_LENGTH / 2 - 2 + i, MAP_HEIGTH / 2); //让它生成在中间位置
   	snake.push_back(node);
   }
}

void hideCursor() {
     

   CONSOLE_CURSOR_INFO cci;    //定义光标结构体
   GetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);    //获取当前光标
   cci.bVisible =  false;  //设置光标不可见
   SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);    //将当前光标设置为cci
}

void paintMap() {
     

   cout<<"┏";
   for(int i = 0;i < MAP_LENGTH * 2 + 2;i++)
   	cout<<"━";
   cout<<"┓"<<endl;
   for(int i = 0;i < MAP_HEIGTH;i++) {
     
   	cout<<"┃ ";
   	for(int j = 0;j < MAP_LENGTH * 2;j++)
   		cout<<" ";
   	cout<<" ┃"<<endl;
   }
   cout<<"┗";
   for(int i = 0;i < MAP_LENGTH * 2 + 2;i++)
   	cout<<"━";
   cout<<"┛";
}

void gotoXy(int x, int y) {
     
   
   COORD coord;    //定义坐标结构体
   coord.X = 2 * x + 2;
   coord.Y = y;
   SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),coord);    //设置光标位置到coord
}

void paintSnake() {
     

   for(SNAKE::iterator iter = snake.begin();iter != snake.end();++iter)
   	iter->print();  //遍历list调用每个元素的print方法
}

void createFood() {
     

   while(1) {
     
   	
   	SNAKE::iterator iter;

   	srand((unsigned int)time(NULL));    //惯用套路,用时间作随机种子
   	food.setPosition(rand() % MAP_LENGTH + 1, rand() % MAP_HEIGTH + 1);
       //生成食物应在地图范围内
       
   	for(iter = snake.begin();iter != snake.end();++iter)
   		if(food.isHit(*iter)) 
   			break;

   	if(iter == snake.end()) //遍历到最后,即没有重叠
   		break;
   }
   food.print();
}

void input() {
     

   if(GetAsyncKeyState(VK_UP) & 0x8000 && Snake::getMoveOrder() != VK_UP && Snake::getMoveOrder() != VK_DOWN)
   	Snake::setMoveOrder(VK_UP);
   	
   if(GetAsyncKeyState(VK_DOWN) & 0x8000 && Snake::getMoveOrder() != VK_UP && Snake::getMoveOrder() != VK_DOWN)
   	Snake::setMoveOrder(VK_DOWN);

   if(GetAsyncKeyState(VK_LEFT) & 0x8000 && Snake::getMoveOrder() != VK_LEFT && Snake::getMoveOrder() != VK_RIGHT)
   	Snake::setMoveOrder(VK_LEFT);

   if(GetAsyncKeyState(VK_RIGHT) & 0x8000 && Snake::getMoveOrder() != VK_LEFT && Snake::getMoveOrder() != VK_RIGHT)
   	Snake::setMoveOrder(VK_RIGHT);
}

void update() {
     

   moveTo();
   judge();
}

void moveTo() {
     

   gotoXy(snake.back().getX(), snake.back().getY());
   cout<<' ';  //用空格把尾巴覆盖,实现擦去的效果,后面输出时就不用因清屏而频闪
   snake.pop_back();
   Snake head(-1, -1);
   switch(Snake::getMoveOrder()) {
      //根据命令决定头的位置
   case VK_UP:
   	head.setPosition(snake.front().getX(), snake.front().getY() - 1);
   	break;
   case VK_DOWN:
   	head.setPosition(snake.front().getX(), snake.front().getY() + 1);
   	break;
   case VK_LEFT:
   	head.setPosition(snake.front().getX() - 1, snake.front().getY());
   	break;
   case VK_RIGHT:
   	head.setPosition(snake.front().getX() + 1, snake.front().getY());
   	break;
   }

   snake.push_front(head); //把“新头”安上
}

void judge() {
     

   int x = snake.front().getX(), y = snake.front().getY();

   if(x == 0 || x == MAP_LENGTH + 1 || y == 0 || y == MAP_HEIGTH + 1)  //判断是否撞墙
   	exit(1);

   int i = 1;
   for(SNAKE::iterator iter = snake.begin();iter != snake.end();++iter) {
     
   //判断是否咬到自己
   	if(i) {
     
   		i--;
   		continue;
   	}

   	if(iter->isHit(snake.front()))
   		exit(1);
   }
   
   if(snake.front().isHit(food)) {
     //判断是否吃到食物
   	snake.push_back(Snake(Snake::getToilX(), Snake::getToilY()));
       //创建原尾部坐标的Snake节点,达到延长目的
   	createFood();   //重置食物
   }
   Snake::setToilX(snake.back().getX());   //重置静态尾部坐标
   Snake::setToilY(snake.back().getY());
}

void repaint() {
     
   snake.front().print();
   food.print();
}

你可能感兴趣的:(c++)