这是开始做的时候写的:
2020.5.18
By Mr CanLiu
1.Game.class:
游戏逻辑:角色的位置信息、地图信息等
2.GameJpanel.class
本身是一个大的容器
装入容器列表
//这里可以不使用小容器,但是为了试一试内部类这些用法,强行使用一下
8*8个小容器,每个容器加入图片就绘制出了整个游戏的界面
3.GameFrame.class
是游戏的窗体
可能实现的:
4.一个容器作为计时功能的显现
5.一个容器装载按钮实现选择关卡的功能
注:很多遇到的问题都在写的过程中写下来了,有点随心所欲。。。。。顺序大致是文件实现的顺序
负责游戏逻辑实现,包括地图(数组)和英雄移动
package allcode;
import java.io.BufferedReader;
import java.io.FileReader;
import javax.swing.JOptionPane;
public class Game {
//公共静态成员列表,据说相当于C语言的宏定义
public static final int wall = 0;//墙
public static final int box = 1;//箱子
public static final int aim = 2;//目的地
public static final int nothing = 3;//空地
public static final int hero = 4;//英雄
public static final int boxonaim = 5;//箱子在目的地//因为有不同的图片表示,所以单独的列出来
//游戏角色位置信息
private int x;
private int y;
/*这里我遭了一个意想不到的
在Java中不会定义数组了,写成了private int map[]
在IDE这里不会报错,运行起来就说是没有初始化这个东西,他也应该是一个对象才对
*/
//游戏生成的初始地图
//0:墙体;1:箱子;2:目的地;3:空地;4:英雄;5:箱子在目的地上;
private int map[][]=new int[8][8];
//游戏的状态地图,表示了展现给用户的样子
//0:墙体;1:箱子;2:目的地;3:空地;4:英雄;5:箱子在目的地上;
public int maptoman[][]=new int[8][8];
//
public int flag;
//作为最大的关卡数量
public final int max=2;
//构造函数,从文件获取当前关卡的初始地图信息
//现在先设置为固定地图;
public Game()
{
flag=1;
get_map();
}
/*public Game(int level) {
this.flag=level;
get_map(flag);
}*/
//重新开始游戏,或者重新设置游戏关卡的函数
public void regame(int level) {
if(level>max) {
JOptionPane.showMessageDialog(null, "暂无该关卡!", "警告",JOptionPane.WARNING_MESSAGE);
this.flag=max;
get_map();
}
else {
this.flag=level;
get_map();
}
}
public void get_map() {
int i=0;
try {
BufferedReader reader=new BufferedReader(new FileReader("res/map/map"+flag+".txt"));
String line=reader.readLine();
while(line!=null) {
String str[]=line.split("");
for(int j=0;j<8;j++) {
map[i][j]=(int)str[j].charAt(0)-48;
maptoman[i][j]=map[i][j];
if(maptoman[i][j]==4) {
//这个位置是英雄
this.x=i;
this.y=j;
}
}
line=reader.readLine();
i++;
}
reader.close();
}catch(Exception e) {
e.printStackTrace();
}
}
public void print()
{
for(int i=0;i<8;i++) {
for(int j=0;j<8;j++) {
System.out.format("%d ",map[i][j]);
}
System.out.println();
}
}
//开始书写逻辑结构的函数
//推箱子游戏就是人物的上下左右移动
//一个角色的移动要考虑两个点:目的点是什么情况,当前的点移开后是什么情况
//向左
//注!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//意!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//因为一开始逻辑反了,所以在ToUp函数中注释是向左。。。。。
//但是道理是相同的,我就不改了
public void ToUp()
{
//首先是移动的位置横轴要大于1,才有移动的价值
if(x>=1)
{
//左边当前是空地或者一个目的地,把英雄左移,当前的位置恢复成地图初始化的样子(有问题,地图初始化的样子可能是箱子)
//所以应该是判定初始化的时候是空地还是目的地
if(maptoman[x-1][y]==nothing || maptoman[x-1][y]==aim) {
//处理用户看到的地图
maptoman[x-1][y]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理英雄坐标
x-=1;
}
//左边是箱子或者在目的地的箱子
else if(maptoman[x-1][y]==box || maptoman[x-1][y]==boxonaim) {
if(x>=2) {//判断箱子是不是可以推动,其实是保证数组不超界
if(maptoman[x-2][y]==nothing) {//箱子的左边是空地
//箱子移过去
maptoman[x-2][y]=box;
//英雄移过去
maptoman[x-1][y]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
x-=1;
}
else if(maptoman[x-2][y]==aim) {//箱子左边是目的地
//箱子移过去
maptoman[x-2][y]=boxonaim;
//英雄移过去
maptoman[x-1][y]=hero;
//当前位置恢复
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
x-=1;
}
//箱子左边是:墙、箱子或者其他的都不做变化
}
}
//左边是:墙,不可能是英雄,不做处理
}
}
//其实前后左右的逻辑都一样,应该可以封装成一个函数的就是对最多:4个位置的变化
//向右
public void ToDown()
{
//首先是移动的位置横轴要小于等于6,才有移动的价值
if(x<=6)
{
if(maptoman[x+1][y]==nothing || maptoman[x+1][y]==aim) {
//处理用户看到的地图
maptoman[x+1][y]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理英雄坐标
x+=1;
}
else if(maptoman[x+1][y]==box || maptoman[x+1][y]==boxonaim) {
if(x<=5) {//判断箱子是不是可以推动,其实是保证数组不超界
if(maptoman[x+2][y]==nothing) {//箱子的右边是空地
//箱子移过去
maptoman[x+2][y]=box;
//英雄移过去
maptoman[x+1][y]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
x+=1;
}
else if(maptoman[x+2][y]==aim) {//箱子右边是目的地
//箱子移过去
maptoman[x+2][y]=boxonaim;
//英雄移过去
maptoman[x+1][y]=hero;
//当前位置恢复
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
x+=1;
}
//箱子右边是:墙、箱子或者其他的都不做变化
}
}
//右边是:墙,不可能是英雄,不做处理
}
}
//向上
public void ToLeft()
{
//首先是移动的位置纵轴要大于1,才有移动的价值
if(y>=1)
{
if(maptoman[x][y-1]==nothing || maptoman[x][y-1]==aim) {
//处理用户看到的地图
maptoman[x][y-1]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理英雄坐标
y-=1;
}
//上面是箱子或者在目的地的箱子
else if(maptoman[x][y-1]==box || maptoman[x][y-1]==boxonaim) {
if(y>=2) {//判断箱子是不是可以推动,其实是保证数组不超界
if(maptoman[x][y-2]==nothing) {//箱子的上边是空地
//箱子移过去
maptoman[x][y-2]=box;
//英雄移过去
maptoman[x][y-1]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
y-=1;
}
else if(maptoman[x][y-2]==aim) {//箱子上边是目的地
//箱子移过去
maptoman[x][y-2]=boxonaim;
//英雄移过去
maptoman[x][y-1]=hero;
//当前位置恢复
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
y-=1;
}
}
}
}
}
//向下
public void ToRight()
{
//首先是移动的位置纵轴要大于1,才有移动的价值
if(y<=6)
{
if(maptoman[x][y+1]==nothing || maptoman[x][y+1]==aim) {
//处理用户看到的地图
maptoman[x][y+1]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理英雄坐标
y+=1;
}
//下面是箱子或者在目的地的箱子
else if(maptoman[x][y+1]==box || maptoman[x][y+1]==boxonaim) {
if(y<=5) {//判断箱子是不是可以推动,其实是保证数组不超界
if(maptoman[x][y+2]==nothing) {//是空地
//箱子移过去
maptoman[x][y+2]=box;
//英雄移过去
maptoman[x][y+1]=hero;
//判定初始化的时候是不是一个目的地,不然就是变成空地
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
y+=1;
}
else if(maptoman[x][y+2]==aim) {//是目的地
//箱子移过去
maptoman[x][y+2]=boxonaim;
//英雄移过去
maptoman[x][y+1]=hero;
//当前位置恢复
maptoman[x][y]= (map[x][y]==aim) ? aim : nothing;
//处理坐标
y+=1;
}
}
}
}
}
//判断游戏结束的条件是用户可见的范围没有目的地
//一开始是犯了一个错误的,判断是否结束不应该用用户所见中没有aim来判定结束
//因为hero走到aim上,也会覆盖一个,所以要结合原始地图
public boolean IsEnd()
{
for(int i=0;i<8;i++) {
for(int j=0;j<8;j++) {
if(map[i][j]==aim) {//原始地图是一个目标,判定现在是不是箱子在目标上
if(maptoman[i][j]!=boxonaim) {//不是箱子在目标上
return false;//游戏没有结束
}
}
}
}
return true;//游戏结束
}
}
推箱子的窗口
//有一个大坑,以后记住写程序从底层开始,先把底层解决了,再在底层上添加组件
//如果先就把中间容器,吧组件什么的考虑好,反而非常的复杂
//个人把类分为三个要点:
//1.成员变量:包含不同的功能需求的成员变量,所以需要细化
//2.构造函数:特别在GUI的设计里面,构造函数对布局等起着举足轻重的作用
//3.成员方法,对不同的部分可能有不同的需求,同成员变量,感觉在这一部分中还需要再细化
//2020.5.20 By Mr CanLiu
package allcode;
import javax.swing.JFrame;
@SuppressWarnings("serial")
public class GameFrame extends JFrame
{
//成员变量列表
public int[][] maptoman =new int[8][8];//改变用户视图的参数
public GameJpanel gamejpanel = null;
public TimeJpanel timejpanel = null;
public SettingJpanel settingjpanel = null;
//构造函数
public GameFrame() {}
public GameFrame(int maptoman[][])
{
this.maptoman=maptoman;
setLayout(null);//设置布局管理为null
setSize(600,700); //设置窗体大小
setLocation(500, 20); //设置窗体显示位置
setTitle("LiuCan的推箱子"); //设置窗体标题栏
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //设置窗体默认关闭事件
//构造时间容器
timejpanel = new TimeJpanel();
add(timejpanel);
//设置界面容器
settingjpanel = new SettingJpanel();
add(settingjpanel);
//构造游戏界面容器
gamejpanel = new GameJpanel(maptoman);
add(gamejpanel);
}
public void framerefresh(int maptoman[][])
{
this.maptoman=maptoman;
//刷新时处理游戏界面容器部分的刷新
gamejpanel.jpanelrefresh(maptoman);
}
}
作为游戏界面中最为重要的一部分,容器中承载了64个小容器,即:8*8的游戏界面
//遭受了一个天大的坑啊!!!!!!,浪费了十个小时差不多,居然是函数paintComponent会自动调用
//其中的Graphics g也是不用实例化的
//实例化反而导致对象为空,运行报错
package allcode;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class GameJpanel extends JPanel
{
//内部类
//用于绘制的小容器内部类
class PictureJpanel extends JPanel
{
//成员变量应该包含一个地址
String way="";
//构造方法
public PictureJpanel() {}
public PictureJpanel(String str) {
this.way=str;
setSize(60,60); //设置窗体大小
//setBackground(Color.RED);
}
protected void paintComponent(Graphics g)
{
ImageIcon icon = new ImageIcon(this.way);
Image img=icon.getImage();
g.drawImage(img, 0, 0, this.getWidth(), this.getHeight(), this);
}
}
//大容器的成员变量
int[][] maptoman=new int[8][8];
//地址列表写在这里
private final String[] Strs = {"res/jpg/wall.jpg","res/jpg/box.jpg","res/jpg/aim.jpg","res/jpg/nothing.jpg","res/jpg/hero.jpg","res/jpg/boxonaim.jpg"};
//创建小容器列表
public PictureJpanel[][] jpanels=new PictureJpanel[8][8];
//构造函数
public GameJpanel(int[][] maptoman)
{
setSize(480,480); //设置窗体大小
setLocation(55, 150); //设置窗体显示位置
setLayout(null);//设置布局管理为null
setBackground(Color.black);
for(int i=0;i<8;i++) {
for(int j=0;j<8;j++) {
jpanels[i][j]=new PictureJpanel(Strs[(maptoman[i][j])]);
jpanels[i][j].setLocation(j*60, i*60); //设置显示位置
add(jpanels[i][j]);
}
}
}
//界面更新
public void jpanelrefresh(int maptoman[][])
{
this.maptoman=maptoman;
for(int i=0;i<8;i++) {
for(int j=0;j<8;j++) {
jpanels[i][j].way = Strs[(maptoman[i][j])];
jpanels[i][j].repaint();
}
}
}
}
实现计时功能
//窗口中的时间显示容器
package allcode;
import java.awt.Color;
import javax.swing.JLabel;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class TimeJpanel extends JPanel
{
//成员
int time=0;
String timestr="";
public JLabel LabelName = null;
public JLabel TextTime = null;
Thread t = new TimeThread(); //创建线程
//一个实现计时的内部类
public class TimeThread extends Thread
{
public void run() {
while(true) {
try{
Thread.sleep(1000);
TimeJpanel.this.time+=1;
TimeJpanel.this.get_timestr();
TimeJpanel.this.TextTime.setText(TimeJpanel.this.timestr);
}catch(Exception e){}
}
}
}
public TimeJpanel() {
setSize(200,100); //设置窗体大小
setLocation(55, 25); //设置窗体显示位置
setBackground(Color.red);
setLayout(null);//设置布局管理为null
LabelName = new JLabel("时间:");
add(LabelName);
LabelName.setSize(60,40);//设置大小
LabelName.setLocation(15,30);//定位
TextTime = new JLabel(timestr);
add(TextTime);
TextTime.setSize(100,40);//设置大小
TextTime.setLocation(90,30);//定位
//线程开始
t.start();
}
public void get_timestr()
{
this.timestr=(time/60)+"分"+(time%60)+"秒";
}
}
//设置部分:
//such as:选择关卡、重新开始
package allcode;
import java.awt.Color;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
@SuppressWarnings("serial")
public class SettingJpanel extends JPanel
{
public JLabel LabelName = null;//设置标签
public JButton button_regame = null;
//构造函数
public SettingJpanel()
{
setSize(200,100); //设置大小
setLocation(300, 25); //设置显示位置
setBackground(Color.white);
setLayout(null);//设置布局管理为null
//添加设置标签
LabelName = new JLabel("设置:");
add(LabelName);
LabelName.setSize(60,40);//设置大小
LabelName.setLocation(0,0);//定位
//添加重新开始按钮
button_regame = new JButton("重新开始");
add(button_regame);
button_regame.setSize(100,30);
button_regame.setLocation(80,5);
}
}
为什么放在最后。。。。因为笔者比较菜,整个的功能结构很乱,导致,这夹杂了很多重要的功能实现
package allcode;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JOptionPane;
public class GameMain {
Game game = null;
GameFrame gameframe = null;
Thread monitor = new monitorThread();
@SuppressWarnings("unused")
public static void main(String[] args) {
GameMain gamemain = new GameMain();
}
//设置一个线程类监听游戏是否结束
public class monitorThread extends Thread{
public void run(){
while(true) {
try {
if(game.IsEnd()) {
//先把时间停了,原来资源好像没有加锁,不需要把时间线程停止,只需要直接把时间置0就可以了
//GameMain.this.gameframe.timejpanel.t.interrupt();
int s = JOptionPane.showConfirmDialog(null,"耗时"+GameMain.this.gameframe.timejpanel.timestr+"是否进入下一关?","真棒",JOptionPane.YES_NO_OPTION);
if(s==0) {//下一关
GameMain.this.game.regame(GameMain.this.game.flag+1);
}else {
GameMain.this.game.regame(GameMain.this.game.flag);
}
GameMain.this.gameframe.framerefresh(GameMain.this.game.maptoman);
//处理时间模块
GameMain.this.gameframe.timejpanel.time=0;
//GameMain.this.gameframe.timejpanel.t.start();
Thread.sleep(2000);//适当让线程休眠,避免不停的检测
}
}catch(Exception e){}//调用interrupt()的时候做的事
}
}
}
//构造函数
public GameMain(){//无参构造
game = new Game();
gameframe = new GameFrame(game.maptoman);
gameframe.setVisible(true); //显示窗体
beginkeylisten();//开启键盘和按钮监听
monitor.start();//开启游戏结束检测的线程
//System.out.println("开启游戏结束检测");
//此时的焦点因为是设置按钮监听在后面,所以是在设置那里,需要交还给主界面
gameframe.settingjpanel.button_regame.setFocusable(false);
//单独设置主界面申请焦点好像没用,发现要先把按钮设置为false才可以
//这一条好像也可以通过设置主界面为true实现
gameframe.requestFocus();
}
//重新开始游戏或者选择游戏关卡的刷新函数
public void regamemain(int level) {
game.regame(level);
gameframe.framerefresh(game.maptoman);
}
//开启各种监听器:键盘、按钮
//有一个巨坑:关于焦点问题,在添加按钮之后,焦点到了按钮,点击键盘失效,需要将焦点交还给主界面
public void beginkeylisten() {
//键盘监听
gameframe.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) { // 按键被按下时被触发
if(e.getKeyCode()==KeyEvent.VK_UP) {
//System.out.println("点击向上!");
game.ToUp();
gameframe.framerefresh(game.maptoman);
}
else if(e.getKeyCode()==KeyEvent.VK_LEFT) {
//System.out.println("点击向左!");
game.ToLeft();
gameframe.framerefresh(game.maptoman);
}
else if(e.getKeyCode()==KeyEvent.VK_DOWN) {
//System.out.println("点击向下!");
game.ToDown();
gameframe.framerefresh(game.maptoman);
}
else if(e.getKeyCode()==KeyEvent.VK_RIGHT) {
//System.out.println("点击向右!");
game.ToRight();
gameframe.framerefresh(game.maptoman);
}
}
public void keyTyped(KeyEvent e) {}// 发生击键事件时被触发,就是点击字母按键是在这里
public void keyReleased(KeyEvent e) {}
});
//System.out.println("开启键盘监听");
//按钮监听
gameframe.settingjpanel.button_regame.addActionListener(
new ActionListener(){
public void actionPerformed(ActionEvent event) {
//重新开始游戏
regamemain(game.flag);
//重新开始时间计算 //处理时间模块
GameMain.this.gameframe.timejpanel.time=0;
//把焦点交还给主界面
gameframe.requestFocus();//
}
}
);
}
}
资源文件放置的问题:
比如建立的project叫:Sokoban
那么GameMain.java的路径是这样的:巴拉巴拉(project放置的地方)/Sokoban/src/allcode/GameMain.java
比如地图1 名叫:map1.txt
路径:巴拉巴拉(project放置的地方)/Sokoban/res/map/map1.txt
在项目内访问他只需要使用路径:res/map/map1.txt
1.txt
00000000
03333330
03133430
03331330
03330330
03323330
03333230
00000000
2.txt
00000000
00020000
00030000
00013120
02314000
00001000
00002000
00000000
用PPT做的。。。。。。很简陋:不知道怎么上传,,,反正就是在PPT中形状,搞个正方形改改就行了。注意:字体不要改动,会导致图片大小微弱改变,在游戏界面中就会出现残缺图片。
一个小总结:虽然写的不是很如意,但是还是有很多的收获,包括界面的优化和功能扩展就做到这里了
后面就是大同小异,
在制作过程中发现对于参数的控制还有哪一部分类实现什么都不够清楚,,,,,
比如,如果我的更新是在frame类中实现的,每一次开局就是传入一个game类给窗口,感觉很好处理很多
还有,对于地图的判定方面,初始图只需要保留的信息,和用户视图(二维数组)混淆不清,,,,,有待优化
界面的刷新,这个游戏一次性涉及的变化其实只有三个格子,传入了整个数组去刷新界面,可以想想有没有更加优化的办法
线程,监听的设置等等。。。