面向对象方法编一个简易的控制台版贪吃蛇(二)

本文介绍了如何使用面向对象方法在控制台版贪吃蛇游戏中实现蛇的移动控制。通过键盘操作,设定按键,更新蛇的位置,并解决移动过程中蛇体增长和屏幕显示混乱的问题。通过在循环中添加适当延迟、清除蛇的身体并刷新控制台,成功实现了蛇的平滑移动。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在《 面向对象方法编一个简易的控制台版贪吃蛇(一)》中,我们已经让食物、地图蛇等可以在控制台上显示出来。下面我们应该考虑的问题是:如何通过键盘操作让蛇动起来。只有先把这个问题解决了,你在以后实现诸如吃到食物、碰到墙壁等功能的时候才可以直接拿过来测试。

首先,需要明确的一个问题是。这个操控蛇的方法应该写在哪里?由于我们是对蛇进行操作,因此,我们可以将这个方法写在蛇类(Snake)里面。那么,如何捕获按键?我们首先需要一个char类型的变量(char c)。专门用来捕获你所按下的按键。

那么,按什么按键能够实现蛇的上下左右移动呢?我们可以专门定义一个函数(setKey)用来设置你想要的按键,如果你想用WASD来控制的话,你就可以直接调用这个函数来设置。相应的,在蛇类里面也要增加四个变量(keyUp, keyDown, keyLeft, keyRight),代表四个按键,便于在setKey函数里面进行传值操作。

setKey函数声明:

void setKey(int up,int down,int left,int right);

setKey函数定义:

void Snake::setKey(int up, int down, int left, int right)
{
	keyUp = up;
	keyDown = down;
	keyLeft = left;
	keyRight = right;
}

设置按键完成之后,如何捕获按键并让蛇在屏幕上动起来?我们需要在蛇类里面专门定义一个函数来实现控制蛇的功能,这个函数我命名为controlSnake;其对应的函数声明和函数定义如下所示:

controlSnake函数声明:

void controlSnake();

controlSnake函数定义:

void Snake::controlSnake() 
{
	int stateU = (GetAsyncKeyState(keyUp) & 0x8000);
	int stateD = (GetAsyncKeyState(keyDown) & 0x8000);
	int stateL = (GetAsyncKeyState(keyLeft) & 0x8000);
	int stateR = (GetAsyncKeyState(keyRight) & 0x8000);
	//当按下指定按键的时候,我们就把char c设置成如下变量。
	if (stateU != 0)
	{
		c = 'u';
	}
	if (stateD != 0)
	{
		c = 'd';
	}
	if (stateL != 0)
	{
		c = 'l';
	}
	if (stateR != 0)
	{
		c = 'r';
	}

	if (c == 'u' || c == 'd' || c == 'l' || c == 'r') //如果捕获了按键
	{
		for (int i = m_snake.size() - 1; i > 0; i--)//蛇除去头部的结点依次向前覆盖,以达到走动的效果
		{
			m_snake.at(i)->x = m_snake.at(i - 1)->x;
			m_snake.at(i)->y = m_snake.at(i - 1)->y;
		}
		switch (c) //对蛇的头部进行操作
		{
		case 'u'://捕获到向上键的话,蛇头y值减去1,以达到向上走的目的
			m_snake.front()->y--;
			break;
		case 'd':
			m_snake.front()->y++;
			break;
		case 'l':
			m_snake.front()->x--;
			break;
		case 'r':
			m_snake.front()->x++;
			break;
		}
	}
}

这些函数都写完之后,我们开始对主函数进行修改,以检验我们刚才所写的东西是否奏效。这是就需要我们之前所说的loop循环了,因为你没有循环的话,程序只能运行一次就直接退出了,这个loop循环我们暂时这么写:

主函数:

#include<iostream>
#include"Controller.h"
#include"baseNode.h"
#include"Map.h";
#include"Snake.h"
#include"Food.h"

#include<Windows.h>

void main()
{
	Map* map = new Map();
	Snake* snake = new Snake(5, 6);
	Food* food = new Food(30, 15);

	snake->showSnake();
	food->showFood();
	map->showMap();
	while (true)
	{
		Sleep(100);
		snake->setKey(VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT);//用键盘的上下左右键来控制蛇
		snake->controlSnake();

		//food->showFood();
		//map->showMap();
		snake->showSnake();
	}
	system("pause");
}

点击运行之后,我点击一下向右按键,却是这样的运行效果:



显示起来真的好混乱。为什么是这样?因为我一旦按了一个按键之后就不停地执行while循环,于是就会从左到右的把蛇画满整个控制台。而且,我们在对蛇进行操作的时候除了将前一个结点的值覆盖以外,还应该擦除蛇屁股的那个结点,如何将这些问题解决呢?

对于显示过于混乱的问题,我们可以在while循环里面让程序“歇一会儿”,只需增加一个语句:Sleep(num)即可,num自己指定。我将num设置为100试一下,运行程序,同样,我点击右键。运行结果:


这下,没有一开始那么混乱了。但是还有一个问题,那就是:蛇越来越长(我用这个特点,实力地玩了好长时间的画地图,哈哈~)。只有在蛇吃到东西的时候才能变长,一边走一边变长显然不是我们想要的效果。那么,如何解决?

首先,我们得知道为什么会造成这一情况,因为我们最开始在对蛇身进行控制的时候,也就是这段语句(位于Snake.cpp文件中的controlSnake函数定义里):

for (int i = m_snake.size() - 1; i > 0; i--)

{

m_snake.at(i)->x = m_snake.at(i - 1)->x;

m_snake.at(i)->y = m_snake.at(i - 1)->y;

}

对于这个问题我一开始认为:在这个语句中我们只是将点的坐标一次向前覆盖,但是前一次的蛇的尾部结点却还在vector里面并没有消失,所以再一次遍历vector的时候,前一次的蛇的尾部依然会显示出来。那么,既然这样,那我们只需把蛇尾部的那个结点给删除掉就可以了,即:一个m_snake.pop_back()就可以了。但是,我没意识到的一个问题是,尽管在屏幕上显示了这么多的结点,但并不代表vector里面多出了结点。在实际操作的时候发现,由于在目前为止,蛇的结点只有一个,因此,无论我在哪儿写这个函数,只要pop掉,蛇的结点就会一个不剩。于是,程序就会出现异常。因此,造成这个情况的原因并不是我一开始想的那样。

真正的原因是:由于我在while循环里面并没有对控制台进行刷新操作,因此,上一个while循环留下的痕迹,在本次while循环中依然会显示。所以,这只是单纯的显示问题。那么,我每一次循环就用语句system(“cls”)语句刷新一下如何?我用了,蛇倒是不会越来越长了,但是整个控制台却闪的非常厉害,看着就头晕。

那么,有没有其他办法?打一个空格键过去不就可以不显示了了嘛,所以,我们的思路是:在绘制蛇之前,先把蛇的身子全都用空格擦除掉,然后再绘制新的结点。具体做法如下:

由于controller类是专门用来调控整个控制台的显示。因此我们可以在controller.h文件里面声明一个clearNode函数,其声明和定义如下(分别位于controller.h和controller.cpp内):

clearNode函数声明:

static void clearNode(BaseNode* node);

clearNode函数定义:

void Controller::clearNode(BaseNode* node)
{
	moveXY(node->x, node->y);
	cout << " ";
}

写好了之后,我们在snake类里的controlSnake方法进行一下修改。

修改后的controlSnake方法:

void Snake::controlSnake() 
{
	int stateU = (GetAsyncKeyState(keyUp) & 0x8000);
	int stateD = (GetAsyncKeyState(keyDown) & 0x8000);
	int stateL = (GetAsyncKeyState(keyLeft) & 0x8000);
	int stateR = (GetAsyncKeyState(keyRight) & 0x8000);
	//当按下指定按键的时候,我们就把char c设置成如下变量。
	if (stateU != 0)
	{
		c = 'u';
	}
	if (stateD != 0)
	{
		c = 'd';
	}
	if (stateL != 0)
	{
		c = 'l';
	}
	if (stateR != 0)
	{
		c = 'r';
	}

	if (c == 'u' || c == 'd' || c == 'l' || c == 'r') //如果捕获了按键
	{
		for (int i = m_snake.size() - 1; i > 0; i--)//蛇除去头部的结点依次向前覆盖,以达到走动的效果
		{
			m_snake.at(i)->x = m_snake.at(i - 1)->x;
			m_snake.at(i)->y = m_snake.at(i - 1)->y;
		}
		for (auto node : m_snake)  //蛇的尾巴用空格覆盖掉
		{
			Controller::clearNode(node);
		}
		switch (c) //对蛇的头部进行操作
		{
		case 'u'://捕获到向上键的话,蛇头y值减去1,以达到向上走的目的
			m_snake.front()->y--;
			break;
		case 'd':
			m_snake.front()->y++;
			break;
		case 'l':
			m_snake.front()->x--;
			break;
		case 'r':
			m_snake.front()->x++;
			break;
		}
	}
}

在运行一次,发现,蛇可以自由的移动了,而且没有自己越走越长。

下面,将目前为止整个贪吃蛇的完整代码张贴出来。至于碰撞、吃食物等功能以后再说。

1.baseNode.h

/*
	baseNode.h
*/

#pragma once

enum nodeType  //定义了结点的类型
{
	TYPE_SNAKE,   //蛇类
	TYPE_MAP,	  //地图类
	TYPE_FOOD     //食物类
};

class BaseNode
{
public:
	BaseNode();
	BaseNode(int x, int y, nodeType type);
	~BaseNode();


	int x;
	int y;
	nodeType type;
};
baseNode.cpp

//baseNode.cpp
#include "baseNode.h"

BaseNode::BaseNode()
{
}

BaseNode::BaseNode(int x, int y, nodeType type)
{
	this->x = x;
	this->y = y;
	this->type = type;
}

BaseNode::~BaseNode()
{
}

2.Controller.h
/*
	Controller.h
	控制类:从某些方面讲,这个类是对整个游戏的“调配”
	“掌管”控制台上蛇、地图等物体的显示
*/

#pragma once
#include<vector>
#include"baseNode.h"
using namespace std;
class Controller
{
public:
	static void moveXY(int x, int y);  //移动光标的位置
	static void showBaseNode(vector<BaseNode*> v); //把地图,蛇,食物什么的在控制台上显示出来
	static void clearNode(BaseNode* node);   //擦除结点
};

Controller.cpp

//Controller.cpp

#include "Controller.h"
#include"baseNode.h"
#include<iostream>
#include<Windows.h>
#include<conio.h>
using namespace std;

/*
	moveXY这个函数通过SetConsoleCursorPosition这个函数可以定位到(x,y)在控制台上的位置
	这样,我在showBaseNode函数里面调用这个函数,就可以在控制台上相应的位置上输出我想要的样式。
*/

void Controller::moveXY(int x, int y)
{
	COORD coord;
	coord.X = x;
	coord.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord);
}

void Controller::showBaseNode(vector<BaseNode*> v)
{
	for (int i = 0; i < v.size(); i++)
	{
		BaseNode* node = v.at(i);
		Controller::moveXY(node->x, node->y);  //moveXY方法的作用在这里体现了出来,若去掉这个语句的话
		                                       //在控制台上显示的图像是不管是啥都是按控制上的行依次输出的。
		if (node->type == TYPE_SNAKE)
		{
			cout << "o";
		}
		else if (node->type == TYPE_MAP)
		{
			cout << "*";
		}
		else if (node->type == TYPE_FOOD)
		{
			cout << "#";
		}
	}
}

void Controller::clearNode(BaseNode* node)
{
	moveXY(node->x, node->y);
	cout << " ";
}

3.Snake.h

//Snake.h

#pragma once
#include<vector>
#include"Controller.h"
#include"baseNode.h"
using namespace std;

class Snake
{
public:
	Snake(int x, int y);  //构造函数是用来初始化蛇的位置的
	~Snake();

	
	void showSnake();
	void setKey(int up,int down,int left,int right);
	void controlSnake();
private:
	vector<BaseNode*> m_snake;

	char c;
	int keyUp;
	int keyDown;
	int keyLeft;
	int keyRight;
};

Snake.cpp

//Snake.cpp
#include "Snake.h"
#include"Controller.h"
#include<Windows.h>

Snake::Snake(int x, int y)
{
	m_snake.push_back(new BaseNode(x, y, TYPE_SNAKE));//创建一个蛇,就是将相应的结点压入vector,地图和食物同理。
}

Snake::~Snake()
{
	while (m_snake.size())
	{
		delete m_snake.back();
		m_snake.pop_back();
	}
}

void Snake::showSnake()
{
	Controller::showBaseNode(m_snake);
}

void Snake::setKey(int up, int down, int left, int right)
{
	keyUp = up;
	keyDown = down;
	keyLeft = left;
	keyRight = right;
}

void Snake::controlSnake() 
{
	int stateU = (GetAsyncKeyState(keyUp) & 0x8000);
	int stateD = (GetAsyncKeyState(keyDown) & 0x8000);
	int stateL = (GetAsyncKeyState(keyLeft) & 0x8000);
	int stateR = (GetAsyncKeyState(keyRight) & 0x8000);
	//当按下指定按键的时候,我们就把char c设置成如下变量。
	if (stateU != 0)
	{
		c = 'u';
	}
	if (stateD != 0)
	{
		c = 'd';
	}
	if (stateL != 0)
	{
		c = 'l';
	}
	if (stateR != 0)
	{
		c = 'r';
	}

	if (c == 'u' || c == 'd' || c == 'l' || c == 'r') //如果捕获了按键
	{
		for (int i = m_snake.size() - 1; i > 0; i--)//蛇除去头部的结点依次向前覆盖,以达到走动的效果
		{
			m_snake.at(i)->x = m_snake.at(i - 1)->x;
			m_snake.at(i)->y = m_snake.at(i - 1)->y;
		}
		for (auto node : m_snake)  //蛇的尾巴用空格覆盖掉
		{
			Controller::clearNode(node);
		}
		switch (c) //对蛇的头部进行操作
		{
		case 'u'://捕获到向上键的话,蛇头y值减去1,以达到向上走的目的
			m_snake.front()->y--;
			break;
		case 'd':
			m_snake.front()->y++;
			break;
		case 'l':
			m_snake.front()->x--;
			break;
		case 'r':
			m_snake.front()->x++;
			break;
		}
	}
}

4.Food.h

//Food.h
#pragma once
#include"Controller.h"
#include"baseNode.h"
class Food
{
public:
	Food(int x,int y);
	~Food();

	void showFood();
private:
	vector<BaseNode*> m_food;
};

Food.cpp

//Food.cpp
#include "Food.h"

Food::Food(int x,int y)
{
	m_food.push_back(new BaseNode(x, y, TYPE_FOOD));
}

Food::~Food()
{
	while (m_food.size())
	{
		delete m_food.back();
		m_food.pop_back();
	}
}

void Food::showFood()
{
	Controller::showBaseNode(m_food);
}


5.Map.h

//Map.h
#pragma once
#include"baseNode.h"
#include"Controller.h"
using namespace std;
class Map
{
public:
	Map();
	~Map();

	void showMap();

private:  
	vector<BaseNode*> m_map;

};

Map.cpp

//Map.cpp
#include "Map.h"

Map::Map()  //最早初始化地图的时候,显然是周围的四面墙
{
	for (int i = 0; i < 60; i++)
	{
		m_map.push_back(new BaseNode(i, 0, TYPE_MAP));
		m_map.push_back(new BaseNode(i, 19, TYPE_MAP));
	}

	for (int i = 0; i < 20; i++)
	{
		m_map.push_back(new BaseNode(0, i, TYPE_MAP));
		m_map.push_back(new BaseNode(59, i, TYPE_MAP));
	}
}

Map::~Map()
{
	while (m_map.size())
	{
		delete m_map.back();
		m_map.pop_back();
	}
}

void Map::showMap()
{
	Controller::showBaseNode(m_map);
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值