Skip to content

Esenthel-Book-Tetris

Hơn một tuần qua tôi đã tìm hiểu các chức năng căn bản của của Esenthel Engine (Book-Esenthel-Basics) và một ít về C++ được sử dụng trong game engine này (Book-Esenthel-2D-Basics)

1 Introduction

Bây giờ đã đến lúc áp dụng các kiên thức vào thực hiện một game hoàn chỉnh từ A-Z

1.1 Setup

Esenthel có thể tạo các Application (Giống Blue Folder) và Library (Green Folder).

Tác giả của Esenthel thì không recommend tạo các Library, nhưng chúng ta có thể sử dụng nó để test các game, đặc biệt là beginner như tôi.

Ví dụ như tôi tạo Game là Tetris (tức là Application-Tetris), tôi muốn làm một vài test với tạo thêm một Application-Tester. Thay vì phải xoá nhiều Code và Class file đã được tạo trong Application-Tetris để thử nghiệm các tính năng mới. Tôi sử dụng Application-Tester để thử nghiệm.

Nhưng để chạy Tester thì cần phải có các đoạn code khác, tôi phải copy từ Tetris sang Tester. Điều này tốn thời gian, vì vậy nhưng đoạn code mà sử dụng chung cho cả hai Applications này thì tôi bỏ vào Library (Green Folder), nó sẽ tương đương với Esenthel Engine (Tên mới là Titan Engine - tôi không thích tên mới này lắm, vì nó quá là generic). Như vậy khi nào các code liên quan đến class mà không cần thay đổi gì nữa thì cho vào Library.

1.2 Game constant

Đầu tiên là tạo một code là constants để liệt kê tất cả các constants mà sẽ được sử dụng cho game. Chúng sẽ được cập nhật dần dần trong lúc làm game.

Nguyên tắc chung để đặt tên của constants là IN HOA tên của chúng. Khi khai báo thì phải có const phía trước, hoặc viết tắt là C

1.3 Game enumerations

Trong Tetris library tạo thêm một code có tên là enumerations. Đây là bảng liệt kê của BLOCK_TYPE, như hình chữ L, T, …

enum BLOCK_TYPE
{
   BT_SQUARE      ,
   BT_T           ,
   BT_L;          ,
   BT_BACKWARDS_L ,
   BT_STRAIGHT    ,
   BT_S           ,
   BT_BACKWARDS_S ,
   BT_NUM         , // Number of block types in the game
   BT_BACKGROUND  , //
   BT_WALL        ,
}

BT_NUM được sử dụng để dễ dàng quản lý hoặc tạo các Random Type ví dụ như

blockType type = Random(BT_NUM);

Tức là nó sẽ cho giá trị Random từ BT_SQUARE cho đến BT_BACKWARD_S

2 Game objects

Sau khi tạm xác định mốt số constants cho game thì cần tạo các objects bao gồm các nhân vật cho game. Trong Tetris thì nhân vật chính là các blocks, wall

Mỗi objects sẽ tương đương với một class để dễ dàng quản lý và tạo các functions cho class đó, vì chúng đều có các đặc tình riêng biệt.

2.1 Squares

Bây giờ tạo một codesquare đại diện cho class square trong library
Điểm khác biệt của game này là square, vì các objects không di chuyển WASD từng step nhỏ là đi theo một block. Vì vậy phải có khai báo về các di chuyển trong game.

Thông thường sử dụng Vec2 để mô tả position của game, theo dạng float, nhưng Tetris thì cần dùng integerVecI2

Trong class này cần tạo các functions như:

  1. create()
  2. move()
  3. getPosition
  4. setPosition
  5. draw()

Dàn ý cho class square

class square // class square of Tetris Game
{
private: // thuộc tính riêng của từng block là position, type
   VecI2       pos; // sử dụng Vecto integer cho pos vì game di chuyển theo từng block
   BLOCK_TYPE type; // sử dụng BLOCK_TYPE enum được tạo trong enumerations

public: // tạo functions // trong public không dùng ; tại cuối hàng
   void create (C VecI2& pos, BLOCK_TYPE type)// Create blocks Tetris
   {
      T.pos = pos; // local pos in private= pos in argument
      T.type = type; // local type in private = type from argument
   } 
   // sử dụng `reference` trong create() cho pos chứ không cho type
   // vì pos sẽ thay đổi liên tục, sử dụng reference sẽ cải thiện tốc độ cho game
   // còn type thì type sẽ không thay đổi


   void move(DIRECTION dir)// Move blocks in Tetris based on Direction
   {
      switch(dir)
      {
         case D_LEFT : pos.x--; break;
         case D_RIGHT : pos.x++; break;
         case D_DOWN : pos.y--; break; // vì VecI2 chỉ có từ 0 đến vô cùng
      }                                // nên khi nút down sẽ hạ thấp y
   } 
   VecI2 getPos ()// Get position of blocks, return VecI2
   {
      return pos;
   } 
   void setPos (C VecI2& pos)// set block position
   {
      T.pos = pos; // sử dụng this local pos =  pos in nargument
   } 
   void draw()
   {
      Color color(BLACK);

      switch(type)
      {
         case BT_SQUARE     : color = RED   ; break;
         case BT_T          : color = PURPLE; break;
         case BT_L          : color = GREY  ; break;
         case BT_BACKWARDS_L: color = BLUE  ; break;
         case BT_STRAIGHT   : color = GREEN ; break;
         case BT_S          : color = PINK  ; break;
         case BT_BACKWARDS_S: color = YELLOW; break;
         case BT_BACKGROUND : color = Color(50, 50, 50) ; break;
         case BT_WALL       : color = WHITE ; break;
      }

      Rect r(GAMEAREA + (pos * SQUARE_SIZE), GAMEAREA + (pos * SQUARE_SIZE) + SQUARE_SIZE);
      Images(UID(4160076518, 1164279343, 1870342572, 3050302410)).draw(color, TRANSPARENT, r);
   }

}

Chú ý trong create() có sử dụng constant reference. Được giải thích như sau:

  1. Sử dụng reference đơn giản vì tiết kiệm bộ nhớ. Nếu không thì khi pass pos đến variable khác thì sẽ tạo một copy.
  2. Nhưng tại sao lại dùng C constant cho reference ? Vì chúng ta chỉ muốn đây là read-only pos khi create() chứ không muốn nó thay đổi. Compiler sẽ giúp tránh trường hợp modification

Ngoài ra cần chú ý đến hệ toạ độ. Vì sử dụng VecI2 tức là integer nên hệ tộ độ ở (phía dưới, góc trái) sẽ là (0,0). Bây giờ (0,0) không cần là trung tâm màn hình như khi sử dụng Vec2 float nữa.

Bây giờ sử dụng code trong Application Tester để test class mình vừa tạo

Memc<square> squares; // tạo một containter thuộc class square

void InitPre()
{
   INIT();
}

bool Init()
{
   // Test thử square class
   squares.New().create(VecI2(2, 2), BT_T);
   squares.New().create(VecI2(4, 2), BT_S);
   squares.New().create(VecI2(6, 7), BT_L);
   squares.New().create(VecI2(10, 2), BT_BACKWARDS_S);
   squares.New().create(VecI2(8, 8), BT_BACKWARDS_L);
   squares.New().create(VecI2(5, 1), BT_SQUARE);
   return true;
}

void Shut() {}

bool Update()
{
   if(Kb.bp(KB_ESC)) return false;

   // update input cách di chuyển của blocks
   DIRECTION d = D_NONE;
   if(Kb.bp(KB_DOWN)) d = D_DOWN;
   if(Kb.bp(KB_LEFT)) d = D_LEFT;
   if(Kb.bp(KB_RIGHT)) d = D_RIGHT;

   // update move() theo input Direction
   REPA(squares)
   {
      squares[i].move(d);
   }

   if(Kb.bp(KB_SPACE))
   {
      REPA(squares)
      {
         VecI2 pos = squares[i].getPos();
         pos.y +=4;
         squares[i].setPos(pos);
      }
   }
   return true;
}

void Draw()
{
   D.clear(BLACK);
   REPA(squares)
   {
      squares[i].draw();
   }

}

Kết quả như sau

Hiện tại game chỉ có khả năng tạo các squares chứ không thể tạo các blocks hình L, T, U, S.. vì trong class này chỉ có squares mặc dù có khai báo các TYPE khác nhau, nhưng chúng chỉ cho phép khác nhau về màu sắc.

Vì vậy cần phải tạo một class khác cho phép sử dụng kết quả của class square này để ghép thành các hình blocks khác nhau dựa trên Type đã khai báo.

2.2 Blocks

Bây giờ tạo một class blocks tức là hình dạng của các blocks như chữ L, T,… dựa theo sự kết hợp của các khối square từ class class. Trong class này cũng có tất cả functions như trong squares nhưng có thêm hình dạng

Trong private class, khai báo các functions tạo hình dạng của blocks.
Nguyên tắt chung để tạo các shape như L, S, T… như sau:

  • 1 Shape gồm 4 squares
  • Giả sử square đầu tiên có pos là (0,0)
  • 3 squares còn lại dựa trên pos của square đầu tiên
  • các squares còn lại được tạo dựa trên class function create() của classSquare vừa được tạo ở trên.
  • class block
    {
    private:
    
       VecI2 pos;
       BLOCK_TYPE type;
    
       Mems<square> squares;
    
       void setupSquares()
       {
          squares.clear();
    
          switch(type)
          {
             case BT_SQUARE     : makeSquareBlock    (); break;
             case BT_T          : makeTBlock         (); break;
             case BT_L          : makeLBlock         (); break;
             case BT_BACKWARDS_L: makeBackwardsLBlock(); break;
             case BT_STRAIGHT   : makeStraightBlock  (); break;
             case BT_S          : makeSBlock         (); break;
             case BT_BACKWARDS_S: makeBackwardsSBlock(); break;  
          }
       }
    
       void makeSquareBlock()
       {
          // [0][2]
          // [1][3]
          squares.New().create(VecI2(pos.x    ,  pos.y    ), BT_SQUARE);
          squares.New().create(VecI2(pos.x    ,  pos.y - 1), BT_SQUARE);
          squares.New().create(VecI2(pos.x + 1,  pos.y    ), BT_SQUARE);
          squares.New().create(VecI2(pos.x + 1,  pos.y - 1), BT_SQUARE);
       }
    
       void makeTBlock()
       {
          //    [1]
          // [2][0][3]
          squares.New().create(VecI2(pos.x    , pos.y    ), BT_T);
          squares.New().create(VecI2(pos.x    , pos.y + 1), BT_T);
          squares.New().create(VecI2(pos.x - 1, pos.y    ), BT_T);
          squares.New().create(VecI2(pos.x + 1, pos.y    ), BT_T);
       }
    
       void makeLBlock()
       {
          // [2]
          // [1]
          // [0][3]
          squares.New().create(VecI2(pos.x    , pos.y    ), BT_L);
          squares.New().create(VecI2(pos.x    , pos.y + 1), BT_L);
          squares.New().create(VecI2(pos.x    , pos.y + 2), BT_L);
          squares.New().create(VecI2(pos.x + 1, pos.y    ), BT_L);     
       }
    
       void makeBackwardsLBlock()
       {
          //    [2]
          //    [1]
          // [3][0] 
    
          squares.New().create(VecI2(pos.x    , pos.y    ), BT_BACKWARDS_L);
          squares.New().create(VecI2(pos.x    , pos.y + 1), BT_BACKWARDS_L);
          squares.New().create(VecI2(pos.x    , pos.y + 2), BT_BACKWARDS_L);     
          squares.New().create(VecI2(pos.x - 1, pos.y    ), BT_BACKWARDS_L);  
       }
    
       void makeStraightBlock()
       {
          // [2]
          // [1]
          // [0]
          // [3] 
          squares.New().create(VecI2(pos.x, pos.y    ), BT_STRAIGHT);
          squares.New().create(VecI2(pos.x, pos.y + 1), BT_STRAIGHT);
          squares.New().create(VecI2(pos.x, pos.y + 2), BT_STRAIGHT);     
          squares.New().create(VecI2(pos.x, pos.y - 1), BT_STRAIGHT); 
       }
    
       void makeSBlock()
       {
          //    [0][1]
          // [3][2] 
    
          squares.New().create(VecI2(pos.x    , pos.y    ), BT_S);
          squares.New().create(VecI2(pos.x + 1, pos.y    ), BT_S);
          squares.New().create(VecI2(pos.x    , pos.y - 1), BT_S);
          squares.New().create(VecI2(pos.x - 1, pos.y - 1), BT_S);     
       }
    
       void makeBackwardsSBlock() 
       {
          // [1][0]
          //    [2][3]
          squares.New().create(VecI2(pos.x    , pos.y    ), BT_BACKWARDS_S);
          squares.New().create(VecI2(pos.x - 1, pos.y    ), BT_BACKWARDS_S);     
          squares.New().create(VecI2(pos.x    , pos.y - 1), BT_BACKWARDS_S);
          squares.New().create(VecI2(pos.x + 1, pos.y - 1), BT_BACKWARDS_S);  
       }
    

Trong public thì tạo các functions bao gồm move, pos, draw

public:
   void create(C VecI2 & pos, BLOCK_TYPE type)
   {
      T.pos  = pos ;
      T.type = type;
      setupSquares();
   }

   void create(C block & other)
   {
      T.pos  = other.pos ;
      T.type = other.type;

      squares.clear();
      FREPA(other.squares)
      {
         squares.New().create(other.squares[i].getPos(), other.type);
      }
   }

   void move(DIRECTION dir)
   {
      switch(dir)
      {
         case D_LEFT : pos.x--; break;
         case D_RIGHT: pos.x++; break;
         case D_DOWN : pos.y--; break;
      }

      FREPA(squares)
      {
         squares[i].move(dir);
      }
   }

   void rotate()
   {
      FREPA(squares)
      {
         VecI2 pos = squares[i].getPos();
         pos -= T.pos;
         VecI2 newPos(-pos.y, pos.x);
         newPos += T.pos;
         squares[i].setPos(newPos);
      }
   }

   C Mems<square> & getSquares() C
   {
      return squares;
   }

   BLOCK_TYPE getType() C
   {
      return type; 
   }

   void draw() C
   {
      REPA(squares)
      {
         squares[i].draw();
      }
   }
}

Bây giờ tạo một tester Block code để test class vừa tạo. Lưu ý cần phải xoá square class tester đi. Chỉ để duy nhất một code application thôi.

Memc<block> tetrisBlock; // tạo một containter thuộc class square

void InitPre()
{
   INIT();
}

bool Init()
{

   tetrisBlock.New().create(VecI2(2, 2), BT_T);
   tetrisBlock.New().create(VecI2(4, 0), BT_S);
   tetrisBlock.New().create(VecI2(6, 6), BT_L);
   tetrisBlock.New().create(VecI2(10, 0), BT_BACKWARDS_S);
   tetrisBlock.New().create(VecI2(8, 2), BT_BACKWARDS_L);
   tetrisBlock.New().create(VecI2(5, 10), BT_SQUARE);
   return true;
}

void Shut() {}

bool Update()
{
   if(Kb.bp(KB_ESC)) return false;

   // update input cách di chuyển của blocks
   DIRECTION d = D_NONE;
   if(Kb.bp(KB_DOWN)) d = D_DOWN;
   if(Kb.bp(KB_LEFT)) d = D_LEFT;
   if(Kb.bp(KB_RIGHT)) d = D_RIGHT;

   // update move() theo input Direction
   REPA(tetrisBlock)
   {
      tetrisBlock[i].move(d);
   }

   if(Kb.bp(KB_SPACE))
   {
      REPA(tetrisBlock)
      {
         tetrisBlock[i].rotate();
      }
   }
   return true;
}

void Draw()
{
   D.clear(BLACK);
   REPA(tetrisBlock)
   {
      tetrisBlock[i].draw();
   }

}

Kết quả như sau

Hiện tại đã có thể ghép các squares thành blocks, nhưng khi di chuyển chúng (sử dụng phím mũi tên) thì chúng sẽ đi xuyên màn hình chứ không thể gộp lại tại Hàng cuối cùng.

2.3 Pile - Tetris ground

Tạo một class Pile để ghép các blocks lại với nhau khi chúng chạm vào nền của Game. Ngoài ra khi các blocks này tạo thành một hàng ngang thì chúng sẽ bị remove

Trong private class của pile

class pile
{
private:
   Memx<square> list;

   bool canMove(C square & s,  DIRECTION dir) C
   {
      // get the current position
      VecI2 pos = s.getPos();

      // move to new position
      switch(dir)
      {
         case D_DOWN: pos.y--; break;
         case D_LEFT: pos.x--; break;
         case D_RIGHT: pos.x++; break;
      }

      // go through pile of squares
      REPA(list)
      {
         if(pos == list[i].getPos())
         {
            // this position is occupied
            return false;
         }
      }

      // position is free
      return true;
   }

   void removeRow(int row)
   {
      REPA(list)
      {
         if(list[i].getPos().y == row)
         {
            list.removeValid(i);
         } else if(list[i].getPos().y > row)
         {
            list[i].move(D_DOWN);
         }
      }
   }

public:

   void init()
   {
      list.clear();
   }

   bool collides(C block & b, DIRECTION dir) C
   {
      // get squares from this block
      C Mems<square> & squares = b.getSquares();

      // check all of them
      REPA(squares)
      {
         if(!canMove(squares[i], dir))
         {
            return true;
         }
      }
      return false;
   }

   void add(C block & b)
   {
      C Mems<square> & squares = b.getSquares();
      REPA(squares)
      {
         list.New().create(squares[i].getPos(), b.getType());
      }
   }

   int checkLines()
   {
      int squaresInRow[ROWS + 3]; // make room for blocks on top position

      REPA(squaresInRow)
      {
         squaresInRow[i] = 0;
      }

      REPA(list)
      {
         int row = list[i].getPos().y;
         squaresInRow[row]++;
      }

      int completedLines = 0;

      REPA(squaresInRow)
      {
         if(squaresInRow[i] == SQUARES_PER_ROW)
         {
            removeRow(i - completedLines);
            completedLines++;           
         }
      }

      return completedLines;
   }

   void draw() C
   {
      REPA(list)
      {
         list[i].draw();
      }
   }
}

pile Pile;

The following pages link to this page:



Created : Mar 4, 2022