# Snake_C++ Final Project ## 題目整理 ### 說明 交大電機「計算機概論於程式設計」110學年度期末專題, 雖然為簡單的程式練習,希望同時增進專案開發通整的能力以及研究紀錄的撰寫。 [Snake_Final_Project [GitHub]](https://github.com/william0503tw/Snake_Final_Project) ### :mountain: 目標 1. 玩家須控制貪吃蛇在地圖上行走。 2. 地圖需隨機產生多個食物,蛇吃掉後則長度增加1個單位。 3. 地圖需包含邊界以及障礙物,若蛇撞到邊界/障礙物,或是吃到自己的身體,則遊戲結束。 4. 需記錄遊戲過程中的得分於地圖上方顯示欄中。 ### :book: 規則 - [x] 鍵盤W、S、A、D代表蛇上下左右的移動。 - [x] 遊戲地圖長寬 50*25 (不包含邊界)。 - [x] 初始畫面中,需顯示蛇、食物、障礙物、邊界、記分板、等...。 - [x] 食物被吃掉後,在地圖中任意位置重生。 - [x] 記分板需能「及時」刷新分數,顯示當前蛇的身長。 - [x] 蛇撞到邊界、障礙物、本身則死亡,地圖下方顯示遊戲結束特效。 ### :robot_face: 評分項目 - [x] 蛇能夠正確上下左右操控,不可穿越障礙物、邊界。 - [x] 食物能更新 - [x] 記分板能即時更新 - [x] 蛇的死亡條件正確 - [x] 蛇死亡後,地圖下方能正確顯示特效 ==加分項目== - [x] 邊界上出現一對傳送門,從任一方進入,可從另一方出來。 - [ ] 食物具備保存期限,過期的話,食物閃爍三秒後消失。 - [ ] 遊戲結束時詢問是否完下一場遊戲。 - [x] 隨著時間增加,蛇行走速度不斷上升。 - [x] 吃到特殊食物後蛇立刻變型 ex: 原本用*表示蛇,吃到食物候用@表示蛇。 - [ ] 蛇能夠發射子彈破壞障礙物(限定2發)。 - [ ] 蛇經過一段時間沒吃到食物(15秒),則餓死,遊戲結束。 - [ ] 遊戲開始前可以自訂遊戲地圖長寬其他,視難易程度加分。 - [x] 新增顏色 - [x] 自行設定移動速度、顏色、是否要障礙物、障礙物是否隨機 ### :file_folder: 繳交格式 1. 原始檔案,包含CPP檔(請詳細註解、縮排)、執行檔..等等。 2. 流程圖(用圖表表示或文字敘述均可)。 3. 心得報告。 --- ## 心得報告 ### :book: 筆記 1. 用到的函式庫 ```cpp= #include <iostream> #include <ctime> //srand #include <cstdlib> //srand、rand #include <windows.h> //SetConsoleCursorPosition() #include <thread.h> //readInput #include <conio.h> //kbhit()、getch() ``` 2. readInput - kbhit(): 檢查鍵盤是否有鍵按下 - getch(): 從鍵盤讀入一個字元,並回傳其ASCII值 - 兩者皆須#include <conio.h> ```cpp= void readInput(void* id) //判斷使用者的input { char c; do{ c = getch(); //從鍵盤讀入任意字元 switch(c){ case 'w': direction = UP; break; case 'a': direction = LEFT; break; case 's': direction = DOWN; break ; case 'd': direction = RIGHT; break ; } }while( (c != 'q') || c != (char)27); _endthread(); //终止由 _beginthread所創建的線程 return; //cout << direction << endl ; 檢查判斷是否正確 } ``` 3. 利用srand去改變食物的位置 - srand、rand概念 ```cpp= #include <iostream> #include <cstdlib> //亂數相關函數 #include <ctime> //時間相關函數 int main() { srand( time(NULL) );//利用時間作亂數種子 int x = rand();//產生亂數 return 0; } ``` - 隨機決定食物的初始位置座標 ```cpp= int temp ; srand((unsigned int)time(NULL)); for(int i = 1 ; i <= 7 ; i++) { if(temp != rand()) { int x = rand() % (x_size - 2) + 1 ;//x範圍為1~50 int y = rand() % (y_size - 2) + 1 ;//y範圍為1~25 ground[x][y] = FOOD ;//食物位置座標 } else { srand(0); int x = rand() % (x_size - 2) + 1 ; int y = rand() % (y_size - 2) + 1 ; ground[x][y] = FOOD ; } temp = rand(); } ``` 4. Sleep - 讓程式暫停執行一段時間,單位為「毫秒」,1秒 = 1000毫秒 - 須#include<windows.h> ```cpp= Sleep(1000) // 暫停程式1秒(1000毫秒) ``` 5. system("cls") - 把已經輸出的文字清掉,然後再把游標移回到第一行最前面的位置。 6. 設定顏色 - 利用SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),a); 7. 游標移動函式 - 利用設定游標位置 SetConsoleCursorPosition(),記得#include <windows.h> - HANDLE : 從一個標準設備中取得句炳 - COORD : 一个字符在螢幕上的座標 ```cpp= void gotoxy(short x,short y)//游標移動函式 { HANDLE hStdOut; hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);//從一個標準設備中取得句炳 if (hStdOut == INVALID_HANDLE_VALUE) return; //調用失敗則返回 COORD pos={x,y}; //pos.x = x; //pos.y = y; SetConsoleCursorPosition(hStdOut,pos);//設定游標位置 } ``` 8. 設定視窗大小 setWindowSize - HWND : Handle of WiNDow = 視窗的句柄 - GetConsoleWindow() : 取得視窗的句炳 - RECT : 儲存一個矩形左上角的横座標、縱座標以及矩形的高度、宽度 - GetWindowRect(width , height) : 取得RECT的指定尺寸 - LPRECT : 指向一個RECT結構的指標,該結構接收視窗的左上角和右下角的螢幕座標 - MoveWindow : 改變視窗的位置和尺寸,結構如下 ```cpp= void MoveWindow( int x, //視窗左上角的橫坐標 int y, //視窗左上角的縱座標 int nWidth, //視窗寬度 int nHeight, //視窗高度 BOOL bRepaint = TRUE //TRUE => 重畫視窗 ); void MoveWindow( LPCRECT lpRect, //接收視窗的左上角和右下角的螢幕座標。 BOOL bRepaint = TRUE ); ``` - setWindowSize的完整程式 : ```cpp= void setWindowSize(int width, int height) { HWND console = GetConsoleWindow(); //取得視窗的句炳 RECT ConsoleRect; GetWindowRect(console, &ConsoleRect); //取得RECT的指定尺寸(console讀入width、&ConsoleRect讀入height) MoveWindow(console, ConsoleRect.left, ConsoleRect.top, width, height, TRUE); //改變指定視窗的位置和大小 } ``` 9. 更新屏幕 ( void clearScreen() ) - CONSOLE_SCREEN_BUFFER_INFO : 包含主控台螢幕緩衝區的資訊,可利用此資訊來決定資料在視窗中的顯示方式 - CONSOLE_SCREEN_BUFFER_INFO 結構如下 ```cpp= typedef struct _CONSOLE_SCREEN_BUFFER_INFO { COORD dwSize; //目前主控台螢幕緩衝區的大小 COORD dwCursorPosition; //主控台螢幕緩衝區中資料指標的資料行和資料列座標 WORD wAttributes; SMALL_RECT srWindow; //視窗左上角和右下角的主控台螢幕緩衝區座標 COORD dwMaximumWindowSize; //指定目前螢幕緩衝區大小、目前的字型大小和螢幕視窗的大小上限 } ``` - DWORD : 相當於 unsigned long,一般用於回傳值不會有負數的情況 - FillConsoleOutputCharacter : 從指定的座標開始,將字元從主控台螢幕緩衝區寫入指定的次數。寫法如下 ```cpp= BOOL WINAPI FillConsoleOutputCharacter ( _In_ HANDLE hConsoleOutput, //螢幕緩衝區的句炳 _In_ TCHAR cCharacter, //寫入緩衝區的字元 _In_ DWORD nLength, //寫入字元的數量 _In_ COORD dwWriteCoord, //給定要寫入的資料格的座標 _Out_ LPDWORD lpNumberOfCharsWritten //接收實際寫入螢幕緩衝區的字元 // _In_ 表一個輸入的參數 // _Out_ 表一個輸出的參數 ); ``` - FillConsoleOutputAttribute : 設定從螢幕緩衝區中的指定座標開始的字元屬性。寫法如下 ```cpp= BOOL WINAPI FillConsoleOutputAttribute( _In_ HANDLE hConsoleOutput,//螢幕緩衝區的句炳 _In_ WORD wAttribute, //字元的屬性 _In_ DWORD nLength, //要設定為指定屬性的儲存格數 _In_ COORD dwWriteCoord, //首個設定屬性的字元位置座標 _Out_ LPDWORD lpNumberOfAttrsWritten //接收寫入的字元數 ); ``` - void clearScreen()的完整程式 : ```cpp= void clearScreen() { HANDLE hStdOut; //螢幕緩衝區句炳 CONSOLE_SCREEN_BUFFER_INFO csbi; //螢幕緩衝區資訊 DWORD count; //接收的字元數量 DWORD cellCount;//寫入的字元數量 COORD homeCoords = { 0, 0 }; //取得螢幕緩衝區的句炳 hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdOut == INVALID_HANDLE_VALUE) return; if (!GetConsoleScreenBufferInfo(hStdOut, &csbi)) return; //寫入的字元數量 = 視窗大小 cellCount = csbi.dwSize.X *csbi.dwSize.Y; //用空格填充整個緩衝區 if (!FillConsoleOutputCharacter( hStdOut, //螢幕緩衝區的句炳 (TCHAR) ' ', //填入空格 cellCount, //填入空格的數量 homeCoords, //填入第一個空格的座標 &count //接收寫入的字元 )) return; //用當前的顏色和屬性填充整個緩衝區 if (!FillConsoleOutputAttribute( hStdOut, //螢幕緩衝區的句炳 csbi.wAttributes, //字元的屬性 cellCount, //要設定為指定屬性的儲存格數 homeCoords, //第一個設定屬性的字元位置座標 &count //接收寫入的字元數 )) return; //將游標移回原點 SetConsoleCursorPosition(hStdOut, homeCoords); } ``` 更/ 讚讚 : ) ### :pencil: 紀錄 邱[2021/11/30] : - 完成期末專題HackMd共筆 - 確認題目需求 邱、林[2021/12/2] : - 使用Clion協作 - 訂定目標:自修預習到Class (CH10) - Host端在邱,林欲編輯時,邱開 - 圖形介面直接參考範例(如果OK,上顏色) - 完成時間規劃(如下) 邱[2021/12/3] : - snake::initPlayground() - 新增updateScreen(), updateScreen::drawScore() - 新增class "snake" - 場地畫面截圖 - ![場地圖片](https://i.imgur.com/GZProWq.png =75%x) 邱[2021/12/4] : - snake::onTouch() //tells what thing does the snake's head touched 邱、林[2021/12/5] : - 蛇的初始狀態先設定為■,之後再想辦法加長 - 新增七個食物的初始位置(使用srand+rand) - updateScreen()修正 - 邏設加加油! XD 12/7更 邏設大爆炸 - 12/7更 我邏設也大爆炸哭哭 邱[2021/12/7] : - func gotoXY,左上為(1,7) 右下為(50,31)可增加「偏移量」使(0,0開始) - setWindowSize : 打開時視窗大小才不會跑掉 - readInput完成 - direction 移至class snake variables裡面 - 最下面那一行(場地正下方) y=33 - 經過偏移,左上笑臉(1,1) 右下笑臉(50,25),使用時以左上為(1,1) - xy向右向下為正 - ![](https://i.imgur.com/g37i15u.png =50%x) - ㄟ不是,到底要怎麼順toggole direction,網路上都是用螢幕刷新 嗚嗚 - 林 : 你太快了哭哭,這樣我好像小廢廢,你要不要分配一點東東給我做 邱[2021/12/8] : - readInput成功了 哈哈哈 - random不再是同sequence - [Moving Test 1](https://youtu.be/JAo3QygDOzA) 邱、林[2021/12/8] : - 生成障礙物,但非隨機,若有時間再研究如何隨機產生障礙物 - 新增recreateFood,但是失敗ㄌ => 再修正 邱[2021/12/9] : - 為了確認符合程式整體邏輯,cout障礙物程式碼放在[這裡](https://hackmd.io/J91IKjL9QBS5NTBWJuGYiQ) - 印出所有index(Debug且可判斷位置) - ![](https://i.imgur.com/uPkGxQ2.png =50%x) - 食物隨機生成不會卡牆(recreate food, init food) - [GamePlay Video](https://youtu.be/VAo5DUKyNwM) - main裡面可以觸發gmaneOver條件 - 把障礙物index進ground[][]裡面 - 把所有snake class的變數移到public - 每吃5個食物,速度會增加 - 輸出障礙物不是用gotoXY,而是丟到ground[][]裡面,但日後不易更改樣式 - 更改score的方式是用gotoXY - food initialize時從5個更改為3個 - 最後,尾巴到底怎麼用== 邱[2021/12/14] : - 哇! 全部完成 - 碰到食物時不pop_front,所以在下一次更新時長度就會變長 - 使用deque來implement身體的東西 - 要注意drawBody在updateSnake中的位置,會影響到x,y值,這就是之前一直失敗的原因。 - 此外,之前失敗也是因為許多先後呼叫順序影響的,可以跟上次的commit來比較 - [完成影片](https://youtu.be/CSZxJPydoRk) 邱、林[2021/12/15] : - 可以重新開始遊戲 - 每吃到6個食物就新增一個 Special Food - 吃到一個 Special Food 就改變身體符號及顏色 - 研究Ctime - 完成若user的輸入與蛇現在的行進方向相反,則維持原本的方向行進 林[2021/12/17] : - 顯示現在速度並即時更新 (之前速度沒有正確的被增加,因為吃到Special Food時的food count沒有 ++) - 先用cin >> skin,去改變初始顏色,看之後能不能改成讓使用者用選單的方式改顏色 - 但是body的顏色還沒調整好QQ (調整好了!) 林[2021/12/20] : - 用cin >> speed,改變初始速度 - 哭哭我不知道怎麼用選單的方式來讓user調整啦 -> 超難的啦 - 今天會搞定食物計時,先在這裡打下目標 :angry: (好好笑但不知道做不做得到) - 已經隨機建立出傳送門,但還在想到底要怎麼傳送 :( - 發現輸入y重新開始遊戲會有點bug 邱[2021/12/20] : - 新增共用[心得報告Google Docs](https://docs.google.com/document/d/1YewclVUv1ugUc74W5dVGUulyxIV8o6P1-1EVpfLdfUo/edit?usp=sharing) 林[2021/12/20] : - 可以隨機建立障礙物了 - 用cin讓使用者選擇是否要障礙物以及障礙物是否隨機 (不隨機障礙物還沒好哭哭) - 重新開始遊戲還是有點怪怪的 (輸入及分數顯示) - 目前剩下不隨機障礙物跟傳送門還沒完成 (不隨機障礙物完成!) - 我真的不會寫計時器QQ 林[2021/12/30] : - 遊戲初始介面完成 - 哭哭剩下"再玩一次"和"傳送門"沒有搞定好 - 完成傳送門! 邱、林[2022/1/8] : - 完成流程圖 ![](https://i.imgur.com/L23mdXY.png) ### :heavy_check_mark: TODO List (待做事項) - [x] 研究Github如何協作,並寫出簡短教學 (順便練習Git) - [x] 確認兩人各使用何種IDE - [x] 完成場地印出 - [x] 研究需要哪些Library - [x] 讀懂網路上的範例(Wrote By Poor Engineer) - [x] 流程圖產出 - [x] 研究gotoxy(Window.h) - [x] Auto set console window size - [x] fix random 在同個位置 - [x] 研究Sleep - [x] 碰到食物增加分數 - [x] 碰到食物增加身長 - [ ] 吃宵夜睡覺 - [x] 判斷死亡(碰到障礙物、蛇身) - [x] 死亡產生GAME OVER - [x] 自行新增一些加分條件(做得出來的XD) - [x] Function新增註解 - [ ] 心得報告 ## :clock10: 時間規劃 :heavy_check_mark: **[Week1]** [11/29~12/5] 完成流程圖以及撰寫想法 :heavy_check_mark: **[Week2]** [12/6~12/12] 寫Code :heavy_check_mark: **[Week3]** [12/12~12/19] 寫Code(加分題如果有空), 心得 (12/14完成) **[Week4]** [12/20~12/26] 時間緩衝+檢查 P.S. 最好狀況Week2, Week3完成 --- ## 網路資源 [Snake Game/PoorEnginerr](https://thepoorengineer.com/en/snake-cplusplus/) [Snake Cpp/Cplusplus.com](http://www.cplusplus.com/articles/3U75fSEw/) [如何設定顏色 ?](https://www.youtube.com/watch?v=E_-lMZDi7Uw&list=PLrjEQvEart7dPMSJiVVwTDZIHYq6eEbeL) [Youtube上的流程教學](https://www.youtube.com/watch?v=PSoLD9mVXTA) [不用執行緒的讀入keyboard方法](https://www.google.com/amp/s/www.geeksforgeeks.org/kbhit-c-language/amp/) [中文貪吃蛇範例](https://www.796t.com/article.php?id=15871) [遊戲開始前可自行設定顏色、選擇是否要障礙物、初始移動速度、是否開啟加速](https://github.com/TreeYeah/Snake) [貪吃蛇](https://www.bilibili.com/read/cv6183110) [Timer](https://crmne0707.pixnet.net/blog/post/316734972-c%2B%2B-%E8%A8%88%E6%99%82%E5%99%A8) [限時食物](https://iter01.com/146949.html) [介面](https://www.twblogs.net/a/5c2692bdbd9eee16b3dba17a) ## 備忘 [期末分組名單](https://docs.google.com/spreadsheets/d/1lYH35O7wEUSYWoxB0fEOuOG3gaYE-x_o4revlsdCJVA/edit#gid=0) [流程圖](https://drive.google.com/file/d/1dx8aroci1TWFalambsf7X0qTSc0fQhDf/view?usp=sharing) ###### tags: `交大課業`