俄罗斯方块:类的布局

发布于 2024-08-16 06:35:20 字数 3184 浏览 4 评论 0原文

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

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

  1. 在 main 中创建了一个 World。

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

  3. 通过以下方式检测到碰撞(***Collide) 函数,通过使用 (If*****) 函数将每个 Tetroid 与所有 StaticBlock 进行比较。

  4. 当 tetroid 停止(到达底部/块)时,它被复制(CopyTetroid)到 StaticBlocks 并将 Tetroid 清空,然后通过使用(SearchY)搜索 StaticBlocks 来测试完整的行,块被销毁/丢弃等)。

  5. 创建了一个新的 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;
};

抱歉,如果这有点不清楚或冗长,我只是在寻求一些帮助重组。

I've written a working tetris clone but it has a pretty messy layout. Could I please get feedback on how to restructure my classes to make my coding better. I focuses on making my code as generic as possible, trying to make it more an engine for games only using blocks.

Each block is created seperately in the game.
My game has 2 BlockLists (linked lists): StaticBlocks and Tetroid.
StaticBlocks is obviously the list of all non-moving blocks, and tetroid are the 4 blocks of the current tetroid.

  1. In main a World is created.

  2. First a new tetroid (4 blocks in a list Tetroid) is created by (NewTetroid)

  3. Collision is detected by the (***Collide) functions, by comparing each of Tetroid with all of the StaticBlocks using the (If*****) functions.

  4. When the tetroid stops (hits the bottom/blocks), it is copied (CopyTetroid) to the StaticBlocks and Tetroid is made empty, then tests are made for complete lines, blocks are destroyed/dropped etc by searching StaticBlocks with (SearchY).

  5. A new tetroid is created.

(TranslateTetroid) and (RotateTetroid) perform operations on each block in the Tetroid list one by one ( I think this is bad practise).

(DrawBlockList) just goes through a list, running the Draw() function for each block.

Rotation is controlled by setting rotation axis relative to the first block in Tetroid when (NewTetroid) is called. My rotation function (Rotate) for each block rotates it around the axis, using an input +-1 for left/right rotation. RotationModes and States are for blocks that rotate in 2 or 4 different ways, defining what state they are currently in, and whether they should be rotated left or right. I am not happy with how these are defined in "World", but I don't know where to put them, whilst still keeping my (Rotate) function generic for each block.

My classes are as follows

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

Sorry if this is a bit unclear or long winded, I'm just looking for some help restructuring.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

海未深 2024-08-23 06:35:20
  • World 类的单一职责是什么?
    它只是一个包含几乎所有功能的 blob。这不是好的设计。一项明显的职责是“代表放置块的网格”。但这与创建 tetroids 或操作块列表或绘图无关。事实上,其中大部分可能根本不需要在课堂上进行。我希望 World 对象包含您称为 StaticBlocks 的 BlockList,以便它可以定义您正在玩的网格。
  • 为什么要定义自己的阻止列表?您说您希望您的代码是通用的,那么为什么不允许使用任何容器呢?如果我愿意,为什么不能使用 std::vector ?或者一个 std::set,或者一些自制的容器?
  • 使用不会重复信息或相互矛盾的简单名称。 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)YX() 我们知道它是一个 getter,因为它不带参数并返回一个值,而且我们知道另一个是 setter。因为它需要一个参数并返回 void。
  • BlockList 不是一个很好的抽象。您对“当前 tetroid”和“当前网格上的静态块列表”有非常不同的需求。静态块可以用简单的块序列来表示(尽管行序列或二维数组可能更方便),但当前活动的 tetroid 需要附加信息,例如旋转中心(其中不属于世界)。
    • 表示 tetroid 并简化旋转的一种简单方法可能是让成员块存储距旋转中心的简单偏移量。这使得旋转更容易计算,并且意味着成员块在转换过程中根本不需要更新。只需移动旋转中心即可。
    • 在静态列表中,块甚至无法有效地知道它们的位置。相反,网格应该将位置映射到块(如果我询问网格“单元格 (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);
}

并且我们需要重新实现推测碰撞检查。考虑到非变异的平移/旋转方法,这很简单:我们只需创建旋转/平移的副本,并测试它们的碰撞:

// 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))) { ... }
  • What is the single responsibility of the World class?
    It's just a blob containing practically every kind of functionality. That's not good design. One obvious responsibility is "represent the grid onto which blocks are placed". But that has nothing to do with creating tetroids or manipulating block lists or drawing. In fact, most of that probably doesn't need to be in a class at all. I would expect the World object to contain the BlockList you call StaticBlocks so it can define the grid on which you're playing.
  • Why do you define your own Blocklist? You said you wanted your code to be generic, so why not allow any container to be used? Why can't I use a std::vector<Block> if I want to? Or a std::set<Block>, or some home-brewed container?
  • Use simple names that don't duplicate information or contradict themselves. TranslateTetroid doesn't translate a tetroid. It translates all the blocks in a blocklist. So it should be TranslateBlocks or something. But even that is redundant. We can see from the signature (it takes a BlockList&) that it works on blocks. So just call it Translate.
  • Try to avoid C-style comments (/*...*/). C++-style (//..)behaves a bit nicer in that if you use a C-style comment out an entire block of code, it'll break if that block also contained C-style comments. (As a simple example, /*/**/*/ won't work, as the compiler will see the first */ as the end of the comment, and so the last */ won't be considered a comment.
  • What's with all the (unnamed) int parameters? It's making your code impossible to read.
  • Respect language features and conventions. The way to copy an object is using its copy constructor. So rather than a CopyTetroid function, give BlockList a copy constructor. Then if I need to copy one, I can simply do BlockList b1 = b0.
  • Rather than void SetX(Y) and Y GetX() methods, drop the redundant Get/Set prefix and simply have void X(Y) and Y X(). We know it's a getter because it takes no parameters and returns a value. And we know the other one is a setter because it takes a parameter and returns void.
  • BlockList isn't a very good abstraction. You have very different needs for "the current tetroid" and "the list of static blocks currently on the grid". The static blocks can be represented by a simple sequence of blocks as you have (although a sequence of rows, or a 2D array, may be more convenient), but the currently active tetroid needs additional information, such as the center of rotation (which doesn't belong in the World).
    • A simple way to represent a tetroid, and to ease rotations, might be to have the member blocks store a simple offset from the center of rotation. That makes rotations easier to compute, and means that the member blocks don't have to be updated at all during translation. Just the center of rotation has to be moved.
    • In the static list, it isn't even efficient for blocks to know their location. Instead, the grid should map locations to blocks (if I ask the grid "which block exists in cell (5,8), it should be able to return the block. but the block itself doesn't need to store the coordinate. If it does, it can become a maintenance headache. What if, due to some subtle bug, two blocks end up with the same coordinate? That can happen if blocks store their own coordinate, but not if the grid holds a list of which block is where.)
    • this tells us that we need one representation for a "static block", and another for a "dynamic block" (it needs to store the offset from the tetroid's center). In fact, the "static" block can be boiled down to the essentials: Either a cell in the grid contains a block, and that block has a colour, or it does not contain a block. There is no further behavior associated with these blocks, so perhaps it is the cell into which it is placed that should be modelled instead.
    • and we need a class representing a movable/dynamic tetroid.
  • Since a lot of your collision detection is "predictive" in that it deals with "what if I moved the object over here", it may be simpler to implement non-mutating translation/rotation functions. These should leave the original object unmodified, and a rotated/translated copy returned.

So here's a first pass on your code, simply renaming, commenting and removing code without changing the structure too much.

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

Now, let's add some of the missing pieces:

First, we'll need to represent the "dynamic" blocks, the tetroid owning them, and the static blocks or cells in a grid.
(We'll also add a simple "Collides" method to the world/grid class)

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

and we'll need to re-implement the speculative collision checks. Given the non-mutating Translate/Rotate methods, that is simple: We just create rotated/translated copies, and test those for collision:

// 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))) { ... }
夏花。依旧 2024-08-23 06:35:20

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

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

世界也拥有一个活跃的区块,就像你现在一样。该类应该有一个旋转和平移方法。该块显然需要维护对世界的引用,以确定它是否会与现有的砖块或板的边缘发生碰撞。

当活动块停止运行时,它将调用类似 world.update() 的方法,它将活动块的各个部分添加到适当的行,清除所有完整行,确定是否丢失等,最后创建一个如果需要的话,新的活动块。

I would personally ditch the static blocks and deal with them as rows. Having a static block you are keeping a lot more information than you need.

A world is made of rows, which is an array of single squares. The squares can be either empty, or a color (or extend it if you have special blocks).

The world also owns a single active block, as you have now. The class should have a rotate and translate method. The block will obviously need to maintain a reference to the world to determine if it will collide with existing bricks or the edge of the board.

When the active block goes out of play, it will call something like world.update() which will add the pieces of the active block to the appropriate rows, clear all full rows, decide if you have lost, etc, and finally create a new active block if needed.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文