瞿 蘇
(江蘇旅游職業(yè)學院, 江蘇 揚州 225000)
Python是一種開源的簡單易學的高級語言,應用場景涉及到Web應用開發(fā)、操作系統(tǒng)管理、科學計算、游戲等領域。飛機大戰(zhàn)是一款飛行射擊類游戲,游戲方法是玩家操作飛機與敵機在空中戰(zhàn)斗。本文主要介紹用Python軟件實現(xiàn)飛機大戰(zhàn)游戲。
游戲的整個界面是一張背景圖片,游戲中用到的其他角色同樣都對應著相應的圖片,這些圖片需要借助Pygame模塊搭載窗口以顯示[1]。在窗口中,對象顯示的位置通過坐標標注。其中,窗口的左上角坐標為(0,0),x軸向右延伸坐標數(shù)值增大,y軸向下延伸坐標數(shù)值增大。所有的游戲元素都參考這個坐標系,對象在窗口的移動就是坐標位置的變化。
主要設計原則如下:(1)簡單性。在實現(xiàn)軟件的功能的同時,盡量讓軟件操作簡單易懂;(2)針對性?;赑ython軟件,實現(xiàn)飛機大戰(zhàn)游戲的各種要求;(3)一致性。類型、變量和其他元素的命名規(guī)則保持一致;完成同樣的功能應該盡量使用同樣的元素;界面元素的外觀風格、擺放位置在同一個界面和不同界面之間是一致的。
系統(tǒng)中只有玩家一種用戶,不必進行身份驗證等操作。玩家點擊應用圖標直接進入開始界面。整個游戲的操作包括:顯示玩家飛機、控制移動方向;顯示玩家發(fā)射子彈(子彈移動);顯示敵人飛機,控制敵人飛機移動、敵人飛機發(fā)射子彈。
1.3.1 添加pygame模塊
pygame是一套用來開發(fā)游戲的Python模塊,該模塊允許在Python程序中創(chuàng)建功能豐富的游戲和多媒體程序。PyCharm作為一款開發(fā)Python的編輯器,它不僅可以幫助開發(fā)人員提高開發(fā)效率,而且包含了像pygame這樣功能豐富的第三方模塊。
在PyCharm中添加pygame模塊,在程序中導入pygame模塊進行測試,編譯器編譯通過,就證明成功導入了模塊。
1.3.2 搭建游戲界面
飛機大戰(zhàn)游戲的整個場景都需要一個窗口作為載體,展示游戲中的畫面。為了讓整個游戲的角色和背景的風格統(tǒng)一,準備了一張背景圖片。在開發(fā)中,導入pygame模塊,就能直接調(diào)用模塊中的方法。
1.3.3 檢查鍵盤的輸入
當敵人飛機發(fā)射子彈的時候,玩家飛機需要使用鍵盤適時地調(diào)整位置,以躲避子彈的攻擊。在Python程序中,移動鼠標、敲擊鍵盤等人機交互的動作屬于事件,它交由pygame的event模塊進行控制。event模塊采用列表形式存儲的事件,可以通過get函數(shù)來獲取。如果要獲取鍵盤和鼠標產(chǎn)生的事件,就使用for循環(huán)遍歷事件列表,取出每個事件與event.type(事件的類型)進行對比。如果event.type的值為Quit,說明用戶使用鼠標點擊了窗口右上角的關閉按鈕,此時就要退出程序;如果event.type的值為Keydown,說明用戶使用了鍵盤,此時就要明確按下的是哪個鍵。
在while循環(huán)中,已經(jīng)顯示了游戲的背景圖片。這時,在程序中需要檢測是否有事件發(fā)生,比如按下鍵盤等。如果沒有事件發(fā)生,就執(zhí)行更新操作,如果有事件發(fā)生,就先處理鍵盤事件以后再更新。
在窗口中要顯示玩家飛機,可以根據(jù)玩家飛機圖片的名稱創(chuàng)建圖像,再把這些圖像顯示到屏幕上設定的位置。玩家飛機左右移動功能,可以通過改變坐標x的值實現(xiàn),飛機向左移動減小坐標x的值,反之則增大x的值。
新建一個Python File,取名為“plane”.在plane.py文件中,導入pygame模塊,之后定義一個表示玩家飛機的類HeroPlane。
程序設計中,定義為display、move-left和move-right三種方法。其中,display用于在默認的位置顯示玩家飛機;moveleft用于讓飛機向左移動;moveright用于讓飛機向右移動。在start函數(shù)中創(chuàng)建飛機對象,并且顯示到窗口中。在while true語句中,根據(jù)玩家按下的按鍵來調(diào)用相應的方法,以控制飛機移動的方向。
當按“←”鍵或者“A”鍵時,控制玩家的飛機向左移動,當按“→” 或者“D”鍵時,控制玩家的飛機向右移動。
2.2.1 顯示子彈
當按空格鍵時,代表玩家飛機要發(fā)射一枚子彈,此時需要在玩家飛機的頭部位置生成一顆子彈對象。飛機左右移動到任意位置,只要按空格鍵,子彈生成的初始位置永遠會位于玩家飛機的頭部。
新建一個Python File,取名“bullet”。在bullet.py文件中,導入模塊,之后定義一個表示子彈的類。代碼如下:
class Bullet(object):
def-init-(self,x,y,screen):
self.x=x+40
self.y=y-20
self.screen=screen
self.iamge=pygame.image.load(“./feiji/bullet-3.gif”).convert()
def.display(self):
self.screen.blit(self.iamge,(self.x,self.y))
按空格鍵發(fā)射子彈,代碼如下:
heroPlane.launch-bullet()
運行程序,按下空格鍵以后,玩家飛機的頭部顯示了待發(fā)射的子彈。此時,無論飛機移動到哪個位置,生成的新的子彈永遠會位于其頂部。
2.2.2 子彈移動
每執(zhí)行一次while循環(huán),就會調(diào)用一次display方法,讓子彈再次顯示到屏幕上。由于屏幕刷新的速度特別快,肉眼是無法捕捉到的。利用程序的這個特點,每刷新一次屏幕,就讓子彈顯示的位置上移幾個單位,從而形成向上發(fā)射子彈的動畫效果。
如果無限制地往列表中添加子彈對象,終究會耗盡設備的內(nèi)存,所以,一旦子彈離開屏幕可視范圍時,就把子彈對象從列表中刪除。
在Bullet類中添加一個judge方法,用于反饋子彈是否發(fā)射到屏幕以外的情況,若子彈圖像的y值小于0,則表示子彈移出了屏幕,返回True;反之則返回False。
在HeroPlane類的display方法中,定義一個存放待刪除子彈對象的列表。從列表中取出每個帶刪除的子彈對象進行判斷,如果子彈對象已經(jīng)發(fā)射到屏幕的外面, 就添加到剛定義的列表中,然后清空列表中所有被刪除的子彈對象。具體代碼如下:
def display(self):
self.screen.blit(self.image,(self.x,self.y))
need-del-list=[]
for item in self.bullet-list:
if item.judge():
need-del-list.append(item)
for del-item in need-del-list:
self.bullet-list.remove(del-item)
for bullet in self.bullet-list:
bullet.display()
bullet.move()
2.3.1 顯示敵人飛機
跟玩家飛機類似,在plane.py文件中定義一個表示敵人飛機的EnemyPlane類。敵人飛機應該有默認的位置、呈現(xiàn)圖像的窗口、存放子彈的列表這些屬性。然后來到main函數(shù)的while循環(huán)語句中,在創(chuàng)建HeroPlane類對象的后面,創(chuàng)建表示敵人飛機的對象。在顯示玩家飛機的代碼后面,調(diào)用display方法來顯示敵人飛機。
2.3.2 控制敵人飛機移動
當程序啟動以后,敵人的飛機開始在窗口的頂部做直線運動,直到碰到屏幕的邊緣后向反方向做直線運行??梢詾閿橙孙w機類添加一個移動的方法,由于敵人的飛機是不停地運動的,所以放到while語句中最為合適[2]。
飛機碰到左側(cè)的屏幕邊緣時,移動方向變化為向右移動,飛機碰到右側(cè)的屏幕邊緣時,移動方向變?yōu)橄蜃笠苿?。因?在EnemyPlane類的-int-()方法中增加一個direction屬性,用于記錄飛機的初始運動方向:
self. direction=“right”
然后定義一個move方法,根據(jù)飛機移動的方向改變x對應的坐標值,再根據(jù)x的值限定敵人飛機移動的范圍,只要超過屏幕的寬度范圍,就改變飛機運動的方向。
在main.py文件的while語句中,調(diào)用display()方法顯示敵人飛機,調(diào)用move方法實現(xiàn)敵人飛機一直左右移動的效果。
運行程序,敵人飛機在屏幕頂部移動的速度非???。產(chǎn)生這種情況,主要因為屏幕刷新的速度太快,導致敵人飛機移動的速率過快,并且占用了程序過多的內(nèi)存,因此,需要使用time模塊來降低程序執(zhí)行的效率。
2.3.3 敵人飛機發(fā)射子彈
敵人飛機發(fā)射子彈的功能與玩家飛機發(fā)射子彈的功能基本一樣,不同的是子彈反射的方向及發(fā)射的個數(shù)。為此,在bullet.py文件中,新建一個表示敵人飛機發(fā)射的子彈的EnemyBullet類,直接復制Bullet類的代碼到EnemyBullet類中,然后再對發(fā)射子彈的功能代碼進行局部調(diào)整。在main.py文件的循環(huán)語句中,調(diào)用move方法的后,調(diào)用敵人發(fā)射子彈的方法為:
Enemy-plane.launch-bullet()
運行程序,由于敵人飛機發(fā)射子彈的速度太快了,使得子彈形成了一條斜線。所以,在EnenmyPlane類的launch-bullet(發(fā)射子彈)方法中,設置發(fā)射子彈的數(shù)量是隨機的,這樣可以降低子彈發(fā)射的頻率,代碼如下:
def launch-bullet(self):
number=random.randint(1,100)
if number==88:
new-bullet=EnemyBullet(self.x,self.y,self.screen)
self.bullet-list.append(new-bullet)
上述方法實現(xiàn)了敵人飛機發(fā)射子彈的功能,首先使用函數(shù)獲取了從1到100的隨機整數(shù),然后使用if語句判斷隨機數(shù)的值是否與88相等,只有這兩個值相等,才會創(chuàng)建要發(fā)射的子彈對象,通過這種隨機數(shù)的方式,使得敵人發(fā)射子彈的數(shù)量變?yōu)樵瓉淼陌俜种弧S捎谑褂昧藃andom模塊的函數(shù),所以導入 random模塊[3],代碼為:
Import random
在前面的程序代碼中,表示敵人飛機的類(EnemyPlane)和表示玩家飛機的類(HeroPlane)中有很多功能相似的代碼,除此之外,表示子彈的兩個類中有著很多重復的代碼,這些重復的代碼不僅使程序顯得過于臃腫,而且沒有清晰的結(jié)構(gòu)??梢岳美^承的技巧[4],對所有類的代碼進行優(yōu)化,以明確程序的結(jié)構(gòu)。
玩家飛機發(fā)射的子彈和敵人飛機發(fā)射的子彈這兩大類的功能幾乎相同,出現(xiàn)了很多重復的代碼,使得整個程序的結(jié)構(gòu)過于臃腫??梢远x這兩個類的公共類PublicBullet[5],在這個類中既能抽取出它們共同擁有的功能,又能區(qū)分它們的不同情況。
定義一個表示子彈的公共類PublicBullet,把表示玩家飛機子彈類的代碼復制到PublicBullet類中,再根據(jù)兩個子彈類不同的地方進行調(diào)整。
由于PublicBullet類已經(jīng)合并了EnemyBullet類和Bullet類的功能,并對它們各自不用的地方進行了處理,所以創(chuàng)建EnemyBullet類和Bullet類對象時,可以替換為創(chuàng)建PublicBullet類對象:
New-bullet=PublicBullet(self.x,self.y,self.screen,“enemy”)
同樣在HeroPlane類的launch-bullet方法中,把創(chuàng)建Bullet類對象的代碼改為創(chuàng)建PublicBullet類的對象:
New-bullet=PublicBullet(self.x,self.y,self.screen,“hero”)
運行程序,界面依然顯示了跟以前一樣的效果,這證明代碼抽取成功。EnemyBullet類和Bullet類現(xiàn)在對程序沒有起任何作用,可以直接刪除。
進一步抽取EnemyPlane和HeroPlane兩個類中功能相似的代碼。由于這兩個類中實現(xiàn)的功能較多,如果依然使用抽取公共類的技巧,就需要頻繁地使用if-else語句來區(qū)分不同飛機的情況。因此,需要把EnemyPlane和HeroPlane類中相同的功能放到基類中,再讓這兩個類繼承基類后單獨調(diào)整。
通過比較EnemyPlane和HeroPlane類中的代碼,發(fā)現(xiàn) EnemyPlane 類中的-int-、display和 sheBullet 方法與HeroPlane類中的-int-、display和 sheBullet方法功能非常相似。為此,需要把這三個方法的代碼提取到基類中,子類EnemyPlane和HeroPlane 繼承基類以后,會擁有這三個方法,可以根據(jù)自己的要求重寫這些方法。
3.2.1-init-()方法
通過比較兩個飛機類的代碼發(fā)現(xiàn),EnemyPlane類的-init-()方法比HeroPlane類的-init-()方法中多一個屬性,所以需要把HeroPlane類的-init-()方法粘貼到新創(chuàng)建的基類Plane中,具體如下:
Class Plane(object):
def-init-(self,screen):
self.x=230
self.y=600
self.screen=screen
self.image-name=”./feiji/hero.gif”
self.image=pygame.image.load(self.image-name).convert()
self.bullet-list=[]
讓HeroPlane類繼承Plane類,這樣HeroPlane類就擁有了從父類Plane繼承來的-int-()方法。此時,可以刪除類HeroPlane中-int-()方法。同樣讓EnemyPlane類繼承自Plane類,Plane類就擁有了從父類繼承而來的-int-()方法。不過,EnemyPlane類需要增加一個表示方向的屬性,為此需要重寫父類的-int-()方法,具體如下:
def-init-(self,screen):
super().-int-(screen)
self.direction=”right”
3.2.2 display方法
通過比較EnemyPlane類和HeroPlane類的display方法發(fā)現(xiàn),它們的功能是一樣的。因此,可以在Plane類中沿用EnemyPlane類的display方法代碼。
由于EnemyPlane類和HeroPlane類都已經(jīng)繼承了Plane類,所以就擁有了display方法,而且它們的功能沒有任何變化,直接刪除它們每個類中的display方法就行了[6]。
3.2.3 launch-bullet方法
為了能區(qū)分是哪架飛機要發(fā)射子彈,在創(chuàng)建飛機對象構(gòu)造方法的參數(shù)列表中,增加一個表示飛機名稱的字符串。雖然這樣能區(qū)分飛機的名稱,但是方法參數(shù)中直接使用字符串擴展性不是很好。因此,在Plane類的-init-方法中,添加表示飛機名稱的name屬性,使得EnemyPlane類 和HeroPlane類創(chuàng)建對象時就有了名字。
在EnemyPlane類重寫的方法中,增加name參數(shù)。在類的方法中,把創(chuàng)建PublicBullet類對象的參數(shù)列表中的最后一個參數(shù)改為self.name。改完以后,把整個launch-bullet方法剪切到Plane類中。代碼如下:
def launch-bullet(self):
new-bullet=PublicBullet(self.x,self.y,self.screen,self.name)
self.bullet-list.append(new-bullet)
EnemyPlane類中的launch-bullet方法,與從父類直接繼承的launch-bullet方法在功能上存在著一些差異。為此,EnemyPlane類需要重寫從父類繼承的launch-bullet方法。在Plane類的-int-()方法中,由于圖片素材的名稱、x值和y值是變化的,每個子類需要單獨進行調(diào)整,所以在Plane類中,把這三個屬性移動到子類HeroPlane重寫的-int-()方法中。在EnemyPlane類的-int-()方法中,同樣增加這三個屬性的設置。
圖1 程序中所有類的繼承結(jié)構(gòu)
再次運行程序,發(fā)現(xiàn)跟以前運行的場景一樣,這表示抽取成功。此時,程序中幾個類的結(jié)構(gòu),如圖1所示。
本文圍繞面向?qū)ο蟮木幊趟枷?開發(fā)了飛機大戰(zhàn)游戲的部分功能,包括搭建游戲界面,創(chuàng)建玩家飛機和敵人飛機類,飛機發(fā)射子彈等,并且利用繼承的技巧優(yōu)化了代碼。模塊以對象劃分,有界面模塊、Hero模塊、子彈模塊、Enemy模塊、游戲控制模塊;難點在于多架戰(zhàn)機同時出動情況下,判斷發(fā)生碰撞的算法設計。整個軟件按照預期目標大致實現(xiàn)了飛機大戰(zhàn)游戲的功能。整個游戲還存在一些不足。比如,怎么動態(tài)及時地顯示積分數(shù),怎么能夠讓各種敵機在不同的時間點以不同速度飛行且不發(fā)生碰撞;還有如何在多人聯(lián)網(wǎng)下共同作戰(zhàn),及時分享自己的戰(zhàn)績等,都需要進一步研究。