# Python遊戲開發 ---火輪手槍 > 運用 Python 程式語言自行設計一款單人遊戲 ---**火輪手槍**,介紹 Python 程式語言與 Scratch 的不同,並說明如何利用**幾何**、**三角函數**等數學原理解決遊戲中的實際問題,再以程式語言實踐。 ## 自我介紹 我是一名國中生,在國小六年級開始接觸Scratch,到了國二開始學習寫程式。 平常除了軟體開發之外,還有經營部落格「LancatServer 懶貓伺服器」、寫一些音樂。 還算擅長:Python, Arduino, Linux系統, 寫作, 音樂創作 我的網站:https://lancatlin.github.io 我的Medium: https://medium.com/@lancatlin 我的Github: https://github.com/lancatlin 我的專頁:https://facebook.com/lancatserver --- ## FireWheel火輪手槍 FireWheel火輪手槍是我在寒假時製作的一個遊戲專案,當時是將它作為一個休閒來做。在寒假的時候我們在進行科展,當一天研究結束後,我就拿這個專案出來做,一做就又是好幾個小時。 這個遊戲對我的特別意義在於,**這不是我第一次設計它**,其實在我國一,還在使用Scratch時,我就曾經製作過。 在場有去年也參加JSP的人嗎?去年我的筆電就有當時的Scratch版,當時應該有不少人有看過。 ### 遊戲說明 火輪手槍是一個戰鬥遊戲,玩家在一個迷宮中,四面八方會出現怪物來攻擊你,你只能靠著身邊的一把手槍來生存下去。 這個遊戲最特別的就在於我獨創的「火輪」,這把火輪手槍會繞在你身邊,當你按下發射鍵時會停下,開始左右搖擺,鬆開後就會發射出子彈。 新手遇到這樣的操作多半是很不習慣,因為這很難瞄準,必須花很多心思在看那把槍的旋轉;但是當你操作順手之後,你就會愛上它,因為這很有挑戰性,相較於普通的滑鼠瞄準或者是朝向正前方,火輪更能突顯出玩家的技巧,因此更有成就感。 #### 操作說明: * 使用WSAD移動 * 使用空白鍵發射 * 使用JKL選取道具 (道具功能僅Beta版) ### 成果預覽 <iframe width="560" height="315" src="https://www.youtube.com/embed/l0J4UVALaQ4" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe> ### 淺談遊戲設計 這個遊戲是使用Python製作,Python是一門優秀的程式語言,很好學、語法簡單,我在去年暑假時接觸它,花了許多時間學習,到了今年寒假時,已經可以熟練的用它來開發專案。當時寒假無聊,沒事時就做火輪手槍當娛樂。 #### 從scratch 到 Python 我的程式生涯是Scratch起家的,或許有人不承認Scratch是一門程式語言,但對我而言,是啟發我電腦研究生涯的重要推手。 在前面提到過,我曾經用Scratch開發過火輪手槍,但後期因為效能低落與程式架構維護不彰而停止開發。 #### Scratch 的優缺點 **簡單**。使用Scratch開發真的是簡單到不行,老實說在我用Scratch開發的時期,我根本很少去查資料,為什麼? > 所有資料都在裡面了,還要查什麼? > 中文界面的Scratch編輯器,讓小學的我也能輕易理解,再加上一點的嘗試,很快就能完成一點皮毛。 以下列舉一些Scratch的優缺點 ##### 優點 * 簡單好用的碰撞檢測 * 將常用的幾何數學(如三角函數、畢氏定理)處理好 * 可一邊修改程式一邊執行 * **容易學** ##### 缺點 * 糟糕的自訂函數功能(可輸入參數,但沒辦法得到回傳值) * 分身一建立就回不來了(無法存取分身) * 沒有物件導向 * 效能低落(當你的分身數到幾百個時,你就知道) * **無法掌控的異步處理** 雖然對我而言,Scratch非常的不便,但是它的設計初衷本來就是為了小朋友而設計,我所需要的那些功能已經不是它應該提供的了。因此在一年級的下學期,我就非常的積極想要學習更專業的程式語言,最後便選中了簡單易學的Python。 那今天,我會將開發火輪手槍的過程中遇到的兩個問題挑出來講,分別是 碰撞檢測,及 三角函數。 ### Python 不「歹算」 Python是我第一門真正學會的程式語言。對我而言,Python是非常平易近人的,在學習Python之前,我其實想要學習的是 Java ,Java應該算是平常人比較常聽到的東西,因為打遊戲常常要裝。(誤) 當時曾經為了學Java買了一本書,很認真的讀,只是都沒有乖乖的做練習,只想要知道更多東西,後來沒學成,又繼續寫Scratch一個學期。 後來到了升二年級暑假前,剛好聽朋友提到Python,於是上網去試試看,一試不得了,整個開發環境、語法都非常的平易近人,花了兩天的時間就弄出了GUI來,(當然那時候根本只是照抄),後來暑假也同時在玩Arduino,去年有來的同學應該知道,去年我在海報發表,主題就是我的遙控車。 所以在二年級,我幾乎寫的都是Python,我也強力推薦對程式有興趣的朋友以Python開始,Python **真的 不 「歹算」。** --- ## 物件導向基礎 電腦程式設計有許多種專家在研究多年所產生「編程範式」,你的程式符合編程範式,可以讓你的專案用更好的方式開發、或者讓日後容易維護、以及程式碼更容易閱讀。 有名的編程範式有:函數式程式設計、物件導向程式設計等等。在當代,物件導向程式設計最受推崇,不少資訊課程一定會教到,出去工作參與他人專案也幾乎都會遇到。 物件導向程式設計的核心概念是:物件是程式的基本單位,將程式的執行過程視為一群物件的交流過程。 使用物件導向來開發專案的好處是易於維護,物件導向的特性可以將代碼重用,事後修改也較輕鬆。那在我這個專案也不例外。 接下來我會介紹物件導向中的「類與物件」,這只是物件導向的基礎,至於物件導向的詳細知識由於相當複雜,而且我也不敢保證自己完全理解,所以我就不在這次說明。 ### Class (類) 以及 Object (物件) 在剛剛提到,物件導向中的程式是由一群物件建構起來的,那該如何創造一個「物件」呢? 答案是定義Class。Class,也就是類別,你可以將它視為物件的模板,在Class中你定義每個物件有著什麼**屬性**,會做什麼**行為**,然後你再建立此Class的實例,根據你給予的資料,就會產生不一樣的**物件**。 舉例我們要建立一群殭屍,那這些殭屍或許每一隻的顏色不一樣,座標不一樣,但是它們的行為模式是一致的,這些殭屍就是我們產生的「物件」,而定義我們殭屍物件該如何行動的則是殭屍「類別」。 #### 屬性 與 行為 屬性是一個物件所帶有的資料,以殭屍為例會有座標、顏色、移動速度、血量等屬性,這些屬性有些是數字,有些是字串,有些甚至是別的物件;行為則是一個物件的行為,多半是一個函數,例如殭屍會移動、會死掉。 屬性和行為的區分大多是明顯的,且在定義方式就有所不同。但是在某些語言例如Javascript,就直接將行為作為屬性。 一個殭屍物件可能包含的屬性有:x, y座標、顏色、移動速度、血量等等,那它可能包含的行為有:靠近玩家、...抱歉殭屍這個類別它的行為真的很少。 要存取物件的屬性或方法就是使用 **.** ,沒錯就是英文的句點,假如說你有一個Zombie的物件名叫my_zombie,你要讓它向玩家靠近,就使用 my_zombie.near(),如果你想存取它的座標,你就使用 my_zombie.x my_zombie.y ### 參考自己 ---self 當我們在定義類別時,需要取得自己的屬性或方法,這時候無須再傳一個參數到函數中,在Python中,只要用 **self** 就可以存取自己的屬性了。 假如今天這個函數是殭屍的物件方法,那self就會是存取殭屍的屬性;如果今天是玩家呼叫這個函數,那self就會存取玩家的屬性。有了self,我們就可以操作自己的屬性,而無須再多傳送自己進去。 而呼叫自己的函數,在函數的最前面需要加上self參數。例如:殭屍要讓自己靠近玩家,self.near() ``` python class Zombie: def __init__(self): self.x = 0 self.y = 0 self.color = (180, 65, 120) self.speed = 5 self.blood = 1 def near(self): #靠近目標 ``` 而透過此類別產生物件則是: ``` python zombie = Zombie() zombie.near() ``` 在以下的介紹,有部份的參數是物件,例如碰撞檢測中的 circle, rect,或是會有一些程式實做在物件方法中,例如三角函數中的 near, move方法。 --- ## 遊戲的根本—碰撞檢測 ### 碰撞檢測為何重要? 一個好的遊戲中,有什麼是不可或缺的條件呢?除非你是開發桌遊或卡牌遊戲,否則你一定會需要 **「碰撞檢測」** 。 碰撞檢測為何重要?想一想,今天你玩Minecraft,如果你碰不到東西,整個人像幽靈一樣開旁觀者飄來飄去,會好玩嗎?嘿嘿,其實挺好玩的,亂七八糟!如果Minecraft不會碰到東西,絕對不會有人想玩! 那你隨便想以前的2D遊戲,例如超級馬力歐,馬力歐總要踩在地面吧?馬力歐也必須要碰到怪物會死掉,還得要用腳可以踩死怪物。那達成這些需要什麼?就是碰撞檢測,我們要能夠偵測「兩個物件相撞」這件事,而這個動作就稱為「碰撞檢測」。 碰撞檢測在scratch的實踐是非常簡單的,你只要拉出「碰到XXX?」來偵測一個狀態,就可以處理碰撞檢測。 #### 完了... Python沒有碰撞檢測 可是問題來了,Python並沒有內建碰撞檢測。咦?!!你不是說用程式語言功能比較強大?怎麼連這個基礎的功能都沒有。 我們必須要釐清一件事,「碰撞檢測」這類事情是遊戲設計比較會遇到的,Python的主要使用是做資料運算,像是現在很夯的AI、深度學習等等。大部分會有碰撞檢測的工具是「遊戲框架」,框架這種東西就如字面上的意思,它已經幫你把整個專案架構給設計好了,你只要按照需求把剩餘的東西給填入就好。 至於Python上有的遊戲框架...實在不多,而且學習如何使用一個框架也是非常耗費心力的過程,於是最後我就決定自己寫碰撞檢測啦! 在接下來的介紹中,我會按照各個形狀之間的碰撞檢測來做說明,之所以用形狀區分,由於我是用簡單的幾何圖形,例如:圓形、矩形,來進行碰撞檢測。是的,碰撞檢測在每種圖形的「算法」都不一樣,因為我的遊戲就只有圓形和矩形兩種形狀,因此僅實做圓形矩形之間的碰撞檢測。 --- ### 當殭屍碰到了你 ---圓形對圓形 ![圓的碰撞圖](https://i.imgur.com/xmlQDXr.png) **要如何去偵測一個圓形是否碰到了另一個圓形呢?** 在我的遊戲中,例如怪物碰到了玩家,子彈射中了怪物,玩家被子彈射中等等,都是圓形與圓形的碰撞檢測。如果我們無法檢測這些,嗯...那這個遊戲大概會很無聊,因此我們要來實做圓形對圓形的碰撞檢測。 首先,我們列出我們所知道的**資訊**: * x, y座標 * 半徑 就這樣?是的,就只要這樣。 我們利用圓的性質,由於邊長上的任何一點到圓心都等長,不管從那個角度相撞,撞擊點到圓心的距離都會是圓的半徑。 所以如果一個點距離圓心小於半徑,就代表跟它重疊了。 那由於有兩個圓,所以只要「兩個圓心的距離」小於「兩個圓的半徑之合」,就代表兩個圓重疊,也就是「碰到」了。 至於如何取得兩點之間的距離,就是使用「畢氏定理」囉!在場的一年級應該還沒上到,礙於時間有限,我就只說重點。畢氏定理可以讓我們取得一個直角三角形的斜邊長,那我們兩個相異點之間的距離,其實就是以這兩點為頂點,畫出的直角三角形的斜邊。 畢氏定理數學公式: ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/7ef0a5a4b8ab98870ae5d6d7c7b4dfe3fb6612e2) 由此可得出: ![](https://wikimedia.org/api/rest_v1/media/math/render/svg/2fef66265112bc5378959992887ca76314b1681e) ![](https://i.imgur.com/6ay8mM5.png) 以下為Python的實做: ``` python ''' 程式碼:計算c1與c2是否有相撞的函數 輸入值:兩個「圓形物件」,分別帶有x座標(x),y座標(y),半徑(r),三種屬性 回傳值:true(代表有碰到) or false(代表沒碰到) ''' def circle_to_circle (c1, c2) : #c1 c2分別為圓1,圓2 #d為兩點距離 d = ((c1.x - c2.x) ** 2 + (c1.y - c2.y) ** 2) ** 0.5 return d < (c1.r + c2.r) #c1.x 代表存取c1這個圓的x座標,以此類推 #Python的次方使用「**」來計算,開根號即為0.5次方 #程式語言中()與[]的意義不同,用於表示計算優先順序的一律為小括號 #撰寫者:林宏信 2018 7/21 ``` ### 矩形對矩形 ![矩形概念圖](https://i.imgur.com/v2x1sZP.png) 那今天如果是矩形之間的碰撞呢?雖然在我的遊戲中並沒有運用到矩形對矩形的碰撞,但是這算是碰撞檢測的基礎之一,因此我還是一併說明。 *#註:此處說明之矩形皆為**不旋轉**的矩形* 矩形對矩形的碰撞檢測需要用到「座標」的概念,總的來說,就是判斷兩個矩形的x範圍與y範圍有沒有**重疊**。只要x範圍與y範圍同時重疊了就代表這兩個矩形碰到了。 同樣的,我們先來列出已知資訊: * 座標(x, y) * 長寬(w, h) #### 註:矩形的表示方式 XYWH ![矩形xywh](https://i.imgur.com/VUjAnCP.png) 在電腦中,表示一個矩形的方法跟數學上有些許不同。首先,在電腦的座標系中,Y是向下增加的,這樣的座標系稱為「繪圖座標系」。 在繪圖座標系之中,原點在螢幕的左上角,因此x向右增加,y向下增加。 --- 先退一步來講,在數線中,該如何判斷一條線與另一條線重疊? 我們可以用邊界的角度來思考,今天有兩條線段 AB線段, CD線段 在同一直線上,會有兩種情形,CD 在 AB 的左邊或者右邊。假如 CD 在 AB 右邊時,只要 C 比 B 還要左邊,就代表重疊了;假如 CD 在 AB 左邊,只要 D 比 A 還右,就代表重疊了。 發現了嗎?只要我的左邊比你的右邊還左 and, 我的右邊比你的左邊還右,就代表我們站在前後了,(人不能相交啦)。 ![](https://i.imgur.com/n37mztb.png) 那今天我們把左邊右邊換一個說法,在繪圖座標系中一個矩形的座標位置會是左上角,那這樣左邊就是x,右邊是x + w;上面是y,下面是y + h。 你沒聽錯,**y愈上面愈小,y愈下面愈大**,在電腦的**繪圖座標系**中確實如此。 以下用 r1, r2 來代表矩形1、矩形2。 左邊在數線上通常是變小,因此就是小於。因此剛才的陳述就可以換成這樣: **r1.x < r2.x + r2.w** # r1的左邊比r2的右邊還左(考慮r1在右時) **r1.x + r1.w > r2.x** # r1的右邊比r2的左邊還右(考慮r1在左時) 以上判斷可以讓我們知道r1與r2的x範圍是否有重疊,注意,這邊並不是判斷r1與r2的邊有沒有相交喔! 那只判斷x範圍還不夠,我們還需要判斷y範圍有沒有重疊。y的判斷也一樣,只要照一樣的方式寫,把x換成y,w換成h就可以了。 由於y點在上方,所以愈上數字會愈小,只是表達的方式不同,但實際計算是沒有差別的。 **r1.y < r2.y + r2.h** # r1的上邊比r2的下邊還上(考慮r1在下時) **r1.y + r1.h > r2.y** # r1的下邊比r2的上邊還下(考慮r1在上時) 而這所有條件必須要全部符合才代表兩個矩形相撞,因此我們將每個條件用and連起來,就會只有在全部符合的時候才回傳true。 以下為Python實做: ``` python ''' 判斷矩形是否相交函數 輸入:r1, r2兩個矩形物件 r1與r2分別帶有 x, y, w, h四個屬性 輸出:碰到(true), 沒碰到(false) ''' def rect_to_rect (r1, r2) : return ( r1.x < r2.x + r2.w and r2.x < r1.x + r1.w and r1.y < r2.y + r2.h and r2.y < r1.y + r1.h ) #在函數裡為了排版整齊,我將第2及第4行的大小於順序調換,對程式執行是沒有差別的 #實作者:林宏信 2018 7/21 ``` ### ...那圓形對矩形呢? ![](https://i.imgur.com/GxFjSHp.png) 在我的遊戲中有「牆壁」,是玩家與怪物都無法穿過的障礙物。玩家與怪物是圓形,而牆壁卻是矩形。完蛋了,現在不能用兩物件的距離,因為牆壁不是圓的;也不能用座標系重疊,因為圓形不是方的。 沒關係,我們先列出已知的資訊: | 項目 | 屬性 | 附註 | | -------- | -------- | -------- | | 圓形 | x, y, r | x, y為圓心座標 | | 矩形 | x, y, w, h | x, y為矩形左上角座標 | 好,這次是真正的難題了,在場有同學能為我們指引方向嗎? (等一下) #### 找尋規律 我們先來亂猜看看,今天如果把一顆球從矩形右邊靠近,我們只要注意球的圓心到矩形右邊的距離,是否小於半徑就可以了;那如果從上面靠近呢?嗯...那應該就要跟上邊比才對;那如果從左邊來就跟左邊比、下面來就跟下邊比,好像沒有很難吼? #### 發現例外 那如果從**左上方**來呢?該跟誰比勒? 今天這顆球假如在矩形的右上方,圓心跟矩形左側及上側的距離已經小於半徑,也就是我們原先以為應該要碰到的情況,但實際上並沒有碰到。這又該怎麼辦? ![](https://i.imgur.com/hYTYJxk.png) 看來計算圓心到邊的距離並不完全正確, 如果我們要計算圓形是否有碰到矩形,那是否能利用圓形到矩形的最短距離呢?! 只要判斷圓心到矩形的最短距離是否有小於半徑,應該就可以了吧! 沒錯!這就是圓形對矩形的核心概念,透過判斷最短距離是否小於半徑,來檢測圓是否有碰到矩形。 因此我們要來找出一個矩形最靠近圓心的那個**點**在哪裡,透過這個點和圓心的距離,來判斷圓是否有碰觸到矩形。 #### 計算最靠近的點 ![](https://i.imgur.com/74E1q4y.png) 首先,假如矩形不存在,最靠近圓心的點在哪裡?**就在圓心**,我知道這樣講是廢話,但只要我們把圓心的x, y給限制在矩形的範圍中,讓它盡可能的接近圓心,就可以找到最近點了。 所謂 **「限制」** 的意思是說,假如這個點比矩形的**右邊還右**,那就將它**設定為矩形的右邊**;如果比矩形的**左邊還左**,就將它**設定為矩形的左邊**;假如它沒有超過矩形的座標範圍,那就保持它原先的座標。上下邊以此類推。 那我們將上述講的「限制範圍」這件事寫成一個函數,讓我們後面可以重複呼叫。 ``` python ''' 定義 set_range 函數,將數值 number 設定在 minimal 到 maximum之間的範圍。 輸入:最小值、最大值、欲計算數字 輸出:計算後結果,介於minimal ~ maximum 之間 ''' def set_range (minimal, maximum, number): if number > maximum: return maximum elif number < minimal: return minimal else : return number # 實做:林宏信 2018-7-25 ``` 那我們接下來只要將數字分別帶入函數中即可。 矩形的左邊即x, 右邊為x + w 矩形的上面是y, 下面是y + h ``` python ''' 定義 圓形矩形碰撞檢測函數 輸入: c: 圓形物件,帶有 x, y, r屬性 r: 矩形物件, 帶有 x, y, w, h屬性 輸出:碰到: True / 沒碰到: False ''' def circle_to_rect (c, r): x = set_range(r.x, r.x + r.w, c.x) # 呼叫上方定義的限制範圍函數 y = set_range(r.y, r.y + r.h, c.y) d = ((c.x - x) ** 2 + (c.y - y) ** 2) ** 0.5 # 跟前面一樣利用畢氏定理求距離的算式 return d < c.r #回傳「最短距離是否小於圓的半徑」的比較結果 #實做:林宏信 2018-7-25 ``` --- ## Move Your Body!! ---三角函數 (未完成) 在遊戲中,各個角色例如玩家、怪物,都需要**移動**,如果是水平或垂直移動,那很容易,只要增減x, y座標即可達成;但如果今天要移動的方向並非0, 90, 45度,而是60度呢? 或者,該如何找到朝向一個座標的**方向**?讓物件能夠行走到指定的位置。甚至是將座標旋轉,製造出可以旋轉的多邊形。 這些,都需要利用**三角函數**。 ### 三角函數是什麼? 正如同上面我所描述的各個需求,三角函數其實就是 **「轉換長度與角度」**。 想一想,今天我們要朝著一個角度移動,我們不就是要將這個「角度」給轉換成x與y分別移動的「長度」嗎?算出到一個座標的「角度」,也是將x與y的「長度」換算而來。 因此,三角函數在遊戲開發中有著非常廣泛的用途,牽涉到一切物件的移動都需要依賴它。很幸運的,在Python中已經有開發好的函式庫,我只要呼叫並傳入數字就好,因此我們只需要專注在邏輯上面。 ### 讓我朝著60度前進吧! 該如何讓玩家朝向60度前進?由於在遊戲中我們都是使用直角座標,因此我們要將「角度」以及「移動距離」轉換成x和y分別增加的量。這時就要使用三角函數最廣為人知的兩位大大:**sin** and **cos** !! ![](https://upload.wikimedia.org/wikipedia/commons/d/dc/Trigonometry_triangle_sim.png) 上圖連結自[維基百科](https://zh.wikipedia.org/zh-tw/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0) 先談談一個三角形的各部位名稱,首先請看 角A ,角A這個角很重要,在接下來的說明中,我們所提到的「角度」都會是指這個角的角度,而這個角度我們就稱為 theta ,由於電腦沒辦法打出來,接下來我會以 angle 代稱之。 而圖中的 邊a 則是所謂的**對邊**,我們y座標的值就是對邊的長度;而邊b則是**鄰邊**,我們x座標的值就是鄰邊的長度。最後邊h,就是**斜邊**,我們前進的距離就是斜邊的長度。 那在這個例子中,我們就是要將 *a的角度 與 斜邊的長度* 替換成 *鄰邊與對邊的長度* 。 那接下來介紹我們的幫手大大,首先是 **sin** 大大,sin大大會負責將我們的角度angle轉換成 斜邊為1時 對邊的長度,只要再將此數值乘上我們的斜邊長,就可以求出對邊長,也就是y座標的值了。 那 **cos** 大大其實做的也是差不多的工作,只是它負責的是鄰邊,也就是x。所以我們只要將cos所求出的值乘上斜邊長,就可以得到x的長度了。 那該如何請這兩位大大工作呢?由於sin與cos兩位大大都是函數,我們只要將angle放入「括號」中,送給它們處理,它們計算完,就會告訴我們答案了,是不是很簡單呢? 但是如果你就這樣傻傻的直接把60傳給它,它可是會森77的喔!因為sin與cos以及全部的三角函數,使用的都不是我們平常習慣的,360一圈的「度」,而是「弧度」。 #### 弧度 弧度跟度一樣,是一種角度單位,度是360為一個單位,而弧度則是2倍圓周率為一個圓。 聽起來...好像是有點討厭的東西,算個角度也要扯到圓周率?真是夠了。但其實弧度的定義是十分優雅而簡潔的: > 單位弧度定義為圓弧長度等於半徑時的圓心角 > [name=維基百科] > 哇...我到底看了什麼...簡單來說,就是當一個扇形,它的弧長跟半徑相等時,它的圓心角角度就稱為「一弧度」,而一弧度則約為57.2957795度...看了很討厭?沒關係我們是工程師,這種工作就交給電腦去算就好了。 一個完整的圓它的弧度就會是 2pi,我們要將60度換算成弧度,就是將 (60 / 360) * 2pi = **60 * pi / 180**。 那我們就來寫一個換算的函數,來將「度」換成「弧度」。 ``` python ''' 定義 函數 deg_to_rad 將「度(degree)」換為「弧度(rad)」 輸入:度 輸出:弧度 ''' import math #引用了Python的 數學庫 def deg_to_rad (deg): return deg * math.pi / 180 # 其中的 math.pi 是一個定義在Python math 函式庫的常數,我們透過呼叫它來取得較精準的 pi 值。 # 實作者:林宏信 ``` 完成了弧度的處理,我們回到前面來計算三角函數。 大致的流程如下:將角度(angle)換為弧度(rad_angle),再來將值分別傳入sin 與 cos 之中,將回傳值乘上移動距離(distance),即可算出x, y分別須移動的量。 ``` python ''' 定義 move函數 用來計算朝angle角度移動distance距離須移動之x, y座標 輸入:角度angle, 距離distance 輸出: (x, y) 元組,可用 x, y = move(angle, distance)來接收回傳值 ''' import math class Player: def __init__(self): self.x = 0 self.y = 0 def move (angle, distance): rad_angle = deg_to_rad(angle) self.x += math.cos(rad_angle) * distance self.y += math.sin(rad_angle) * distance # 實做者:林宏信 2018-7-26 ``` ### 叫那隻殭屍...給我滾開!! 在我的遊戲中,有一種怪物 ---殭屍,它會不停朝著玩家移動,玩家碰到就會被攻擊。但問題來了,我們只會朝著一個「角度」前進,並不會朝著一組「座標」前進啊! 要朝著一組座標前進其實有兩種解法,一是先算出朝向目標的角度,再等速前進;二是算出x與y分別平均須移動多少。 在這個單元我會先介紹第二個方法,找出角度的部份我會放在下個單元介紹。 #### 平均的 x 與 y 假設我們要從 (0, 0) 移動到 (3, 4) ,那我們就是將x座標增加3、y座標增加4。你可能會覺得:嗯...這還用說嗎?但是!假如我們今天要**慢動作**呢? 假如今天我不要一瞬間就從 (0, 0) 移動到 (3, 4),而是分成十次進行呢? 簡單嘛!把 3 / 10、 4 / 10,一次移動 (0.3, 0.4) 不就好了? 是的,剛才這一串很像是廢話的描述,其實就是解題的關鍵! 怎麼說?從我們剛才的推論,我們得知,要將x平均移動,只要把總共**需要移動的x長度**,在本範例中就是3,除上**分解的次數**,在本範例中就是10。 不過,有時候我們並不知道要分解成幾次啊。如果我想要等速運動,我只會知道我的速度會是多少,但不會知道我總共要分解幾次,這該怎麼辦? 我們搬出距離與速度公式: 距離 = 速度 * 時間。所以時間就會等於 **距離 / 速度**。速度我們已經設定好了,那距離就是用畢氏定理,以此範例來說就是5,我們接下來用 distance 作為代號。 因此,計算x的完整算式就是:x / (distance / speed),我們化簡一下,就會變成 x / distance * speed。那y就會是 y / distance * speed。 這一段...怎麼看起來有點眼熟?x就是鄰邊,distance就是斜邊,鄰邊除上斜邊...天啊!我們正在算三角函數!cos的定義就是鄰邊除上斜邊,而這正是我們現在正在做的! 所以其實我們並不一定要使用三角函數才能達成,有的時候,我們甚至可以透過實際的數字來「自己算出三角函數」。 那以下實做將定義在殭屍類別 Zombie 之中的物件方法: ``` python ''' 定義 Zombie 與 Zombie.near 方法,near輸入target目標與移動速度 輸入:target :帶有 x, y兩個屬性 speed :移動的速度 輸出 ''' class Zombie: def __init__(self): self.x = 0 self.y = 0 def near (self, target, speed): x, y = target.x - self.x, target.y - self.y d = (x ** 2 + y ** 2) ** 0.5 self.x += x / d * speed self.y += y / d * speed ``` ### 不~~為什麼它的槍一直瞄準我啦! ### 把手槍變成三角形 --- ## 總結 當然,在我說的這些遇到的難題之外,這個專案還有太多太多的細節了,就比如架構的問題,該如何讓碰撞檢測用最有效率的方式執行,該如何讓新的技能可以被輕易的加入,該如何讓效能更好;或者是遊戲的吸引力,該如何讓更多人看見我的遊戲,並願意玩它。許許多多的問題,都是直到今天,我仍在思考,而仍沒有一個正確答案的。 那這個專案為我帶來了什麼?一個玩過的人不超過20,默默無聞的小遊戲,為我帶來了什麼? 在開發這個專案的過程中,我踩過很多雷,曾經爬了許多網站去解決一個荒謬的錯誤,曾經苦思很久去設計專案的架構。在這些過程中,我發現寫程式並不只是「寫程式」,當你要面臨的是一個**專案**時,寫程式往往不是最重要的,最重要的是**組織你的夢想並將它建築的能力**。 --- ## 參考資料 ["等一下,我碰"—常見的2D碰撞檢測](https://aotu.io/notes/2017/02/16/2d-collision-detection/) [繪圖座標系](https://openhome.cc/Gossip/ComputerGraphics/GUIDimension.htm) [基本座標轉換](https://openhome.cc/Gossip/ComputerGraphics/Basic2DTransform.htm) [LancatServer: 使用三角函數移動物件](http://lancat1499.pixnet.net/blog/post/199860669) [LancatServer: 三角函數 —旋轉座標](https://medium.com/lancatserver/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B8-%E6%97%8B%E8%BD%89%E5%BA%A7%E6%A8%99-d57b085e72f7) [維基百科 ---物件導向程式設計](https://zh.wikipedia.org/zh-tw/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1#%E9%A1%9E%E8%88%87%E5%AF%B9%E8%B1%A1)