作者:林Lychee
https://blog.csdn.net/weixin_43776741/article/details/88924146
有誰沒玩過植物大戰殭屍嗎?一位讀者用Java語言開發了自己的植物大戰殭屍遊戲。雖然系統相對簡單,但是麻雀雖小五臟俱全,對遊戲開發感興趣的小夥伴可以學習一下。
遊戲設計
植物大戰殭屍中有一個小遊戲關卡,屏幕的正上方有一個滾輪機,會隨機生成植物,玩家可以選中植物後自由選擇草坪來進行安放。基於此遊戲模式,我將該關卡抽取出來,單獨做成了一個簡易版的植物大戰殭屍。遊戲的畫面大概如下:
屏幕左側會自動生成植物的卡牌,單擊選中後可以放置在草坪上。右側會自動生成殭屍,不同的殭屍移動速度不同,血量不同,還有的殭屍有隱藏獎勵,比如:全屏殭屍靜止、全屏殭屍死亡等。當時竟然沒有做遊戲的暫停的功能,導致現在截圖的時機很難把控,那這裡就先說一下游戲暫停的功能應該怎麼做吧。
最簡單的一種暫停方式是鼠標移出屏幕,遊戲暫停。所以這裡需要引入一個鼠標監聽器事件。
<code>import
psutil pl = psutil.pids()/<code>
當然,這只是一個簡單的通過監聽鼠標的位置來改變遊戲狀態方法。還可以使用鍵盤監聽器,當按下某個鍵時遊戲暫停,這樣的用戶體驗更好。但原理是一樣的,這裡就不展示代碼了。
遊戲對象
首先分析一下游戲中有哪些對象。各式各樣的植物,各式各樣的殭屍,各式各樣的子彈。那麼這裡就可以抽出三個父類,分別是植物、殭屍、子彈。在面向對象中,子類將繼承父類所有的屬性和方法。所以可以將三大類中,共有的屬性和方法抽到各自的父類中。比如殭屍父類:
<code>for
pid in pl: if psutil.Process(pid).name()==ProcessName: get_desk() Warnning()/<code>
植物父類、子彈父類就同理可得了。
上面說到子類共有的方法需要抽到父類中,那麼部分子類共有的方法該如何處理呢?比如,豌豆射手、寒冰射手可以發射子彈,堅果牆就沒有射擊的這個行為。所以這裡就需要用到接口(Interface)。
<code>public
interface
Shoot
{public
abstract
Bullet[]shoot
(); }/<code>
到此為止,遊戲對象的屬性、方法基本都定義完了,至於圖片的顯示以及如何將圖片畫出來,只需要使用相應的API即可,這裡就不做描述了。工作一年回過來看看,這裡能優化的地方還有很多,比如對象的血量、攻擊力、移動等都可以統統寫入到配置文件中,這樣在做遊戲參數的調整時,不需要去修改代碼相關的內容,只需要修改配置文件裡面的參數即可。
遊戲內容
現在我們有了遊戲的對象,該開始讓對象加入到遊戲中來,接著讓他們動起來,最後還得讓他們打起來。首先,讓對象加入到遊戲中來我是這麼做的,這裡還是以殭屍為例:
<code>private
List zombies =new
ArrayList();public
ZombienextOneZombie
() { Random rand =new
Random();int
type = rand.nextInt(20
);if
(type<5
) {return
new
Zombie0(); }else
if
(type<10
) {return
new
Zombie1(); }else
if
(type<15
) {return
new
Zombie2(); }else
{return
new
Zombie3(); } }int
zombieEnterTime =0
;public
void
zombieEnterAction
() { zombieEnterTime++;if
(zombieEnterTime%300
==0
) { zombies.add
(nextOneZombie()); } }/<code>
最早時候我用的數據結構是數組,但在後續的編碼中發現,對殭屍對象有很多的遍歷以及增刪操作,數組的增刪操作是十分麻煩複雜的,所以我就換成了集合。在工作中也一樣,先思考在編碼,選擇正確的數據結構往往能起到事半功倍的效果。
植物入場的設計,是我當時自認為很精妙的一個點。先說一下當時在編碼中發現的問題。首先植物入場時是在滾輪機上的,滾輪機上的移動就會涉及到追擊和停止的問題。追擊的方式當然是追前一個植物卡牌,但當第一個植物卡牌被選中放置到草地上後,那該如何追擊呢?
最開始我的做法是給植物多加幾個狀態來解決這個問題,但是發現狀態過多會導致if判斷中的條件將大大增加,並且在嘗試後還是沒有實現想要的效果,於是我就將植物集合一分為二,在後面的遊戲功能設計中,回頭過來看才發現將植物集合分為滾輪機上的集合和戰場上的集合實在是太精妙了。請聽我娓娓道來:
<code>private
List plants =new
ArrayList();private
List plantsLife =new
ArrayList();public
void
plantBangAction
() {for
(int
i=1
;i/<code>
當然,滾輪機上的對植物狀態判斷的代碼還是顯得生澀,也正是自己想優化這段代碼時萌生了分享遊戲設計過程和遊戲代碼的念頭。那麼下面就說說,這段代碼該如何優化:
<code>if
(!(plants.get
(i).isStop()||plants.get
(i).isWait()) {break
; }if
(!(plants.get
(i-1
).isStop()||plants.get
(i-1
).isWait())) {break
; }if
(!(plants.get
(i).getY()<=plants.get
(i-1
).getY()+plants.get
(i-1
).getHeight())) {break
; } plants.get
(i).goStop();/<code>
boolean條件當然也可以進行優化,甚至還可以簡化一下植物的狀態。這裡因為遊戲的規則,殭屍只能攻擊在草坪上的植物,所以把帶放置的植物和草坪上的植物分為兩個集合,是十分合理精妙的。在判斷殭屍是否攻擊植物,只需要去遍歷草坪上的植物集合即可。如果不拆分,當要判斷殭屍是否攻擊植物的時候,需要遍歷的集合將是所有的植物集合,並且需要增加至少2個狀態來區分植物是在草坪上還是在滾輪機上,這段代碼想想就是又臭又長。
接下來該讓對象們都動起來了。之前說到在父類中的移動方法是抽象方法,在各自的子類中都進行重寫後,不同的對象移動方式就是各式各樣的了。
<code>public
void
BulletStepAction
()
{for
(Bullet b:bullets) { b.step(); } }int
zombieStepTime =0
;public
void
zombieStepAction
()
{if
(zombieStepTime++%3
==0
) {for
(Zombie z:zombies) {if
(z.isLife()) { z.step(); } } } }/<code>
看著代碼中對集合複雜的遍歷,不得不感概lambda表達式真是個好東西:
<code>def
Warnning
()
: win32api.MessageBox(0
,"別打遊戲,我看著你呢"
,"提醒"
, win32con.MB_ICONWARNING)/<code>
這裡好像還是沒法展示lambda表達式強大的功能,請看下面的例子:
<code>public
ListfilterStudentByStrategy
(List students, SimpleStrategy strategy)
{ List filterStudents =new
ArrayList<>();for
(Student student : filterStudents) {if
(strategy.operate(student)){ filterStudents.add(student); } }return
filterStudents; }public
interface
SimpleStrategy
<T
> {public
boolean
operate
(T t)
; }/<code>
但好像還是有點麻煩,又要寫接口,又要寫實現類,後續的維護也是個頭疼問題,這個時候救世主lambda表達式就出現了:
<code>List
lambdaStudents = students.stream().filter(student -> student.getGender()==1
).collect(Collectors.toList());/<code>
讓我們看看上面到底發生了啥。首先將數據的集合流化,接著調用過濾方法,強大lambda表達式讓代碼變得簡潔,並且判斷條件的修改可在代碼中直接維護無需在策略接口的實現類維護。最後在轉成集合,返回一個滿足產品需求的集合。
回到正題,如何讓對象們打起來呢?下面以殭屍攻擊植物為例:
<code>public
boolean
zombieHit
(Plant p)
{int
x1 =this
.x-p.getWidth();int
x2 =this
.x+this
.width;int
y1 =this
.y-p.getHeight();int
y2 =this
.y+this
.width;int
x = p.getX();int
y = p.getY();return
x>=x1 && x<=x2 && y>=y1 && y<=y2; }/<code>
結合圖片來看,上述代碼應該就更好理解。黑框P代表植物,黑框Z代表植物,虛線是指兩者接觸的極限距離,當殭屍進入虛線內,就保證可以攻擊到植物。
<code>int
zombieHitTime =0
;public
void
zombieHitAction
()
{if
(zombieHitTime++%100
==0
) {for
(Zombie z:zombies) {if
(!z.isDead()) { z.goLife(); }for
(Plant p:plantsLife) {if
(z.isLife()&&!p.isDead()&&z.zombieHit(p)&&!(pinstanceof
Spikerock)) { z.goAttack(); p.loseLive(); } } } } }/<code>
如果出現了一些效果的偏移,造成的原因是圖片大小不一造成的座標偏移,因為圖片都是網上找的,所以效果不是太理想。
至此,遊戲的基本功能基本實現了。Java是一門面向對象的語言,萬物皆對象,特徵皆屬性,行為皆方法。肉眼能看到的殭屍、植物、草坪都是對象,對象的特性比如血量、移動速度都是屬性,對象的行為比如移動、攻擊、死亡都是方法。
下面說說對遊戲功能的優化。
遊戲優化
1.放置植物的優化
已經放置過植物的草地不能再放置植物了。之前是將草地設計成empty和hold兩種狀態,現在來看其實只需要返回一個true和false就行了,將整個植物集合定義成一個虛擬的boolean集合即可。
2.移除植物的優化
設計思路是新增一個鏟子對象:
<code>private
List shovels =new
ArrayList();public
void
shovelEnterAction
() {if
(shovels.size()==0
) { shovels.add
(new
Shovel()); } } Iterator it = shovels.iterator(); Iterator it2 = plantsLife.iterator();while
(it.hasNext()) { Shovel s = it.next();if
(s.isMove()) {while
(it2.hasNext()) { Plant p = it2.next();int
x1 = p.getX();int
x2 = p.getX()+p.getWidth();int
y1 = p.getY();int
y2 = p.getY()+p.getHeight();if
((p.isLife()||((Blover) p).isClick())&&Mx>x1&&Mxy1&&My/<code>
看著這極其複雜好像很厲害的代碼,我又萌生了痛下狠手的想法,但為了保持原生,我忍住。於是乎還發現了一個BUG。如果選中鏟子後,戰場上唯一的植物被殭屍吃掉了,那麼這個鏟子將一直跟隨著鼠標無法達到使用後消除的效果了。解決方案當然也很簡單,當戰場上植物集合的size為0時,清空鏟子集合即可。
3.遊戲可玩性的優化
上文在遊戲設計中提到的擊殺殭屍後可能隨機獲得獎勵類型是這樣實現的。還是從設計分析開始,並非擊殺任何類型的殭屍都可以獲得獎勵,所以獎勵應該放在接口中:
<code>public
interface
Award
{public
static
final
int
CLEAR =0
;public
static
final
int
STOP =1
;public
abstract
int
getAwardType
()
; }/<code>
當殭屍死亡時,需要去判斷該殭屍是否有獎勵接口,如果有則執行相應獎勵的方法:
<code>public
void
checkZombieAction
() { Iterator it = zombies.iterator();while
(it.hasNext()) { Zombie z = it.next();if
(z.getLive()0
) {if
(z instanceof Award) { Award a = (Award)z;int
type = a.getAwardType();switch
(type) {case
Award.CLEAR:for
(Zombie zo:zombies) { zo.goDead(); }break
;case
Award.STOP:for
(Zombie zom:zombies) { zom.goStop(); timeStop =1
; }break
; } } z.goDead(); it.remove
(); }if
(z.OutOfBound()) { gameLife--; it.remove
(); } } }/<code>
4.添加遊戲背景音樂
bgm是一個遊戲的靈魂之一。這裡給遊戲添加背景音樂,我的選擇是新建一條線程專門用來執行音樂的解析和播放:
<code> Runnable r =new
zombieAubio("bgm.wav"
); Thread t =new
Thread(r); t.start();public
class
zombieAubio
implements
Runnable
{private
String filename;public
zombieAubio
(String wavfile)
{ filename=wavfile; } ....../<code>
這裡需要注意的是,Java中解析音樂的API只支持WAV格式的文件,文件格式的轉換大多數音樂播放器都可以做到。
後續優化
1.植物種類的擴充及對應功能的實現
比如殺傷力最大的玉米加農炮。需要4個小玉米進行合成,那麼在判斷是否能夠合成玉米加農炮時,需要對植物集合進行遍歷來做座標的判斷,所以這邊建議最好把可合成的植物單獨放在一個集合中,這樣在做合成判斷的時候會簡單很多,當集合的size小於4時,就可以提示合成失敗了。冰凍西瓜的設計思路也是如此。
2.動作類殭屍的加入,如撐杆跳殭屍、跳舞殭屍等
說一下撐杆跳殭屍的設計思路,此類殭屍和其他殭屍相比,多了一種跳的行為,所以會有一個單獨的方法和單獨的狀態。並且,跳只能觸發一次,所以撐杆跳殭屍的狀態變化應該是行走->遇到植物跳過去->再遇到植物就開始攻擊,在執行狀態變化的時候,應該要去考慮當前的狀態是否還可跳躍。
3.當植物攻擊範圍內不存在殭屍時,植物停止攻擊
這個就簡單拉,在植物執行攻擊方法時,校驗一下是否有Y座標相同的殭屍即可。
GitHub項目源碼地址:
https://github.com/llx330441824/plant_vs_zombie_simple.git