我最初在“代码评论”中发布了这个问题,但后来认为我可能会在这里得到更多关于设计的反馈。
我刚刚在C++中完成了一个简单的Snake克隆,目的是要有一个实际的设计,并坚持OOP的坚实原则。所以我做了一个游戏的类图,并编写了所有的代码,它工作得很好,但是有些地方我觉得我可以做得更好。例如,有一些硬编码元素我无法优雅地删除,而且有一个地方,我使用继承,但我也有一个枚举,以确定一个消息来自哪个孩子,当我不想关心孩子是什么类型。
不管怎么说,下面是类图:
Engine是一个简单的引擎,我写它是为了方便地显示2d图形,我只使用它来呈现和一些助手类,比如v2di,它是一个用于存储位置信息的2d整数向量。
这是游戏类,它负责游戏的开始和运行。
OnCreate()
在启动时被调用一次,
OnUpdate()
和OnRender()
每帧调用一次,应该包含游戏循环,
当内部游戏循环退出且程序即将退出时,将调用OnDestroy()
:
///////////////// .h
class SnakeGame : public rge::DXGraphicsEngine {
public:
SnakeGame();
bool OnCreate();
bool OnUpdate();
bool OnRender();
void OnDestroy();
protected:
Snake snake;
FieldGrid field;
int score;
bool gameOver;
int updateFreq;
int updateCounter;
};
//////////////////////// .cpp
SnakeGame::SnakeGame() : DXGraphicsEngine(), field(10, 10), snake(3, rge::v2di(3, 1), Direction::RIGHT), score(0), gameOver(false), updateFreq(10), updateCounter(0){
}
bool SnakeGame::OnCreate() {
field.AddSnake(snake.GetBody());
field.GenerateFood();
return true;
}
bool SnakeGame::OnUpdate() {
//check user input
if(GetKey(rge::W).pressed) {
snake.SetDirection(Direction::UP);
}
if(GetKey(rge::S).pressed) {
snake.SetDirection(Direction::DOWN);
}
if(GetKey(rge::A).pressed) {
snake.SetDirection(Direction::LEFT);
}
if(GetKey(rge::D).pressed) {
snake.SetDirection(Direction::RIGHT);
}
updateCounter++;
if(!gameOver && updateCounter >= updateFreq) {
updateCounter = 0;
//clear snake body from field
field.ClearSnake(snake.GetBody());
//move
snake.MoveSnake();
//add snake body to field
field.AddSnake(snake.GetBody());
//testcollision
CollisionMessage cm = field.CheckCollision(snake.GetHead());
gameOver = cm.gameOver;
score += cm.scoreChange ? snake.GetLength() * 10 : 0;
if(cm.tileType == TileType::Food) {
field.GenerateFood();
snake.ExtendSnake();
}
}
return true;
}
bool SnakeGame::OnRender() {
std::cout << score << std::endl;
field.Draw(&m_colorBuffer, 100, 20, 10);
snake.DrawHead(&m_colorBuffer, 100, 20, 10);
return true;
}
接下来是移动和扩展蛇的Snake
类。蛇还可以进入Direction
:
///////////// .h
enum class Direction {
UP, DOWN, LEFT, RIGHT
};
class Snake {
public:
Snake();
Snake(int length, rge::v2di position, Direction direction);
rge::v2di GetHead() { return head; }
std::vector GetBody() { return body; }
void MoveSnake();
void ExtendSnake();
Direction GetDirection() { return direction; }
void SetDirection(Direction direction);
int GetLength() { return body.size() + 1; }
void DrawHead(rge::Buffer* buffer, int x, int y, int size);
protected:
std::vector body;
rge::v2di head;
Direction direction;
Direction oldDirection;
};
////////////// .cpp
Snake::Snake(): head(rge::v2di(0, 0)), direction(Direction::UP), oldDirection(Direction::UP), body(std::vector()){
body.push_back(rge::v2di(head.x, head.y + 1));
}
Snake::Snake(int length, rge::v2di position, Direction direction) : head(position), direction(direction), oldDirection(direction), body(std::vector()) {
for(int i = 0; i < length-1; ++i) {
rge::v2di bodyTile;
switch(direction) {
case Direction::UP:{
bodyTile.x = head.x;
bodyTile.y = head.y + (i + 1);
break;
}
case Direction::DOWN:{
bodyTile.x = head.x;
bodyTile.y = head.y - (i + 1);
break;
}
case Direction::LEFT: {
bodyTile.y = head.y;
bodyTile.x = head.x + (i + 1);
break;
}
case Direction::RIGHT: {
bodyTile.y = head.y;
bodyTile.x = head.x - (i + 1);
break;
}
}
body.push_back(bodyTile);
}
}
void Snake::MoveSnake() {
oldDirection = direction;
for(int i = body.size()-1; i > 0; --i) {
body[i] = body[i - 1];
}
body[0] = head;
switch(direction) {
case Direction::UP: {
head.y--;
break;
}
case Direction::DOWN: {
head.y++;
break;
}
case Direction::LEFT: {
head.x--;
break;
}
case Direction::RIGHT: {
head.x++;
break;
}
}
}
void Snake::ExtendSnake() {
body.push_back(body[body.size() - 1]);
}
void Snake::SetDirection(Direction direction) {
switch(this->oldDirection) {
case Direction::UP:
case Direction::DOWN: {
if(direction != Direction::UP && direction != Direction::DOWN) {
this->direction = direction;
}
break;
}
case Direction::LEFT:
case Direction::RIGHT: {
if(direction != Direction::LEFT && direction != Direction::RIGHT) {
this->direction = direction;
}
break;
}
}
}
void Snake::DrawHead(rge::Buffer* buffer, int x, int y, int size) {
rge::Color c(100, 100, 200);
buffer->DrawRegion(x + head.x * size, y + head.y * size, x + head.x * size + size, y + head.y * size + size, c.GetHex());
}
然后是负责碰撞检测、食物生成和存储映射状态的FieldGrid
类:
//////////// .h
class FieldGrid {
public:
FieldGrid();
FieldGrid(int width, int height);
~FieldGrid();
void GenerateFood();
CollisionMessage CheckCollision(rge::v2di head);
void ClearSnake(std::vector body);
void AddSnake(std::vector body);
void Draw(rge::Buffer* buffer, int x, int y, int size);
protected:
std::vector> field;
int width;
int height;
};
//////////// .cpp
FieldGrid::FieldGrid() : width(10), height(10), field(std::vector>()) {
for(int i = 0; i < width; ++i) {
field.push_back(std::vector());
for(int j = 0; j < height; ++j) {
field[i].push_back(new EmptyTile());
}
}
}
FieldGrid::FieldGrid(int width, int height): width(width), height(height), field(std::vector>()) {
for(int i = 0; i < width; ++i) {
field.push_back(std::vector());
for(int j = 0; j < height; ++j) {
field[i].push_back(new EmptyTile());
}
}
}
FieldGrid::~FieldGrid() {
for(int i = 0; i < field.size(); ++i) {
for(int j = 0; j < field[i].size(); ++j) {
delete field[i][j];
}
field[i].clear();
}
field.clear();
}
void FieldGrid::GenerateFood() {
int x = rand() % width;
int y = rand() % height;
while(!field[x][y]->IsFree()) {
x = rand() % width;
y = rand() % height;
}
delete field[x][y];
field[x][y] = new FoodTile();
}
CollisionMessage FieldGrid::CheckCollision(rge::v2di head) {
if(head.x < 0 || head.x >= width || head.y < 0 || head.y >= height) {
CollisionMessage cm;
cm.scoreChange = false;
cm.gameOver = true;
return cm;
}
return field[head.x][head.y]->OnCollide();
}
void FieldGrid::ClearSnake(std::vector body) {
for(int i = 0; i < body.size(); ++i) {
delete field[body[i].x][body[i].y];
field[body[i].x][body[i].y] = new EmptyTile();
}
}
void FieldGrid::AddSnake(std::vector body) {
for(int i = 0; i < body.size(); ++i) {
delete field[body[i].x][body[i].y];
field[body[i].x][body[i].y] = new SnakeTile();
}
}
void FieldGrid::Draw(rge::Buffer* buffer, int x, int y, int size) {
for(int xi = 0; xi < width; ++xi) {
for(int yi = 0; yi < height; ++yi) {
int xp = x + xi * size;
int yp = y + yi * size;
field[xi][yi]->Draw(buffer, xp, yp, size);
}
}
}
Tile
类用于FieldGrid
:
class Tile {
public:
virtual CollisionMessage OnCollide() = 0;
virtual bool IsFree() = 0;
void Draw(rge::Buffer* buffer, int x, int y, int size) {
buffer->DrawRegion(x, y, x + size, y + size, color.GetHex());
}
protected:
rge::Color color;
};
class EmptyTile : public Tile {
public:
EmptyTile() {
this->color = rge::Color(50, 50, 50);
}
CollisionMessage OnCollide() {
CollisionMessage cm;
cm.scoreChange = false;
cm.gameOver = false;
cm.tileType = TileType::Empty;
return cm;
}
bool IsFree() { return true; }
};
class FoodTile : public Tile {
public:
FoodTile() {
this->color = rge::Color(50, 200, 70);
}
CollisionMessage OnCollide() {
CollisionMessage cm;
cm.scoreChange = true;
cm.gameOver = false;
cm.tileType = TileType::Food;
return cm;
}
bool IsFree() { return false; }
};
class SnakeTile : public Tile {
public:
SnakeTile() {
this->color = rge::Color(120, 130, 250);
}
CollisionMessage OnCollide() {
CollisionMessage cm;
cm.scoreChange = false;
cm.gameOver = true;
cm.tileType = TileType::Snake;
return cm;
}
bool IsFree() { return false; }
};
最后,下面是用于在蛇头与任何CollisionMessage
碰撞时向游戏发送消息的D14
类:
enum class TileType {
Empty,
Snake,
Food
};
class CollisionMessage {
public:
bool scoreChange;
bool gameOver;
TileType tileType;
};
我省略了所有的包含和主要方法,因为它们与设计无关,只会占用额外的空间。
我感谢您花时间阅读我所有的代码(或者只是查看类图),并且非常想了解您对我所选择的总体设计的看法。
发布于 2020-01-12 11:19:35
你在正确的地方审查你的设计。但这不是一个代码审查网站,所以不要期待在这里进行深入检查。
首先让我印象深刻的是:
Snake
与FieldGrid
或Tile
之间没有任何关系:很难相信两者都能像下图所示的那样独立,特别是当考虑到某些Tile
代表Snake
的一部分时。CollisionMessage
与Tile
相关联,而不是与协调实体间协作的SnakeGame
相关联。TileType
是与消息相关联的,而不是与瓦本身相关联的。TileType
似乎是多余的,因为Tile
专门化为EmptyTile
、FoodTile
、SnakeTile
。UML模型:
CollisionMessage
相关联(这实际上更多地是一个事件而不是消息)。tiles创建消息并将其返回给暂时拥有消息的Game
,并在处理消息时丢弃。Tile
和CollisionMessage
之间的关联。CollisionMessage
和Game
之间的关联,并且可以显示Tile
和CollisionMessage
之间的创建依赖。另一个缺点是缺乏separation关注点:
tileType
向Game
显示,游戏根据其(泄露的)瓷砖知识来决定该做什么。游戏循环不应该协调,让其他类来决定细节吗?MountainTile
(随机致死与否,因为有时石头会掉下来杀死蛇)、SeeTile
(一些蛇游泳)、MongooseTile
(总是致命的):您真的想让游戏循环知道在每种情况下应该做什么吗?幸运的是,通过增加关注点分离和加强开放/关闭原则,有一个改进这一设计的解决方案:
FieldGrid::CheckCollision()
,在Tile::OnColide()
的支持下)确定事件。例如,如果冲突与FoodTile
发生冲突,则创建FeedingEvent
,而SnakeTile
或MongooseTile
将创建FatalEvent
。所有这些甚至都来自抽象的CollisionMessage
CollisionMessage
改为引用CollisionType
而不是TileType
。至少,游戏循环中的动作会更符合逻辑(例如,如果类型是FeedignEvent .,如果类型是FatalEvent.等等.shared_ptr
。其优点可能是游戏循环可以调用CollisionMessage的一些虚拟函数,而不是单独检查所有的情况。最后,我发现了一个讨厌的虫子:
FieldGrid::CheckCollision()
,它在蛇出界时创建一个CollisionMessage
。tileType
,顺便说一句,它既不是蛇,也不是空的,也不是食物。关于UML:
Snake
、v2di
和Direction
之间使用组合。这些类作为或在几个Snake
成员中使用,但按值计算。因此,它实际上是一种独占所有权和生命周期控制。然后是一个代码建议:忘记原始指针:在内存管理中很容易出错。让库关心您的内存管理:
所以
std::vector> field;
可以用
std::vector>> field;
和所有的
delete field[body[i].x][body[i].y];
field[body[i].x][body[i].y] = new EmptyTile(); // or SnakeTile or...
将被替换为
field[body[i].x][body[i].y] = make_shared(); // or SnakeTile or...
共享指针处理不再使用的释放块。安全多了。
所以,这就是所有的人!很抱歉,这个问题的答案太长了。但希望能帮上忙。
发布于 2020-01-12 12:40:19
我想说的是,这里的问题是FieldGrid作为一个瓷砖数组的概念。
您将自己限制在游戏的经典实现上,在该实现中,用户界面( ascii )显示与游戏本身紧密耦合。
好吧,如果这是你想要的,但你不需要蛇类。蛇只是SnakeTiles的集合。
一个更现代的设计将建模蛇和食物的位置与三维浮动矢量,脱离UI与模型,并允许您忽略瓷砖的概念,特别是EmptyTile。
这允许您更灵活地处理特性请求,使蛇对角移动!把蛇放在z索引上!使游戏区无限大等
https://softwareengineering.stackexchange.com/questions/403693
复制相似问题