2009-12-26 102 views
7

我写了一个工作俄罗斯方块克隆,但它有一个相当凌乱的布局。我能否获得关于如何重组我的课程以使我的编码更好的反馈。我专注于尽可能地使我的代码尽可能通用,并试图让它成为仅使用块的游戏的引擎。俄罗斯方块:类的布局

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

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

  2. 首先新tetroid(在列表Tetroid 4块)由(NewTetroid)创建

  3. 碰撞由(***碰撞)功能检测到的,由每个Tetroid与所有的比较使用(If *****)函数的StaticBlocks。当tetroid停止时(命中底部/块),它被复制(CopyTetroid)到StaticBlocks并且Tetroid被设置为空,然后通过搜索StaticBlocks来完成测试,完成所有行,块被销毁/丢弃等等。 (SearchY)。

  4. 创建新的tetroid。

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

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

当调用(NewTetroid)时,通过相对于Tetroid中的第一个块设置旋转轴来控制旋转。我的旋转功能(旋转)为每个块围绕轴旋转,使用输入+ -1进行左/右旋转。 RotationModes和States用于以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类的责任? 这只是一个包含几乎所有功能的blob。这不是很好的设计。一个明显的责任是“代表块放置在其上的网格”。但是这与创建tetroids或操作块列表或绘图无关。事实上,其中大部分可能根本不需要在课堂上。我期望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
  • 不是void SetX(Y)Y GetX()方法,删除多余的get/set前修复和简单地有void X(Y)Y X()。我们知道这是一个getter,因为它不需要参数并返回一个值。而我们知道另一个是setter,因为它需要一个参数并返回void。
  • BlockList不是一个很好的抽象。您对“当前tetroid”和“当前在网格上的静态块列表”有着非常不同的需求。静态块可以用一个简单的块序列来表示(虽然一系列的行或2D数组可能更方便),但是当前活动的tetroid需要附加信息,例如旋转中心(其中不属于World)。
    • 表示tetroid并简化旋转的简单方法可能是让成员块存储距旋转中心的简单偏移量。这使得旋转更容易计算,并且意味着在翻译期间不必更新成员块。旋转的中心必须移动。
    • 在静态列表中,对于块知道它们的位置,效率并不高。相反,网格应该将位置映射到块(如果我询问网格“单元格(5,8)中存在哪个块,它应该能够返回块,但块本身不需要存储坐标,如果它确实存在,可能会成为一个维护头痛,如果由于一些细微的错误,两个块最终具有相同的坐标?如果块存储它们自己的坐标,会发生这种情况,但如果网格包含哪个块的列表,则不会发生。)
    • 这告诉我们,我们需要一个“静态块”的表示,另一个表示“动态块”(它需要存储从tetroid中心的偏移量)。事实上,“静态”块可以被降低要点:网格中的一个单元格包含一个块,并且该块有一个颜色,或者它不包含一个块。没有进一步的行为与这些块关联,因此它可能是它放置的单元格那个sh应该改为建模。
    • 我们需要一个表示可移动/动态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); 
} 

,我们需要重新实现投机碰撞检测。鉴于非不同诱变平移/旋转的方法,说起来很简单:我们只需创建旋转/翻译件,并测试那些碰撞:

// 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 2009-12-28 15:11:27

2

我个人将静态块丢弃并作为行进行处理。有一个静态块,你保存了比你需要更多的信息。

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

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

当活动模块退出游戏时,它会调用类似world.update()的函数,它会将活动模块的各部分添加到相应的行中,清除所有完整行,决定是否丢失等等,最后根据需要创建一个新的活动块。