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ư
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 code
là square
đạ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 integer
là VecI2
Trong class này cần tạo các functions như:
- create()
- move()
- getPosition
- setPosition
- 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:
- 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. - Nhưng tại sao lại dùng
C
constant choreference
? 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ợpmodification
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ủaclassSquare
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;
Backlinks¶
The following pages link to this page:
Created : Mar 4, 2022
Recent Posts
- 2024-11-02: BUỔI 10 - Phân tích thị trường
- 2024-11-02: BUỔI 11 - Phân tích thị trường
- 2024-11-02: BUỔI 12 - Phân tích sóng tăng
- 2024-11-02: BUỔI 13 - Phân tích hỏi đáp
- 2024-11-02: BUỔI 14 - Yếu tố kiểm soát
- 2024-11-02: BUỔI 15 - Hỏi đáp
- 2024-11-01: BUỔI 6 - Ôn lại và bổ sung
- 2024-11-01: BUỔI 7 - Chiến thuật Trend
- 2024-11-01: BUỔI 8 - Công thức điểm vào lệnh
- 2024-11-01: K2023 - BUỔI 9 - Quy trình vào lệnh