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