web-dev-qa-db-ja.com

C ++のシンプルなスネークゲーム

私はこの質問を最初にコードレビューで投稿しましたが、ここでデザインについてのフィードバックをもっと得ることができると思いました。

今回は実際のデザインを持ち、SOLID OOPの原則に準拠することを目標として、C++で簡単なスネーククローンを書き終えたところです。そのため、ゲームのクラス図を作成し、すべてのコード、そしてそれは正常に動作しますが、私がもっと上手くできたと感じるいくつかの領域があります。たとえば、エレガントに削除できなかったいくつかのハードコードされた要素があり、使用する場所が1つあります継承ですが、次に、メッセージがどの子から来ているのかを把握するための列挙型もあり、子のタイプをまったく気にしたくない場合に使用します。

とにかく、ここにクラス図があります:

class diagram

Engineは、2Dグラフィックスを簡単に表示するために書いたシンプルなエンジンです。レンダリングと、位置情報を格納するための2D整数ベクトルであるv2diのようないくつかのヘルパークラスにのみ使用します


以下は、ゲームの開始と実行を担当するゲームクラスです。

OnCreate()は起動時に1回呼び出されます。

OnUpdate()およびOnRender()はフレームごとに1回呼び出され、ゲームループを含む必要があります。

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<rge::v2di> 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<rge::v2di> 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<rge::v2di>()){
    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<rge::v2di>()) {
    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<rge::v2di> body);
    void AddSnake(std::vector<rge::v2di> body);
    void Draw(rge::Buffer* buffer, int x, int y, int size);
protected:
    std::vector<std::vector<Tile*>> field;
    int width;
    int height;
};

//////////// .cpp

FieldGrid::FieldGrid() : width(10), height(10), field(std::vector<std::vector<Tile*>>()) {
    for(int i = 0; i < width; ++i) {
        field.Push_back(std::vector<Tile*>());
        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<std::vector<Tile*>>()) {
    for(int i = 0; i < width; ++i) {
        field.Push_back(std::vector<Tile*>());
        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<rge::v2di> 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<rge::v2di> 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と衝突したときにゲームにメッセージを送信するために使用されるTileクラスを次に示します。

enum class TileType {
    Empty,
    Snake,
    Food
};

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

すべてのインクルードとメインメソッドは省略しました。これらはデザインに関連しておらず、余分なスペースしかとらないためです。


私のすべてのコードを読んで(またはクラス図を見て)時間をかけていただき、私が選択した全体的なデザインについてのご意見をお待ちしております。

1
Sam

あなたはあなたのデザインのレビューのための適切な場所にいます。しかし、これはコードレビューサイトではないので、ここで詳細な検査を期待しないでください。

概観

私が最初にクラスを見て驚いたのは、

  • SnakeFieldGridまたはTileの間に関係はありません。特に、いくつかのTileSnakeの一部を表していることを考えると、この図が示すように両方が独立しているとは思えません。
  • CollisionMessageTileに関連付けられており、エンティティ間のコラボレーションを調整するSnakeGameではありません
  • TileTypeはメッセージに関連付けられていますが、タイル自体には関連付けられていません
  • TileTypeTileEmptyTileFoodTileに特殊化すると、SnakeTileは冗長になります。

CollisionMessage、TileType、および懸念の分離

UMLモデル:

  • コードでは、CollisionMessage(実際にはメッセージよりもイベント)に関連付けられているタイルはありません。タイルはメッセージを作成し、それを一時的に所有するGameに返し、処理されると破棄します。

  • そのため、クラス図には、TileCollisionMessageの間の関連を示すべきではありません。

  • 代わりに、CollisionMessageGame間の関連付けを示す必要があり、TileCollisionMessage間に creation dependency を示すことができます。

他の弱点は、分離の懸念がないことです

  • calssはtileTypeからGameを明らかにし、ゲームはタイルの(漏洩した)知識に基づいて何をするかを決定します。ゲームループは調整すべきではなく、他のクラスに詳細を決定させるべきではありませんか?

  • ここで、いくつかの種類のタイルを追加するとします。MountainTile(ランダムに致命的かどうかは時々岩が落下してヘビを殺すため)、SeeTile(一部のヘビは泳ぐ)、MongooseTile(常に致命的)それぞれの場合に行うには?

幸い、懸念事項の分離を追加し、オープン/クローズの原則を強化することにより、この設計を改善するソリューションがあります。

  • 衝突検出器(ここではFieldGrid::CheckCollision()をサポートするTile::OnColide())がイベントを決定するようにします。たとえば、衝突がFoodTileとの場合はFeedingEventを作成しますが、SnakeTileまたはMongooseTileFatalEventを作成します。これらすべては、抽象CollisionMessageから派生したものでもあります
  • OnColide()の独自の実装がすでにある各種のタイルは、非常に簡単です。各タイルに適切なイベントを返すだけです。
  • 最も簡単な実装は、CollisionMessageではなくCollisionTypeを参照するようにTileTypeを変更することです。少なくとも、ゲームループのアクションは読み取るロジックが多くなります(たとえば、タイプがFeedignEvent ...の場合、タイプがFatalEvent ...の場合など)。
  • より複雑な方法は、polymorphismを使用して、CollisionMessageの特殊化されたサブタイプを返すことです。ただし、C++の戻り値の型で安全に機能する多態性には、_shared_ptr_を使用する必要があります。アドバテージは、ゲームループが単独ですべてのケースを調べるのではなく、CollisionMessageのいくつかの仮想関数を呼び出す可能性があることです。

最後に、私は厄介なバグを見つけました:

  • 現在、ヘビが境界を超えたときにCollisionMessageを作成するFieldGrid::CheckCollision()もあります。
  • 残念ながら、tileTypeを設定しないでください。ちなみに、それはヘビでも、空でも、食べ物でもありません。
  • さらに、コンストラクターはこのメンバーを初期化しません。したがって、その値は何でもかまいません。間違ったタイルタイプに対応する正当な値(ゲームループが誤った結論を引き出す可能性がある)または不正な値。列挙型の初期化について this SO question も参照してください。

グリッドとヘビ

UMLについて:

  • Snakeと_v2di_とDirectionの間の構成を使用する必要があります。これらのクラスは、いくつかのSnakeメンバーとして、またはその中で使用されますが、値によって使用されます。したがって、これは実際には排他的な所有権とライフサイクル制御です。

  • 私の最初の質問にもかかわらず、モデルは大丈夫です。コードでは、実際にはヘビをグリッドから切り離しています。移動するたびに、図のヘビタイルをクリアし、ヘビを追加して新しい位置のタイルを変更します。グリッドでヘビの 使用依存関係 を表示して、図をより表現力豊かにすることができます。

  • もちろん、グリッドではタイルがどのヘビに属しているかがわからないため、このデザインではゲームが1つのヘビに制限されます。

次に、コードの推奨事項:生のポインタを忘れてください:メモリ管理でエラーが発生するのは非常に簡単です。ライブラリにメモリ管理を任せましょう:

そう

_std::vector<std::vector<Tile*>> field;
_

と置き換えることができます

_std::vector<std::vector<shared_ptr<Tile>>> 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<EmptyTile>();  // or SnakeTile or...
_

共有ポインターは、使用されなくなったタイルの割り当てを解除します。それははるかに安全です。

つまり、これですべてです。長い質問に対する長い回答でごめんなさい。しかし、それが役に立てば幸いです。

2
Christophe

ここでの問題は、タイルの配列としてのFieldGridの概念だと思います。

UI、ASCIIディスプレイがゲーム自体に密接に結合されている、ゲームのクラシックな実装に制限されます

それでいいのであれば問題ありませんが、Snakeクラスは実際には必要ありません。ヘビはSnakeTilesのコレクションです。

よりモダンなデザインでは、ヘビと食べ物の位置を3Dフロートベクトルでモデル化し、モデルからUIの関連付けを解除して、タイル、特にEmptyTileの概念を無視できるようにします。

これにより、機能のリクエストで柔軟性を高め、蛇を斜めに動かすことができます! Zインデックスでヘビを作ろう!プレイエリアを無限大にするなど

0
Ewan