僵尸課程設(shè)計(jì)源碼:Swing游戲開(kāi)發(fā)與塔防邏輯實(shí)現(xiàn))
簡(jiǎn)介這是一份基于Java實(shí)現(xiàn)的植物大戰(zhàn)僵尸游戲項(xiàng)目設(shè)計(jì)源碼面向具備Java基礎(chǔ)、希望以經(jīng)典游戲?yàn)榘咐钊肜斫饷嫦驅(qū)ο笤O(shè)計(jì)與游戲開(kāi)發(fā)流程的學(xué)習(xí)者。項(xiàng)目完整還原了種植植物抵御僵尸進(jìn)攻的核心玩法涵蓋植物、僵尸、子彈、卡片、陽(yáng)光、鏟子等模塊并配有背景、音樂(lè)與關(guān)卡界面適合課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)參考或Java圖形界面與多線(xiàn)程練習(xí)。壓縮包共86個(gè)文件約25.99MB其中Java源代碼29個(gè)承擔(dān)游戲邏輯與實(shí)體類(lèi)實(shí)現(xiàn)GIF、JPG、PNG圖片共42個(gè)用于植物、僵尸、卡片與場(chǎng)景素材WAV音頻8個(gè)提供背景音樂(lè)與攻擊、種植等音效另有XML配置、iml與gitignore等項(xiàng)目文件便于直接導(dǎo)入IDE運(yùn)行。目前已有459人學(xué)習(xí)下載。代碼結(jié)構(gòu)清晰、注釋詳盡讀者可借此梳理游戲循環(huán)、碰撞檢測(cè)、資源加載與狀態(tài)管理等實(shí)現(xiàn)思路并在此基礎(chǔ)上進(jìn)行二次開(kāi)發(fā)與功能擴(kuò)展。1. 從一份 Java 版植物大戰(zhàn)僵尸源碼說(shuō)起它到底能跑出什么很多人第一次搜「基于Java的植物大戰(zhàn)僵尸游戲項(xiàng)目設(shè)計(jì)源碼」心里想的其實(shí)不是玩而是想找一個(gè)能跑、能改、能寫(xiě)進(jìn)課程設(shè)計(jì)里的完整工程。我當(dāng)年也是這樣翻了一堆壓縮包解壓出來(lái)要么只有幾個(gè) class 文件要么圖片路徑寫(xiě)死在 C 盤(pán)跑起來(lái)直接黑屏。后來(lái)自己照著經(jīng)典塔防邏輯重寫(xiě)了一遍才明白這類(lèi)項(xiàng)目的價(jià)值不在「像不像原版」而在于它把 Java 面向?qū)ο?、Swing 繪圖、定時(shí)器循環(huán)、碰撞檢測(cè)這幾件事全串起來(lái)了是一個(gè)能真正動(dòng)手的 java課程設(shè)計(jì)案例源碼。這篇文章面向三類(lèi)人正在找 java課程設(shè)計(jì) 題目的學(xué)生、想用一個(gè)小項(xiàng)目練手 Java 基礎(chǔ) 的轉(zhuǎn)行者、以及需要一份可講清楚設(shè)計(jì)思路的答辯材料的人。我會(huì)把植物大戰(zhàn)僵尸這個(gè)題材拆成能復(fù)現(xiàn)的模塊——陽(yáng)光經(jīng)濟(jì)、植物放置、僵尸尋路、子彈碰撞、關(guān)卡波次每一塊給出可抄的代碼骨架和參數(shù)含義。植物大戰(zhàn)僵尸素材 網(wǎng)上很多但真正卡人的是邏輯怎么組織這篇就講這個(gè)。2. 先想清楚架構(gòu)為什么用 Swing 而不是 JavaFX實(shí)體怎么分層2.1 技術(shù)選型Swing Timer 循環(huán)是這類(lèi)項(xiàng)目最穩(wěn)的組合做 Java 桌面小游戲繞不開(kāi)兩個(gè)選擇Swing 還是 JavaFX主動(dòng)重繪還是定時(shí)器驅(qū)動(dòng)。我的結(jié)論很直接——課程設(shè)計(jì)級(jí)別的植物大戰(zhàn)僵尸用 Swing javax.swing.Timer 就夠了別上 JavaFX。原因有三個(gè)第一Swing 是 JDK 自帶的java環(huán)境變量配置 好之后直接能編譯不需要額外引入模塊答辯機(jī)器上不會(huì)因?yàn)槿?JavaFX 運(yùn)行時(shí)而翻車(chē)第二Swing 的 paintComponent 重繪模型非常直觀(guān)你只要在 Timer 的 tick 里更新坐標(biāo)然后 repaint畫(huà)面就動(dòng)起來(lái)了新手能看懂每一幀發(fā)生了什么第三網(wǎng)上能搜到的 植物大戰(zhàn)僵尸素材 大多是 png 序列幀Swing 的 Graphics.drawImage 直接吃不用做紋理圖集。JavaFX 當(dāng)然更現(xiàn)代但它的場(chǎng)景圖、屬性綁定、FXML 對(duì)沒(méi)接觸過(guò)的人是一層額外認(rèn)知負(fù)擔(dān)。課程設(shè)計(jì)要的是「我能講清楚每一行在干嘛」不是炫技。所以選型上Swing 是性?xún)r(jià)比最高的。游戲主循環(huán)用 Timer 而不是 Thread.sleep 死循環(huán)也是血淚經(jīng)驗(yàn)。Thread.sleep 會(huì)阻塞事件分發(fā)線(xiàn)程EDT導(dǎo)致按鈕點(diǎn)不動(dòng)、窗口拖不動(dòng)看起來(lái)像卡死。Timer 是事件驅(qū)動(dòng)的每 16 毫秒觸發(fā)一次 actionPerformed在 EDT 里安全更新?tīng)顟B(tài)再重繪幀率穩(wěn)定在 60 幀左右夠用。2.2 實(shí)體分層把「會(huì)動(dòng)的東西」抽象成一個(gè)基類(lèi)植物大戰(zhàn)僵尸里所有會(huì)動(dòng)、會(huì)畫(huà)的東西——向日葵、豌豆射手、普通僵尸、路障僵尸、子彈、陽(yáng)光——本質(zhì)都是「有坐標(biāo)、有寬高、有貼圖、每幀要更新、要能畫(huà)出來(lái)」。所以第一件事是抽一個(gè) GameObject 基類(lèi)把公共字段和方法收進(jìn)去。// 所有游戲?qū)嶓w的基類(lèi)統(tǒng)一坐標(biāo)、尺寸、貼圖和生命周期 public abstract class GameObject { protected int x, y; // 左上角坐標(biāo)單位像素 protected int width, height; // 碰撞盒尺寸 protected BufferedImage image; // 當(dāng)前貼圖 protected boolean alive true; // false 時(shí)從列表移除 public GameObject(int x, int y, int width, int height) { this.x x; this.y y; this.width width; this.height height; } // 每幀調(diào)用子類(lèi)實(shí)現(xiàn)自己的移動(dòng)/狀態(tài)邏輯 public abstract void update(); // 每幀繪制子類(lèi)可覆蓋做動(dòng)畫(huà) public void draw(Graphics g) { if (image ! null) { g.drawImage(image, x, y, width, height, null); } } // 軸對(duì)齊矩形碰撞檢測(cè)夠用且便宜 public boolean collidesWith(GameObject other) { return this.x other.x other.width this.x this.width other.x this.y other.y other.height this.y this.height other.y; } public boolean isAlive() { return alive; } public void setAlive(boolean alive) { this.alive alive; } }這段代碼的關(guān)鍵點(diǎn)update()聲明為抽象方法強(qiáng)制每個(gè)子類(lèi)想清楚自己每幀要做什么collidesWith用的是軸對(duì)齊包圍盒AABB對(duì)塔防這種格子化布局完全夠用比像素級(jí)碰撞快得多也不會(huì)出現(xiàn)「明明沒(méi)碰到卻判定命中」的玄學(xué)。alive標(biāo)志位是延遲刪除的核心——不要在遍歷列表時(shí)直接 remove會(huì)拋 ConcurrentModificationException正確做法是標(biāo)記 alivefalse遍歷結(jié)束后統(tǒng)一清理。參數(shù)上width 和 height 建議和貼圖原始尺寸一致比如豌豆射手 71×71僵尸 166×144原版素材常見(jiàn)尺寸碰撞盒可以比貼圖略小讓手感更寬容。2.3 游戲面板的骨架狀態(tài)列表 定時(shí)器 重繪有了基類(lèi)主面板 GamePanel 就負(fù)責(zé)持有所有實(shí)體列表、驅(qū)動(dòng)循環(huán)、處理輸入。public class GamePanel extends JPanel implements ActionListener { private final ListGameObject plants new ArrayList(); private final ListGameObject zombies new ArrayList(); private final ListGameObject bullets new ArrayList(); private final Timer timer; private int sunCount 50; // 初始陽(yáng)光原版是 50 public GamePanel() { setPreferredSize(new Dimension(900, 600)); timer new Timer(16, this); // 約 60 FPS timer.start(); } Override public void actionPerformed(ActionEvent e) { updateAll(); repaint(); // 觸發(fā) paintComponent } private void updateAll() { plants.forEach(GameObject::update); zombies.forEach(GameObject::update); bullets.forEach(GameObject::update); // 統(tǒng)一清理死亡實(shí)體避免遍歷時(shí)刪除 plants.removeIf(o - !o.isAlive()); zombies.removeIf(o - !o.isAlive()); bullets.removeIf(o - !o.isAlive()); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 先畫(huà)背景草坪再畫(huà)植物、僵尸、子彈順序決定遮擋關(guān)系 plants.forEach(o - o.draw(g)); zombies.forEach(o - o.draw(g)); bullets.forEach(o - o.draw(g)); } }邏輯說(shuō)明Timer 每 16ms 調(diào)一次 actionPerformed先更新所有實(shí)體狀態(tài)再 repaint。repaint 不會(huì)立刻畫(huà)而是給 EDT 排一個(gè)重繪請(qǐng)求Swing 會(huì)合并多次請(qǐng)求所以不用擔(dān)心 60 次 repaint 把 CPU 打滿(mǎn)。繪制順序很重要——后畫(huà)的蓋住先畫(huà)的所以背景最先子彈最后這樣子彈看起來(lái)在僵尸前面飛。參數(shù)說(shuō)明16ms 對(duì)應(yīng)約 62.5 FPS是桌面游戲的常用值如果機(jī)器卡可以調(diào)到 33ms30 FPS邏輯速度靠每幀位移量補(bǔ)償不要靠改 Timer 間隔來(lái)調(diào)游戲速度否則不同機(jī)器上僵尸走路速度會(huì)不一致。3. 核心玩法落地陽(yáng)光、種植、僵尸尋路和子彈碰撞3.1 陽(yáng)光經(jīng)濟(jì)定時(shí)掉落 點(diǎn)擊拾取 向日葵產(chǎn)出陽(yáng)光是植物大戰(zhàn)僵尸的經(jīng)濟(jì)命脈做不好整個(gè)節(jié)奏就崩。原版邏輯是天空每隔一段時(shí)間隨機(jī)掉一個(gè)陽(yáng)光向日葵每隔固定時(shí)間產(chǎn)一個(gè)陽(yáng)光玩家點(diǎn)擊陽(yáng)光加 25。這里有兩個(gè)坑一是陽(yáng)光掉落要用獨(dú)立計(jì)時(shí)器不能和主循環(huán)幀數(shù)硬綁否則幀率一變經(jīng)濟(jì)就亂二是點(diǎn)擊拾取要做命中檢測(cè)且拾取后立刻標(biāo)記死亡。public class Sun extends GameObject { private int targetY; // 下落到這個(gè) y 就停住 private int lifeTicks 500; // 約 8 秒后自動(dòng)消失 private boolean collected false; public Sun(int x, int y, int targetY) { super(x, y, 60, 60); this.targetY targetY; } Override public void update() { if (y targetY) { y 1; // 每幀下落 1 像素 } else { lifeTicks--; if (lifeTicks 0) alive false; // 超時(shí)消失 } } // 由鼠標(biāo)監(jiān)聽(tīng)調(diào)用判斷點(diǎn)擊是否落在陽(yáng)光上 public boolean tryCollect(int mouseX, int mouseY) { if (!collected mouseX x mouseX x width mouseY y mouseY y height) { collected true; alive false; return true; } return false; } }邏輯說(shuō)明陽(yáng)光先下落到 targetY 后開(kāi)始倒計(jì)時(shí)lifeTicks 歸零就消失模擬原版「不撿就沒(méi)了」。tryCollect 返回布爾值讓面板知道要不要加陽(yáng)光職責(zé)清晰。參數(shù)說(shuō)明下落速度 1 像素/幀在 60FPS 下約 60 像素/秒從頂部掉到草坪大概 3 秒節(jié)奏合適lifeTicks500 約 8 秒想更難就調(diào)小到 300。向日葵產(chǎn)出間隔建議 24 秒原版數(shù)據(jù)用幀計(jì)數(shù)就是 24×601440 幀。3.2 植物放置網(wǎng)格吸附 陽(yáng)光校驗(yàn) 冷卻種植是玩家操作的核心。草坪是 5 行 9 列的網(wǎng)格每格約 80×100 像素。點(diǎn)擊卡片選中植物再點(diǎn)草坪格子要判斷三件事陽(yáng)光夠不夠、這格有沒(méi)有植物、卡片是否在冷卻。// 網(wǎng)格參數(shù)和背景圖對(duì)齊 private static final int GRID_COLS 9; private static final int GRID_ROWS 5; private static final int CELL_W 80; private static final int CELL_H 100; private static final int GRID_LEFT 250; // 草坪起始 x private static final int GRID_TOP 100; // 草坪起始 y public boolean plantAt(int mouseX, int mouseY, PlantType type) { int col (mouseX - GRID_LEFT) / CELL_W; int row (mouseY - GRID_TOP) / CELL_H; if (col 0 || col GRID_COLS || row 0 || row GRID_ROWS) return false; int cellX GRID_LEFT col * CELL_W; int cellY GRID_TOP row * CELL_H; // 該格已有植物則拒絕 for (GameObject p : plants) { if (p.getX() cellX p.getY() cellY) return false; } if (sunCount type.cost) return false; // 陽(yáng)光不足 if (type.isOnCooldown()) return false; // 卡片冷卻中 sunCount - type.cost; plants.add(type.create(cellX, cellY)); type.startCooldown(); return true; }邏輯說(shuō)明先把鼠標(biāo)坐標(biāo)換算成行列越界直接拒絕再檢查格子占用、陽(yáng)光、冷卻。三個(gè)校驗(yàn)順序可以調(diào)整但建議先做便宜的坐標(biāo)判斷再做列表遍歷。參數(shù)說(shuō)明CELL_W/CELL_H 必須和背景草坪圖嚴(yán)格對(duì)齊否則植物會(huì)「飄」在格子外這是最常見(jiàn)的翻車(chē)點(diǎn)。GRID_LEFT 和 GRID_TOP 要按你實(shí)際背景圖量別照抄。冷卻用毫秒時(shí)間戳記錄比幀計(jì)數(shù)更穩(wěn)。3.3 僵尸尋路與子彈碰撞直線(xiàn)推進(jìn) 同行判定植物大戰(zhàn)僵尸的僵尸尋路其實(shí)很簡(jiǎn)單——從右往左直線(xiàn)走遇到植物就啃。不需要 A*不需要導(dǎo)航網(wǎng)格。真正要處理的是「僵尸和植物在同一行且重疊時(shí)停下啃食」以及「子彈只打同一行的僵尸」。public class Zombie extends GameObject { private int row; // 所在行0-4 private int speed 1; // 每幀左移像素 private int hp 100; private int attackTimer 0; Override public void update() { // 檢查本行是否有植物擋路 Plant blocker findBlocker(); if (blocker ! null) { attackTimer; if (attackTimer 30) { // 每 30 幀咬一口 blocker.takeDamage(10); attackTimer 0; } } else { x - speed; // 沒(méi)阻擋就繼續(xù)走 } if (x width 0) alive false; // 走到最左邊算通關(guān)失敗 } }邏輯說(shuō)明僵尸每幀先找同行、且在自己前方的植物找到就進(jìn)入啃食狀態(tài)否則左移。子彈的碰撞在 Bullet.update 里做只遍歷同一行的僵尸命中后雙方扣血。參數(shù)說(shuō)明speed1 在 60FPS 下約 60 像素/秒從最右走到最左約 15 秒給玩家反應(yīng)時(shí)間hp100豌豆傷害 20五發(fā)打死一個(gè)普通僵尸和原版接近。attackTimer 閾值 30 表示每 0.5 秒咬一口調(diào)小僵尸更兇。提示子彈和僵尸的碰撞一定要限定同一行否則會(huì)出現(xiàn)「上一行的豌豆打死下一行僵尸」的詭異現(xiàn)象這是新手最常見(jiàn)的邏輯漏洞。4. 避坑與排查那些讓項(xiàng)目跑不起來(lái)的細(xì)節(jié)4.1 圖片加載報(bào) NullPointerException現(xiàn)象運(yùn)行后直接拋異常堆棧指向 drawImage 或 getWidth提示 image 為 null。 原因素材路徑寫(xiě)的是絕對(duì)路徑比如C:\Users\xxx\Desktop\pvz\sun.png換臺(tái)機(jī)器就找不到或者用getClass().getResource()時(shí)路徑少了開(kāi)頭的斜杠。 解決把素材放進(jìn)src/main/resources/images/用getClass().getResource(/images/sun.png)加載斜杠必須有。打包成 jar 后資源也能正常讀取絕對(duì)路徑則一定失敗。4.2 遍歷列表時(shí)刪除元素導(dǎo)致并發(fā)修改異常現(xiàn)象游戲運(yùn)行幾秒后拋 ConcurrentModificationException位置在 for-each 循環(huán)里。 原因在for (GameObject z : zombies)循環(huán)體內(nèi)直接zombies.remove(z)。 解決統(tǒng)一用alivefalse標(biāo)記循環(huán)結(jié)束后removeIf(o - !o.isAlive())。所有實(shí)體列表都遵守這個(gè)約定不要圖省事直接刪。4.3 畫(huà)面閃爍、植物和僵尸拖影現(xiàn)象移動(dòng)的僵尸后面拖著一串殘影或者整個(gè)畫(huà)面一閃一閃。 原因重繪時(shí)沒(méi)有先清空背景或者用了repaint()卻沒(méi)在 paintComponent 里調(diào)用super.paintComponent(g)。 解決paintComponent 第一行必須super.paintComponent(g)它會(huì)用背景色填充整個(gè)面板然后再畫(huà)草坪背景圖。如果還閃檢查是不是在 Timer 之外又開(kāi)了線(xiàn)程調(diào) repaint。4.4 僵尸走到最左邊游戲沒(méi)結(jié)束現(xiàn)象僵尸越過(guò)左邊界后消失但游戲繼續(xù)玩家還能種植物。 原因只寫(xiě)了alivefalse移除僵尸沒(méi)有觸發(fā)失敗狀態(tài)。 解決在僵尸 update 里判斷x width 0時(shí)調(diào)用面板的gameOver()把狀態(tài)切到 GAME_OVERTimer 停止或只重繪結(jié)束畫(huà)面。別讓游戲「無(wú)聲無(wú)息」地繼續(xù)。4.5 不同電腦上僵尸速度不一樣現(xiàn)象自己電腦上正常同學(xué)電腦上僵尸快得像飛。 原因用Thread.sleep(16)或 Timer 間隔當(dāng)速度基準(zhǔn)但實(shí)際幀率受機(jī)器性能影響掉幀時(shí)每幀位移不變整體就變慢反之高刷屏上 Timer 觸發(fā)更頻繁就變快。 解決用「每秒位移」而不是「每幀位移」來(lái)定義速度在 update 里乘以實(shí)際 deltaTime。簡(jiǎn)單做法是固定 Timer 間隔并接受輕微差異嚴(yán)格做法是記錄System.nanoTime()算真實(shí)幀間隔。5. 進(jìn)階技巧用狀態(tài)模式和波次配置把項(xiàng)目講出深度如果你想讓這份 java課程設(shè)計(jì)案例源碼 在答辯時(shí)更有說(shuō)服力別停在「能跑」。加兩個(gè)東西狀態(tài)模式和波次配置表。狀態(tài)模式解決的是「游戲有開(kāi)始、進(jìn)行、暫停、結(jié)束多個(gè)狀態(tài)用一堆 if-else 判斷很亂」的問(wèn)題。定義一個(gè) GameState 接口每個(gè)狀態(tài)實(shí)現(xiàn)自己的 update 和 handleClickGamePanel 只持有一個(gè)當(dāng)前狀態(tài)引用。這樣代碼結(jié)構(gòu)清晰答辯時(shí)能講出設(shè)計(jì)模式j(luò)ava策略模式多種組合 那套思路在這里同樣適用。public interface GameState { void update(); void handleClick(int x, int y); void draw(Graphics g); } public class PlayingState implements GameState { private final GamePanel panel; public PlayingState(GamePanel panel) { this.panel panel; } Override public void update() { panel.updateEntities(); // 正常游戲邏輯 } Override public void handleClick(int x, int y) { panel.tryPlantOrCollect(x, y); } Override public void draw(Graphics g) { panel.drawEntities(g); } }波次配置用一張表驅(qū)動(dòng)比硬編碼 if 好維護(hù)得多。下面是一個(gè)關(guān)卡配置的示例結(jié)構(gòu)波次觸發(fā)時(shí)間(秒)僵尸類(lèi)型數(shù)量間隔(秒)110普通僵尸35240普通僵尸54380路障僵尸464130普通路障835180鐵桶僵尸38這張表可以用一個(gè)ListWave存每幀檢查當(dāng)前時(shí)間是否到達(dá)下一波觸發(fā)點(diǎn)到了就按數(shù)量和間隔生成僵尸。想加難度就改表不用動(dòng)邏輯代碼。參數(shù)上觸發(fā)時(shí)間要留出玩家攢陽(yáng)光的窗口第一波別早于 10 秒否則開(kāi)局就崩。驗(yàn)證方法也很實(shí)在把 Timer 間隔臨時(shí)改成 100ms10 FPS游戲會(huì)變慢但邏輯不變方便你肉眼觀(guān)察碰撞和狀態(tài)切換是否正確確認(rèn)無(wú)誤再改回 16ms。這個(gè)「慢放調(diào)試法」我每次做游戲都會(huì)用比打斷點(diǎn)高效。最后說(shuō)個(gè)我自己的習(xí)慣每加一個(gè)新植物或新僵尸先只畫(huà)一個(gè)純色矩形代替貼圖把邏輯跑通再換素材。這樣能把「邏輯 bug」和「素材路徑 bug」分開(kāi)省下大量排查時(shí)間。植物大戰(zhàn)僵尸素材 再全也救不了一個(gè)邏輯混亂的工程。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取