
hello大家好,用java实现小游戏真的很锻炼编程技术,而且很有成就感。比起做增删改查的管理系统来说,简直是不同的两个阶层的程序员。
今天我就教大家用JDK17原生库来实现一个完整的 植物大战僵尸 ,初始编程的你,只要用心就能学会。会大大加深你对面向对象的理解!
源码为自己开发的源码, 商用必究!!!
从这个游戏中你可以学到:
1. 游戏二维地图是如何实现的。
2. 游戏关卡是如何在数据库中配置的(以后新增关卡只需在数据库中插入数据)。
3. 植物,僵尸,阳光等游戏实体的管理。
4. 2D游戏的分层架构: 画图+逻辑管理器+实体 。
5. 游戏内的物理系统:碰撞检测,边界检测。





- Java SE 17 - 主要编程语言 - Swing - GUI框架(JFrame、JPanel、Timer等) - Java 2D API - 图形渲染(Graphics2D、BufferedImage) - Java Sound API - 音效处理(Clip、AudioSystem) - Maven - 项目构建管理 - JDBC - 数据库连接
- 双缓冲渲染 - 消除画面闪烁 - 60 FPS游戏循环 - 流畅的游戏体验 - 资源缓存机制 - 图片和音效缓存 - 多线程音频 - 并发音效播放
应用场景:
- `GameManager` :游戏核心管理器 - `ImageLoader` :图片资源管理器 - `SoundManager` :声音管理器 - `DatabaseConfig` :数据库配置类 - `LayerManager` :层级渲染管理器
优势: 确保全局只有一个实例,便于资源管理和状态控制。
应用场景:
- `PlantManagerExt` 中的 plantPlant 方法根据 PlantType 枚举创建不同类型的植物对象 - 根据植物类型(向日葵、豌豆射手、坚果墙等)动态创建相应的植物实例 优势: 封装对象创建逻辑,便于扩展新的植物类型。
应用场景:
- `GameObject` 抽象类定义了 update() 和 render() 抽象方法 - 不同的植物类( `Sunflower` 、 `Peashooter` 等)实现不同的行为策略 优势: 每种植物都有自己独特的行为逻辑,易于维护和扩展。
应用场景:
- 游戏事件处理系统,如鼠标点击事件通过 `MouseListenerManagerExt` 分发给各个游戏实体 - 游戏状态变化时通知相关组件更新
应用场景:
- `GameObject` 基类定义了游戏对象的通用结构和行为模板 - 子类重写特定方法实现自己的逻辑,如 update() 、 render() 等
应用场景:
- `DrawManagerExt` 提供统一的绘制接口,封装了复杂的渲染逻辑 - 各种 ManagerExt 类为复杂的游戏逻辑提供简化的接口
应用场景:
- 游戏场景中的层级结构,通过 `LayerManager` 管理不同渲染层级的对象 - 统一处理单个对象和对象集合的渲染
应用场景:
- 游戏中的各种操作(种植植物、收集阳光等)被封装成具体的方法调用 - 便于实现撤销、重做等功能
应用场景:
- 游戏状态管理(游戏中、暂停、结束、胜利)通过 `Constants.java` 中定义的状态常量进行切换 - 不同状态下游戏有不同的行为表现
应用场景:
- `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目录下面。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。