首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >C++中的简单蛇游戏

C++中的简单蛇游戏
EN

Software Engineering用户
提问于 2020-01-12 01:25:15
回答 2查看 2.1K关注 0票数 1

我最初在“代码评论”中发布了这个问题,但后来认为我可能会在这里得到更多关于设计的反馈。

我刚刚在C++中完成了一个简单的Snake克隆,目的是要有一个实际的设计,并坚持OOP的坚实原则。所以我做了一个游戏的类图,并编写了所有的代码,它工作得很好,但是有些地方我觉得我可以做得更好。例如,有一些硬编码元素我无法优雅地删除,而且有一个地方,我使用继承,但我也有一个枚举,以确定一个消息来自哪个孩子,当我不想关心孩子是什么类型。

不管怎么说,下面是类图:

Engine是一个简单的引擎,我写它是为了方便地显示2d图形,我只使用它来呈现和一些助手类,比如v2di,它是一个用于存储位置信息的2d整数向量。

这是游戏类,它负责游戏的开始和运行。

OnCreate()在启动时被调用一次,

OnUpdate()OnRender()每帧调用一次,应该包含游戏循环,

当内部游戏循环退出且程序即将退出时,将调用OnDestroy()

代码语言:javascript
运行
复制
///////////////// .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

代码语言:javascript
运行
复制
///////////// .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类:

代码语言:javascript
运行
复制
//////////// .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

代码语言:javascript
运行
复制
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类:

代码语言:javascript
运行
复制
enum class TileType {
    Empty,
    Snake,
    Food
};

class CollisionMessage {
public:
    bool scoreChange;
    bool gameOver;
    TileType tileType;
};

我省略了所有的包含和主要方法,因为它们与设计无关,只会占用额外的空间。

我感谢您花时间阅读我所有的代码(或者只是查看类图),并且非常想了解您对我所选择的总体设计的看法。

EN

回答 2

Software Engineering用户

回答已采纳

发布于 2020-01-12 11:19:35

你在正确的地方审查你的设计。但这不是一个代码审查网站,所以不要期待在这里进行深入检查。

概述

首先让我印象深刻的是:

  • SnakeFieldGridTile之间没有任何关系:很难相信两者都能像下图所示的那样独立,特别是当考虑到某些Tile代表Snake的一部分时。
  • CollisionMessageTile相关联,而不是与协调实体间协作的SnakeGame相关联。
  • TileType是与消息相关联的,而不是与瓦本身相关联的。
  • TileType似乎是多余的,因为Tile专门化为EmptyTileFoodTileSnakeTile

CollisionMessage,TileType和关注点分离

UML模型:

  • 在您的代码中,没有任何平铺与CollisionMessage相关联(这实际上更多地是一个事件而不是消息)。tiles创建消息并将其返回给暂时拥有消息的Game,并在处理消息时丢弃。
  • 因此,类图不应该显示TileCollisionMessage之间的关联。
  • 相反,它应该显示CollisionMessageGame之间的关联,并且可以显示TileCollisionMessage之间的创建依赖

另一个缺点是缺乏separation关注点

  • calss将tileTypeGame显示,游戏根据其(泄露的)瓷砖知识来决定该做什么。游戏循环不应该协调,让其他类来决定细节吗?
  • 现在假设您添加了更多类型的瓷砖:MountainTile (随机致死与否,因为有时石头会掉下来杀死蛇)、SeeTile (一些蛇游泳)、MongooseTile (总是致命的):您真的想让游戏循环知道在每种情况下应该做什么吗?

幸运的是,通过增加关注点分离和加强开放/关闭原则,有一个改进这一设计的解决方案:

  • 让碰撞检测器(这里是FieldGrid::CheckCollision(),在Tile::OnColide()的支持下)确定事件。例如,如果冲突与FoodTile发生冲突,则创建FeedingEvent,而SnakeTileMongooseTile将创建FatalEvent。所有这些甚至都来自抽象的CollisionMessage
  • 每种类型的tile都已经有自己的OnColide()实现,这将非常容易:只需为每个瓷砖返回正确的事件即可。
  • 最简单的实现是将CollisionMessage改为引用CollisionType而不是TileType。至少,游戏循环中的动作会更符合逻辑(例如,如果类型是FeedignEvent .,如果类型是FatalEvent.等等.
  • 更详细的方法是使用多态,并返回CollisionMessage的分隔子类型。但是,为了在C++中安全地处理返回类型,需要使用shared_ptr。其优点可能是游戏循环可以调用CollisionMessage的一些虚拟函数,而不是单独检查所有的情况。

最后,我发现了一个讨厌的虫子:

  • 目前,您还有一个FieldGrid::CheckCollision(),它在蛇出界时创建一个CollisionMessage
  • 不幸的是,您没有设置tileType,顺便说一句,它既不是蛇,也不是空的,也不是食物。
  • 此外,构造函数不初始化该成员。因此它的价值可以是任何东西:对应于错误的瓷砖类型的合法值(这可能导致游戏循环得出错误的结论)或非法的值。也请参阅关于枚举初始化的这个问题

网格与蛇

关于UML:

  • 您应该在Snakev2diDirection之间使用组合。这些类作为或在几个Snake成员中使用,但按值计算。因此,它实际上是一种独占所有权和生命周期控制。
  • 尽管我最初的问题,模型是好的。在您的代码中,您确实将蛇与网格分开:在每次移动时,清除图表中的蛇块,然后添加蛇以更改新位置的块。您也许可以通过网格显示蛇的使用依赖性,以使图表更具表现力。
  • 这个设计当然限制了游戏中的一条蛇,因为在网格中你不知道哪个蛇是瓷砖的。

然后是一个代码建议:忘记原始指针:在内存管理中很容易出错。让库关心您的内存管理:

所以

代码语言:javascript
运行
复制
std::vector> field;

可以用

代码语言:javascript
运行
复制
std::vector>> field;

和所有的

代码语言:javascript
运行
复制
    delete field[body[i].x][body[i].y];
    field[body[i].x][body[i].y] = new EmptyTile();  // or SnakeTile or...

将被替换为

代码语言:javascript
运行
复制
    field[body[i].x][body[i].y] = make_shared();  // or SnakeTile or...

共享指针处理不再使用的释放块。安全多了。

所以,这就是所有的人!很抱歉,这个问题的答案太长了。但希望能帮上忙。

票数 2
EN

Software Engineering用户

发布于 2020-01-12 12:40:19

我想说的是,这里的问题是FieldGrid作为一个瓷砖数组的概念。

您将自己限制在游戏的经典实现上,在该实现中,用户界面( ascii )显示与游戏本身紧密耦合。

好吧,如果这是你想要的,但你不需要蛇类。蛇只是SnakeTiles的集合。

一个更现代的设计将建模蛇和食物的位置与三维浮动矢量,脱离UI与模型,并允许您忽略瓷砖的概念,特别是EmptyTile。

这允许您更灵活地处理特性请求,使蛇对角移动!把蛇放在z索引上!使游戏区无限大等

票数 0
EN
页面原文内容由Software Engineering提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://softwareengineering.stackexchange.com/questions/403693

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档