简介
我将为大家分享我的推箱子3.0版本项目。为什么是3.0版本?因为这个版本意义重大,从2.0更新到3.0过程中发现,由于代码混乱,几乎无法往里面添加新的更为复杂的代码了,于是我为项目重整了代码结构,消除了因代码混乱带来的软件危机,从而顺利更新到3.0版本。我希望将我的经验分享给更多人,也希望可以与大家交流学习。
废话不多说,先给大家演示一下我的成果。由于图片大小有限制,就没有录更多功能了哈。(如果觉得一些行动很奇怪,那是快捷键的缘故):
录屏软件不是特别给力,人物移动本来是一格一格快速溜过去的,但这里貌似不是很顺畅…
.
文章目录
前言
这是我第一次写博客,如果内容中讲解不够详细的话,还请大家多多包涵。同时也欢迎大家与我进行学习交流哈。
这是我第一次尝试制作小游戏,最近打算给这个推箱子添加一个“自动求解”的功能并将其更新为3.0版本,但是我发现由于刚刚制作游戏的时候不懂设计模式,将所有代码挤到同一张页面里,挤了900多行,特别乱,加上自动求解功能的代码量并不简单,导致现在很难将这个新功能添加到源码中,要升级为3.0几乎是不可能的事了。于是我花了一个星期的时间将项目进行结构重整,才得以添加自动求解功能,将游戏升级为3.0。
希望我的这次经历能够提醒各位小白猿友们,软件危机真的很可怕,才900多行代码就花了我一个星期进行重整。随着项目越做越大开发成本也会越来越高,因此大家在做项目前一定要做好充足的准备,否则最后可能会因为超出维护成本的可承受范围而废弃项目。
我将在这里分享我的项目,我会尽可能的重头到尾详细讲解我的设计模式以及游戏实现方法,希望可以为新手猿友们的学习带来一些帮助。
好啦废话不多说,讲解之前先给大家交出我的项目哈。
Gitee链接:https://gitee.com/zhu-haocheng-2001/java-push-box/tree/master/
一、模块设计
模块的命名是通过地图符号的ASCII编号来命名的。
这些模块都是用电脑自带的图画工具和PS自制的,图片是网上找的(不过这只猫咪除外)。
二、功能设计
主要有两大功能:游戏,帮助。
三、GUI界面结构设计
主体界面总体采取BorderLayout布局方式,地图区域的地图面板采取GridLayout布局方式。
另外“帮助”功能中的三个按钮也分别触发三个JFrame显示内容,就像一开始的动图演示的那样。这里就不展示了吧。
除了原有的功能对应的JFrame之外,扩展功能也采用了这种方式,比如“自动求解”按钮也会触发一个JFrame,只不过扩展功能的全部类包括JFrame都放到一个包里。
因为我希望扩展功能模块尽可能与主体功能模块相互独立,这样一来不论要修改主体模块开始扩展模块都不容易影响到对方。这就是我对代码进行结构重整后的好处,可以很容易的将一个扩展功能嵌入项目中。
四、代码结构
这里就是我将项目从2.0版本升级到3.0版本之前做的代码结构优化工作了。
事实上这个结构并不复杂,希望大家能够理解这部分内容。大家也可以在我的开源项目里尝试用这种结构扩展自己想要的功能,比如自定义地图功能等等。
1.项目整体结构
下面是项目结构
其中功能扩展2中的solver.zwh这个包是别人写的推箱子求解代码,我直接搬过来用了。
推箱子暴力求解程序(SokobanSolver)
.
2.结构核心
我们在开发GUI界面的过程中,通常不同的类之间需要交互,比如点击菜单JMenu的“重新开始”按钮,需要调用地图JPanel的“选关”方法,这时菜单按钮触发的监听器就需要先获取地图JPanel的类对象,才能调用方法(不能通过new获取,必须获取被主窗口JFrame创建的那个地图JPanel才行)。如果需要交互的类越来越多,类对象的管理就会变得非常困难。优化这个项目结构之前我是通过构造方法来传递类对象的,这样一来各个模块之间的耦合度会大大增加,给项目维护造成很大的困难。
.
但是如果我们能够通过某个管理者将需要交互的类统一管理起来,等哪个类需要某个类的类对象时,直接问这个管理者要可以了。
.
正如图中所示,这个结构是围绕一个叫“Management”(我管它叫对象集中管理器)的类来展开的。虽然我称之为管理器,但实际上这是一个单例模式的对象存取器,专门将需要被其他类访问的对象存在这里。
简单来说这个结构就是用一个类作为管理员,专门把需要“被访问”的类对象管理起来,好让其他类能够方便的获取这些类对象。
另外如果想要扩展其他功能的话也是比较方便的,直接把做好的功能拼到管理器中,然后其他类想要用到这个扩展功能的话直接从管理器拿就好了。
.
“类对象集中管理器”代码如下(示例):
import foodBox.FoodBox;
import gui.FoodPanel;
import gui.Frame_about;
import gui.Frame_rule;
import gui.Frame_teach;
import gui.Listeners;
import gui.Menu_primary;
import gui.Panel_Map;
import myMovePath.Frame_MyPath;
import solver.Frame_Solve;
import solver.Frame_wait;
// 资源集中管理器
public class Management {
// 单例模式
private static Management management = new Management();
private Management() {
}
public static Management getManagement() {
return management;}
// ---------公开资源----------------------------------------------------------
// Menu_primary(游戏菜单栏):规定程序中只允许存在一个Panel_Menu对象
private Menu_primary MenuPrimary = null;
public void setMenu_primary(Menu_primary MenuPrimary) throws resourceException {
if(this.MenuPrimary == null) {
this.MenuPrimary = MenuPrimary;
} else {
throw new resourceException("程序规定只允许new一个Menu_primary公开资源,而当前资源已存在!");}
}
public Menu_primary getMenu_primary() {
return this.MenuPrimary;}
// Panel_Map(游戏地图面板):规定程序中只允许存在一个Panel_Map对象
private Panel_Map mapPanel = null;
public void setPanel_Map(Panel_Map mapPanel) throws resourceException {
if(this.mapPanel == null) {
this.mapPanel = mapPanel;
} else {
throw new resourceException("程序规定只允许new一个Panel_Map公开资源,而当前资源已存在!");}
}
public Panel_Map getPanel_Map() {
return this.mapPanel;}
}
由于这个管理器中的对象管理代码基本都是复制粘贴的,所以这里我删掉了大部分代码,只留下了Menu_primary类和Panel_Map类的对象管理。不过大家可以在import中可以看到其实原来是集中管理了很多对象资源的,这些对象资源通过经典的 get-set 存取模式进行管理。
至于里面的异常捕捉,那是我考虑了在团队开发情况下避免发生一些错误而写了一个简单的异常类,目的是将一些只允许创建一次的类强制性的防止再次创建(所以大家可以忽略这个异常捕捉)。
.
以下就是在这些类的构造方法,直接在创建时就将自己的对象存入管理器中
(以Menu_primary类为例):
// 主菜单
public class Menu_primary extends JMenuBar {
// 构造方法
public Menu_primary() {
// 异常捕捉:防止该类二次创建
try {
Management.getManagement() // 获取管理器
.setMenu_primary(this); // 将自身对象传入管理器中
} catch (resourceException e) {
e.printStackTrace();
}
}
......
}
.
而在需要获取这些类对象时,就可以直接从管理器获取(以按钮监听器为例):
// 按钮监听器
private class buttonListener implements ActionListener {
// 来自Menu_primary(主菜单)的组件
private Menu_primary menu_primary = Management.getManagement().getMenu_primary(); // 从管理器中获取Menu_primary对象
private JMenuItem reset = menu_primary.get_reset(); // 获取菜单选项(“重新开始”按钮)
public void actionPerformed(ActionEvent e) {
// 触发了“重新开始”按钮
if(e.getSource() == reset) {
Management.getManagement() // 获取资源管理器
.getPanel_Map() // 获取Panel_Map(地图面板)类对象
.AlterMap(Settings.getSettings().getLevel()); // 调用“更改地图”方法(从设置中获取当前关卡)
}
}
}
为了方便大家看,这里我同样删除了大部分代码,只留下一个例子。(代码里面的“设置”类和管理器一样,同样采取的是单例模式)
五、基本功能算法实现
因为我代码写的比较烂,这里就只给大家讲解主体部分的主要算法实现吧,那些扩展功能的我就不讲了吧。
1.本地文件介绍
关于地图文件 Map.dat:
地图文件中的每一关地图用两行存放,第一行是“M+地图名”,必须在地图名前加上符号M,否则程序无法识别,另外地图名不能重复,虽然游戏不会出错也不会提示,但是只能识别靠前面的那个地图。玩家可以在这里修改或添加地图,地图的符号规定如下:
墙外:“-”
墙: “+”
走道:“ ”(空格)
箱子:“#”
人物:“^”
终点:“.”
在终点位置的箱子:“@”
在终点位置的人物:“$”
关于储存文件 Notes.dat:
储存文件用于储存用户退出游戏时的一些设置。
第一行:模块风格名称
第二行:当前关卡数
第三行:是否自动提示下一关
第四行:是否开启人物移动的动画效果
关于求解文件 Route.dat:
当地图首次求解路线时,会把路线存到这里,下次求解时直接从这里获取。
用户可在Route.dat文件中修改和添加求解路线。
求解路线用大小字母 W、S、A、D 分别对应方向 上、下、左、右。
求解路线用英文符号“,”将每一次推动箱子的小路线隔开。
关于模块风格(游戏皮肤):
img文件夹里存放着不同风格的模块,玩家可以自制不同风格的模块添加到这个文件夹里。
由于该游戏窗口大小不能改变,因此模块的大小也必须严格相等。最佳大小为50*50像素。
另外模块采用地图符号的ASCII编号进行命名。
.
2.设置变量介绍
这是与设置相关的类。其中Settings.java就是存放着“是否自动提示下一关”等可变动的设置变量,以及对这些变量的一些操作方法,其中包括对上图中的 Notes.dat 文件中问价设置数据的读取与修改。其余两个直接看代码比讲的快一点。
.
Settings类代码:
import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// 一些游戏设置
public class Settings {
// 单例模式
private static Settings settings = new Settings();
private Settings() {
initialization();}
public static Settings getSettings() {
return settings;}
// 初始化
private void initialization() {
searchGameSettings(); // 搜索上一次退出游戏时的游戏(关卡和风格)、设置状态
this.maxSelect = searchMaxSelect(); // 搜索总关数
}
//----与游戏设置有关的数据---------------------------------------------------------
private String stylePath = "./making/img/"; // 地图风格模块根目录
private String style; // 地图风格
private String dataPath = "./making/data/"; // 数据文件夹路径
private String level; // 当前关卡数
private int maxSelect; // 总关数(总关数用数字记录,因此不会记录用字符串命名的隐藏关卡)
private boolean isRemind; // 是否开启自动提示下一关
private boolean isAnimation; // 是否开启人物移动动画效果
// 获取菜单图标目录
public String getMenuImgPath() {
return dataPath+"MenuImg/";}
// 获取地图风格模块根目录
public String getStylePath() {
return this.stylePath;}
// 设置地图风格
public void setStyle(String styleName) {
this.style = styleName;}
// 获取地图风格
public String getStyle() {
return this.style;}
// 获取地图模块路径
public String getImgPath() {
return stylePath+style+"/";}
// 获取数据文件夹路径
public String getDataPath() {
return this.dataPath;}
// 设置总关数
public void setMaxSelect(int maxSelect) {
this.maxSelect = maxSelect;}
// 获取总关数
public int getMaxSelect() {
return this.maxSelect;}
// 设置关卡数
public void setLevel(String number) {
this.level = number;}
// 获取关卡数
public String getLevel() {
return this.level;}
// 设置是否自动提示下一步
public void setIsRemind(boolean isRemind) {
this.isRemind = isRemind;}
// 获取是否自动提示下一步
public boolean getIsRemind() {
return this.isRemind;}
// 设置是否开启人物移动动画效果
public void setIsAnimation(boolean isAnimation) {
this