我已经写了一个工作正常的俄罗斯方块克隆,但是它的布局很乱。我能否获得有关如何重组我的类(class)以使我的编码更好的反馈。我致力于使我的代码尽可能通用,试图使它成为仅使用块的游戏引擎。

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

  • 通常,将创建一个世界。
  • 首先由(NewTetroid)创建一个新的tetroid(列表Tetroid中的4个块)
  • (*** Collide)函数通过使用(If *****)函数将每个Tetroid与所有StaticBlocks进行比较来检测冲突。
  • 当tetroid停止(击中底部/块)时,将其复制(CopyTetroid)到StaticBlocks,并将Tetroid设为空,然后测试完整行,通过使用(SearchY)搜索StaticBlocks来破坏/丢弃块等。
  • 创建了一个新的tetroid。

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

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

    当调用(NewTetroid)时,通过设置相对于Tetroid中第一个程序段的旋转轴来控制旋转。我的每个块的旋转功能(Rotate)都使用输入+ -1左右旋转,使其绕轴旋转。 RotationModes和State用于以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;
    };
    

    抱歉,如果这还不清楚或太麻烦了,我只是在寻找一些重组的帮助。

    最佳答案

  • World类的唯一职责是什么?
    这只是包含几乎所有功能的Blob。那不是一个好的设计。一个明显的责任是“代表放置块的网格”。但这与创建类曲面或操作块列表或图形无关。实际上,大多数可能根本不需要上课。我希望World对象包含您称为StaticBlocks的BlockList,以便它可以定义正在播放的网格。
  • 为什么要定义自己的Blocklist?您说过希望代码通用,那么为什么不使用任何容器呢?如果需要,为什么不能使用std::vector<Block>?还是std::set<Block>,还是一些自制的容器?
  • 使用简单名称,这些名称不会重复信息或彼此矛盾。 TranslateTetroid不会翻译tetroid。它转换了阻止列表中的所有块。所以应该是TranslateBlocks或其他名称。但这甚至是多余的。从签名(它需要BlockList&)中我们可以看到它可以在块上工作。因此,只需将其称为Translate即可。
  • 尝试避免使用C样式的注释(/*...*/)。 C++样式(//..)表现得更好,因为如果您在整个代码块中使用C样式注释,则如果该代码块也包含C样式注释,它就会中断。 (作为一个简单的示例,/*/**/*/将不起作用,因为编译器将第一个*/作为注释的结尾,因此最后一个*/将不被视为注释。
  • 所有(未命名)int参数有什么用?这使您的代码无法阅读。
  • 尊重语言功能和约定。复制对象的方法是使用其复制构造函数。因此,给CopyTetroid一个副本构造函数,而不是BlockList函数。然后,如果需要复制一个,只需执行BlockList b1 = b0即可。
  • 删除冗余的Get/Set前缀,而不是void SetX(Y)Y GetX()方法,仅使用void X(Y)Y X()即可。我们知道这是一个 setter/getter ,因为它不带任何参数并返回一个值。而且我们知道另一个是setter,因为它需要一个参数并返回void。
  • BlockList不是一个很好的抽象。您对“当前tetroid”和“当前在网格上的静态块列表”有非常不同的需求。静态块可以用简单的块序列表示(尽管更方便的是行序列或2D数组),但是当前 Activity 的拟变节需要其他信息,例如旋转中心(不属于World)。
  • 表示四面体并简化旋转的一种简单方法可能是使成员块存储一个相对于旋转中心的简单偏移量。这使旋转更易于计算,并且意味着在翻译期间完全不必更新成员块。只需旋转中心即可。
  • 在静态列表中,阻止块知道其位置甚至效率不高。相反,网格应该将位置映射到块(如果我问网格“在单元格(5,8)中存在哪个块,它应该能够返回该块。但是该块本身不需要存储坐标。如果存在,则它会存储该坐标如果由于一些细微的错误而导致两个块最终以相同的坐标结尾怎么办?如果块存储了自己的坐标会发生这种情况,但是如果网格中保存了哪个块所在的列表则不会发生这种情况。)
  • 这告诉我们,对于“静态块”,我们需要一个表示形式;对于“动态块”,我们需要另一个表示形式(它需要存储距拟锥中心的偏移量)。实际上,“静态”块可以归结为以下要点:网格中的单元格包含一个块,并且该块具有颜色,或者不包含一个块。这些块没有进一步的行为,因此也许应该对它所在的单元进行建模。
  • ,我们需要一个表示可动/动态拟变体的类。
  • 因为您的许多碰撞检测都是“预测性的”,因为它处理“如果我将对象移到此处会发生什么”,因此实现不变的平移/旋转功能可能会更简单。这些应该使原始对象保持不变,并返回旋转/翻译的副本。

  • 因此,这是您代码的第一步,您无需更改太多结构即可简单地重命名,注释和删除代码。
    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);
    }
    

    现在,让我们添加一些缺失的部分:

    首先,我们需要表示“动态”块,拥有它们的四面体以及网格中的静态块或单元。
    (我们还将向world/grid类添加一个简单的“Collides”方法)
    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))) { ... }
    

    关于c++ - 俄罗斯方块:类的布局,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/1964007/

    10-10 06:45