2009-12-26 5 views
7

Я написал рабочий клон tetris, но у него довольно грязный макет. Могу ли я получить обратную связь о том, как реструктурировать мои классы, чтобы улучшить кодирование. Я сосредотачиваюсь на том, чтобы сделать мой код как можно более общим, пытаясь сделать его более движимым для игр, используя только блоки.Тетрис: макет классов

Каждый блок создается отдельно в игре. В моей игре 2 BlockLists (связанные списки): StaticBlocks и Tetroid. StaticBlocks - это, очевидно, список всех не движущихся блоков, а tetroid - 4 блока текущего тетеида.

  1. В основном создается мир.

  2. Сначала новый tetroid (4 блоков в списке Tetroid) создается (NewTetroid)

  3. Столкновение обнаруживается с помощью функций (*** Collide), путем сравнения каждого из Tetroid со всеми из StaticBlocks с использованием функций (if *****).

  4. Когда тетеид останавливается (попадает в нижнюю часть/блоки), он копируется (CopyTetroid) в StaticBlocks и Tetroid становится пустым, тогда тесты проводятся для полных строк, блоки уничтожаются/удаляются и т. Д., Путем поиска StaticBlocks с (SearchY).

  5. Создан новый тетоид.

(TranslateTetroid) и (RotateTetroid) выполняют операции на каждом блоке в списке Tetroid один на один (я думаю, что это плохая практика).

(DrawBlockList) просто просматривает список, запустив функцию Draw() для каждого блока.

Вращение управляется установкой оси вращения относительно первого блока в Tetroid при вызове (NewTetroid). Моя функция вращения (поворот) для каждого блока вращает ее вокруг оси, используя вход + -1 для вращения влево/вправо. RotationModes и состояния для блоков, которые вращаются на 2 или 4 различных способах, определяя, в каком состоянии они находятся, и должны ли они поворачиваться влево или вправо. Я не доволен тем, как они определены в «Мире», но я не знаю, куда их поместить, сохраняя при этом мою функцию (Поворот) для каждого блока.

Мои следующие классы

class World 
{ 
    public: 
    /* Constructor/Destructor */ 
    World(); 
    ~World(); 

    /* Blocks Operations */ 
    void AppendBlock(int, int, BlockList&); 
    void RemoveBlock(Block*, BlockList&);; 

    /* Tetroid Operations */ 
    void NewTetroid(int, int, int, BlockList&); 
    void TranslateTetroid(int, int, BlockList&); 
    void RotateTetroid(int, BlockList&); 
    void CopyTetroid(BlockList&, BlockList&); 

    /* Draw */ 
    void DrawBlockList(BlockList&); 
    void DrawWalls(); 

    /* Collisions */ 
    bool TranslateCollide(int, int, BlockList&, BlockList&); 
    bool RotateCollide(int, BlockList&, BlockList&); 
    bool OverlapCollide(BlockList&, BlockList&); // For end of game 

    /* Game Mechanics */ 
    bool CompleteLine(BlockList&); // Test all line 
    bool CompleteLine(int, BlockList&); // Test specific line 
    void ColourLine(int, BlockList&); 
    void DestroyLine(int, BlockList&); 
    void DropLine(int, BlockList&); // Drops all blocks above line 

    int rotationAxisX; 
    int rotationAxisY; 
    int rotationState; // Which rotation it is currently in 
    int rotationModes; // How many diff rotations possible 

    private: 
    int wallX1; 
    int wallX2; 
    int wallY1; 
    int wallY2; 
}; 

class BlockList 
{ 
    public: 
    BlockList(); 
    ~BlockList(); 

    Block* GetFirst(); 
    Block* GetLast(); 

    /* List Operations */ 
    void Append(int, int); 
    int Remove(Block*); 
    int SearchY(int); 

    private: 
    Block *first; 
    Block *last; 
}; 

class Block 
{ 
    public: 
    Block(int, int); 
    ~Block(); 

    int GetX(); 
    int GetY(); 

    void SetColour(int, int, int); 

    void Translate(int, int); 
    void Rotate(int, int, int); 

    /* Return values simulating the operation (for collision purposes) */ 
    int IfTranslateX(int); 
    int IfTranslateY(int); 
    int IfRotateX(int, int, int); 
    int IfRotateY(int, int, int); 

    void Draw(); 

    Block *next; 

    private: 
    int pX; // position x 
    int pY; // position y 
    int colourR; 
    int colourG; 
    int colourB; 
}; 

Извините, если это немного неясно или долго наматывается, я просто искал какой-то реструктуризации помощи.

ответ

8
  • Что такое одного ответственность World класса? Это просто блок, содержащий практически все функции. Это не хороший дизайн. Одна очевидная ответственность - «представлять сетку, на которую размещаются блоки». Но это не имеет никакого отношения к созданию тетроидов или манипулированию списками блоков или рисунками. Фактически, большая часть этого, вероятно, вовсе не должна быть в классе. Я бы ожидал, что объект World будет содержать BlockList, который вы вызываете StaticBlocks, чтобы он мог определить сетку, на которой вы играете.
  • Почему вы определяете ваш собственный Blocklist? Вы сказали, что хотите, чтобы ваш код был общим, поэтому почему бы не позволить использовать любой контейнер? Почему я не могу использовать std::vector<Block>, если захочу? Или std::set<Block>, или какой-то домашний варочный контейнер?
  • Используйте простые имена, которые не дублируют информацию или не противоречат сами себе. TranslateTetroid не переводит tetroid. Он переводит все блоки в блок-лист. Так должно быть TranslateBlocks или что-то в этом роде. Но даже это лишнее. Мы можем видеть из подписи (требуется BlockList&), что она работает на блоках. Поэтому просто назовите это Translate.
  • Старайтесь избегать комментариев в стиле C (/*...*/). C++ - стиль (//..) ведет себя немного лучше, если вы используете компилятор C-стиля для всего блока кода, он сломается, если этот блок также содержит комментарии в стиле C. (В качестве простого примера /*/**/*/ не будет работать, поскольку в конце комментария компилятор увидит первый */, и поэтому последний */ не будет комментарий.
  • Что со всеми (без названия) int Параметры: это делает ваш код невозможным для чтения.
  • Уважайте языковые возможности и соглашения. Способ копирования объекта - это использовать его конструктор копирования. Поэтому вместо функции CopyTetroid дайте BlockList конструктор копирования. Тогда, если мне нужно Скопируйте один, я могу просто сделать BlockList b1 = b0.
  • Вместо void SetX(Y) и Y GetX() методы, сбросьте избыточное Get/Set pre исправить и просто иметь void X(Y) и Y X(). Мы знаем, что это геттер, потому что он не принимает никаких параметров и возвращает значение. И мы знаем, что другой - сеттер, потому что он принимает параметр и возвращает void.
  • BlockList не очень хорошая абстракция. У вас очень разные потребности в «текущем tetroid» и «списке статических блоков в настоящее время на сетке». Статические блоки могут быть представлены простой последовательностью блоков, как у вас (хотя последовательность строк или 2D-массив может быть более удобной), но в настоящее время активный тетеид нуждается в дополнительной информации, такой как центр вращения (который не относится к World).
    • Простым способом представления тетроида и облегчения поворотов может быть наличие блоков-членов, которые хранят простое смещение от центра вращения. Это упрощает вычисления вращения и означает, что блоки элементов не обязательно должны обновляться во время перевода. Только центр вращения должен быть перемещен.
    • В статическом списке это не так эффективно, чтобы блоки знали свое местоположение.Вместо этого сетка должна отображать местоположения в блоки (если я задаю сетку), какой блок существует в ячейке (5,8), он должен иметь возможность вернуть блок, но самому блоку не нужно сохранять координату. Если это так, может стать головной болью обслуживания.Что произойдет, если из-за какой-то тонкой ошибки два блока попадут в одну и ту же координату? Это может произойти, если блоки хранят свою собственную координату, но не если сетка содержит список того, где находится блок.)
    • это говорит нам, что нам нужно одно представление для «статического блока», а другое для «динамического блока» (ему нужно сохранить смещение от центра tetroid). Фактически, «статический» блок можно свернуть к основному: либо ячейка в сетке содержит блок, и этот блок имеет цвет, либо он не содержит блока. Нет никакого дальнейшего поведения, связанного с этими блоками, поэтому, возможно, это ячейка, в которую она помещена что sh вместо этого будет смоделировано.
    • и нам нужен класс, представляющий подвижный/динамический тетеид.
  • Поскольку многие ваши обнаружения столкновений являются «прогностическими», поскольку они касаются «что, если бы я переместил объект здесь», может быть проще реализовать не мутирующие функции перевода/вращения. Они должны оставлять исходный объект неизмененным и возвращать возвращенную/переведенную копию.

Итак, вот первый проход на ваш код, просто переименование, комментирование и удаление кода, не слишком сильно меняя структуру.

class World 
{ 
public: 
    // Constructor/Destructor 
    // the constructor should bring the object into a useful state. 
    // For that, it needs to know the dimensions of the grid it is creating, does it not? 
    World(int width, int height); 
    ~World(); 

    // none of thes have anything to do with the world 
    ///* Blocks Operations */ 
    //void AppendBlock(int, int, BlockList&); 
    //void RemoveBlock(Block*, BlockList&);; 

    // Tetroid Operations 
    // What's wrong with using BlockList's constructor for, well, constructing BlockLists? Why do you need NewTetroid? 
    //void NewTetroid(int, int, int, BlockList&); 

    // none of these belong in the World class. They deal with BlockLists, not the entire world. 
    //void TranslateTetroid(int, int, BlockList&); 
    //void RotateTetroid(int, BlockList&); 
    //void CopyTetroid(BlockList&, BlockList&); 

    // Drawing isn't the responsibility of the world 
    ///* Draw */ 
    //void DrawBlockList(BlockList&); 
    //void DrawWalls(); 

    // these are generic functions used to test for collisions between any two blocklists. So don't place them in the grid/world class. 
    ///* Collisions */ 
    //bool TranslateCollide(int, int, BlockList&, BlockList&); 
    //bool RotateCollide(int, BlockList&, BlockList&); 
    //bool OverlapCollide(BlockList&, BlockList&); // For end of game 

    // given that these functions take the blocklist on which they're operating as an argument, why do they need to be members of this, or any, class? 
    // Game Mechanics 
    bool AnyCompleteLines(BlockList&); // Renamed. I assume that it returns true if *any* line is complete? 
    bool IsLineComplete(int line, BlockList&); // Renamed. Avoid ambiguous names like "CompleteLine". is that a command? (complete this line) or a question (is this line complete)? 
    void ColourLine(int line, BlockList&); // how is the line supposed to be coloured? Which colour? 
    void DestroyLine(int line, BlockList&); 
    void DropLine(int, BlockList&); // Drops all blocks above line 

    // bad terminology. The objects are rotated about the Z axis. The x/y coordinates around which it is rotated are not axes, just a point. 
    int rotationAxisX; 
    int rotationAxisY; 
    // what's this for? How many rotation states exist? what are they? 
    int rotationState; // Which rotation it is currently in 
    // same as above. What is this, what is it for? 
    int rotationModes; // How many diff rotations possible 

private: 
    int wallX1; 
    int wallX2; 
    int wallY1; 
    int wallY2; 
}; 

// The language already has perfectly well defined containers. No need to reinvent the wheel 
//class BlockList 
//{ 
//public: 
// BlockList(); 
// ~BlockList(); 
// 
// Block* GetFirst(); 
// Block* GetLast(); 
// 
// /* List Operations */ 
// void Append(int, int); 
// int Remove(Block*); 
// int SearchY(int); 
// 
//private: 
// Block *first; 
// Block *last; 
//}; 

struct Colour { 
    int r, g, b; 
}; 

class Block 
{ 
public: 
    Block(int x, int y); 
    ~Block(); 

    int X(); 
    int Y(); 

    void Colour(const Colour& col); 

    void Translate(int down, int left); // add parameter names so we know the direction in which it is being translated 
    // what were the three original parameters for? Surely we just need to know how many 90-degree rotations in a fixed direction (clockwise, for example) are desired? 
    void Rotate(int cwSteps); 

    // If rotate/translate is non-mutating and instead create new objects, we don't need these predictive collision functions.x ½ 
    //// Return values simulating the operation (for collision purposes) 
    //int IfTranslateX(int); 
    //int IfTranslateY(int); 
    //int IfRotateX(int, int, int); 
    //int IfRotateY(int, int, int); 

    // the object shouldn't know how to draw itself. That's building an awful lot of complexity into the class 
    //void Draw(); 

    //Block *next; // is there a next? How come? What does it mean? In which context? 

private: 
    int x; // position x 
    int y; // position y 
    Colour col; 
    //int colourR; 
    //int colourG; 
    //int colourB; 
}; 

// Because the argument block is passed by value it is implicitly copied, so we can modify that and return it 
Block Translate(Block bl, int down, int left) { 
    return bl.Translate(down, left); 
} 
Block Rotate(Block bl, cwSteps) { 
    return bl.Rotate(cwSteps); 
} 

Теперь давайте добавим некоторые из недостающих частей:

Во-первых, мы должны представлять «динамические» блоки, tetroid владеющего им, и статические блоки или ячейки в сетке. (Мы также добавим простой метод «столкнувшегося» для мирового класса/сетки)

class Grid 
{ 
public: 
    // Constructor/Destructor 
    Grid(int width, int height); 
    ~Grid(); 

    // perhaps these should be moved out into a separate "game mechanics" object 
    bool AnyCompleteLines(); 
    bool IsLineComplete(int line); 
    void ColourLine(int line, Colour col);Which colour? 
    void DestroyLine(int line); 
    void DropLine(int); 

    int findFirstInColumn(int x, int y); // Starting from cell (x,y), find the first non-empty cell directly below it. This corresponds to the SearchY function in the old BlockList class 
    // To find the contents of cell (x,y) we can do cells[x + width*y]. Write a wrapper for this: 
    Cell& operator()(int x, int y) { return cells[x + width*y]; } 
    bool Collides(Tetroid& tet); // test if a tetroid collides with the blocks currently in the grid 

private: 
    // we can compute the wall positions on demand from the grid dimensions 
    int leftWallX() { return 0; } 
    int rightWallX() { return width; } 
    int topWallY() { return 0; } 
    int bottomWallY { return height; } 

    int width; 
    int height; 

    // let this contain all the cells in the grid. 
    std::vector<Cell> cells; 

}; 

// represents a cell in the game board grid 
class Cell { 
public: 
    bool hasBlock(); 
    Colour Colour(); 
}; 

struct Colour { 
    int r, g, b; 
}; 

class Block 
{ 
public: 
    Block(int x, int y, Colour col); 
    ~Block(); 

    int X(); 
    int Y(); 
void X(int); 
void Y(int); 

    void Colour(const Colour& col); 

private: 
    int x; // x-offset from center 
    int y; // y-offset from center 
    Colour col; // this could be moved to the Tetroid class, if you assume that tetroids are always single-coloured 
}; 

class Tetroid { // since you want this generalized for more than just Tetris, perhaps this is a bad name 
public: 
    template <typename BlockIter> 
    Tetroid(BlockIter first, BlockIter last); // given a range of blocks, as represented by an iterator pair, store the blocks in the tetroid 

    void Translate(int down, int left) { 
     centerX += left; 
     centerY += down; 
    } 
    void Rotate(int cwSteps) { 
     typedef std::vector<Block>::iterator iter; 
     for (iter cur = blocks.begin(); cur != blocks.end(); ++cur){ 
      // rotate the block (*cur) cwSteps times 90 degrees clockwise. 
        // a naive (but inefficient, especially for large rotations) solution could be this: 
     // while there is clockwise rotation left to perform 
     for (; cwSteps > 0; --cwSteps){ 
      int x = -cur->Y(); // assuming the Y axis points downwards, the new X offset is simply the old Y offset negated 
      int y = cur->X(); // and the new Y offset is the old X offset unmodified 
      cur->X(x); 
      cur->Y(y); 
     } 
     // if there is any counter-clockwise rotation to perform (if cwSteps was negative) 
     for (; cwSteps < 0; --cwSteps){ 
      int x = cur->Y(); 
      int y = -cur->X(); 
      cur->X(x); 
      cur->Y(y); 
     } 
     } 
    } 

private: 
    int centerX, centerY; 
    std::vector<Block> blocks; 
}; 

Tetroid Translate(Tetroid tet, int down, int left) { 
    return tet.Translate(down, left); 
} 
Tetroid Rotate(Tetroid tet, cwSteps) { 
    return tet.Rotate(cwSteps); 
} 

и мы должны повторно реализовать умозрительные проверки коллизий. Учитывая, не Mutating Перевести методы/Поворот, который прост: Мы просто создать повернутые/перевод копии, и проверить те, для столкновения:

// test if a tetroid t would collide with the grid g if it was translated (x,y) units 
if (g.Collides(Translate(t, x, y))) { ... } 

// test if a tetroid t would collide with the grid g if it was rotated x times clockwise 
if (g.Collides(Rotate(t, x))) { ... } 
+1

Это совершенно невероятно, именно то, что я хотел! Большое спасибо! – Ash

2

Я бы лично остановился на статических блоках и имел дело с ними в виде строк. Имея статический блок, вы сохраняете гораздо больше информации, чем вам нужно.

Мир состоит из рядов, представляющих собой массив одиночных квадратов. Квадраты могут быть пустыми или цветными (или расширять его, если у вас есть специальные блоки).

Мир также обладает одним активным блоком, как и сейчас. Класс должен иметь метод поворота и перевода. Очевидно, что блоку необходимо поддерживать ссылку на мир, чтобы определить, будет ли он сталкиваться с существующими кирпичами или краем доски.

Когда активный блок выходит из игры, он вызывает что-то вроде world.update(), которое добавит куски активного блока в соответствующие строки, очистит все полные строки, решит, потеряли ли вы и т. Д. и, наконец, при необходимости создать новый активный блок.

Смежные вопросы