# Computer Programming Final Project Report <font color=grey> <font size=5> Author : B09901020 蔡仲廷 B09901035 陳亮瑜 B09901156 張哲銘 </font> </font> ## 0. 簡介 Abstract 這次的期末專題我們想要藉由本學期學過的C++語法來實現一個很久以前在Google Play流行的一款塔防遊戲:**Robo Defense**。Robo Defense的遊戲架構很簡單,就是要在地圖上建立各式各樣的塔,以防止敵人自入口進到右邊的要塞內。 ![](https://i.imgur.com/ZIePTKk.png) **Figure 0-1:The actual Robo Defense game screen.** ### 0.1 [github](https://github.com/tim901231/cp_final.git) 這邊是我們這次project的git,如果說想看作業以外的其他檔案可以在這便查閱 ## 1. 實作過程 Process 這次的final project使用Visual Studio建立的專案,影像處理使用SDL進行。在此專案內建立的標頭檔與其主要內容如下: | 標頭檔 | 內容 | | -------- | -------- | | bullet.h | 建立tower射出的bullet、bullet是否有擊中enemy | | tower.h |建立tower、鎖定enemy與攻擊 | | enemy.h | 建立飛行/一般的enemy、搜尋至要塞的路徑 | |menu.h |起始畫面 | | option_list.h |選單畫面 | | sys_act.h | 初始化、升級畫面;蓋塔畫面與一般遊戲時畫面處理| | wave.h | 不同wave要出的敵人 | |word.h | 文字的輸出| | variable.h | 全域變數| #### ==感謝git有儲存以前版本的功能,讓我們能看看一路怎麼走來== --- ### 1.1. 寫各自的class、鍵盤操作 ![](https://i.imgur.com/7sjMGln.png) **Figure 1-1:github repo** 遊戲中,分別有```tower```、```enemy```、```bullet```三種class,以及一些鍵盤與滑鼠控制(蓋房子和關掉遊戲),我們打算先寫各自的part之後再想辦法合併。 ### 1.2. 第一次合併檔案 ![](https://i.imgur.com/fYFgVSY.png) **Figure 1-2:first buildable program** 在合併檔案發現很容易出現class、variable的命名沒有溝通好的狀況,像是class ```ENEMY``` 和class ```enemy```,還有不小心把render寫在event裡面,導致圖片一直閃爍(每個迴圈畫面都會clear)。 ### 1.3. 加入enemy、介面美化 ![](https://i.imgur.com/NVgQLMi.png) **Figure 1-3:still many problem** 從圖中可以看到加入了各種enemy,但目前bullet的射擊、enemy的路徑計算、目前都還有問題,但右下的按鈕終於被去背了 ### 1.4. 解決砲塔旋轉、子彈射擊、enemy的移動 ![](https://i.imgur.com/YPNe9EJ.jpg) **Figure 1-4:playable game?** 砲塔的旋轉是計算enemy和tower間的arctan;子彈射擊在50個迴圈後會到當初瞄準的位置;此時的enemy是隨機的,會在每個迴圈計算最短路徑;右上角也能顯示money。當時有個好笑的bug是因為當enemy接近出口時,計算速度變快,迴圈變快,所以走的速度加快。 ### 1.5. 設計功能表 ![](https://i.imgur.com/pGN6Xty.jpg) **Figure 1-5:need a pretty option list** 做完功能表的我們發現程式執行的速度慢到會影響遊戲體驗(while的一圈要約30ms),把時間print出來後發現約有90%的時間都用在enemy_motion,這似乎是個不可忽視的問題…… ### 1.6. 程式優化、更美的美術 #### 程式優化:幸好我們的程式還能跑的更快,把enemy計算路徑的時間從每個迴圈改成蓋塔、拆塔、和傳送,讓每個迴圈的時間從約30ms到5ms以下 ![](https://i.imgur.com/st2uzUf.jpg) **Figure 1-5:cool startpage** 這個酷酷的startpage是張哲銘找了圖把中間的圓切開(要把圓心對上需要一些時間),然後用SDL的選轉顯示功能做的,把鼠標移到start上還會加速真的很酷 ![](https://i.imgur.com/nsxTjaD.jpg) **Figure 1-6: every kind of towers** 原本的九種塔升級成十五種!還有有傳送功能的塔。(其實能做出那麼多塔也是感謝網路上有提供塔旋轉到各種角度的3D圖片,所以只要計算角度後,顯示對應的clip,就能讓質感大大上升) ![](https://i.imgur.com/tZby05J.jpg) **Figure 1-7: every kind of enemies** 也是有現成的分解動作圖片,讓遊戲畫面看起來很順暢 #### ==剩下的其他功能應該會在下方提到== 以下為整個程式的主要結構: ```graphviz digraph nodestyling { nodesep=1.0 // increases the separation between nodes node [color=lightblue,style=filled, fontname=Times,shape=note] //All nodes will this shape and colour edge [color=black, style=bold] //All the lines look like this menu->play play->{option upgrade} option->{pause accelerate menu} play->end end->{win lose} win->leaderboard lose->leaderboard {rank=same;upgrade option play} {rank=same; end} {rank=same;win lose} } ``` **Figure 1-8:Program structure.** ## 2. 技術特點 Highlight 我們的遊戲有幾個特別值得注意的特色: ### 2.1. class ```ENEMY``` 這個class負責處理小怪的資料包括血量、掉的金錢、路徑、是否可以飛……等等 ```cpp=1 class ENEMY { public: bool CanFly, wtflag; int TYPE, hp, pos, dir, money, period; double speed, freeze, nowx, nowy, current_phase; vector<pii> PATH; SDL_Texture *pic; SDL_Rect rect, green, red; ENEMY(int type); bool FindPath(bool isair); void GoPath(); void calculate_hp(); }; ``` **Figure 2-1: ```ENEMY.h``` class.** 其中還有幾個member function: #### 2.1.1 ```bool FindPath(bool isair)``` 這個函式會在建塔或賣塔的時候被呼叫,其中的參數```isair```是一個```bool```型態的值,代表這個怪物是否能飛,而回傳值也是bool型態,代表對於目前這隻怪物是否存在至少一條路徑能讓他走到終點。 對空軍而言,只需要從起點直直飛到終點,且一定存在路徑。 ```cpp=1 if (isair) { PATH.clear(); pos = 0; for (int i = 0; i < 18; i++) { PATH.push_back({ i,5 }); } return true; } ``` **Figure 2-2: ```ENEMY::FindPath(isair)``` in enemy.cpp. (Part 1)** 但是對陸軍就沒這麼簡單了,為了尋找怪物當時所在位置的最短路徑,我們必須使用**廣度優先搜尋演算法(Breadth First Search, BFS)**。這個演算法的作法是檢查目前所在位置的右、上、左、下是否為「可以走到而且還沒走過」的點,如果是的話,將符合條件的點丟進一個```queue```裡,並且記錄這些點是從目前的位置走到的。一直重複這樣的操作,如果走到```queue```變空仍無法走到終點則代表蓋下這個塔會擋住這隻怪物到終點的唯一路徑,這個塔就是不合法的塔,不可以被蓋下;反之,若在上述的過程中走到了終點,就表示從怪物目前的位置能夠走到終點,再藉由搜尋過程的紀錄就可以找到整最短路的路徑了,怪物就會依照搜尋好的最短路徑走向終點。 ```cpp=1 typedef pair<int, int> pii; #define X first #define Y second class val{ public: pii pos; vector<pii > shortest_path; }; queue<val> q; val start, path; start.pos = PATH[pos]; start.shortest_path.push_back(start.pos); q.push(start); bool visited[18][10] = {}, exist_path = false; visited[start.pos.X][start.pos.Y] = true; while (!q.empty()) { path = q.front(); q.pop(); if (path.pos == make_pair(17, 5)) { exist_path = true; PATH.clear(); PATH = path.shortest_path; pos = 0; while (!q.empty()) q.pop(); break; } for (int i = 0; i < 4; i++) { if (check(path.pos + DIR[i]) && !visited[path.pos.X + DIR[i].X][path.pos.Y + DIR[i].Y] && !towers[path.pos.X + DIR[i].X][path.pos.Y + DIR[i].Y]) { visited[path.pos.X + DIR[i].X][path.pos.Y + DIR[i].Y] = true; val tmp = path; tmp.pos = path.pos + DIR[i]; tmp.shortest_path.push_back(tmp.pos); q.push(tmp); } } } return exist_path; ``` **Figure 2-3: ```ENEMY::FindPath(isair)``` in enemy.cpp. (Part 2)** #### 2.1.2 ```void calculat_hp()``` 這個函式是用來計算怪物的血條,依照目前的血量和滿血量的比例調整綠色正方形和紅色正方形的長度,有了血條的設計,玩家就可以知道怪物目前的血量並依此考量戰術。 ```cpp=1 void ENEMY::calculate_hp() { green.w = hp * 90 / (MAX_HEALTH[TYPE] * rate); green.h = 10; red.w = 90 - green.w; red.h = 10; green.x = rect.x; green.y = rect.y + 90; red.x = green.x + green.w; red.y = green.y; } ``` **Figure 2-4: ```ENEMY::calculate_hp()``` in enemy.cpp.** ### 2.2. 使用者控制加速、停止 在全域變數中的```speedy```可以透過使用者控制而使遊戲加速進行。 當遊戲為一般的進行狀態時,```speedy```=1,當按下停止鍵時,會進入一個```while true```迴圈,直到玩家再次按下start才會跳出,形成停止的效果。而當玩家按下加速鍵時,則會把```speedy```的值改成2。這些改變```speedy```的效果在各個class裡面使用到時就會產生加速或停止的效果。 ```cpp=1 void option_act() { if (point_in_rect(mouse_position, pausebottom)) { while (1) { if (SDL_PollEvent(&e) != 0) { if (e.type == SDL_KEYDOWN && e.key.keysym.sym == SDLK_ESCAPE) { quit = true; break; } if (e.type == SDL_QUIT) { quit = true; } if (e.type == SDL_MOUSEBUTTONDOWN) { SDL_GetMouseState(&mouse_position.x, &mouse_position.y); if (point_in_rect(mouse_position, startbottom)) { speedy = 1; break; } } } } } if (point_in_rect(mouse_position, startbottom)) { speedy = 1; } if (point_in_rect(mouse_position, fastbottom)) { if (speedy <= 2) { speedy += 1; } } ``` **Figure 2-5:```option_act()``` in option_list.cpp.** ```cpp=1 if (PATH[pos + 1] - PATH[pos] == DIR[RIGHT]) { nowx += speed * (1 - freeze / 100) * speedy; dir = RIGHT; } ``` **Figure 2-6:```ENEMY::GoPath()``` in enemy.cpp.** ```cpp=1 bool tower::ableatk(int t_c) { if (t_c - t > cooltime / speedy) { t = t_c; return true; } return false; } ``` **Figure 2-7:```tower::ableatk()``` in tower.cpp.** ```cpp=1 ... v_x = (enemy1->rect.x - tower1->x) * 1.0 / speed * speedy; v_y = (enemy1->rect.y - tower1->y) * 1.0 / speed * speedy; ... ``` **Figure 2-8:```bullet::bullet()``` in bullet.cpp.** ### 2.3. 使用者感應優化 #### 2.3.1 大小控制 在sys_act.cpp中加入一個能隨時改變SDL_Rect大小的函數。 ```cpp=1 void setrect(SDL_Rect& r, int a, int b, int c, int d) { r.x = a; r.y = b; r.w = c; r.h = d; return; } ``` **Figure 2-9:```setrect()```in sys_act.cpp.** 當有```SDL_MOUSEMOTION```在option button render到的```SDL_Rect```時,則會改變圖片的大小,就能夠顯示出使用者的滑鼠有明顯的經過該處,強調點擊時的玩家感受。 而所有選項清單內的按鈕均有做過此處理。 ```cpp=1 void animation() { SDL_GetMouseState(&mouse_position.x, &mouse_position.y); if (status == play){ if (point_in_rect(mouse_position, option_bottom) == true) { setrect(option_bottom, 0, 940, 70, 70); } else { setrect(option_bottom, 10, 950, 50, 50); } } else if (status == option){...} //format are similar } ``` **Figure 2-10:```animation()```in sys_act.cpp.** ![](https://i.imgur.com/Ri2LPMQ.png) ![](https://i.imgur.com/iWzbNG1.png) **Figure 2-11:Implementation our version of changing the size when moving out (up) the option button and in (down) the botton.** #### 2.3.2 旋轉速度控制 startpage的旋轉速度由```SDL_RenderCopyEx```可以旋轉圖片而實現: ```cpp=1 if (e.type == SDL_MOUSEMOTION) { SDL_GetMouseState(&mouse_position.x, &mouse_position.y); startflag = false; if (point_in_rect(mouse_position, turning1)) { startflag = true; } } ``` **Figure 2-12: ```menu_act()``` in menu.cpp. (Part 1)** ```cpp=1 if (startflag == true) { SDL_RenderCopyEx(gRenderer, startturning3, NULL, &turning3, degrees, NULL, flipType); SDL_RenderCopyEx(gRenderer, startturning1, NULL, &turning1, 1.3 * degrees, NULL, flipType); SDL_RenderCopyEx(gRenderer, startturning2, NULL, &turning2, -1.6 * degrees, NULL, flipType); degrees += 1; } else { SDL_RenderCopyEx(gRenderer, startturning3, NULL, &turning3, degrees, NULL, flipType); SDL_RenderCopyEx(gRenderer, startturning1, NULL, &turning1, 1.3 * degrees, NULL, flipType); SDL_RenderCopyEx(gRenderer, startturning2, NULL, &turning2, -1.6 * degrees, NULL, flipType); degrees += 0.1; } ``` **Figure 2-13: ```menu_act()``` in menu.cpp. (Part 2)** #### 2.3.3 蓋塔/放置模式 在建立塔的時候,為了使玩家能夠知道現在再蓋塔還是一般的放置模式,因此在蓋塔時進入的state會在當前滑鼠位置也render一個塔模擬你之後預設要蓋的位置。此外,在錢足夠時才能夠顯示完全不透明的圖片,則是透過alpha blending來實現。 ```cpp=1 if (lightflag == true) { SDL_GetMouseState(&mouse_position.x, &mouse_position.y); lightrect.x = mouse_position.x - 45;// lightrect.y = mouse_position.y - 45; SDL_SetTextureBlendMode(light, SDL_BLENDMODE_BLEND); SDL_SetTextureAlphaMod(light, 192); //3/4 transparent SDL_RenderCopy(gRenderer, light, NULL, &lightrect); } if (slowflag == true){...} // format are similar if (rocketflag == true){...} // format are similar ``` **Figure 2-14: ```show_building()``` in sys_act.cpp.** ### 2.4. 排行榜設計 遊戲結束時會跳出一張排行榜,上面會記錄目前玩過的前5高分,以及本次遊玩的分數。 ![](https://i.imgur.com/Vje7g3B.png) **Figure 2-15: Demo:game over and leaderboard.** 當你的分數破紀錄了,可以把自己的名字登記到排行榜上,但是一定要輸入英文! 我們的排行榜以兩個函式維護: #### 2.4.1 ```int loadLeaderboard()``` 排行榜的資料會儲存在rank.txt中,這個函式會讀入```rank.txt```的資料然後檢查玩家的成績是否有破紀錄(進入前5名),有的話把玩家排到正確的位子並回傳玩家的成績為榜上的第幾名,否則回傳-1。 ```cpp=1 int loadLeaderboard() { int n = -1; fstream file; file.open("rank.txt", ios::in); for (int i = 0; i < 5; i++) { file >> id[i] >> scores[i]; } for (int i = 0; i < 5; i++) { if (currentscore > scores[i]) { for (int j = 4; j > i; j--) { scores[j] = scores[j-1]; id[j] = id[j - 1]; } scores[i] = currentscore; id[i] = "Yourname"; n = i; break; } } file.close(); return n; } ``` **Figure 2-16: ```loadLeaderboard()``` in sys_act.cpp.** #### 2.4.2 ```void save_leaderboard()``` 將更新後的leaderboard重新寫入rank.txt。 ```cpp=1 void save_leaderboard() { fstream file; file.open("rank.txt", ios::trunc|ios::out); for (int i = 0; i < 5; i++) { file << id[i] << " " << scores[i] << "\n"; } file.close(); } ``` **Figure 2-17: ```save_leaderboard()``` in sys_act.cpp.** ## 3. 其他 others ### 3.1 心得感想 * **蔡仲廷** 很開心可以跟凱瑞的組員同組,一開始其實還蠻擔心自己會雷,不過還好有同學們互相討論,一一的解決了問題。覺得在這次的project中,學到最多的是怎麼和別人合作吧,上了大學第一次和別人合作做專案,發現很多時候要把各自負責的事情說清楚,才能繼續進行,現在使用git更加熟悉、練習了分檔,學到了很多而且很開心。 * **陳亮瑜** 我覺得這次的計程專題是一個很棒的合作體驗,大家分工完成一件大型專案的感覺真的很舒服,大家都為了讓專案變得更完整而日夜顛倒(?)的寫code,還常常導致上傳github時產生conflict,不過看到這個遊戲逐漸完成且功能越來越齊全真的很有成就感,組員也都很carry,一起討論解決問題才做出了這個project。 * **張哲銘** 這次我處理的部分可以說是三個人裡面最簡單的,畢竟相對於其他兩位的程式實力,我顯然無法寫出很困難的程式,但很高興的是一開始認為不可能完成的遊戲,最後在一步步學習SDL,並且和隊友合併、擴大程式的過程中,最後竟然實現了!這是這次學習最多的地方,也很感謝另外兩位適時的協助我使用git跟教導我使用不會的SDL功能~ ### 3.2 未來可以改進的地方 <font color=#FF0000> 1. 音樂在哪裡? </font><font color=orange> 2. 更多地圖 </font><font color=#FFBF00> 3. 線上化: </font><font color=green> 3-1. 競爭mode:先把兵清完的人會讓另一人提早出兵,造成對手陷入危機 </font><font color=blue> 3-2. 合作mode:兩人合玩一張地圖 </font><font color=#800080> 4. 隨機性:可能可以設定打完第幾關的boss就得到某種能力 </font><font color=violet> 5. 利息:加上利息制度可以讓它不是個無腦猛蓋的遊戲,有時候貪心存錢也是個策略? </font><font color=#FFAAAA> 6. 功能:一次選取多個位置進行建塔、拆除 </font> ### 3.3 工作分工表 * **蔡仲廷** - [x] class tower, bullet - [x] leaderboard * **陳亮瑜** - [x] class enemy - [x] enemy waves * **張哲銘** - [x] menu - [x] user's mouse control - [x] inkscape drawing :::info Inkscape是向量圖形編輯器,是強大的繪圖軟體。使用這個畫圖雖然很好看可是很麻煩qq ::: ## 4. 助教打勾處 - [ ] 我看完了 - [ ] 我跳著看 - [ ] 我懶得看