hello大家好,用java实现小游戏真的很锻炼编程技术,而且很有成就感。比起做增删改查的管理系统来说,简直是不同的两个阶层的程序员。
今天我就教大家用JDK17原生库来实现一个完整的 植物大战僵尸 ,初始编程的你,只要用心就能学会。会大大加深你对面向对象的理解!
源码为自己开发的源码, 商用必究!!!
从这个游戏中你可以学到:
-
游戏二维地图是如何实现的。
-
游戏关卡是如何在数据库中配置的(以后新增关卡只需在数据库中插入数据)。
-
植物,僵尸,阳光等游戏实体的管理。
-
2D游戏的分层架构: 画图+逻辑管理器+实体 。
-
游戏内的物理系统:碰撞检测,边界检测。
视频演示
完整游戏源码,我已经整理清楚,移步:
图片演示
技术栈描述
项目框架
- 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() 抽象方法- 不同的植物类(
Sunflower、Peashooter等)实现不同的行为策略
优势: 每种植物都有自己独特的行为逻辑,易于维护和扩展。
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目录下面。