JDK17实现植物大战僵尸+完整版最新源码

177 阅读8分钟

hello大家好,用java实现小游戏真的很锻炼编程技术,而且很有成就感。比起做增删改查的管理系统来说,简直是不同的两个阶层的程序员。

今天我就教大家用JDK17原生库来实现一个完整的 植物大战僵尸 ,初始编程的你,只要用心就能学会。会大大加深你对面向对象的理解!

源码为自己开发的源码, 商用必究!!!

 

从这个游戏中你可以学到:

  1. 游戏二维地图是如何实现的。

  2. 游戏关卡是如何在数据库中配置的(以后新增关卡只需在数据库中插入数据)。

  3. 植物,僵尸,阳光等游戏实体的管理。

  4. 2D游戏的分层架构: 画图+逻辑管理器+实体 。

  5. 游戏内的物理系统:碰撞检测,边界检测。

 

 

视频演示

githubs.xyz/show/caf74f…

完整游戏源码,我已经整理清楚,移步:

 gitcode.com/hadluo2/jav…

 

图片演示

 

 

 

 

 

技术栈描述

项目框架

  • Java SE 17 - 主要编程语言
  • Swing - GUI框架(JFrame、JPanel、Timer等)
  • Java 2D API - 图形渲染(Graphics2D、BufferedImage)
  • Java Sound API - 音效处理(Clip、AudioSystem)
  • Maven - 项目构建管理
  • JDBC - 数据库连接

关键技术特性

  • 双缓冲渲染 - 消除画面闪烁
  • 60 FPS游戏循环 - 流畅的游戏体验
  • 资源缓存机制 - 图片和音效缓存
  • 多线程音频 - 并发音效播放

设计模式

1. 单例模式 (Singleton Pattern)

应用场景:

  • GameManager :游戏核心管理器
  • ImageLoader :图片资源管理器
  • SoundManager :声音管理器
  • DatabaseConfig :数据库配置类
  • LayerManager :层级渲染管理器

优势: 确保全局只有一个实例,便于资源管理和状态控制。

 

2. 工厂模式 (Factory Pattern)

应用场景:

  • PlantManagerExt 中的 plantPlant 方法根据 PlantType 枚举创建不同类型的植物对象
  • 根据植物类型(向日葵、豌豆射手、坚果墙等)动态创建相应的植物实例
    优势: 封装对象创建逻辑,便于扩展新的植物类型。

 

3. 策略模式 (Strategy Pattern)

应用场景:

  • GameObject 抽象类定义了 update() 和 render() 抽象方法
  • 不同的植物类( SunflowerPeashooter 等)实现不同的行为策略
    优势: 每种植物都有自己独特的行为逻辑,易于维护和扩展。

 

4. 观察者模式 (Observer Pattern)

应用场景:

  • 游戏事件处理系统,如鼠标点击事件通过 MouseListenerManagerExt 分发给各个游戏实体
  • 游戏状态变化时通知相关组件更新

5. 模板方法模式 (Template Method Pattern)

应用场景:

  • GameObject 基类定义了游戏对象的通用结构和行为模板
  • 子类重写特定方法实现自己的逻辑,如 update() 、 render() 等

6. 外观模式 (Facade Pattern)

应用场景:

  • DrawManagerExt 提供统一的绘制接口,封装了复杂的渲染逻辑
  • 各种 ManagerExt 类为复杂的游戏逻辑提供简化的接口

7. 组合模式 (Composite Pattern)

应用场景:

  • 游戏场景中的层级结构,通过 LayerManager 管理不同渲染层级的对象
  • 统一处理单个对象和对象集合的渲染

8. 命令模式 (Command Pattern)

应用场景:

  • 游戏中的各种操作(种植植物、收集阳光等)被封装成具体的方法调用
  • 便于实现撤销、重做等功能

9. 状态模式 (State Pattern)

应用场景:

  • 游戏状态管理(游戏中、暂停、结束、胜利)通过 Constants.java 中定义的状态常量进行切换
  • 不同状态下游戏有不同的行为表现

10. 享元模式 (Flyweight Pattern)

应用场景:

  • ImageLoader 使用缓存机制避免重复加载相同的图片资源
  • SoundManager 缓存音频资源

 

 

游戏实现的功能

植物系统

  • 向日葵(Sunflower) :生产阳光,带高亮效果
  • 豌豆射手(Peashooter) :发射普通子弹攻击僵尸
  • 双发豌豆射手(DoublePeashooter) :发射双倍子弹
  • 寒冰豌豆射手(IcePeashooter) :发射冰冻子弹,减缓僵尸速度
  • 坚果墙(Wallnut) :防御型植物,阻挡僵尸
  • 火树(TorchWood) :增强经过的子弹伤害
  • 植物种植系统 :检查网格位置、阳光消耗、卡片冷却

僵尸系统

  • 普通僵尸 :基础移动和攻击能力

  • 铁桶僵尸(BucketheadZombie) :高血量僵尸

  • 拿旗子的僵尸(FlagZombie) :走的很快

  • 僵尸AI :自动移动、攻击植物、冰冻状态管理

- 动画系统 :行走、攻击、冰冻动画效果

战斗系统

  • 碰撞检测 :子弹与僵尸、僵尸与植物、僵尸与小推车
  • 伤害计算 :不同武器造成不同伤害
  • 特殊效果 :冰冻效果、火焰增强效果
  • 小推车防线 :最后防御机制

 

关卡系统

关卡的配置都是在数据库里面,主要分为以下表:

level_config 表: 配置了主关卡信息,包括关卡 名称,初始阳光值, 背景图,背景音乐,关卡时长等。

plant_config表: 植物信息表, 描述了关卡有哪些植物, 每个植物消耗阳光值,生命值,攻击力等。

zombie_config表: 僵尸配置表, 描述了关卡有哪些僵尸,每个僵尸的生命值,出现的时间点, 攻击力,移动速度等。

 

目前游戏只有2关,后续可以直接在表中插入数据配置关卡场景。无需改动任何代码。

 

游戏实现原理

本小结将讲解游戏中各大类的具体功能,每个类都是实现游戏不可或缺的部分,他们紧密相连来实现一个完整的游戏系统。

数据库加载关卡

DatabaseManager 类是连接数据库的核心,里面加载了db.properties得到数据库信息,然后连接数据库。EnemyDAO,LevelDAO 就是 基础的表的增删改查。然后通过 LevelManager的loadLevel方法

加载到了数据库指定关卡的数据。

复制代码

/**
 * 开始指定关卡
 */
public boolean startLevel(int levelNumber) {
    LevelConfig level = databaseManager.getLevelByNumber(levelNumber);
    if (level != null) {
        currentLevel = level;
        currentLevelNumber = levelNumber;

        // 加载当前关卡的僵尸配置
        currentLevelZombies = databaseManager.getZombiesByLevel(levelNumber);
        System.out.println("加载关卡 " + levelNumber + " 的僵尸配置: " + currentLevelZombies.size() + " 种僵尸");

        // 加载当前关卡的植物配置
        currentLevelPlants = databaseManager.getPlantsByLevel(levelNumber);
        System.out.println("加载关卡 " + levelNumber + " 的植物配置: " + currentLevelPlants.size() + " 种植物");

        System.out.println("开始关卡: " + level.getLevelName());
        return true;
    } else {
        System.err.println("关卡不存在: " + levelNumber);
        return false;
    }
}

复制代码

 

游戏循环的启动

点击开始游戏时会初始化 GameFrame 类,这个就是游戏的主界面,里面有一个 GamePanel ,就是游戏内容画图的核心。在GamePanel 的里面有一个循环定时器,就是游戏的主循环位置:

复制代码

/**
 * 开始游戏循环
 */
public void startGameLoop() {
    if (gameTimer == null || !running) {
        gameManager.startGame(1);
        running = true;
        
        // 创建Timer,每隔FRAME_DELAY毫秒执行一次
        gameTimer = new Timer(FRAME_DELAY, e->{
            if (running) {
                // 更新游戏逻辑
                try {
                    gameManager.update();
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                // 重绘画面
                repaint();
            }
        });
        gameTimer.start();
    }
}

/**
 * 停止游戏循环
 */
public void stopGameLoop() {
    running = false;
    if (gameTimer != null) {
        gameTimer.stop();
        gameTimer = null;
    }
    // 停止背景音乐
    if(LevelManager.getInstance().getCurrentLevel() != null){
        SoundManager.getInstance().stopBackgroundMusic(LevelManager.getInstance().getCurrentLevel().getBackgroundMusic());
    }
}

复制代码

游戏循环的逻辑很清晰, 就是先更新一些逻辑数据,比如玩家的坐标值,敌人,子弹的状态,是否死亡等等。然后调用 repaint(); 方法去画图,就会执行当前类的画图逻辑:

复制代码

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    
    // 清空后缓冲
    backGraphics.setColor(Color.BLACK);
    backGraphics.fillRect(0, 0, Constants.WINDOW_WIDTH, Constants.WINDOW_HEIGHT);
    
    // 根据游戏状态绘制不同内容
    switch (gameManager.getGameState()) {
        case Constants.GAME_STATE_PLAYING:
            drawGame(backGraphics);
            break;
        case Constants.GAME_STATE_PAUSED:
            drawGame(backGraphics);
            drawPauseOverlay(backGraphics);
            break;
        case Constants.GAME_STATE_GAME_OVER:
            drawGameOver(backGraphics);
            break;
        case Constants.GAME_STATE_VICTORY:
            drawVictory(backGraphics);
            break;
    }
    
    // 将后缓冲绘制到屏幕
    g.drawImage(backBuffer, 0, 0, null);
}

复制代码

画图的逻辑就是获取到所有的游戏实体,然后调用实体自身的 render方法进行画图(传递了Graphics 用来画图的对象 )。

 

植物的种植

为了分离GameManager的代码。 我们将一些典型的例如植物种植等方法提取到管理器扩展类里面,PlantManagerExt 类就分离了植物的种植逻辑。

复制代码

 

/**
 * 种植植物
 */
public static boolean plantPlant(int gridX, int gridY, PlantConfig.PlantType plantType) {
    GameManager gameManager = GameManager.getInstance();
    LevelManager levelManager = gameManager.getLevelManager();

    if (plantType == null) {
        return false;
    }
    int rows = levelManager.getCurrentLevel().getMapRows();
    int cols = levelManager.getCurrentLevel().getMapCols();
    // 检查网格位置是否有效
    if (gridX < 0 || gridX >= cols ||
            gridY < 0 || gridY >= rows) {
        return false;
    }

    // 检查该位置是否已有植物
    GameObject[][] plantGrid = gameManager.getPlantGrid();
    if (plantGrid[gridY][gridX] != null) {
        return false;
    }

    // 获取植物成本
    int cost = 0;

    // 检查阳光是否足够
    if (gameManager.getSunCard().getSunValue() < cost) {
        return false;
    }

    // 计算实际坐标(放置在网格中心)
    int x = Constants.GRID_START_X + gridX * Constants.GRID_WIDTH + Constants.GRID_WIDTH / 2 - 60 / 2;
    int y = Constants.GRID_START_Y + gridY * Constants.GRID_HEIGHT + Constants.GRID_HEIGHT / 2 - 80 / 2;

    // 创建植物
    GameObject plant = null;
    if (plantType == PlantConfig.PlantType.SUNFLOWER) {
        cost = LevelManager.getInstance().getCurrentLevelPlant(PlantConfig.PlantType.SUNFLOWER).getPlantCost();
        if(gameManager.getSunCard().getSunValue() < cost) {
            return false;
        }

        // 向日葵
        plant = new Sunflower(x, y , gridY,gridX);
        // 添加植物
        gameManager.getSunflowers().add((Sunflower) plant);

    } else if (plantType == PlantConfig.PlantType.PEASHOOTER) {
        cost = LevelManager.getInstance().getCurrentLevelPlant(PlantConfig.PlantType.PEASHOOTER).getPlantCost();

        if(gameManager.getSunCard().getSunValue() < cost) {
            return false;
        }
        // 豌豆射手
        plant = new Peashooter(x, y, gridY , gridX);
        // 添加植物
        gameManager.getPeashooters().add((Peashooter) plant);
    } else if (plantType == PlantConfig.PlantType.WALLNUT) {
        cost = LevelManager.getInstance().getCurrentLevelPlant(PlantConfig.PlantType.WALLNUT).getPlantCost();
        if(gameManager.getSunCard().getSunValue() < cost) {
            return false;
        }
        // 坚果墙
        plant = new Wallnut(x, y, gridY, gridX);
        // 添加植物
        gameManager.getWallnuts().add((Wallnut) plant);
    } else if (plantType == PlantConfig.PlantType.REPEATER) {
        // 双发豌豆射手
        cost = LevelManager.getInstance().getCurrentLevelPlant(PlantConfig.PlantType.REPEATER).getPlantCost();

        if(gameManager.getSunCard().getSunValue() < cost) {
            return false;
        }
        plant = new DoublePeashooter(x, y, gridY, gridX);
        // 添加植物
        gameManager.getDoublePeashooters().add((DoublePeashooter) plant);
    } else if (plantType == PlantConfig.PlantType.ICE_PEASHOOTER) {
        // 寒冰豌豆射手
        cost = LevelManager.getInstance().getCurrentLevelPlant(PlantConfig.PlantType.ICE_PEASHOOTER).getPlantCost();

        if(gameManager.getSunCard().getSunValue() < cost) {
            return false;
        }
        plant = new IcePeashooter(x, y, gridY, gridX);
        // 添加植物
        gameManager.getIcePeashooters().add((IcePeashooter) plant);
    } else if (plantType == PlantConfig.PlantType.TORCHWOOD) {
        // 火树
        cost = LevelManager.getInstance().getCurrentLevelPlant(PlantConfig.PlantType.TORCHWOOD).getPlantCost();

        if(gameManager.getSunCard().getSunValue() < cost) {
            return false;
        }
        plant = new TorchWood(x, y, gridY, gridX);
        // 添加植物
        gameManager.getTorchWoods().add((TorchWood) plant);
    }
    // 场景地图 种植了植物了
    plantGrid[gridY][gridX] = plant;

    //当前的阳光值减少
    SunCard sunCard = gameManager.getSunCard();
    sunCard.setSunValue(sunCard.getSunValue() - cost);

    // 种植成功后,触发对应卡片的冷却
    triggerCardCooldown(plantType);
    // 清除当前选中的卡片
    clearSelectedCard();
    // 播放声音
    SoundManager.getInstance().playSound("sounds/plant.wav");
    return true;
}

 

复制代码

 

 

游戏还涉及到很多有趣的设计,比如: 子弹碰到火树后变成火弹, 寒冰射手的子弹碰到僵尸等等特效,我就不一一讲解了,大家可以跟着源码来打开新世界的大门。。。

 

 

游戏启动

将源码导入到idea中,这个项目就是一个普通的maven管理的项目, 导入前,请设置好maven的仓库配置。

 

设置好JDK的环境为17

 

用navicate工具连接数据库,新建数据库,然后执行sql创建表:

 

 

 

数据库的版本用8就可以了。

 

修改数据库配置db.properties:

 

等待编译好,启动Main就可以了。游戏图片,声音素材资源在resource目录下面。