Asteroids

《爆破彗星》(英語:Asteroids)是Atari公司在1979年發行的一個大型電玩遊戲。它是大型電玩黃金年代裏其中一個最受歡迎及具影響力的遊戲。《爆破彗星》使用向量圖形與二維視角。玩家需要操控一架太空飛行器去避開及以子彈摧毀飛來飛去的彗星。

Asteroids Atari GIF - Asteroids Atari Video Game - Discover & Share GIFs

0. 本章重點

  1. 熟習class的用法

  2. 明白object入面可以指向另一個object

  3. pushMatrix()popMatrix()translate()rotate()的用法

  4. Processing mode怎樣分頁

1. 製作飛船

image-20230210175332243

開一個新的class,名字為Rocket,這艘飛船有自己的位置,速度和加速度,所以分別開3個PVectorpos, vecaccel,另外飛船有一個指向,就是船頭的方向,名叫heading

在class Rocket中,pushMatrix()popMatrix()是用來告訴程式,兩者之間的所有translate()rotate()都是暫時的。Python Mode for Processing在繪圖圖形時,有兩種方法,一是直接指定在某座標點繪圖某圖形,好處是簡單直覺,但如果好像今次般,除了基本的偏移(translate)外,還要加上旋轉角度的話,計算座標就會十分麻煩,涉及到旋轉矩陣,所以今次我們用translate()rotate()的方法,將整個座標系統的原點改變,再用vertex()繪劃火箭形將的箭頭。

2. 為每個class開一個獨立分頁

asteroids.pyde:

rocket.py:

image-20230210190553633image-20230210190606391

為方便管理整個程式,一般工程師都會習慣將相關的內容放在同一個分頁,每個分頁通常都不會太長。

image-20230210191755453

按下New Tab,或用Ctrl+Shift+N開一個分頁名叫rocket.py,將原本Rocket這個class的所有內容都剪下貼到這個分頁。(注意⚠⚠⚠⚠⚠: rocket.py的名字不能完全與Rocket這個class相同,我是大小字不同的,否則會有error)

之後在主頁面最上加上一句from rocket import *,這裡的rocket是小寫的,即代表著rocket.py 這個檔案。

3. 令火箭懂得旋轉

asteroids.pyde:

rocket.py:

rocket1

rocket.py中,加入rotateRocket(self, angle)的函數,每次當玩家按下左方向鍵或右方向鍵時,就將heading(即船指著的方向)累加,而同時也要將加速度accel的向量旋轉。

返回主頁,在draw()中,加入按下方向鍵的控制項。keyPressed()可以作為一個外置的event,當按下按鍵時先中斷主程式執行再返回,或者作為一個boolean變數,當用家按下按鍵時就會變為True,而這時就可以讀取鍵盤的內容。由於方向鍵並非像一般字母般係輕易表達,所以processing中將四個方向鍵(UP, DOWN, LEFT, RIGHT),還有ALT, CONTROL, SHIFT都列入特別類型keyCode

4. 為火箭加上推進

asteroids.pyde:

rocket.py:

rocket2

rocket.pyrocketclass中,加入propulsion(),就是推進的意思。這裡模擬在太空中,火箭要在尾部點火推進前進。

self.accel.setMag(0.1)就是將加速度向量的量設定為0.1,再將速度向量累加,但速度如果一直累加的話,太大很難控制,為顧及遊戲性,限制速度向量的量為6

跟之前所有物理模擬一樣,我們為程式加入update()函數,每一個影格為其位置作更新。如果火箭超出了螢幕,就令火箭在另一邊出現。為增加遊戲可玩性,為火箭加入點阻力,令火箭不在推進的話就會慢下來。

rocketclass的show()中,加入顏色作推進識別。如果推進中的話,火箭就但是紅色,否則就會變回白色。

返回主頁的draw()中,在方向鍵中加入上方向鍵UP,當按下時火箭就會向前推進。在show()前記得要先update()

5. 加入子彈

asteroids.pyde:

rocket.py

bullet.py

rocket3

開一個新的分頁叫bullet.py,定義一個class叫做Bullet,用來管理有關子彈的變數和函數。

子彈的速度要跟發射子彈時火箭的方向一致,但只需要知道方向就可以,我們固定它的速度為4,所以首先要用self.vec = _vec.copy().setMag(4)將輸入的速度向量的長度設定成4個單位。

返回主頁,在最上面加入bullet.py分頁的內容,之後開一個空的陣列叫bullets用來裝起全部子彈。

setup()draw()之外,加入keyPressed()event,今次不在繼續在draw()中加入子彈,原因是draw()的更新率太快,按一下鍵就已經產生了十多發子彈。當按下空白鍵,就新增一顆子彈,子彈的位置就是火箭當前的位置,而子彈的速度,是火箭當前的指向,你可以用heading變數去指定子彈速度的方向,或直接使用火箭的加速度,這個加速度的方向應該是與heading一致的。

在主程式最下加入,用來更新和顯示所有子彈,如果子彈超出了畫面,則將子彈從陣列中移除,以免佔用大量內存。

6. 加入隕石

asteroids.pyde:

rock.py

bullet.pyrocket.py沒有修改,就不貼出來了。

rocket4

加入一個叫rock的class,本質上跟其他的class差不多,都是初始化時決定位置和速度,為了其顯示上多點動態,所以也加入了angle旋轉角度的變化。

比較特別的是level變數,由於原遊戲有幾款不同大小的隕石,當子彈打碎了大隕石後就會變成細隕石,所以要加入level變數,數字越大代表size越大。

返回主程式頁。

在最上匯入rock.py的內容,加入一個空的陣列名叫rocks

setup()中,先清空rocks的內容變成空陣列,接著加入 3 顆level 3 大小和 2 顆 level 2 大小的隕石。

draw()的最後,更新和顯示全部隕石。

7. 讓子彈能擊碎隕石

asteroids.pyde:

bullet.pyrock.pyrocket.py沒有修改,就不貼出來了。

rocket5

在主程式頁面的draw()中,加入每粒隕石對每發子彈的計算。

由於隕石的大小是以其基礎vertex座標再乘以系數3level,而vertex座標的最大值為2,所以我簡單當成隕石是一個半徑為2×3level的圓去計算,而子彈的直徑本身又很少,所以就忽略不計。

隕石每打碎一次後,就會分裂成兩顆細一個level的的小隕石,直到level 1的隕石被擊碎就不再分裂。所以每打中一次,就給一個for i in range(2):的for迴圈,temp = Rock(r.level-1)重新生成一顆size為level-1的隕石,temp.pos = r.pos指定temp隕石的位置為原本被打中的隕石位置,rocks.append(temp)新增到隕石陣列中,之後就將原本的這塊大隕石和子彈都移除。最後, break是跳出for b in bullets:的for迴圈,不用再計算餘下的子彈,可以直接計算下一塊隕石。

8. 加入敵方火箭

asteroids.pyde

其他版面沒有變,所以就不再貼出來。

rocket6

在一開始的宣告區,加入enemy敵人的火箭,之後宣告多兩個時間的變數一會兒用。

setup()中宣告enemy,記得要先將enemy變成宣告成global

draw()的最後,更新一下enemy的行為。首先,敵船永遠指著我方的火箭,用向量運算就十分簡單,只要將再個位置向量相減就是方向向量(由於processing中y軸是跟數學中的y軸正負極相反的,所以向量相減的次序跟數學是相反的)。

接著用PVector.fromAngle(enemy2rocketAngle).setMag(0.1),用一個指定的角度建立一個新的向量,今次則是指定用剛剛找到的兩者間的角度來建立,並設定其值為0.1

enemy.heading = degrees(enemy2rocketAngle)+90rocketclass的heading變數一直都是用degree來表達而非radian,單位小心不要錯,所以要先轉成degrees,再加90度的原因也是因為processing的角度計算,0度是指向天空(即一般數學的y軸方向),但PVector中的角度卻跟數學一樣是由x軸計起的,所以要再加上90度。

是一個常見的用法,取代delay()等線式的延時方法,上述500即每500毫秒執行中間的命令一次。

最後就是更新和顯示。

9. 考考你

  1. 遊戲到這個階段都沒有game over的。原遊戲的設定是有3條生命的,火箭被隕石或子彈打中後就會失去一條生命,你可以將火箭被打中或擊中後,將其位置放回畫面中間,速度歸零。