As promised, I will show you today the source code of a small game that I programmed. But it is not an original creation, but the reprogramming of a game that has been around a very long time - since 1984. Even today there are many new versions of it and it's still a popular pastime in many ages. Most probably know it from Nintendo consoles, others play it mid way on their smartphones.
It is TETRIS!
This game is liked to be cloned and imitated, and not least because it is so "simple". The programming, however, is perhaps not as trivial as one might think ... depending on how you implement it.
I'd like to share my version, I made with GameMaker: Studio (version 1.4.1657) and explain a little bit to it.
In this archive you will find the entire project, including all images and code.
Overall the game is not necessarily written beginner friendly - we want see how it's done "correctly" for learning, and let's see how it looks like. That is, the code is partly highly optimized and no unnecessary Drag'n'Drop was used. Nevertheless, I have the scripts clearly structured and extensively commented (in English).
Below are listed some individual parts of the game I want to show and explain shortly.
The Rooms:
In order for a game can be started, there must be at least one room. In Tetris, we have three rooms:
room_intro
room_title
room_game
Relevant is only room_game, the others are only prepared for bringing you directly through to the next room, for now.
The Objects:
obj_all
| Persistent (ubiquitous object), which is to take care of everything that can happen always and in every room. |
obj_intro
| An object that is to take care of an intro at the very beginning of the game. |
obj_title
| This object is to look at the title screen. From there different game modes should be available to be started. |
obj_tetris_controller
| The controller is to look after the in-game happenings. It is the centerpiece of the game. |
obj_tetris_stone
| This embodies a tetris block of any form. |
obj_tetris_back
| Helper object for drawing a background. |
The Scripts:
These are just five of ten scripts, which are used in the game. For the rest you can see in the attached archive.
When a new tetris stone is created a random form is passed to it. This script sets a 2D array (matrix) for any of the possible forms that determines which parts of the maximum of 4x4 large stone are set and which are not.
↓ tetris_set_stone_parts(form) ↓
↑ tetris_set_stone_parts(form) ↑
1: /// tetris_set_stone_parts(form);
2: // Initializes the parts array of a tetris stone to the given form enum
3: // by BadToxic
4:
5: var form_enum = argument0;
6:
7: parts[3, 3] = 0;
8:
9: switch (form_enum) {
10: case global.TETRIS_FORM_T: {
11: parts[0, 0] = 0; parts[0, 1] = 0; parts[0, 2] = 0; parts[0, 3] = 0;
12: parts[1, 0] = 0; parts[1, 1] = 1; parts[1, 2] = 0; parts[1, 3] = 0;
13: parts[2, 0] = 1; parts[2, 1] = 1; parts[2, 2] = 1; parts[2, 3] = 0;
14: parts[3, 0] = 0; parts[3, 1] = 0; parts[3, 2] = 0; parts[3, 3] = 0;
15: break;
16: }
17: case global.TETRIS_FORM_I: {
18: parts[0, 0] = 0; parts[0, 1] = 1; parts[0, 2] = 0; parts[0, 3] = 0;
19: parts[1, 0] = 0; parts[1, 1] = 1; parts[1, 2] = 0; parts[1, 3] = 0;
20: parts[2, 0] = 0; parts[2, 1] = 1; parts[2, 2] = 0; parts[2, 3] = 0;
21: parts[3, 0] = 0; parts[3, 1] = 1; parts[3, 2] = 0; parts[3, 3] = 0;
22: break;
23: }
24: case global.TETRIS_FORM_Z: {
25: parts[0, 0] = 0; parts[0, 1] = 0; parts[0, 2] = 0; parts[0, 3] = 0;
26: parts[1, 0] = 1; parts[1, 1] = 1; parts[1, 2] = 0; parts[1, 3] = 0;
27: parts[2, 0] = 0; parts[2, 1] = 1; parts[2, 2] = 1; parts[2, 3] = 0;
28: parts[3, 0] = 0; parts[3, 1] = 0; parts[3, 2] = 0; parts[3, 3] = 0;
29: break;
30: }
31: case global.TETRIS_FORM_S: {
32: parts[0, 0] = 0; parts[0, 1] = 0; parts[0, 2] = 0; parts[0, 3] = 0;
33: parts[1, 0] = 0; parts[1, 1] = 1; parts[1, 2] = 1; parts[1, 3] = 0;
34: parts[2, 0] = 1; parts[2, 1] = 1; parts[2, 2] = 0; parts[2, 3] = 0;
35: parts[3, 0] = 0; parts[3, 1] = 0; parts[3, 2] = 0; parts[3, 3] = 0;
36: break;
37: }
38: case global.TETRIS_FORM_Q: {
39: parts[0, 0] = 0; parts[0, 1] = 0; parts[0, 2] = 0; parts[0, 3] = 0;
40: parts[1, 0] = 0; parts[1, 1] = 1; parts[1, 2] = 1; parts[1, 3] = 0;
41: parts[2, 0] = 0; parts[2, 1] = 1; parts[2, 2] = 1; parts[2, 3] = 0;
42: parts[3, 0] = 0; parts[3, 1] = 0; parts[3, 2] = 0; parts[3, 3] = 0;
43: break;
44: }
45: case global.TETRIS_FORM_L: {
46: parts[0, 0] = 0; parts[0, 1] = 0; parts[0, 2] = 0; parts[0, 3] = 0;
47: parts[1, 0] = 0; parts[1, 1] = 1; parts[1, 2] = 0; parts[1, 3] = 0;
48: parts[2, 0] = 0; parts[2, 1] = 1; parts[2, 2] = 0; parts[2, 3] = 0;
49: parts[3, 0] = 0; parts[3, 1] = 1; parts[3, 2] = 1; parts[3, 3] = 0;
50: break;
51: }
52: case global.TETRIS_FORM_J: {
53: parts[0, 0] = 0; parts[0, 1] = 0; parts[0, 2] = 0; parts[0, 3] = 0;
54: parts[1, 0] = 0; parts[1, 1] = 0; parts[1, 2] = 1; parts[1, 3] = 0;
55: parts[2, 0] = 0; parts[2, 1] = 0; parts[2, 2] = 1; parts[2, 3] = 0;
56: parts[3, 0] = 0; parts[3, 1] = 1; parts[3, 2] = 1; parts[3, 3] = 0;
57: break;
58: }
59: }
60:
61: // Count the parts
62: parts_number = 0;
63: for (var yy = 0; yy < array_height_2d(parts); yy++) {
64: for (var xx = 0; xx < array_length_2d(parts, yy); xx++) {
65: if (parts[yy, xx]) {
66: parts_number++;
67: }
68: }
69: }
The following two scripts are called for the currently falling stone when you press the button for a right or left turn. It assigns the set of tetris parts of the above matrix, so that the entire stone is rotated by 90°.
↓ tetris_stone_turn_right() ↓
↑ tetris_stone_turn_right() ↑
1: /// tetris_stone_turn_right();
2: // Turn tetris stone clockwise
3: // by BadToxic
4:
5: var turned;
6:
7: turned[0, 0] = parts[3, 0]; turned[0, 1] = parts[2, 0]; turned[0, 2] = parts[1, 0]; turned[0, 3] = parts[0, 0];
8: turned[1, 0] = parts[3, 1]; turned[1, 1] = parts[2, 1]; turned[1, 2] = parts[1, 1]; turned[1, 3] = parts[0, 1];
9: turned[2, 0] = parts[3, 2]; turned[2, 1] = parts[2, 2]; turned[2, 2] = parts[1, 2]; turned[2, 3] = parts[0, 2];
10: turned[3, 0] = parts[3, 3]; turned[3, 1] = parts[2, 3]; turned[3, 2] = parts[1, 3]; turned[3, 3] = parts[0, 3];
11:
12: // Check if the turning is ok - the stone still is in the game area and has no collision with another stone
13: tetris_stone_turn_check_collision(turned);
↓ tetris_stone_turn_left() ↓
↑ tetris_stone_turn_left() ↑
1: /// tetris_stone_turn_left();
2: // Turn tetris stone counter clockwise
3: // by BadToxic
4:
5: var turned;
6:
7: turned[0, 0] = parts[0, 3]; turned[0, 1] = parts[1, 3]; turned[0, 2] = parts[2, 3]; turned[0, 3] = parts[3, 3];
8: turned[1, 0] = parts[0, 2]; turned[1, 1] = parts[1, 2]; turned[1, 2] = parts[2, 2]; turned[1, 3] = parts[3, 2];
9: turned[2, 0] = parts[0, 1]; turned[2, 1] = parts[1, 1]; turned[2, 2] = parts[2, 1]; turned[2, 3] = parts[3, 1];
10: turned[3, 0] = parts[0, 0]; turned[3, 1] = parts[1, 0]; turned[3, 2] = parts[2, 0]; turned[3, 3] = parts[3, 0];
11:
12: // Check if the turning is ok - the stone still is in the game area and has no collision with another stone
13: tetris_stone_turn_check_collision(turned);
Once a stone has been moved, you can check with the next script if it collides with another stone. For this all the other stones are checked: do the boundaries of the maximum possible size of 4x4 stone parts overlap? Only if that is the case, the individual stone parts are checked. This approach is very efficient and not necessary with today's computing power... you also can implement this much easier. But as I said - this is supposed to show how make it "right". ;)
↓ tetris_check_collision() ↓
↑ tetris_check_collision() ↑
1: /// tetris_check_collision();
2: // Checks for a collision at the current position
3: // by BadToxic
4:
5: with (obj_tetris_stone) {
6:
7: if (id != other.id) {
8:
9: var in_grid = true;
10:
11: if (other.x + (controller.stone_grid_size - 1) * controller.stone_size < x) {
12: in_grid = false;
13: }
14: else if (other.x > x + (controller.stone_grid_size - 1) * controller.stone_size) {
15: in_grid = false;
16: }
17: else if (other.y + (controller.stone_grid_size - 1) * controller.stone_size < y) {
18: in_grid = false;
19: }
20: else if (other.y > y + (controller.stone_grid_size - 1) * controller.stone_size) {
21: in_grid = false;
22: }
23:
24: // Other stone must be in my grid
25: if (in_grid) {
26: // Calculate shift of other stone grid to mine
27: var x_shift = (x - other.x) div controller.stone_size;
28: var y_shift = (y - other.y) div controller.stone_size;
29:
30: var xx_min = max(0, x_shift);
31: var yy_min = max(0, y_shift);
32: var xx_max = min(controller.stone_grid_size, x_shift + controller.stone_grid_size);
33: var yy_max = min(controller.stone_grid_size, y_shift + controller.stone_grid_size);
34:
35: // Run through all grid fields of my stone
36: for (var yy = yy_min; yy < yy_max; yy++) {
37: for (var xx = xx_min; xx < xx_max; xx++) {
38: if (other.parts[yy, xx]) {
39: if (parts[yy - y_shift, xx - x_shift]) {
40: return true;
41: }
42: }
43: }
44: }
45: }
46: }
47: }
48:
49: return false;
If a falling stone finally hit the ground or other stones (collide after moving), it can be tested with this script if lines are completed and can be cleared.
↓ tetris_check_for_clearing(clear_y1, clear_y2) ↓
↑ tetris_check_for_clearing(clear_y1, clear_y2) ↑
1: /// tetris_check_for_clearing(clear_y1, clear_y2);
2: // This script is called after a stone was placed and checks for lines that can be cleared
3: // by BadToxic
4:
5: var clear_y1 = /*max( 0, */argument0;
6: var clear_y2 = /*min(fields_y, */argument1;
7:
8: var lines_to_clear = ds_list_create();
9:
10: // Check for lines
11: for (var yy = clear_y1; yy < clear_y2; yy++) {
12: var clear_line = true;
13: for (var xx = 0; xx < fields_x; xx++) {
14: if (game_area[yy, xx] < 0) {
15: // In this line is a gap - skip it
16: xx = fields_x;
17: clear_line = false;
18: }
19: }
20: if (clear_line) { // Remember this line to clear
21: ds_list_add(lines_to_clear, yy);
22: }
23: }
24:
25:
26: // Clear found lines
27: if (!ds_list_empty(lines_to_clear)) {
28: fix_bottoms = ds_list_create();
29:
30: switch (ds_list_size(lines_to_clear)) {
31: case 1: {
32: score += global.SCORE_LINES_CLEARED_1;
33: break;
34: }
35: case 2: {
36: score += global.SCORE_LINES_CLEARED_2;
37: break;
38: }
39: case 3: {
40: score += global.SCORE_LINES_CLEARED_3;
41: break;
42: }
43: case 4: {
44: score += global.SCORE_LINES_CLEARED_4;
45: }
46: }
47:
48: while (!ds_list_empty(lines_to_clear)) {
49: var yy = ds_list_find_value(lines_to_clear, 0);
50: for (var xx = 0; xx < fields_x; xx++) {
51: //if (game_area[yy, xx] >= 0) {
52: var stone_instance = game_area[yy, xx];
53:
54: var parts_index_y = yy - (stone_instance.y - border_top) div stone_size;
55: var parts_index_x = xx - (stone_instance.x - border_left) div stone_size;
56:
57: stone_instance.parts[parts_index_y, parts_index_x] = 0;
58: stone_instance.parts_number--;
59: if (stone_instance.parts_number <= 0) {
60: with (stone_instance) {
61: instance_destroy();
62: }
63: }
64: else {
65: if (stone_instance.border_top == parts_index_y) {
66: stone_instance.border_top++;
67: }
68: else if (stone_instance.border_bottom > parts_index_y + 1) { // Stone has to be divided
69: // Duplicate stone
70: var new_stone_instance = tetris_duplicate_stone(stone_instance);
71:
72: // Delete all parts of new stone above the cleared line
73: for (var yyy = 0; yyy <= parts_index_y; yyy++) {
74: for (var xxx = 0; xxx < stone_grid_size; xxx++) {
75: new_stone_instance.parts[yyy, xxx] = 0;
76: }
77: }
78: new_stone_instance.border_top = parts_index_y + 1;
79:
80: // Refresh game area array references for the new stone
81: tetris_link_stone_to_game_area(new_stone_instance);
82:
83: // Delete all parts of old stone underneath the cleared line
84: for (var yyy = parts_index_y + 1; yyy < stone_grid_size; yyy++) {
85: for (var xxx = 0; xxx < stone_grid_size; xxx++) {
86: stone_instance.parts[yyy, xxx] = 0;
87: }
88: }
89:
90: // Refresh game area array references for the old stone
91: tetris_link_stone_to_game_area(stone_instance);
92:
93: stone_instance.border_bottom = parts_index_y + 1; // Don't divide the same stone two times in a row
94: }
95: if (stone_instance.border_bottom > parts_index_y) { // Stone needs a bottom border correction
96: // Only add it, if not already included
97: if (ds_list_find_index(fix_bottoms, stone_instance) < 0) {
98: ds_list_add(fix_bottoms, stone_instance);
99: }
100: }
101: }
102: game_area[yy, xx] = -1;
103: //}
104: }
105: ds_list_delete(lines_to_clear, 0);
106:
107: // Fix bottom borders of divided stones
108: for (var stone_index = 0; stone_index < ds_list_size(fix_bottoms); stone_index++) {
109: var stone_instance = ds_list_find_value(fix_bottoms, stone_index);
110: if (instance_exists(stone_instance)) { // Could already be deleted
111: var parts_index_y = yy - (stone_instance.y - border_top) div stone_size;
112: stone_instance.border_bottom = parts_index_y;
113: }
114: }
115: ds_list_clear(fix_bottoms);
116: }
117:
118: // Clean up the lists memory
119: ds_list_destroy(lines_to_clear);
120: ds_list_destroy(fix_bottoms);
121:
122: // Now remember all stones above the cleared line(s) to let them fall later // TODO: AND BELOW also stone parts may need to fall
123: ds_list_clear(falling_stones);
124:
125: for (var yy = clear_y2 - 2; yy >= 0; yy--) {
126: for (var xx = 0; xx < fields_x; xx++) {
127: if (game_area[yy, xx] >= 0) {
128: // Only add it, if not already included
129: if (ds_list_find_index(falling_stones, game_area[yy, xx]) < 0) {
130: ds_list_add(falling_stones, game_area[yy, xx]);
131: }
132: }
133: }
134: }
135: }
The interaction of items:
So now you have learned about the most important parts of the game, but how do they work together? This I also want to keep short:
The object obj_tetris_controller is assigned to the room room_game in the room editor.
If the game has been started and the room was reached, the controller will take care of everything. For example it also creates an instance of the obj_tetris_back object which is for handling the background.
The controller will now create an obj_tetris_stone instance at the top of the screen. Always after a certain time, which is set to a half of a second, or 500,000 microseconds (fall_time = 500000) in the create event of the controller, the current stone is moved one square down. This movement takes place in the step event of the controller.
What also happens in this event, is cecking various keystrokes, such as the arrow keys or WASD, to control the stone falling. Depending on what is pressed, the stone is moved to the left, right or down or is rotated by the scripts mentioned above. After each such action it is checked for collision again and if necessary the movement or turning will be reversed, because we do not want overlapping stones.
When a stone strikes down, that means reaching a specific height in the game area, or collides with a stone under it, then its movement is stopped. After this stop it's checked via tetris_check_for_clearing for completely filled rows and then a new stone will be generated at the top of the screen.
However, if fully filled lines were found, they will be removed. This process is probably the most complicated one in this program because the matrices of the stones need to be adapted and maybe stones have to be split into several pieces if the mid part(s) of these stones were cleared...
When a new stone was "thrown" in, it is to be checked if there's enough space for it. If there already a collision takes place, the playing field is full and the game therefore is over.
And so the end result looks like. It should be noted that the background was found in a Google search and the font is not by me, either. Everything else can be used freely (it will be nice to have credits to BadToxic, of course ;) ). But errors are also still included in this version - the finder will be allowed to keep them...
I know this tutorial may not be suitable for beginners, but with it you can see how GameMaker works or learn from it if you are already ready for it. ^^
Also, I would have liked it to be able to show and explain a lot more, but unfortunately I do not have the time. But if you have any questions, please feel free to comment uses this blog's functionality - I'll try to help. :)
And at this point I will tell you that I continue with this game and will try to make something nice. ;)
Keine Kommentare:
Kommentar veröffentlichen