6.贪吃蛇实践
6.1 知识点
原型图
开始场景
游戏场景
结束场景
UML类图
总览类图
场景系统类图
游戏场景类图
游戏主入口、游戏类和游戏帧更新接口
将实现的类图
创建游戏帧更新接口
#pragma once
//场景更新相关的接口
class ISceneUpdate
{
public:
virtual void Update() = 0;
virtual ~ISceneUpdate() {}
};
创建场景枚举
#pragma once
enum class E_SceneType
{
//开始
Begin,
//游戏
Game,
//结束
End,
};
导入自定义控制台,包括一些辅助方法
#pragma once
#include <windows.h>
#include <random>
using namespace std;
//设置光标位置函数
void setCursorPosition(int x, int y);
//设置控制台大小函数
void setConsoleSize(int width, int height);
//设置文本颜色函数
void setTextColor(WORD color);
//设置光标显示隐藏
void setCursorVisibility(bool visible);
//关闭控制台
void closeConsole();
//获取随机数
int getRandom(int min, int max);
#include "CustomConsole.h"
//设置光标位置
void setCursorPosition(int x, int y) {
// 获取当前的标准输出句柄
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
// 设置光标位置的坐标
COORD cursorPosition;
cursorPosition.X = x; // 横坐标(列)
cursorPosition.Y = y; // 纵坐标(行)
// 调用 Windows API 函数设置光标位置
SetConsoleCursorPosition(hConsole, cursorPosition);
}
//设置控制台大小
void setConsoleSize(int width, int height) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
// 设置控制台屏幕缓冲区大小
COORD bufferSize;
bufferSize.X = width + 2;
bufferSize.Y = height + 1;
SetConsoleScreenBufferSize(hConsole, bufferSize);
// 设置控制台窗口大小
SMALL_RECT windowSize;
windowSize.Left = 0;
windowSize.Top = 0;
windowSize.Right = width;
windowSize.Bottom = height;
SetConsoleWindowInfo(hConsole, TRUE, &windowSize);
}
//设置文本颜色
void setTextColor(WORD color) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, color);
}
//设置光标显隐
void setCursorVisibility(bool visible) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
// 获取当前光标信息
GetConsoleCursorInfo(hConsole, &cursorInfo);
cursorInfo.bVisible = visible; // 设置光标是否可见
SetConsoleCursorInfo(hConsole, &cursorInfo);
}
//关闭控制台
void closeConsole() {
HWND hConsole = GetConsoleWindow(); // 获取控制台窗口句柄
if (hConsole != NULL) {
PostMessage(hConsole, WM_CLOSE, 0, 0); // 发送关闭消息
}
}
int getRandom(int min, int max)
{
// 创建随机数生成器
random_device rd; // 获得随机数种子
mt19937 gen(rd()); // 使用 Mersenne Twister 生成器
uniform_int_distribution<> dis(min, max); // 定义均匀分布 [0, 99]
// 生成一个随机数
int randomNumber = dis(gen);
return randomNumber;
}
游戏类包括初始化,开始游戏和切换场景方法,开始游戏时调用当前场景的主循环
#pragma once
#include "ISceneUpdate.h"
#include "E_SceneType.h"
class Game
{
public:
static const int w = 80;
static const int h = 20;
//管理当前场景的 接口 之后 用它来存储 游戏、开始、结束场景对象
//之所以把它改成静态 是因为静态方法中要使用它
static ISceneUpdate* nowScene;
Game();
~Game();
//开始游戏主循环
void Start();
//切换到哪个场景
static void ChangeScene(E_SceneType type);
};
#include "Game.h"
#include "CustomConsole.h"
#include "GameScene.h"
#include "BeginScene.h"
#include "EndScene.h"
ISceneUpdate* Game::nowScene = nullptr;
Game::Game()
{
//初始化控制台相关
//隐藏光标
setCursorVisibility(false);
//设置窗口大小
setConsoleSize(w, h);
//一开始游戏 初始化时 就应该前往 开始场景
ChangeScene(E_SceneType::Begin);
}
Game::~Game()
{
if (nowScene != nullptr)
{
delete nowScene;
nowScene = nullptr;
}
}
void Game::Start()
{
while (true)
{
//游戏主循环其实就是去更新游戏当前场景
//只要场景不为空 我们就更新他们里面的逻辑
if (nowScene != nullptr)
nowScene->Update();
}
}
void Game::ChangeScene(E_SceneType type)
{
//切场景之前 应该擦除当前控制台行所有内容
system("cls");
//释放之前的场景
if (nowScene != nullptr)
{
delete nowScene;
nowScene = nullptr;
}
switch (type)
{
case E_SceneType::Begin:
nowScene = new BeginScene();
break;
case E_SceneType::Game:
nowScene = new GameScene();
break;
case E_SceneType::End:
nowScene = new EndScene();
break;
default:
break;
}
}
在主入口启动游戏
#include <iostream>
#include "Game.h"
int main()
{
Game* game = new Game();
game->Start();
}
多场景切换以及具体实现
将实现的类图
开始场景和结束场景有共同之处,可以抽出来一个类
#include "BeginOrEndBaseScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include "E_Color.h"
#include "conio.h"
void BeginOrEndBaseScene::Update()
{
//开始和结束场景的 游戏逻辑
//1.显示标题
setTextColor(White);
setCursorPosition(Game::w / 2 - strTitle.size() / 2, 5);
cout << strTitle;
//2.显示下方的选项
setCursorPosition(Game::w / 2 - strOne.size() / 2, 8);
setTextColor(nowSelIndex == 0 ? Red : White);//根据当前是否选中设置颜色
cout << strOne;
setCursorPosition(Game::w / 2 - 4, 10);
setTextColor(nowSelIndex == 1 ? Red : White);//根据当前是否选中设置颜色
cout << "结束游戏";
//3.检测输入
int input = _getch();
switch (input)
{
case 'W':
case 'w':
--nowSelIndex;
if (nowSelIndex < 0)
nowSelIndex = 0;
break;
case 'S':
case 's':
++nowSelIndex;
if (nowSelIndex > 1)
nowSelIndex = 1;
break;
case 'J':
case 'j':
EnterJDoSomthing();
break;
}
}
#pragma once
#include "ISceneUpdate.h"
#include <iostream>
using namespace std;
class BeginOrEndBaseScene :
public ISceneUpdate
{
public:
//当前选择的选项
int nowSelIndex = 0;
//标题
string strTitle;
//第一个选项
string strOne;
void Update() override;
//按J键处理的逻辑 交给子类去重写即可
virtual void EnterJDoSomthing() = 0;
};
开始场景类
#pragma once
#include "BeginOrEndBaseScene.h"
class BeginScene : public BeginOrEndBaseScene
{
public:
BeginScene();
void EnterJDoSomthing() override;
};
#include "BeginScene.h"
#include "CustomConsole.h"
#include "Game.h"
BeginScene::BeginScene()
{
strTitle = "贪食蛇";
strOne = "开始游戏";
}
void BeginScene::EnterJDoSomthing()
{
//切换到游戏场景
if (nowSelIndex == 0)
Game::ChangeScene(E_SceneType::Game);
else//如果没有选择第一个选项 那一定是退出游戏的操作
closeConsole();
}
结束场景类
#pragma once
#include "BeginOrEndBaseScene.h"
class EndScene :
public BeginOrEndBaseScene
{
public:
EndScene();
void EnterJDoSomthing() override;
};
#include "EndScene.h"
#include "Game.h"
#include "CustomConsole.h"
EndScene::EndScene()
{
strTitle = "结束游戏";
strOne = "回到开始界面";
}
void EndScene::EnterJDoSomthing()
{
//切换到开始场景
if (nowSelIndex == 0)
Game::ChangeScene(E_SceneType::Begin);
else//如果没有选择第一个选项 那一定是退出游戏的操作
closeConsole();
}
游戏场景类
#pragma once
#include "ISceneUpdate.h"
#include "Map.h"
#include "Snake.h"
#include "Food.h"
class GameScene :
public ISceneUpdate
{
public:
Map* map;
Snake* snake;
Food* food;
int updateIndex = 0;
GameScene();
~GameScene();
void Update() override;
};
#include "GameScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include <iostream>
#include <conio.h>
using namespace std;
GameScene::GameScene()
{
map = new Map();
snake = new Snake(40, 10);
food = new Food(snake);
}
GameScene::~GameScene()
{
if (map != nullptr)
{
delete map;
map = nullptr;
}
if (snake != nullptr)
{
delete snake;
snake = nullptr;
}
if (food != nullptr)
{
delete food;
food = nullptr;
}
}
void GameScene::Update()
{
if (updateIndex % 9999 == 0)
{
/*setCursorPosition(0, 0);
cout << "游戏场景";*/
map->Draw();
food->Draw();
//先让蛇动起来
snake->Move();
//再去绘制
snake->Draw();
//移动过后 检测蛇是否撞墙或者身体
//来判断是否结束
if (snake->CheckEnd(map))
{
//结束相关逻辑
Game::ChangeScene(E_SceneType::End);
return;
}
//蛇移动结束后 判断是否和食物重合(是否吃到了食物)
snake->CheckEatFood(food);
updateIndex = 1;
}
++updateIndex;
//我们的这个输入检测 希望马上就执行 不应该延时执行
if (_kbhit())
{
int input = _getch();
switch (input)
{
case 'W':
case 'w':
snake->ChangeDir(E_MoveDir::Up);
break;
case 'S':
case 's':
snake->ChangeDir(E_MoveDir::Down);
break;
case 'A':
case 'a':
snake->ChangeDir(E_MoveDir::Left);
break;
case 'D':
case 'd':
snake->ChangeDir(E_MoveDir::Right);
break;
}
}
}
绘制接口,游戏对象类,位置结构体
将实现的类图
绘制接口
#pragma once
//绘制接口 提供绘制的行为抽象
class IDraw
{
public:
virtual void Draw() = 0;
virtual ~IDraw() {}
};
游戏对象类,实现绘制接口
#pragma once
#include "IDraw.h"
#include "Position.h"
#include <iostream>
using namespace std;
class GameObject :
public IDraw
{
public:
Position pos = Position(0,0);
};
位置结构体,代表位置,重载运算符
#pragma once
struct Position
{
public:
int x;
int y;
Position(int x, int y) :x(x), y(y)
{
}
//贪食蛇项目中 肯定存在位置的比较 判断是否是重合
bool operator ==(const Position& p) const;
bool operator !=(const Position& p) const;
};
#include "Position.h"
bool Position::operator==(const Position& p) const
{
if (this->x == p.x && this->y == p.y)
return true;
return false;
}
bool Position::operator!=(const Position& p) const
{
if (this->x == p.x && this->y == p.y)
return false;
return true;
}
食物类、墙壁类和地图墙壁类
将实现的类图
食物类,默认不会在墙上产生,如果随机到蛇重合递归重新随机
#pragma once
#include "GameObject.h"
#include "Snake.h"
class Food :
public GameObject
{
public:
Food(Snake* snake);
void Draw() override;
//随机位置 之后有蛇对象了 才好去写
//因为随机位置时 需要得到蛇的头、身体位置 用来判断不能重合
void RandomPos(Snake* snake);
};
#include "Food.h"
#include "CustomConsole.h"
#include "E_Color.h"
#include "Game.h"
Food::Food(Snake* snake)
{
RandomPos(snake);
}
void Food::Draw()
{
setCursorPosition(pos.x, pos.y);
setTextColor(Blue);
cout << "¤";
}
void Food::RandomPos(Snake* snake)
{
//x方向的随机数 排除了 墙体的随机数
int x = getRandom(1, Game::w / 2 - 2) * 2;
//y方向的随机数 排除了 墙体的随机数
int y = getRandom(1, Game::h - 2);
//设置食物位置
pos.x = x;
pos.y = y;
//传入食物的位置 和蛇判断是否重合 如果重合 那么应该重新随机位置
//我们通过递归函数 来得到一个一定不会和蛇重合的位置
if (snake->CheckSamePos(pos))
RandomPos(snake);
}
墙壁类
#pragma once
#include "GameObject.h"
class Wall:public GameObject
{
public:
Wall();
Wall(int x, int y);
void Draw() override;
};
#include "Wall.h"
#include "CustomConsole.h"
#include "E_Color.h"
Wall::Wall()
{
this->pos.x = 0;
this->pos.y = 0;
}
Wall::Wall(int x, int y)
{
this->pos.x = x;
this->pos.y = y;
}
void Wall::Draw()
{
setCursorPosition(pos.x, pos.y);
setTextColor(Red);
cout << "■";
}
地图墙壁类,拥有墙壁集合
#pragma once
#include "IDraw.h"
#include "Wall.h"
class Map :
public IDraw
{
public:
Wall* walls;//数组指针
int size;//存储数组容量的成员
Map();
~Map();
void Draw() override;
};
#include "Map.h"
#include "Game.h"
Map::Map()
{
//初始化墙壁数组
size = Game::w + (Game::h - 2) * 2;
//初始化输入 容量是算好的
walls = new Wall[size];
int index = 0;
for (int i = 0; i < Game::w; i+=2)
{
walls[index].pos.x = i;
walls[index].pos.y = 0;
++index;
}
for (int i = 0; i < Game::w; i += 2)
{
walls[index].pos.x = i;
walls[index].pos.y = Game::h - 1;
++index;
}
for (int i = 1; i < Game::h - 1; i++)
{
walls[index].pos.x = 0;
walls[index].pos.y = i;
++index;
}
for (int i = 1; i < Game::h - 1; i++)
{
walls[index].pos.x = Game::w - 2;
walls[index].pos.y = i;
++index;
}
}
Map::~Map()
{
//释放所有墙壁
if (walls != nullptr)
{
delete[] walls;
walls = nullptr;
}
}
void Map::Draw()
{
//绘制所有墙壁
for (int i = 0; i < size; i++)
{
walls[i].Draw();
}
}
蛇相关
蛇身体类型枚举
#pragma once
enum class E_SnakeBody_Type
{
//头类型
Head,
//身体类型
Body,
};
蛇身类
#pragma once
#include "GameObject.h"
#include "E_SnakeBody_Type.h"
class SnakeBody :
public GameObject
{
public:
E_SnakeBody_Type type;
SnakeBody(E_SnakeBody_Type type, int x, int y);
void Draw() override;
};
#include "SnakeBody.h"
#include "CustomConsole.h"
#include "E_Color.h"
SnakeBody::SnakeBody(E_SnakeBody_Type type, int x, int y)
{
this->type = type;
this->pos.x = x;
this->pos.y = y;
}
void SnakeBody::Draw()
{
setCursorPosition(pos.x, pos.y);
setTextColor(type == E_SnakeBody_Type::Head ? Green : White);
cout << (type == E_SnakeBody_Type::Head ? "●" : "◎");
}
蛇类
#pragma once
#include "IDraw.h"
#include "SnakeBody.h"
#include "Map.h"
class Food;
//蛇移动方向枚举
enum class E_MoveDir
{
Up,//上
Down,//下
Left,//左
Right,//右
};
class Snake :
public IDraw
{
public:
//由于蛇是动态成长的,不应该用new的方式去声明数组
//因为new的方式 一开始相当于就创建了对应个数个 身体对象
//我们应该吃了食物再去动态创建对象
SnakeBody* bodys[200] = {};
int nowNum = 0;
E_MoveDir nowMoveDir = E_MoveDir::Right;
Snake(int x, int y);
~Snake();
void Draw() override;
void Move();
void ChangeDir(E_MoveDir dir);
bool CheckEnd(Map* map);
//判断外面传入的一个位置 是否和自己的头和身体重合
bool CheckSamePos(Position& pos);
void CheckEatFood(Food* food);
private:
void AddBody();
};
#include "Snake.h"
#include "CustomConsole.h"
#include "Food.h"
Snake::Snake(int x, int y)
{
//首先应该有对应的蛇头
bodys[0] = new SnakeBody(E_SnakeBody_Type::Head, x, y);
//通过一个索引去记录蛇有多长 不能通过数组的长度
//因为数组默认一来就很长
nowNum = 1;
}
Snake::~Snake()
{
for (int i = 0; i < nowNum; i++)
{
delete bodys[i];
bodys[i] = nullptr;
}
}
void Snake::Draw()
{
for (int i = 0; i < nowNum; i++)
{
bodys[i]->Draw();
}
}
void Snake::Move()
{
//我们可以在改变蛇位置之前 利用蛇之前的位置
//去擦除之前绘制的内容(用空格将之前绘制的图像覆盖了)
//为了考虑之后蛇有身体 所以我们应该获取身体的最后一截 去擦除它
//最后一截身体 就是数组中存储的最后一个身体
SnakeBody* lastBody = bodys[nowNum - 1];
setCursorPosition(lastBody->pos.x, lastBody->pos.y);
cout << " ";
for (int i = nowNum - 1; i > 0; i--)
{
bodys[i]->pos.x = bodys[i - 1]->pos.x;
bodys[i]->pos.y = bodys[i - 1]->pos.y;
}
switch (nowMoveDir)
{
case E_MoveDir::Up:
--bodys[0]->pos.y;
break;
case E_MoveDir::Down:
++bodys[0]->pos.y;
break;
case E_MoveDir::Left:
bodys[0]->pos.x -= 2;
break;
case E_MoveDir::Right:
bodys[0]->pos.x += 2;
break;
default:
break;
}
}
//改变当前前进方向的方法
void Snake::ChangeDir(E_MoveDir dir)
{
//如果蛇有身体 就不能够直接 左转右 右转左 上转下 下转上
//如果转向和上次一样 也不用再转
if (dir == nowMoveDir ||
nowNum > 1 &&
(nowMoveDir == E_MoveDir::Right && dir == E_MoveDir::Left ||
nowMoveDir == E_MoveDir::Left && dir == E_MoveDir::Right ||
nowMoveDir == E_MoveDir::Up && dir == E_MoveDir::Down ||
nowMoveDir == E_MoveDir::Down && dir == E_MoveDir::Up))
{
//如果是特殊情况 就直接return 不要执行转向逻辑
return;
}
//改变当前的移动方向为传入的方向
this->nowMoveDir = dir;
}
bool Snake::CheckEnd(Map* map)
{
//撞墙
for (int i = 0; i < map->size; i++)
{
if (bodys[0]->pos == map->walls[i].pos)
return true;
}
//撞身体
for (int i = 1; i < nowNum; i++)
{
if (bodys[0]->pos == bodys[i]->pos)
return true;
}
return false;
}
bool Snake::CheckSamePos(Position& pos)
{
//判断蛇身体 是否有和传入位置重合的 如果有 就直接返回true
//认为有重合
for (int i = 0; i < nowNum; i++)
{
if (pos == bodys[i]->pos)
return true;
}
return false;
}
void Snake::CheckEatFood(Food* food)
{
if (bodys[0]->pos == food->pos)
{
//吃到食物的逻辑处理
//1.随机食物位置
food->RandomPos(this);
//2.长身体
AddBody();
}
}
void Snake::AddBody()
{
//当前最后一截身体
SnakeBody* frontBody = bodys[nowNum - 1];
//新键一个身体 让其位置和最后一个身体位置一致
bodys[nowNum] = new SnakeBody(E_SnakeBody_Type::Body, frontBody->pos.x, frontBody->pos.y);
//蛇身体涨了 计数就应该增加
++nowNum;
}
6.2 知识点代码
贪吃蛇实践.cpp
#include <iostream>
#include "Game.h"
int main()
{
Game* game = new Game();
game->Start();
}
ISceneUpdate.h
#pragma once
//场景更新相关的接口
class ISceneUpdate
{
public:
virtual void Update() = 0;
virtual ~ISceneUpdate() {}
};
E_SceneType.h
#pragma once
enum class E_SceneType
{
//开始
Begin,
//游戏
Game,
//结束
End,
};
CustomConsole.h和CustomConsole.cpp
#pragma once
#include <windows.h>
#include <random>
using namespace std;
//设置光标位置函数
void setCursorPosition(int x, int y);
//设置控制台大小函数
void setConsoleSize(int width, int height);
//设置文本颜色函数
void setTextColor(WORD color);
//设置光标显示隐藏
void setCursorVisibility(bool visible);
//关闭控制台
void closeConsole();
//获取随机数
int getRandom(int min, int max);
#include "CustomConsole.h"
//设置光标位置
void setCursorPosition(int x, int y) {
// 获取当前的标准输出句柄
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
// 设置光标位置的坐标
COORD cursorPosition;
cursorPosition.X = x; // 横坐标(列)
cursorPosition.Y = y; // 纵坐标(行)
// 调用 Windows API 函数设置光标位置
SetConsoleCursorPosition(hConsole, cursorPosition);
}
//设置控制台大小
void setConsoleSize(int width, int height) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
// 设置控制台屏幕缓冲区大小
COORD bufferSize;
bufferSize.X = width + 2;
bufferSize.Y = height + 1;
SetConsoleScreenBufferSize(hConsole, bufferSize);
// 设置控制台窗口大小
SMALL_RECT windowSize;
windowSize.Left = 0;
windowSize.Top = 0;
windowSize.Right = width;
windowSize.Bottom = height;
SetConsoleWindowInfo(hConsole, TRUE, &windowSize);
}
//设置文本颜色
void setTextColor(WORD color) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hConsole, color);
}
//设置光标显隐
void setCursorVisibility(bool visible) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorInfo;
// 获取当前光标信息
GetConsoleCursorInfo(hConsole, &cursorInfo);
cursorInfo.bVisible = visible; // 设置光标是否可见
SetConsoleCursorInfo(hConsole, &cursorInfo);
}
//关闭控制台
void closeConsole() {
HWND hConsole = GetConsoleWindow(); // 获取控制台窗口句柄
if (hConsole != NULL) {
PostMessage(hConsole, WM_CLOSE, 0, 0); // 发送关闭消息
}
}
int getRandom(int min, int max)
{
// 创建随机数生成器
random_device rd; // 获得随机数种子
mt19937 gen(rd()); // 使用 Mersenne Twister 生成器
uniform_int_distribution<> dis(min, max); // 定义均匀分布 [0, 99]
// 生成一个随机数
int randomNumber = dis(gen);
return randomNumber;
}
Game.h和Game.cpp
#pragma once
#include "ISceneUpdate.h"
#include "E_SceneType.h"
class Game
{
public:
static const int w = 80;
static const int h = 20;
//管理当前场景的 接口 之后 用它来存储 游戏、开始、结束场景对象
//之所以把它改成静态 是因为静态方法中要使用它
static ISceneUpdate* nowScene;
Game();
~Game();
//开始游戏主循环
void Start();
//切换到哪个场景
static void ChangeScene(E_SceneType type);
};
#include "Game.h"
#include "CustomConsole.h"
#include "GameScene.h"
#include "BeginScene.h"
#include "EndScene.h"
ISceneUpdate* Game::nowScene = nullptr;
Game::Game()
{
//初始化控制台相关
//隐藏光标
setCursorVisibility(false);
//设置窗口大小
setConsoleSize(w, h);
//一开始游戏 初始化时 就应该前往 开始场景
ChangeScene(E_SceneType::Begin);
}
Game::~Game()
{
if (nowScene != nullptr)
{
delete nowScene;
nowScene = nullptr;
}
}
void Game::Start()
{
while (true)
{
//游戏主循环其实就是去更新游戏当前场景
//只要场景不为空 我们就更新他们里面的逻辑
if (nowScene != nullptr)
nowScene->Update();
}
}
void Game::ChangeScene(E_SceneType type)
{
//切场景之前 应该擦除当前控制台行所有内容
system("cls");
//释放之前的场景
if (nowScene != nullptr)
{
delete nowScene;
nowScene = nullptr;
}
switch (type)
{
case E_SceneType::Begin:
nowScene = new BeginScene();
break;
case E_SceneType::Game:
nowScene = new GameScene();
break;
case E_SceneType::End:
nowScene = new EndScene();
break;
default:
break;
}
}
BeginOrEndBaseScene.h和BeginOrEndBaseScene.cpp
#include "BeginOrEndBaseScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include "E_Color.h"
#include "conio.h"
void BeginOrEndBaseScene::Update()
{
//开始和结束场景的 游戏逻辑
//1.显示标题
setTextColor(White);
setCursorPosition(Game::w / 2 - strTitle.size() / 2, 5);
cout << strTitle;
//2.显示下方的选项
setCursorPosition(Game::w / 2 - strOne.size() / 2, 8);
setTextColor(nowSelIndex == 0 ? Red : White);//根据当前是否选中设置颜色
cout << strOne;
setCursorPosition(Game::w / 2 - 4, 10);
setTextColor(nowSelIndex == 1 ? Red : White);//根据当前是否选中设置颜色
cout << "结束游戏";
//3.检测输入
int input = _getch();
switch (input)
{
case 'W':
case 'w':
--nowSelIndex;
if (nowSelIndex < 0)
nowSelIndex = 0;
break;
case 'S':
case 's':
++nowSelIndex;
if (nowSelIndex > 1)
nowSelIndex = 1;
break;
case 'J':
case 'j':
EnterJDoSomthing();
break;
}
}
#pragma once
#include "ISceneUpdate.h"
#include <iostream>
using namespace std;
class BeginOrEndBaseScene :
public ISceneUpdate
{
public:
//当前选择的选项
int nowSelIndex = 0;
//标题
string strTitle;
//第一个选项
string strOne;
void Update() override;
//按J键处理的逻辑 交给子类去重写即可
virtual void EnterJDoSomthing() = 0;
};
BeginScene.h和BeginScene.cpp
#pragma once
#include "BeginOrEndBaseScene.h"
class BeginScene : public BeginOrEndBaseScene
{
public:
BeginScene();
void EnterJDoSomthing() override;
};
#include "BeginScene.h"
#include "CustomConsole.h"
#include "Game.h"
BeginScene::BeginScene()
{
strTitle = "贪食蛇";
strOne = "开始游戏";
}
void BeginScene::EnterJDoSomthing()
{
//切换到游戏场景
if (nowSelIndex == 0)
Game::ChangeScene(E_SceneType::Game);
else//如果没有选择第一个选项 那一定是退出游戏的操作
closeConsole();
}
EndScene.h和EndScene.cpp
#pragma once
#include "BeginOrEndBaseScene.h"
class EndScene :
public BeginOrEndBaseScene
{
public:
EndScene();
void EnterJDoSomthing() override;
};
#include "EndScene.h"
#include "Game.h"
#include "CustomConsole.h"
EndScene::EndScene()
{
strTitle = "结束游戏";
strOne = "回到开始界面";
}
void EndScene::EnterJDoSomthing()
{
//切换到开始场景
if (nowSelIndex == 0)
Game::ChangeScene(E_SceneType::Begin);
else//如果没有选择第一个选项 那一定是退出游戏的操作
closeConsole();
}
GameScene.h和GameScene.cpp
#pragma once
#include "ISceneUpdate.h"
#include "Map.h"
#include "Snake.h"
#include "Food.h"
class GameScene :
public ISceneUpdate
{
public:
Map* map;
Snake* snake;
Food* food;
int updateIndex = 0;
GameScene();
~GameScene();
void Update() override;
};
#include "GameScene.h"
#include "CustomConsole.h"
#include "Game.h"
#include <iostream>
#include <conio.h>
using namespace std;
GameScene::GameScene()
{
map = new Map();
snake = new Snake(40, 10);
food = new Food(snake);
}
GameScene::~GameScene()
{
if (map != nullptr)
{
delete map;
map = nullptr;
}
if (snake != nullptr)
{
delete snake;
snake = nullptr;
}
if (food != nullptr)
{
delete food;
food = nullptr;
}
}
void GameScene::Update()
{
if (updateIndex % 9999 == 0)
{
/*setCursorPosition(0, 0);
cout << "游戏场景";*/
map->Draw();
food->Draw();
//先让蛇动起来
snake->Move();
//再去绘制
snake->Draw();
//移动过后 检测蛇是否撞墙或者身体
//来判断是否结束
if (snake->CheckEnd(map))
{
//结束相关逻辑
Game::ChangeScene(E_SceneType::End);
return;
}
//蛇移动结束后 判断是否和食物重合(是否吃到了食物)
snake->CheckEatFood(food);
updateIndex = 1;
}
++updateIndex;
//我们的这个输入检测 希望马上就执行 不应该延时执行
if (_kbhit())
{
int input = _getch();
switch (input)
{
case 'W':
case 'w':
snake->ChangeDir(E_MoveDir::Up);
break;
case 'S':
case 's':
snake->ChangeDir(E_MoveDir::Down);
break;
case 'A':
case 'a':
snake->ChangeDir(E_MoveDir::Left);
break;
case 'D':
case 'd':
snake->ChangeDir(E_MoveDir::Right);
break;
}
}
}
IDraw.h
#pragma once
//绘制接口 提供绘制的行为抽象
class IDraw
{
public:
virtual void Draw() = 0;
virtual ~IDraw() {}
};
GameObject.h
#pragma once
#include "IDraw.h"
#include "Position.h"
#include <iostream>
using namespace std;
class GameObject :
public IDraw
{
public:
Position pos = Position(0,0);
};
Position.h和Position.cpp
#pragma once
struct Position
{
public:
int x;
int y;
Position(int x, int y) :x(x), y(y)
{
}
//贪食蛇项目中 肯定存在位置的比较 判断是否是重合
bool operator ==(const Position& p) const;
bool operator !=(const Position& p) const;
};
#include "Position.h"
bool Position::operator==(const Position& p) const
{
if (this->x == p.x && this->y == p.y)
return true;
return false;
}
bool Position::operator!=(const Position& p) const
{
if (this->x == p.x && this->y == p.y)
return false;
return true;
}
Food.h和Food.cpp
#pragma once
#include "GameObject.h"
#include "Snake.h"
class Food :
public GameObject
{
public:
Food(Snake* snake);
void Draw() override;
//随机位置 之后有蛇对象了 才好去写
//因为随机位置时 需要得到蛇的头、身体位置 用来判断不能重合
void RandomPos(Snake* snake);
};
#include "Food.h"
#include "CustomConsole.h"
#include "E_Color.h"
#include "Game.h"
Food::Food(Snake* snake)
{
RandomPos(snake);
}
void Food::Draw()
{
setCursorPosition(pos.x, pos.y);
setTextColor(Blue);
cout << "¤";
}
void Food::RandomPos(Snake* snake)
{
//x方向的随机数 排除了 墙体的随机数
int x = getRandom(1, Game::w / 2 - 2) * 2;
//y方向的随机数 排除了 墙体的随机数
int y = getRandom(1, Game::h - 2);
//设置食物位置
pos.x = x;
pos.y = y;
//传入食物的位置 和蛇判断是否重合 如果重合 那么应该重新随机位置
//我们通过递归函数 来得到一个一定不会和蛇重合的位置
if (snake->CheckSamePos(pos))
RandomPos(snake);
}
Wall.h和Wall.cpp
#pragma once
#include "GameObject.h"
class Wall:public GameObject
{
public:
Wall();
Wall(int x, int y);
void Draw() override;
};
#include "Wall.h"
#include "CustomConsole.h"
#include "E_Color.h"
Wall::Wall()
{
this->pos.x = 0;
this->pos.y = 0;
}
Wall::Wall(int x, int y)
{
this->pos.x = x;
this->pos.y = y;
}
void Wall::Draw()
{
setCursorPosition(pos.x, pos.y);
setTextColor(Red);
cout << "■";
}
Map.h和Map.cpp
#pragma once
#include "IDraw.h"
#include "Wall.h"
class Map :
public IDraw
{
public:
Wall* walls;//数组指针
int size;//存储数组容量的成员
Map();
~Map();
void Draw() override;
};
#include "Map.h"
#include "Game.h"
Map::Map()
{
//初始化墙壁数组
size = Game::w + (Game::h - 2) * 2;
//初始化输入 容量是算好的
walls = new Wall[size];
int index = 0;
for (int i = 0; i < Game::w; i+=2)
{
walls[index].pos.x = i;
walls[index].pos.y = 0;
++index;
}
for (int i = 0; i < Game::w; i += 2)
{
walls[index].pos.x = i;
walls[index].pos.y = Game::h - 1;
++index;
}
for (int i = 1; i < Game::h - 1; i++)
{
walls[index].pos.x = 0;
walls[index].pos.y = i;
++index;
}
for (int i = 1; i < Game::h - 1; i++)
{
walls[index].pos.x = Game::w - 2;
walls[index].pos.y = i;
++index;
}
}
Map::~Map()
{
//释放所有墙壁
if (walls != nullptr)
{
delete[] walls;
walls = nullptr;
}
}
void Map::Draw()
{
//绘制所有墙壁
for (int i = 0; i < size; i++)
{
walls[i].Draw();
}
}
E_SnakeBody_Type.h和E_SnakeBody_Type.cpp
#pragma once
enum class E_SnakeBody_Type
{
//头类型
Head,
//身体类型
Body,
};
SnakeBody.h和SnakeBody.cpp
#pragma once
#include "GameObject.h"
#include "E_SnakeBody_Type.h"
class SnakeBody :
public GameObject
{
public:
E_SnakeBody_Type type;
SnakeBody(E_SnakeBody_Type type, int x, int y);
void Draw() override;
};
#include "SnakeBody.h"
#include "CustomConsole.h"
#include "E_Color.h"
SnakeBody::SnakeBody(E_SnakeBody_Type type, int x, int y)
{
this->type = type;
this->pos.x = x;
this->pos.y = y;
}
void SnakeBody::Draw()
{
setCursorPosition(pos.x, pos.y);
setTextColor(type == E_SnakeBody_Type::Head ? Green : White);
cout << (type == E_SnakeBody_Type::Head ? "●" : "◎");
}
Snake.h和Snake.cpp
#pragma once
#include "IDraw.h"
#include "SnakeBody.h"
#include "Map.h"
class Food;
//蛇移动方向枚举
enum class E_MoveDir
{
Up,//上
Down,//下
Left,//左
Right,//右
};
class Snake :
public IDraw
{
public:
//由于蛇是动态成长的,不应该用new的方式去声明数组
//因为new的方式 一开始相当于就创建了对应个数个 身体对象
//我们应该吃了食物再去动态创建对象
SnakeBody* bodys[200] = {};
int nowNum = 0;
E_MoveDir nowMoveDir = E_MoveDir::Right;
Snake(int x, int y);
~Snake();
void Draw() override;
void Move();
void ChangeDir(E_MoveDir dir);
bool CheckEnd(Map* map);
//判断外面传入的一个位置 是否和自己的头和身体重合
bool CheckSamePos(Position& pos);
void CheckEatFood(Food* food);
private:
void AddBody();
};
#include "Snake.h"
#include "CustomConsole.h"
#include "Food.h"
Snake::Snake(int x, int y)
{
//首先应该有对应的蛇头
bodys[0] = new SnakeBody(E_SnakeBody_Type::Head, x, y);
//通过一个索引去记录蛇有多长 不能通过数组的长度
//因为数组默认一来就很长
nowNum = 1;
}
Snake::~Snake()
{
for (int i = 0; i < nowNum; i++)
{
delete bodys[i];
bodys[i] = nullptr;
}
}
void Snake::Draw()
{
for (int i = 0; i < nowNum; i++)
{
bodys[i]->Draw();
}
}
void Snake::Move()
{
//我们可以在改变蛇位置之前 利用蛇之前的位置
//去擦除之前绘制的内容(用空格将之前绘制的图像覆盖了)
//为了考虑之后蛇有身体 所以我们应该获取身体的最后一截 去擦除它
//最后一截身体 就是数组中存储的最后一个身体
SnakeBody* lastBody = bodys[nowNum - 1];
setCursorPosition(lastBody->pos.x, lastBody->pos.y);
cout << " ";
for (int i = nowNum - 1; i > 0; i--)
{
bodys[i]->pos.x = bodys[i - 1]->pos.x;
bodys[i]->pos.y = bodys[i - 1]->pos.y;
}
switch (nowMoveDir)
{
case E_MoveDir::Up:
--bodys[0]->pos.y;
break;
case E_MoveDir::Down:
++bodys[0]->pos.y;
break;
case E_MoveDir::Left:
bodys[0]->pos.x -= 2;
break;
case E_MoveDir::Right:
bodys[0]->pos.x += 2;
break;
default:
break;
}
}
//改变当前前进方向的方法
void Snake::ChangeDir(E_MoveDir dir)
{
//如果蛇有身体 就不能够直接 左转右 右转左 上转下 下转上
//如果转向和上次一样 也不用再转
if (dir == nowMoveDir ||
nowNum > 1 &&
(nowMoveDir == E_MoveDir::Right && dir == E_MoveDir::Left ||
nowMoveDir == E_MoveDir::Left && dir == E_MoveDir::Right ||
nowMoveDir == E_MoveDir::Up && dir == E_MoveDir::Down ||
nowMoveDir == E_MoveDir::Down && dir == E_MoveDir::Up))
{
//如果是特殊情况 就直接return 不要执行转向逻辑
return;
}
//改变当前的移动方向为传入的方向
this->nowMoveDir = dir;
}
bool Snake::CheckEnd(Map* map)
{
//撞墙
for (int i = 0; i < map->size; i++)
{
if (bodys[0]->pos == map->walls[i].pos)
return true;
}
//撞身体
for (int i = 1; i < nowNum; i++)
{
if (bodys[0]->pos == bodys[i]->pos)
return true;
}
return false;
}
bool Snake::CheckSamePos(Position& pos)
{
//判断蛇身体 是否有和传入位置重合的 如果有 就直接返回true
//认为有重合
for (int i = 0; i < nowNum; i++)
{
if (pos == bodys[i]->pos)
return true;
}
return false;
}
void Snake::CheckEatFood(Food* food)
{
if (bodys[0]->pos == food->pos)
{
//吃到食物的逻辑处理
//1.随机食物位置
food->RandomPos(this);
//2.长身体
AddBody();
}
}
void Snake::AddBody()
{
//当前最后一截身体
SnakeBody* frontBody = bodys[nowNum - 1];
//新键一个身体 让其位置和最后一个身体位置一致
bodys[nowNum] = new SnakeBody(E_SnakeBody_Type::Body, frontBody->pos.x, frontBody->pos.y);
//蛇身体涨了 计数就应该增加
++nowNum;
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com