说实话,编程语言的学习挺枯燥的,对于已经过了尝鲜期,开始接触比较深层次的初学者来说更是如此。而作为耐得住寂寞的理工/IT男,“苦中作乐”已为常态。写点小游戏娱乐娱乐,满足创造欲的同时巩固、拓展一下所学知识,美哉美哉。写贪吃蛇是最好不过的选择了。
而这篇半吊子文章,教程?心得?破绽百出的胡扯?都算吧,各位随便看看就好,若有不同见识,不吝赐教。
int main() {
init(); //初始化操作
while(1) {
input(); //输入
update(); //数据更新
repaint(); //画面更新
}
return 0;
}
若读者不熟悉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类(代表食物)
#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;
}
#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<<"┛";
}
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方法
}
#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(); //判断情况
}
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();
}
然后当你去运行的时候,发现这条蛇是条暴走的蛇皮怪,光速撞墙去世了
简单的加个Sleep()函数就行。
Sleep(DWORD dwMilliseconds):包含在Windows.h中,让系统中断,windows
下单位是毫秒
但有个问题,Sleep()里的数字填太大的话,你会发现按一下键大部分时候都不灵,只有长按才能让它比较“听话”,而比较小的时候,游戏难度是在难以驾驭,两边不讨好,显然很蠢。
不过这里并不打算改,因为如果改的话我想到的方案工程量比较大,有时间的话我会尝试改一下。
整理后的代码,在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();
}