面向对象C++大作业报告
作业简介:修改github上的一个snake游戏项目,添加一些特性和功能,需要满足下面的需求。
-
贪食蛇的控制
源代码只支持 4 个方向的运行,增加可以通过鼠标控制贪食蛇的运动。当按下鼠标键时,设 置 一 个 方向 向 量 , 该 方 向 向 量 为 鼠 标 所 在 位 置 (MousePosition) 与 蛇 头 所 在 位 置(SnakePosition)的差值。下一时刻,贪食蛇按照该向量的方向运动;运动的距离为 1 个标准单位。
-
水果的控制
源代码只支持 1 种水果,颜色随机且贪食蛇增加的长度固定。现增加黑色、棕色、红、蓝色、绿色、共 5 种水果,且贪食蛇吃了黑色、棕色水果不增加其长度,红色、蓝色、绿色水果增加的长度分别为 3、2、1;增加的长度在贪食蛇的尾部—假设初始是叠加在一起的。 系统随机生成上述 5 种水果,保持黑色和褐色水果所占比例为 25%,其他的占 75%。
-
绘制精灵版本的贪食蛇
源代码中的贪食蛇绘制过于简单—仅仅使用了矩形绘制。要求更改贪食蛇的绘制方法,头部使用图片,通过 sprite 进行绘制。
-
整体界面的修改
可以修改背景的颜色(提供白色、黑色、褐色三种);允许显示(或关闭显示)网格,网格的颜色可以设置(提供白色、黑色、褐色三种)。
-
5)理清代码
代码中,要仔细考虑水果、蛇(蛇头、其他节点)、网格等对象的生命周期,确保你设计的对象周期模型是经济可靠的。
原作的源码:https://github.com/jhpy1024/sfSnake
源代码:
编译环境:
MacOS 10.14.6 Xcode
具体方法:成功安装SFML之后,选择new project,在macOS类别里面选择SFML App。新建项目之后,删除掉默认文件,将本项目中的源代码加入项目文件目录中,将Fonts、Images、Music、Sounds文件夹放入到product的app所在目录中 。直接build即可运行。
按照功能介绍修改情况:
-
移动方式:
增加了鼠标控制蛇移动的功能:用鼠标右键单击游戏界面中的某个位置,蛇会朝着这个方向移动。如果转弯角度过大,可能导致死亡判定。
修改思路:原文件中作者对于移动设定了四个方向,我顺势添加了一个方向,代表蛇将按照一个由鼠标单击生成的方向向量前进。这个方向向量会储存在Snake类里。在每次检测到鼠标单击的时候,通过蛇头位置和鼠标位置计算并更新方向向量。而同时,没有撤销原作者方向键控制的代码,因此修改后,仍然可以使用方向键控制蛇的移动。
相关核心代码(只给出修改部分):
void Snake::handleInput(sf::RenderWindow& window)
{
if ...
...
else if(sf::Mouse::isButtonPressed(sf::Mouse::Right))//鼠标控制-方向处理
{
direction_ = Direction::Mouse_Vec;
sf::Vector2f head_pos = nodes_[0].getPosition();
sf::Vector2i mouse_pos = sf::Mouse::getPosition(window);
mouse_diriction_=SnakeNode::Height*sf::Vector2f(mouse_pos.x-head_pos.x,mouse_pos.y-head_pos.y)/
sqrt((mouse_pos.x-head_pos.x)*(mouse_pos.x-head_pos.x)+(mouse_pos.y-head_pos.y)*(mouse_pos.y-head_pos.y));
}
}
void Snake::move()
{
...
switch (direction_)
{
...
case Direction::Mouse_Vec://如果刚刚是鼠标点击,则朝着对应方向前进
nodes_[0].move(mouse_diriction_.x,mouse_diriction_.y);
break;
}
}
-
水果颜色、分数、数量和初始化方式:
给水果添加了颜色、分数的属性。通过随机数,保证有3/4概率生成“红、绿、蓝”类别的水果,也就是说这三种颜色各占1/4,红色3分,蓝色2分,绿色1分。有1/8概率生成棕色果实,有1/8概率生成黑色果实。这两种果实都是0分。分数表示吃掉一个果实后蛇增加的长度。
由于原作者使用vector来维护果实,因此很容易拓展到多个果实的情况,于是规定场上保持五个果实,每当果实被吃掉后都会刷新出新的果实。
//为果实随机产生属性的函数,可能比较抽象orz sf::Color Fruit::Set_Color() { if(!(rand()%4)) { color_=(rand()%2? sf::Color::Black : sf::Color(90,57,18));//Brown=(90,57,18) score_=0; } else if(rand()%3) { color_= (rand()%2? sf::Color::Red : sf::Color::Blue); score_= (color_==sf::Color::Red?3:2); } else { color_= sf::Color::Green; score_= 1; } return color_; }
-
背景、网格、和蛇的外貌
新增了背景网格的绘制,还给蛇设计了不同的外貌。这部分源于对sprite精灵的运用,通过载入纹理来实现。背景网格的绘制参见BackGround类。
蛇的绘制大致结构和原来相同,只不过使用了sprite。
蛇节点初始化的代码如下:
SnakeNode::SnakeNode(sf::Vector2f position,int indx,sf::Vector2f direction) : position_(position),direction_(direction) { if(!body_tex.loadFromFile(Game::snakebody_loc[MenuScreen2::snake_choose])) { std::cout<<"Failed to load snakenodes' picture"<<std::endl; } if(!head_tex.loadFromFile(Game::snakehead_loc[MenuScreen2::snake_choose])) { std::cout<<"Failed to load snakehead's picture"<<std::endl; } sprite_.setTexture(indx==0?head_tex:body_tex); sprite_.setPosition(position); sprite_.setOrigin(Width/2, Height/2); sprite_.setRotation(std::atan2(direction.y, direction.x)/(2*3.1415926535)*360.0+90.0); }
设计贪吃蛇皮肤时的灵感来源: 星之卡比(粉色),吃豆人(黄色),微信群上某头像(蓝色)(逃)
(这部分的素材(像素画)全部是自己设计的)
-
背景、网格、蛇外貌的选择界面:
既然上面制作好了素材,自然需要一个类似于菜单的界面来选择素材,且要方便玩家。
新增MenuScreen2类来实现对背景、网格、蛇外貌的选择。
特性:在选择界面时可以通过按键选择对应的背景、网格、蛇外貌
背景颜色:【Q】白色背景 【W】黑色背景 【E】棕色背景
网格颜色:【A】白色网格 【S】黑色网格 【D】棕色网格 【F】没有网格(其实就是网格和背景同色)
贪吃蛇外貌:【Z】星之卡比【X】吃豆人【C】脸萌头像 😃
【space】开始游戏
反馈机制:为了给玩家选择以反馈,在按下按键选择后,对应的提示文字的颜色会改变成对应的颜色。
具体实现见MenuScreen2.cpp
按照.h文件介绍修改情况:
- Game.h
namespace sfSnake
{
class Game
{
public:
Game();
void run();
void handleInput();
void update(sf::Time delta);
void render();
sf::Vector2i window_pos();
static const int Width = 640;
static const int Height = 480;
static std::shared_ptr<Screen> Screen;
static std::string back_loc[3],//背景素材的路径
snakehead_loc[3],//蛇头素材的路径
cell_loc[3],//网格素材的路径
snakebody_loc[3]; //蛇身体的路径
private:
sf::RenderWindow window_;
sf::Music bgMusic_;
static const sf::Time TimePerFrame;
};
}
增加了12个静态的string类型的变量,用于表示纹理素材的路径。在Game.cpp里有对应初始化。
该项目中只有一个该类的实例,生命周期贯穿整个项目。
- Screen.h
class Screen
{
public:
virtual void handleInput(sf::RenderWindow& window) = 0;
virtual void update(sf::Time delta) = 0;
virtual void render(sf::RenderWindow& window) = 0;
};
未作修改,与原来的一致。
- MenuScreen.h
namespace sfSnake
{
class MenuScreen : public Screen
{
public:
MenuScreen();
void handleInput(sf::RenderWindow& window) override;
void update(sf::Time delta) override;
void render(sf::RenderWindow& window) override;
private:
sf::Font font_;
sf::Text snakeText_;
sf::Text text_;
};
}
用于菜单界面的显示,未修改,与原来的一致。
在进入菜单界面时创建一个实例,进入下一个界面或者退出游戏时销毁。
- MenuScreen2.h
namespace sfSnake
{
class MenuScreen2 : public Screen
{
public:
MenuScreen2();
void handleInput(sf::RenderWindow& window) override;
void update(sf::Time delta) override;
void render(sf::RenderWindow& window) override;
static int back_choose,cell_choose,snake_choose;//表示当前选择的蛇、网格、背景的号码
private:
sf::Font font_;//选择界面的字体
sf::Text snakeText_,backText_,cellText_;//选择界面的显示文字
};
}
这是自己写的一个类,用于实现对蛇、网格、背景的颜色选择。
实现的基本思路:用三个整数变量表示当前对应颜色的选择,通过检测用户输入来更改当前的选择。为了给予用户反馈,会将对应文字的颜色显示为已选择的颜色。
其中,蛇提供“粉红、黄、蓝”三种颜色,网格和背景提供“白、黑、棕”三种颜色。
界面效果图(例如:蛇选择黄色,背景选择白色、网格选择黑色):
支持关闭网格,实质上是采用了和背景颜色相同的网格素材。
从菜单界面按下空格后创建一个该实例,在游戏开始后销毁。
- BackGround.h
namespace sfSnake
{
class BackGround
{
public:
BackGround();
void setCellColor();//设置网格颜色
void setBackColor();//设置背景颜色
void drawCell(sf::RenderWindow& window);//绘制网格
void drawBack(sf::RenderWindow& window);//绘制背景
private:
sf::Color cell_color_, back_color_;//网格、背景颜色
sf::Sprite cell_,back_;//网格、背景的精灵
};
}
这也是新增的一个类,用于处理背景图案和网格。
该类的实例是GameScreen类中的成员,随着GameScreen类的创建而创建,随之销毁而销毁。
- Fruit.h
namespace sfSnake
{
class Fruit
{
public:
Fruit(sf::Vector2f position = sf::Vector2f(0, 0));
sf::Color Set_Color();//给该果实按照某一概率随机设置一个颜色,同时会设置该果实的分数
void render(sf::RenderWindow& window);
sf::FloatRect getBounds() const;
int Score();//获得该果实的分数
private:
sf::CircleShape shape_;
sf::Color color_;//该果实的颜色
int score_;//该果实对应的分数(增加蛇的长度)
static const float Radius;
};
}
增加了果实的颜色、分数,并且会在果实创建出来的时候设置果实的颜色和分数。可以通过Score()方法从外部访问该果实的分数,便于计算出蛇的生长长度。
该类的实例会在游戏中创建,并且放在vector中,在蛇吃掉后被销毁。
- SnakeNode.h
namespace sfSnake
{
class SnakeNode
{
public:
SnakeNode(sf::Vector2f position = sf::Vector2f(0, 0),int indx=1,sf::Vector2f direction = sf::Vector2f(0,-1));//初始化需要给出节点的类型(头or身体)和方向向量
void setPosition(sf::Vector2f position);
void setDirection(sf::Vector2f position);//设定方向向量
void setPosition(float x, float y);
void move(float xOffset, float yOffset);
void render(sf::RenderWindow& window);
sf::Vector2f getPosition() const;
sf::Vector2f getDirection() const;//获取方向向量
sf::FloatRect getBounds() const;
static const float Width;
static const float Height;
private:
sf::Vector2f position_,direction_;//增加了方向向量
sf::Sprite sprite_;//该节点的精灵,用于绘图
};
}
为了实现蛇方块的角度变化,新增了节点的方向成员与访问该成员的变量。为了区分蛇头和蛇身,在该类创建出来的时候就必须决定,0代表蛇头,1代表蛇身。蛇头蛇身会加载不同的纹理,也就是不同的素材。
该类的实例会在游戏开始或者蛇生长的时候被创建,在游戏结束时销毁。
- Snake.h
namespace sfSnake
{
enum class Direction
{
Left, Right, Up, Down, Mouse_Vec//新增Mouse_Vec表示鼠标矢量
};
class Snake
{
public:
Snake();
void handleInput(sf::RenderWindow& window);
void update(sf::Time delta);
void render(sf::RenderWindow& window);
void checkFruitCollisions(std::vector<Fruit>& fruits);
bool hitSelf() const;
unsigned getSize() const;
private:
void move();
void grow();//在蛇尾方向上延长出新的节点
void grow2();//在蛇尾的位置上重叠出新的节点
void checkEdgeCollisions();
void checkSelfCollisions();
void initNodes();
bool hitSelf_;
sf::Vector2f position_;
Direction direction_;//键盘命令给出的前进方向
sf::Vector2f mouse_diriction_;//鼠标命令给出的前进方向
sf::SoundBuffer pickupBuffer_;
sf::Sound pickupSound_;
sf::SoundBuffer dieBuffer_;
sf::Sound dieSound_;
std::vector<SnakeNode> nodes_;//用来放蛇的节点
static const int InitialSize;
};
}
该类的主要改动是蛇的移动方式和蛇的生长方式。在枚举类型中新增Mouse_Vec表示鼠标控制的方向,新增鼠标控制决定的方向矢量。在鼠标右键单击时,通过计算蛇头位置指向鼠标位置的向量,得到蛇的前进方向,同时据此改变蛇头的方向。
这种修改方法使得既可以用鼠标控制也可以用键盘控制。
新增蛇的堆叠生长方式,如果采用后接式生长,在生长多个长度时会出现怪异的现象,突然冒出一截。因此重写了一个grow2()方法。
该类的实例在游戏开始时创建,游戏结束时销毁。
- GameScreen.h
namespace sfSnake
{
class GameScreen : public Screen
{
public:
GameScreen();
void handleInput(sf::RenderWindow& window) override;
void update(sf::Time delta) override;
void render(sf::RenderWindow& window) override;
void generateFruit();
private:
Snake snake_;
std::vector<Fruit> fruit_;
BackGround back_ground_;//背景网格类的实例
};
}
修改不大,增加了网格类的实例。修改了生成水果的方式,开局生成5个水果,并且在游戏中保持5个水果。避免画面空旷。
该类的实例在游戏界面开始时创建,游戏结束时销毁。
- GameOverScreen.h
namespace sfSnake
{
class GameOverScreen : public Screen
{
public:
GameOverScreen(std::size_t score);
void handleInput(sf::RenderWindow& window) override;
void update(sf::Time delta) override;
void render(sf::RenderWindow& window) override;
private:
sf::Font font_;
sf::Text text_;
unsigned score_;
};
}
无改动。
该类的实例在游戏结束时创建,退出游戏或者重新进入菜单时销毁。
演示视频:
小结:
本次大作业是一次很好的动手机会。在添加功能之前,我阅读了原作者的代码,了解了原作者的实现思路。添加功能的过程中,我阅读文档了解了sfml中许许多多的功能。在绘制素材的时候,我体会到了设计游戏素材的快乐。在实现选择界面类的时候,我很好的承接了原作者的设计思路。收获较多。
不过该项目还可以有很多细节可以去改进优化,比如,此项目的窗口大小必须锁定,那么可以思考如何让玩家自由调节窗口大小。况且,完成该作业的时间有限,测试可能不够充分,也许还会有潜在的bug。因此,还值得进一步改进。