俄羅斯方塊(c++)
這個俄羅斯方塊是用c++基於windows控制檯製作的。
話不多說,先上圖感受一下:(控制檯醜陋的介面不是我的鍋emmm)
我們知道俄羅斯方塊有俄羅斯方塊的規則:
1.生成五種不同形狀的方塊 :L、Z、T、I和方形。
2.通過鍵盤控制左移、右移、旋轉與直接下落。
3.遇到邊界或者靜止方塊停止運動。
4.一行都有方塊時自動消去,分數增加,上層方塊下落。
5.如果頂部出現了堆積方塊時遊戲結束。
6.小框提前放映出待會出現的方塊形狀。
7.等級升高,速度增大。
現在就來講一講如和在這些規則都實現的前提下製作俄羅斯方塊的遊戲,並儘可能的提高使用者的遊戲體驗。
先來總體的規劃一番,我們需要設計的類有:
單元類:Unit; 負責儲存每個小單元的位置(x,y)以便於在控制檯上打印出來。
工具類:Tool; 藉助於windows提供的API,移動到特定的位置,列印圖案或消除圖案。
介面類:Frame; 打印出遊戲執行時的外圍框架和遊戲資訊。
遊戲邏輯類: Diamond(名字與遊戲不是很相關orz); 最長和最複雜的一個類,所有的遊戲邏輯都將在這裡實現。
主程式類: main.cpp;
一:
為了閱讀的方便,我把以後要經常用到的Unit、Tool類先介紹一二:
我們知道windows控制檯預設的大小是80*40,也就是說我們遊戲要打印出來的所有內容的位置座標必須都在0<x<80,y<x<40這個範圍內。
那怎麼樣才能控制列印的位置呢?
我藉助windows提供的函式自己在Tool類裡面封裝了一個函式:
void Tool::gotoxy(int x, int y) { COORD coor; coor.X = x; coor.Y = y; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, coor); }
通過windows裡的SetConsoleCursorPosition函式,能將游標帶到我們指定的控制檯座標(x,y)。
很顯然,使用了gotoxy函式我們就能夠在控制檯上將游標帶到各個位置,繪製出我們想要的任何形狀。
那麼誰來提供這個座標呢?
答案就是Unit單元類了。
Unit.h
1 #pragma once 2 #include"Tool.h" 3 #include"Frame.h" 4 class Unit 5 { 6 private: int pos_x, pos_y; 7char pic='#'; 8 public: 9Unit(int x,int y); 10~Unit(); 11void show(); 12int get_x(); 13int get_y(); 14void set_x(int x); 15void set_y(int y); 16 };
Unit.cpp
1 #include "Unit.h" 2 Unit::Unit(int x,int y) 3 { 4pos_x = x; 5pos_y = y; 6 } 7 Unit::~Unit() 8 { 9 } 10 void Unit::show() 11 { 12Tool T; 13T.gotoxy(pos_x, pos_y); 14cout << pic;//俄羅斯方塊由‘#’號組成 15return; 16 } 17 int Unit::get_x() 18 { 19return pos_x; 20 } 21 int Unit::get_y() 22 { 23return pos_y; 24 } 25 void Unit::set_x(int x) 26 { 27pos_x = x; 28 } 29 void Unit::set_y(int y) 30 { 31pos_y = y; 32 }
說完了這兩個基礎類,我們就來進一步的介紹Frame類。
二:
Frame類要打印出來的資訊如圖:
Frame類不僅要把東西打印出來,它還負責著區域的劃定,在Frame類裡面有一些const引數:
1const int UP = 5; 2const int BUTTOM = 20; 3const int LEFT = 18 ; 4const int RIGHT = 30; 5const int RIGHT2 = 46; 6const int RIGHT3 = 63; 7const int MIDDLE1 = 12; 8const int MIDDLE2 = 17;
這些引數要放在public裡面,以便在Diamond類中實現遊戲區域、小螢幕、得分割槽三個區域各自的任務時能夠訪問的到。
三:
Diamond類
這是最關鍵的一個類,實現出來光是.cpp中就有300多行程式碼。
在Diamond類的主戰場,遊戲區域我設定了一個grid二維陣列,用來儲存遊戲區域的方塊資訊,一個arrays陣列用來保持當前運動的四個方塊的資訊,一個speed陣列用來設定速度,LEVEL是最高級別,speed是當前速度(speed是每次Sleep()函式的時長,speed越小方塊運動越快),level是當前級別,scoretotal是當前得分。
Diamond.h程式碼:
1 #pragma once 2 #include"Frame.h" 3 #include<vector> 4 #include"Unit.h" 5 #include<ctime> 6 #include<cstdlib> 7 class Diamond 8 { 9 private: 10static const int LEVEL = 15; 11bool grid[17][19]; 12vector<Unit> arrays; 13int Speed[LEVEL]; 14int speed=700; 15int level = 0, scoretotal = 0; 16 public: 17Diamond(); 18~Diamond(); 19void create(); 20void rotate(); 21void show(); 22void move(); 23void shift(); 24void set_speed(int m_speed); 25void control(); 26int get_level(); 27bool test();// what function? 28int eliminate(); 29bool judge_death(); 30bool delay(); 31 };
Diamond建構函式:
1 Diamond::Diamond() 2 { 3Frame F; 4grid[F.RIGHT - F.LEFT - 1][F.BUTTOM - F.UP - 1];//有方塊是true,沒有是false 5for (int i = 0; i <= F.RIGHT - F.LEFT - 2; ++i) 6{ 7for (int j = 0; j <= F.BUTTOM - F.UP - 2; ++j) 8{ 9grid[i][j] = false; 10} 11} 12for (int i = 0; i < 13; ++i) 13{ 14Speed[i] = 700 - 30 * i; 15} 16for (int i = 13; i < LEVEL; ++i) 17{ 18Speed[i] = 300;//製作速度等級陣列 19} 20F.Draw_score(0, 0); 21 }
前面講到了俄羅斯方塊的一些規則,第一個就是生成五種不同的方塊,分別是Z\T\L\I\方形。
它們之間雖然形狀各異,但好在有一個共同的地方,它們都是由四個基礎方塊(也就是四個Unit)組成的。
不妨用一個數組arrays來實現這五種方塊。
arrays是由Unit組成的陣列,通過給數組裡unit單元不同的x、y值得到不同的形狀(unit【0】位於遊戲區域的正上方)。
藉助於隨機函式我們可以每次出現不同的方塊(方塊出現的概率有差別時,遊戲體驗更佳),然後使用旋轉函式rotate(之後會實現)得到更為豐富的方塊形狀。
arrays得到之後,我們就要列印了。(由於unit【0】作為基準點在遊戲區域的正上方,不用考慮方塊旋轉之後碰到兩側框架的問題)
列印有兩個地方:1.遊戲區域,2.小螢幕。
列印完之後,需要在grid陣列表示arrays裡面各個單元的元素設定為true(此時已經有方塊)。
程式碼貼出如下:
1 void Diamond::create() 2 { 3Frame F; 4Tool T; 5int key; 6int base_point; 7int rotate_time=0; 8srand(time(NULL)); 9base_point = ( F.RIGHT-F.LEFT) / 2; 10key = rand() % 5;//隨機出形狀 11rotate_time = rand() % 9;//隨機旋轉原始形狀,已達到豐富的目的 12arrays.clear();//為了降低難度,每個圖形出現的比例應該不一樣 13switch(key) 14{ 15case 0: 16arrays.clear(); 17for (int i = 0; i < 4; ++i) 18{ 19Unit unit(i + F.LEFT+base_point+1, F.UP + 1);//****型 20arrays.push_back(unit); 21} 22for (int i = 0; i < rotate_time; ++i) 23{ 24rotate(); 25} 26break; 27case 1:case 5:case 6: 28arrays.clear(); 29for (int i = 0; i < 2; ++i) 30{ 31for (int j = 0; j < 2; ++j) 32{ 33Unit unit(i + F.LEFT + base_point + 1, F.UP + 1 + j);//正方形 34arrays.push_back(unit); 35} 36} 37break; 38case 2:case 7://槍形 39arrays.clear(); 40arrays.push_back(Unit(F.LEFT + base_point + 1, F.UP + 1)); 41arrays.push_back(Unit (F.LEFT + base_point+2, F.UP + 1)); 42arrays.push_back(Unit (F.LEFT + base_point + 3, F.UP + 1)); 43arrays.push_back(Unit(F.LEFT + base_point + 3, F.UP + 2)); 44for (int i = 0; i < rotate_time; ++i) 45{ 46rotate(); 47} 48break; 49case 3://梯形 50arrays.clear(); 51arrays.push_back(Unit(F.LEFT + base_point + 1, F.UP + 1)); 52arrays.push_back(Unit(F.LEFT + base_point+1, F.UP + 2)); 53arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 2)); 54arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 3)); 55for (int i = 0; i < rotate_time; ++i) 56{ 57rotate(); 58} 59break; 60case 4: case 8: 61arrays.clear();//城牆形 62arrays.push_back(Unit(F.LEFT + base_point + 1, F.UP + 1)); 63arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 1)); 64arrays.push_back(Unit(F.LEFT + base_point + 3, F.UP + 1)); 65arrays.push_back(Unit(F.LEFT + base_point + 2, F.UP + 2)); 66for (int i = 0; i < rotate_time; ++i) 67{ 68rotate(); 69} 70break; 71} 72//應該先將小螢幕清空。 73for (int i = 0; i < F.RIGHT2 - F.RIGHT - 1; ++i) 74{ 75for (int j = 0; j < F.MIDDLE1 - F.UP - 1; ++j) 76{ 77T.gotoxy(F.RIGHT + 1 + i, F.UP + j + 1); 78cout << " "; 79} 80} 81for (int i = 0; i < 4; ++i) 82{ 83Unit unit(F.RIGHT + 7 + arrays[i].get_x() - arrays[0].get_x(), F.UP + 4 + arrays[i].get_y() - arrays[0].get_y()); 84unit.show();//按照實際的俄羅斯方塊,小螢幕放映的應該是下一個出現的方塊形狀,這裡先將功能簡單化 85} 86for (int i = 0; i < 4; ++i) 87{ 88grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-1-F.UP] = true; 89} 90 } create
特別注意,在小螢幕列印之前應將其清空。
rotate()旋轉函式:
在create()函式裡用到了rotate()函式,這裡來將其實現。
在開始製作旋轉函式的時候,我想這個應該是特別難的,因為有5種形狀,而且旋轉完之後可能會觸碰到兩側框架和靜止方塊。
但是其實只要用上一點數學知識和技巧,便可以將這些問題輕鬆解決。
因為旋轉(我的遊戲中是逆時針轉90度)其實就是arrays後三個單元對arrays[0]這個基準點來一個相對x、y的互換(看程式碼就明白了)。
1 void Diamond::rotate() 2 { 3Frame F; 4for (int i = 0; i < 4; ++i) 5{ 6grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-F.UP-1] = false; 7} 8for (int i = 1; i <= 3; ++i) 9{ 10int gap1 = arrays[i].get_x() - arrays[0].get_x(); 11int gap2 = arrays[i].get_y() - arrays[0].get_y(); 12arrays[i].set_x(arrays[0].get_x() - gap2); 13arrays[i].set_y(arrays[0].get_y() + gap1); 14} 15return; 16 }
就是這麼簡單,只用了四行,就完成了五種形狀方塊的旋轉。
注意到這裡先將grid裡面arrays每個單元所代表的元素都設定為了false(有方塊是true,沒有是false),而且在旋轉完之後沒有設定回true。這是因為我們的rotate()函式不是獨立存在的,都只是被其他的函式引用。注意到旋轉分為生成時旋轉和過程中旋轉。生成時我們會統一的設定true,而過程中會在shift函式設定(這樣能避開相撞問題)。如果有疑問接著看下去就好了。
說完旋轉,接著說下落吧。
move()函式
這個函式很簡單,直接貼程式碼:
1 void Diamond::move() 2 { 3Frame F; 4//Sleep(speed);暫停會在control函式裡實現 5for (int i = 0; i < 4; ++i) 6{ 7grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-1-F.UP] = false; 8} 9for (int i = 0; i < 4; ++i) 10{ 11arrays[i].set_y(arrays[i].get_y()+1); 12} 13for (int i = 0; i < 4; ++i) 14{ 15grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-F.UP-1] = true; 16}//雖說move後面是shift函式,但是為了保持函式的獨立性還是在每個函式裡面都寫上false、true這部分 17return; 18 }
這裡的move設定回了true,因為考慮到move是和shift()函式並列的,而rotate()實在shift()函式裡面的。
說曹操,曹操到,shift()函式到了。
這個函式中我們要實現一個很重要的功能——使用者操控。
A、a——左移,D、d——右移,R、r——旋轉,D、d——速降。
_kbhit()是windows提供的函式,作用是檢測到鍵盤輸入。用一個while()持續檢測後,在switch語句裡判斷輸入目的。
左移,右移,旋轉都有一個問題,如果新的位置與兩側框架、靜止方塊重合,那麼這個運動應當作廢,還是得回到原來的位置,並且這次運動不能呈現給使用者(不然遊戲體驗太差了emmm)在這個函式中會將這個問題考慮解決掉。
速降功能實現很簡單,只需要把用set_speed()函式設定speed為10(這種速度很快)就好了。
程式碼貼出來就都明白了(hhh)
1 void Diamond::shift() 2 { 3Frame F; 4vector<Unit> array_wait; 5//防止與兩側方塊重合 6bool flag = false; 7for (int i = 0; i < 4; ++i) 8{ 9array_wait.push_back(arrays[i]);//記錄下arrays的原始位置 10grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-F.UP-1] = false; 11} 12while(_kbhit()) 13{ 14switch (_getch()) 15{ 16case 97:case 65:case VK_LEFT://左移 17for (int i = 0; i < 4; ++i) 18{ 19int m_x = arrays[i].get_x(); 20arrays[i].set_x(m_x - 1); 21} 22break; 23case 100:case 68: case VK_RIGHT://右移 24for (int i = 0; i < 4; ++i) 25{ 26int m_x = arrays[i].get_x(); 27arrays[i].set_x(m_x + 1); 28} 29break; 30case 82:case 114://輸入字元R(rotate)的大寫或者小寫,旋轉 31rotate(); 32break; 33case 83:case 115: 34set_speed(10);//實現速降功能這個功能做得簡單又有使用者體驗!!! 35default: 36break; 37} 38} 39for (int i = 0; i < 4; ++i) 40{ 41if (grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y() - 1 - F.UP] == true) 42flag = true; 43if (arrays[i].get_x() <= F.LEFT || arrays[i].get_x() >= F.RIGHT) 44flag = true; 45} 46if (flag == true)//如果相撞,則回到原始位置 47{ 48arrays.clear(); 49for (int i = 0; i < 4; ++i) 50{ 51arrays.push_back(array_wait[i]); 52} 53} 54for (int i = 0; i < 4; ++i) 55{ 56grid[arrays[i].get_x() - 1 - F.LEFT][arrays[i].get_y()-1-F.UP] = true; 57} 58return; 59 }
為了避免與框架或者靜止方塊相撞,設計了一個arrays_wait陣列記錄原來位置。
由於arrays各個單元在grid裡面的投射點已經全部變成了false,這個時候一旦新的arrays有true,肯定是遇到了靜止的方塊,那麼這次運動應該恢復。
講了這麼多移動,我們還沒有把方塊移動後的方塊和靜止方塊一塊列印在遊戲區呢,這就要看show()了。
原理很簡單,掃視整個grid陣列,true列印#,false列印空格(為了將剛剛為true,現在為false的點恢復)。
1 void Diamond::show() 2 { 3Frame F; 4Tool T; 5for (int i = F.LEFT+1; i < F.RIGHT; ++i) 6{ 7for (int j = F.UP + 1; j < F.BUTTOM; ++j) 8{ 9if (grid[i - F.LEFT - 1][j - F.UP - 1] == true) 10{ 11T.gotoxy(i, j); 12cout << "#"; 13} 14else 15{ 16T.gotoxy(i, j); 17cout << " "; 18} 19} 20} 21return; 22 }
好的移動的函式講的差不多了,可以上一些檢測的函數了。
檢測函式共三個:
delay()滯留函式——如果arrays各個方塊下面有靜止方塊,那麼返回true。
eliminate()消除函式——如果有一行被方塊填滿了,消除該行,上面的方塊下落一格,繼續檢測,返回整形,用來得到分數。
judge_death()生死函式——如果靜止的方塊堆積到了最上面一格,那麼返回true,此時該輪遊戲將結束。
我們來一個一個實現:
delay()基本與剛剛shift裡面的方法一致,先設定為false,檢測,再設定為true。
1 bool Diamond::delay() 2 { 3bool flag = false; 4Frame F; 5for (int i = 0; i < 4; ++i) 6{ 7grid[arrays[i].get_x() - F.LEFT - 1][arrays[i].get_y() - F.UP - 1] = false; 8} 9for (int i = 0; i < 4; ++i) 10{ 11if (arrays[i].get_y() == F.BUTTOM -1) 12flag=true; 13if (grid[arrays[i].get_x()-F.LEFT-1][arrays[i].get_y() + 1-F.UP-1] == true) 14flag = true; 15} 16for (int i = 0; i < 4; ++i) 17{ 18grid[arrays[i].get_x() - F.LEFT - 1][arrays[i].get_y() - F.UP - 1] = true; 19} 20return flag; 21 }
eliminate():
從下往上一行行檢測。
1 int Diamond::eliminate() 2 { 3Frame F; 4int level = 0; 5int i; 6for ( i = F.BUTTOM - F.UP - 2; i >= 0; --i) 7{ 8bool temp = true; 9for (int j = 0; j < F.RIGHT - F.LEFT - 1; ++j) 10{ 11if (grid[j][i] == false) 12temp = false; 13} 14if (temp == true) 15{ 16level++; 17for (int k = 0; k < F.RIGHT - F.LEFT - 1; ++k) 18{ 19for (int t = i; t > 0; --t) 20{ 21grid[k][t] = grid[k][t - 1]; 22} 23} 24for (int k = 0; k < F.RIGHT - F.LEFT - 1; ++k) 25{ 26grid[k][0] = false; 27} 28i++;//檢測剛剛替補的一行 29} 30 31temp = true; 32} 33return level; 34 }
judge_death()函式:
很簡單,看程式碼。
1 bool Diamond::judge_death() 2 { 3Frame F; 4int temp = 0; 5for (int i = 0; i < F.RIGHT - F.LEFT - 1; ++i) 6{ 7if (grid[i][0] == true) 8return true; 9} 10return false; 11 }
control函式。
contorl函式是很關鍵的函式,在它裡面將之前的函式依次呼叫才能實現俄羅斯方塊的功能。這個順序也不能隨意安排,必須根據程式的邏輯和各個函式的完善程度。
例如倘若我將create()函式放在了judge_death()之前,中間又沒有delay()函式,那麼judge_death()很可能因為arrays裡面的運動方塊投射在grid裡面是true而返回true,那麼遊戲就結束了,與實際規則完全不同,因此這一個函式要經過多次除錯才能寫出最佳效果。
同時這個函式還包含了一些總體佈局功能,如分數、等級的計算和列印,速度的設定。
程式碼如下:
1 void Diamond::control() 2 { 3Frame F; 4Tool T; 5while (judge_death()==false) 6{ 7create(); 8show(); 9int score = 0; 10while(!delay())//判斷下面是否有方塊,從而停止此次運動 11{ 12shift(); 13move(); 14show(); 15Sleep(speed); 16} 17score = eliminate(); 18scoretotal += score; 19level = scoretotal / 4; 20if (level == LEVEL) 21{ 22break; 23} 24speed = Speed[level];//根據等級改變速度,同時關閉速降功能 25F.Draw_score(scoretotal, level+1); 26Sleep(speed); 27} 28return; 29 }
至此300多行的Diamond類講的差不多了,如果要看整體程式碼可以進入文章末尾的Github賬號。
四
main.cpp
這裡的設定就很簡單了,用一個while迴圈是遊戲實現再來一局的功能。
1 #include<iostream> 2 using namespace std; 3 #include"Tool.h" 4 #include"Frame.h" 5 #include"Unit.h" 6 #include"Diamond.h" 7 /*coded by 郭志 82018/9/18*/ 9 int main() 10 { 11Tool T; 12Frame F; 13char chioce; 14bool flag=true; 15F.Draw_border(); 16F.Draw_message("郭志"); 17 18while (flag) 19{ 20Diamond diamond; 21diamond.control(); 22if (diamond.get_level() < 50) 23{ 24T.gotoxy(0, 0); 25cout << "你這盤達到的等級是" << diamond.get_level() << endl; 26cout << "是否繼續? 是(y)" << endl; 27cin >> chioce; 28if (chioce == 'y') 29{ 30flag = true; 31} 32else flag = false; 33} 34} 35return 0; 36 }
好的,俄羅斯方塊的實現基本就是這些了,會了演算法和遊戲邏輯,雖然現在的介面比較醜陋,但是相信以後也能做出一些好看、好玩的俄羅斯方塊。
原始碼在http://github/Guozhi-explore。