【问题标题】:Tetris: Layout of Classes俄罗斯方块:类的布局
【发布时间】:2010-12-30 03:47:24
【问题描述】:

我已经编写了一个有效的俄罗斯方块克隆,但它的布局非常混乱。我能否获得有关如何重组我的课程以使我的编码更好的反馈。我专注于让我的代码尽可能通用,试图让它更像是一个只使用块的游戏的引擎。

每个方块都是在游戏中单独创建的。 我的游戏有 2 个 BlockLists(链表):StaticBlocks 和 Tetroid。 StaticBlocks 显然是所有不动块的列表,而 tetroid 是当前 tetroid 的 4 个块。

  1. 主要是创建一个世界。

  2. 首先由 (NewTetroid) 创建一个新的 tetroid(Tetroid 列表中有 4 个块)

  3. 通过 (***Collide) 函数检测碰撞,通过使用 (If*****) 函数将每个 Tetroid 与所有静态块进行比较。

  4. 当 tetroid 停止(击中底部/块)时,它被复制 (CopyTetroid) 到 StaticBlocks 并且 Tetroid 为空,然后通过搜索 StaticBlocks 对完整行进行测试,块被破坏/丢弃等与 (SearchY)。

  5. 创建了一个新的 tetroid。

(TranslateTetroid) 和 (RotateTetroid) 对 Tetroid 列表中的每个块逐个执行操作(我认为这是不好的做法)。

(DrawBlockList) 只是遍历一个列表,为每个块运行 Draw() 函数。

当调用 (NewTetroid) 时,通过设置相对于 Tetroid 中第一个块的旋转轴来控制旋转。我的每个块的旋转功能(旋转)围绕轴旋转,使用输入 +-1 进行左/右旋转。 RotationModes 和 States 用于以 2 或 4 种不同方式旋转的块,定义它们当前处于什么状态,以及它们应该向左还是向右旋转。 我对这些在“世界”中的定义方式不满意,但我不知道将它们放在哪里,同时我的 (Rotate) 函数仍然对每个块保持通用

我的课如下

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;
};

抱歉,如果这有点不清楚或啰嗦,我只是在寻求重组方面的帮助。

【问题讨论】:

    标签: c++ class linked-list tetris


    【解决方案1】:
    • World 类的单一职责是什么? 它只是一个包含几乎所有功能的 blob。这不是好的设计。一个明显的责任是“代表放置块的网格”。但这与创建 tetroids 或操纵黑名单或绘图无关。事实上,大部分内容可能根本不需要在课堂上。我希望 World 对象包含您调用 StaticBlocks 的 BlockList,以便它可以定义您正在播放的网格。
    • 为什么要定义自己的Blocklist?你说你希望你的代码是通用的,那么为什么不允许使用 any 容器呢?如果我愿意,为什么不能使用std::vector<Block>?或者std::set<Block>,或者一些自制的容器?
    • 使用不重复信息或自相矛盾的简单名称。 TranslateTetroid 不翻译 tetroid。它翻译阻止列表中的所有块。所以应该是TranslateBlocks 什么的。但即使这样也是多余的。我们可以从签名(它需要一个BlockList&)中看到它适用于块。所以就叫它Translate吧。
    • 尽量避免使用 C 风格的 cmets (/*...*/)。 C++ 风格 (//..) 的表现要好一些,因为如果您使用 C 风格注释掉整个代码块,如果该块还包含 C 风格 cmets,它将中断。 (举个简单的例子,/*/**/*/ 不起作用,因为编译器会将第一个 */ 视为注释的结尾,因此最后一个 */ 不会被视为注释。
    • 所有(未命名的)int 参数是怎么回事?它使您的代码无法阅读。
    • 尊重语言特性和约定。复制对象的方法是使用其复制构造函数。因此,不要给CopyTetroid 函数,而是给BlockList 一个复制构造函数。然后如果我需要复制一个,我可以简单地做BlockList b1 = b0
    • 而不是void SetX(Y)Y GetX() 方法,删除冗余的Get/Set 前缀并简单地使用void X(Y)Y X()。我们知道它是一个 getter,因为它不接受参数并返回一个值。我们知道另一个是 setter,因为它接受一个参数并返回 void。
    • BlockList 不是一个很好的抽象。您对“当前 tetroid”和“当前网格上的静态块列表”有非常不同的需求。静态块可以用一个简单的块序列来表示(尽管行序列或二维数组可能更方便),但当前活动的 tetroid 需要额外的信息,例如旋转中心(不属于World)。
      • 表示四面体和简化旋转的一种简单方法可能是让成员块存储与旋转中心的简单偏移量。这使得旋转更容易计算,并且意味着成员块在翻译过程中根本不需要更新。只需移动旋转中心即可。
      • 在静态列表中,块知道它们的位置甚至效率不高。相反,网格应该将位置映射到块(如果我问网格“单元格(5,8) 中存在哪个块,它应该能够返回块。但块本身不需要存储坐标。如果它确实, 这可能会成为维护的难题。如果由于一些细微的错误,两个块最终具有相同的坐标怎么办?如果块存储自己的坐标,则可能会发生这种情况,但如果网格保存了哪个块在哪里的列表,则不会发生这种情况。 )
      • 这告诉我们,我们需要一种表示“静态块”,另一种表示“动态块”(它需要存储距 tetroid 中心的偏移量)。事实上,“静态”块可以归结为基本要素:网格中的一个单元格包含一个块,并且该块具有颜色,或者它不包含块。没有与这些块相关联的其他行为,因此可能应该对放置它的单元进行建模。
      • 我们需要一个表示可移动/动态 tetroid 的类。
    • 由于您的许多碰撞检测都是“预测性的”,因为它处理“如果我将对象移到此处会怎样”,因此实现非变异平移/旋转功能可能更简单。这些应该使原始对象保持不变,并返回旋转/翻译的副本。

    因此,这是您的代码的第一次传递,只需重命名、注释和删除代码,而无需过多更改结构。

    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);
    }
    

    我们需要重新实现推测性碰撞检查。给定非变异的 Translate/Rotate 方法,这很简单:我们只需创建旋转/平移的副本,并测试它们是否发生碰撞:

    // 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))) { ... }
    

    【讨论】:

    • 这绝对令人难以置信,正是我想要的!非常感谢!
    【解决方案2】:

    我会亲自抛弃静态块并将它们作为行处理。拥有一个静态块,您会保留比您需要的更多的信息。

    世界是由行组成的,行是由单个正方形组成的数组。方块可以是空的,也可以是颜色(如果你有特殊的块,也可以扩展它)。

    世界也拥有一个活动方块,就像你现在拥有的一样。该类应该有一个旋转和翻译方法。该块显然需要保持对世界的引用,以确定它是否会与现有的砖块或棋盘边缘发生碰撞。

    当活动块停止运行时,它会调用诸如 world.update() 之类的东西,它将活动块的碎片添加到适当的行,清除所有完整的行,决定你是否丢失了,等等,并且如果需要,最后创建一个新的活动块。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-07-28
      • 1970-01-01
      • 1970-01-01
      • 2013-03-17
      • 1970-01-01
      • 2018-06-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多