奔跑吧小恐龙(Java)

news2024/11/30 8:48:11

前言 

        Google浏览器内含了一个小彩蛋当没有网络连接时,浏览器会弹出一个小恐龙,当我们点击它时游戏就会开始进行,大家也可以玩一下试试,网址:恐龙快跑 - 霸王龙游戏. (ur1.fun)

        今天我们也可以用Java来简单的实现一下这个小游戏。

一  系统功能结构图

二  系统业务流程图

 

三  程序目录结构



一  游戏模型设计

        游戏模型主要指游戏中出现的刚体。刚体是指不会因为受力而变形的物体。游戏中的刚体包括奔跑的恐龙,石头和仙人掌。背景图片虽然会滚动,但背景图片不参与任何碰撞检测,所以不属于游戏模型。


1.恐龙类 

        奔跑的小恐龙是游戏的主角,也是玩家控制的角色。项目中的model.Dinosaur就是恐龙类。

1-1 定义

        Dinosaur类的成员属性绝大多数都是私有属性,只有少数公有属性用于游戏面板绘图使用,如主图片和横纵坐标。Dinosaur类的私有属性包含3张来回切换的跑步图片,最大起跳高度,落地时的坐标以及各种状态的布尔值和计时器。

Dinosaur类的定义:

public class Dinosaur {
	public BufferedImage image;                    //主图片
	private BufferedImage image1,image2,image3;    //跑步图片
	public int x,y;                                //坐标
	private int jumpValue = 0;                     //跳跃的增变量
	private boolean jumpState = false;             //跳跃的状态
	private int stepTimer = 0;                     //踏步计时器
	private final int JUMP_HIGHT = 100;            //最大跳起高度
	private final int LOWEST_Y = 120;              //落地最低坐标
	private final int FREASH = FreshThread.FREASH; //刷新时间
}

 在构造方法中我们要设置恐龙的初始状态,将恐龙横坐标固定在50像素,纵坐标采用落地时的坐标120像素,构造方法的代码如下:

	public Dinosaur() {
		x=50;//横坐标默认是50;
		y=LOWEST_Y;//纵坐标默认起始值是120
		
		image1=ImageIO.read(new File("image/恐龙1.png"));
		image2=ImageIO.read(new File("image/恐龙2.png"));
		image3=ImageIO.read(new File("image/恐龙3.png"));
	}

1-2.踏步

        游戏中恐龙的横坐标不变但是,背景的运动会使恐龙呈现一中运动的状态,为了使这种假象的运动状态逼真,我们就需要做出恐龙奔跑的动作。step()的方法就是踏步,我们只需要将图片来回切换就可以做到这种效果。

	public void step() {
		// 每过250毫秒,更换一张图片。因为共有3图片,所以除以3取余,轮流展示这三张
		int tmp = stepTimer/250%3;
		switch(tmp) {
		case 1:
			image = image1;
			break;
		case 2:
			image = image2;
			break;
		default:
			image = image3;
		}
		stepTimer += FREASH;//计时器递增
	}

1-3.跳跃

        跳跃是小恐龙躲避障碍的动作,也是我们唯一可以控制恐龙的 行为。当程序调用jump()方法时,该方法会更改恐龙的跳跃属性,也就是让恐龙处于跳跃状态,跳跃的同时也会触发音效。

	/**
	 * 跳跃
	 */
    public void jump() {
        if (!jumpState) {// 如果没处于跳跃状态
            Sound.jump();// 播放跳跃音效
        }
        jumpState = true;// 处于跳跃状态
    }

 1-4.移动

        move方法是恐龙移动方法,该方法将恐龙的所有动作效果封装起来,然后交由游戏面板调用。每一帧画面都会执行一次恐龙的move方法。move 方法不断地调用step踏步方法,因为stepTimer踏步计时器会有效控制图片的切换频率,所以不用担心频繁调用的问题。
move()方法会判断恐龙是否处于跳跃状态,如果处于跳跃状态,并且恐龙站在地上,就让jumpValue跳跃增变量值变为-4,让恐龙的纵坐标不断与jumpValue 相加,纵坐标值越来越小,这样恐龙的图片位置就会越来越高。当恐龙纵坐标达到跳跃最大高度时,再让jumpValue的值变为4,纵坐标值越来越大,恐龙的图片就会越来越低。当恐龙再次回到地面上时,取消跳跃状态。至此,恐龙就完成了一次跳跃动作。

	/*
	 * 移动的方法
	 */
	public void move() {
		step();//不断踏步
		if(jumpState) {//如果正在跳跃
			if(y>=LOWEST_Y) {//如果纵坐标大于等于最低点
				jumpValue = -4;//增变量为负值
				/*
				 * 这是因为我们窗体的显示是按照像素的大小和位置决定的
				 * ,从左上角开始横纵坐标均为0,然后开始增长,向下y增长,向右x增长
				 */	
			}
			if(y<=LOWEST_Y-JUMP_HIGHT) {//如果跳过最高点
				jumpValue = 4;//增变量为正值
			}
			y+=jumpValue;//纵坐标发生变化
			if(y>=LOWEST_Y) {//如果再次落地
				jumpState = false;// 停止跳跃
			}
			
		}
	}

1-5.边界对象

        因为我们这里设计的有跳跃的状态,那么就要设置判断是否发生碰撞,我们这里将物体具体化为矩形类型方便处理,和判断是否发生碰撞,将恐龙的头和脚抽象具体为矩形。

    /**
     * 足部边界区域
     * 
     * @return
     */
    public Rectangle getFootBounds() {
        return new Rectangle(x + 30, y + 59, 29, 18);
    }

    /**
     * 头部边界区域
     * 
     * @return
     */
    public Rectangle getHeadBounds() {
        return new Rectangle(x + 66, y + 25, 32, 22);
    

 2 .障碍类

游戏中设置了两种障碍:

        一种是很矮的石头:

        一种是很高的仙人掌:

  不管是石头还是仙人掌,每一个障碍的特点都大致相同:都会随着背景一起移动,都是可能碰撞的区域。

2-1.定义

         Obstacle类就是障碍类,该类提供了3个共有属性,分别是横坐标,纵坐标和图片对象,其他属性均为私有属性。因为障碍都会随着背景一起移动,所以障碍的移动速度采用背景图片的速度。

public class Obstacle {
    public int x, y;// 横纵坐标
    public BufferedImage image;
    private BufferedImage stone;// 石头图片
    private BufferedImage cacti;// 仙人掌图片
    private int speed;// 移动速度
}

         使用构造方法随机生成仙人掌或石头,采用随机数的方法生成0和1,0表示采用仙人掌的图片,1表示采用石头的图片。

    public Obstacle() {
        try {
            stone = ImageIO.read(new File("image/石头.png"));
            cacti = ImageIO.read(new File("image/仙人掌.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        Random r = new Random();// 创建随机对象
        if (r.nextInt(2) == 0) {// 从0和1中取一值,若为0
            image = cacti;// 采用仙人掌图片
        } else {
            image = stone;// 采用石头图片
        }
        x = 800;// 初始横坐标
        y = 200 - image.getHeight();// 纵坐标
        speed = BackgroundImage.SPEED;// 移动速度与背景同步
    }

2-2.移动

        由于我们的画面中恐龙是在原地不同的,而背景画面是向左走的,因此我们的障碍物也要向左移动,像素的位置向左移动也就是行坐标的像素减少。同样我们也设置障碍物的移动方法为move();

    /**
     * 移动
     */
    public void move() {
        x -= speed;// 横坐标递减
    }

2-3.消除

        当障碍移除游戏画面以后,就不会在的游戏的数据产生影响。为了减除程序计算的压力,我们要将移除游戏画面的障碍消除。isLive()方法用于获取障碍的有效状态,该方法会根据障碍的位置判断返回true和flase,当障碍还在窗体内返回true表示还在窗体内,flase表示没在窗体内,将障碍对象从碰撞集合中删除。

    /**
     * 是否存活
     * 
     * @return
     */
    public boolean isLive() {
        // 如果移出了游戏界面
        if (x <= -image.getWidth()) {
            return false;// 消亡
        }
        return true;// 存活
    }

2-4.边界对象

        为将障碍具体化设置为矩形,方便后面参与碰撞检测,不管是仙人掌还是石头,都要通过getBounds()方法返回边界对象

    public Rectangle getBounds() {
        if (image == cacti) {// 如果使用仙人掌图片
            // 返回仙人掌的边界
            return new Rectangle(x + 7, y, 15, image.getHeight());
        }
        // 返回石头的边界
        return new Rectangle(x + 5, y + 4, 23, 21);
    }


二  音效模块设计

        当然一款游戏离不开音乐的支持。因为音频处理功能是JDK早期版本就有,并且一直没有更新,所以目前JDK支持的音乐格式很少。JDK支持的音乐格式可以参看:在线文档-jdk-zh (oschina.net)

        我们这里使用JDK支持的WAVE格式

1.音频播放器

        MusicPlayer类是音频播放器类,该类实现了Runnable接口,并在线程中定义了一个线程对象,该线程用于启动混音器数据行的业务。

public class MusicPlayer implements Runnable{
	File soundFile;               //音乐文件
	Thread thread;                //父线程
	boolean circulate;            //是否循环播放
}

        它的构造方法有两个参数。filepath表示音乐文件的完整文件名,circulate表示是否重复播放,构造方法抛出找不到文件异常,外部类创建MusicPlayer类对象时,必须要捕捉此异常。

    /**
     * 构造方法,默认不循环播放
     * 
     * @param filepath
     *            音乐文件完整名称
     * @throws FileNotFoundException
     */
    public MusicPlayer(String filepath) throws FileNotFoundException {
        this(filepath, false);
    }
    /**
     * 构造方法
     * 
     * @param filepath
     *            音乐文件完整名称
     * @param circulate
     *            是否循环播放
     * @throws FileNotFoundException
     */
    public MusicPlayer(String filepath, boolean circulate) throws FileNotFoundException {
        this.circulate = circulate;
        soundFile = new File(filepath);
        if (!soundFile.exists()) {// 如果文件不存在
            throw new FileNotFoundException(filepath + "未找到");
        }
    }

        既然此类实现了Runnable接口,必须实现run()方法。在run()方法中声明了一个128kb的缓冲字节数组,程序以不断循环的方式将音乐以音频输入流格式读入缓冲区,在把缓冲区的数据写入混音器数据行中,这样就可以不断向外部音频设备发送音频信号,实现播放音乐的效果。

    /*
     *重写线程执行方法
     */
	@Override
    public void run() {
        byte[] auBuffer = new byte[1024 * 128];// 创建128k缓冲区
        do {
            AudioInputStream audioInputStream = null; // 创建音频输入流对象
            SourceDataLine auline = null; // 混频器源数据行
            try {
                // 从音乐文件中获取音频输入流
                audioInputStream = AudioSystem.getAudioInputStream(soundFile);
                AudioFormat format = audioInputStream.getFormat(); // 获取音频格式
                // 按照源数据行类型和指定音频格式创建数据行对象
                DataLine.Info info = new DataLine.Info(SourceDataLine.class,
                        format);
                // 利用音频系统类获得与指定 Line.Info 对象中的描述匹配的行,并转换为源数据行对象
                auline = (SourceDataLine) AudioSystem.getLine(info);
                auline.open(format);// 按照指定格式打开源数据行
                auline.start();// 源数据行开启读写活动
                int byteCount = 0;// 记录音频输入流读出的字节数
                while (byteCount != -1) {// 如果音频输入流中读取的字节数不为-1
                    // 从音频数据流中读出128K的数据
                    byteCount = audioInputStream.read(auBuffer, 0,
                            auBuffer.length);
                    if (byteCount >= 0) {// 如果读出有效数据
                        auline.write(auBuffer, 0, byteCount);// 将有效数据写入数据行中
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (UnsupportedAudioFileException e) {
                e.printStackTrace();
            } catch (LineUnavailableException e) {
                e.printStackTrace();
            } finally {
                auline.drain();// 清空数据行
                auline.close();// 关闭数据行
            }
        } while (circulate);// 根据循环标志判断是否循环播放
    }

        播放音乐和停止音乐的方法如下:使用start方法启动线程来播放音乐,使用stop方法来强制关闭线程,实现关闭音乐的效果。

    /**
     * 播放
     */
    public void play() {
        thread = new Thread(this);// 创建线程对象
        thread.start();// 开启线程
    }

    /**
     * 停止播放
     */
    public void stop() {
        thread.stop();// 强制关闭线程
    }
    /*

2.音效工具类

        我们知道游戏设计有跳的动作以及碰撞的效果,这些都要添加一些音效才能够使游戏的效果更加好。所以我们可以为每一个动作设计一个单独的线程,当要执行该动作时启动一次线程之后再关闭即可。

package service;


import java.io.FileNotFoundException;
/**
 * 音效类
 * @author JWF
 */
public class Sound {
    static final String DIR = "music/";// 音乐文件夹
    static final String BACKGROUD = "background.wav";// 背景音乐
    static final String JUMP = "jump.wav";// 跳跃音效
    static final String HIT = "hit.wav";// 撞击音效

    /**
     * 播放跳跃音效
     */
    static public void jump() {
        play(DIR + JUMP, false);// 播放一次跳跃音效
    }

    /**
     * 播放撞击音效
     */
    static public void hit() {
        play(DIR + HIT, false);// 播放一次撞击音效
    }

    /**
     * 播放背景音乐
     */
    static public void backgroud() {
        play(DIR + BACKGROUD, true);// 循环播放背景音乐
    }

    /**
     * 播放
     * 
     * @param file
     *            音乐文件完整名称
     * @param circulate
     *            是否循环播放
     */
    private static void play(String file, boolean circulate) {
        try {
            // 创建播放器
            MusicPlayer player = new MusicPlayer(file, circulate);
            player.play();// 播放器开始播放
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}



三  计分器模块设计

        这里计分器使用一个静态的整型数组记录有史以来前三名的成绩,当玩家打破记录时计分器会更新分数,此类为ScoreRecorder类定义如下:

public class ScoreRecorder {
    private static final String SCOREFILE = "data/soure";// 得分记录文件
    private static int scores[] = new int[3];// 当前得分最高前三名
}

读取原始分数数据初始化 

        在使用ScoreRecorder类之前,需要先调用该类的静态方法init。init方法可以让计分器从成绩记录文件中读取到历史前3名数据。成绩记录文件记录了3个历史成绩,这3个成绩升序排列并用“,”分隔。如果成绩记录文件不存在,或者文件中没有记录有效成绩,则会取消读取操作,并让历史前3名成绩均为0。init0方法的具体代码如下:

    /**
     * 分数初始化
     */
    public static void init() {
        File f = new File(SCOREFILE);// 创建记录文件
        if (!f.exists()) {// 如果文件不存在
            try {
                f.createNewFile();// 创建新文件
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;// 停止方法
        }
        FileInputStream fis = null;
        InputStreamReader isr = null;
        BufferedReader br = null;
        try {
            fis = new FileInputStream(f);// 文件字节输入流
            isr = new InputStreamReader(fis);// 字节流转字符流
            br = new BufferedReader(isr);// 缓冲字符流
            String value = br.readLine();// 读取一行
            if (!(value == null || "".equals(value))) {// 如果不为空值
                String vs[] = value.split(",");// 分割字符串
                if (vs.length < 3) {// 如果分割结果小于3
                    Arrays.fill(scores, 0);// 数组填充0
                } else {
                    for (int i = 0; i < 3; i++) {
                        // 将记录文件中的值赋给当前分数数组
                        scores[i] = Integer.parseInt(vs[i]);
                    }
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {// 依次关闭流
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                isr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

写入游戏数据并保存 

        当游戏停止时,要记录最新的前三名的成绩。saveSore()方法可以将当前成绩数组中的值写入成绩记录文件中。


    /**
     * 保存分数
     */
    public static void saveScore() {
        // 拼接得分数组
        String value = scores[0] + "," + scores[1] + "," + scores[2];
        FileOutputStream fos = null;
        OutputStreamWriter osw = null;
        BufferedWriter bw = null;
        try {
            fos = new FileOutputStream(SCOREFILE);// 文件字节输出流
            osw = new OutputStreamWriter(fos);// 字节流转字符流
            bw = new BufferedWriter(osw);// 缓冲字符流
            bw.write(value);// 写入拼接后的字符串
            bw.flush();// 字符流刷新
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {// 依次关闭流
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                osw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

        addNewScore()方法用于向成绩数组中添加新成绩,该方法的score参数就是要添加的新成绩数值。在addNewScoreO方法中,如果添加的新成绩小于历史前3名,则会舍弃;如果新成绩大于历史前3名中的某个成绩,则会重新排列前3名成绩。这个逻辑是通过Arrays 类提供的 sort排序方法和copyOfRange()复制数组元素方法实现的.


    /**
     * 添加分数。如果新添加的分数比排行榜分数高,则会将新分数记入排行榜。
     * 
     * @param score
     *         新分数
     */
    static public void addNewScore(int score) {
        // 在得分组数基础上创建一个长度为4的临时数组
        int tmp[] = Arrays.copyOf(scores, 4);
        tmp[3] = score;// 将新分数赋值给第四个元素
        Arrays.sort(tmp);// 临时数组降序排列
        scores = Arrays.copyOfRange(tmp, 1, 4);// 将后三个元素赋值给得分数组
    }

获取分数的方法

    /**
     * 获取分数
     * 
     * @return
     */
    static public int[] getScores() {
        return scores;
    }


四  视图模块设计

一   主窗体

        主窗体是整个游戏最外层的容器。主窗体的本身没有任何内容,仅是一个宽820像素,高260像素的窗体。项目中 view.MainFrame类表示游戏的主窗体类,该类继承于JFrame类。MainFrame类没有成员属性。MainFrame 类的构造方法中定义了窗体的宽、高、标题等特性,同时也具有游戏启动时的初始化功能。例如,第一次载入游戏面板时,初始化计分器,播放背景音乐等。MainFrame类的构造方法的具体代码如下:

    public MainFrame() {
        restart();// 开始
        setBounds(340, 150, 821, 260);// 设置横纵坐标和宽高
        setTitle("奔跑吧!小恐龙!");// 标题
        Sound.backgroud();// 播放背景音乐
        ScoreRecorder.init();// 读取得分记录
        addListener();// 添加监听
        setDefaultCloseOperation(EXIT_ON_CLOSE);// 关闭窗体则停止程序
    }

        构造方法中调用的 restart方法就是让游戏重新开始的方法,也可以用于第一次启动游戏在restart)方法中,首先获取了窗体的主容器对象,然后删除容器中的所有组件,最后创建一个新的游戏面板对象并添加到容器中,同时添加主窗体的键盘事件。方法中最后一行代码尤为关键,如果在删除原组件并添加新的游戏面板之后不做重新验证操作,将会导致新面板无法正确显示。restart()方法的具体代码如下:

    /**
     * 重新开始
     */
    public void restart() {
        Container c = getContentPane();// 获取主容器对象
        c.removeAll();// 删除容器中所有组件
        GamePanel panel = new GamePanel();// 创建新的游戏面板
        c.add(panel);
        addKeyListener(panel);// 添加键盘事件
        c.validate();// 容器重新验证所有组件
    }

        构造方法中调用了addListener()方法用于让窗体添加键盘以外的监听事件,游戏中主要用于在关闭窗口之前保存最新的得分记录。在窗体关闭之前会触发windowClosing()方法,在此方法中调用ScoreRecord计分器的saveScore()方法保存成绩。

    /**
     * 添加监听
     */
    private void addListener() {
        addWindowListener(new WindowAdapter() {// 添加窗体监听
            public void windowClosing(WindowEvent e) {// 窗体关闭前
                ScoreRecorder.saveScore();// 保存得分记录
            }
        });
    }

二   游戏面板

        游戏面板是整个程序的核心,几乎所有的算法都是以游戏面板为基础实现的。游戏面板的主要作用是绘制游戏界面,将所有的游戏元素都展现出来。游戏界面会按照(默认)20毫秒一次的刷新频率实现游戏帧数的刷新,这样不仅可以让界面中的元素运动起来,也可以让各个元素在运动的过程中进行逻辑的运算。


        项目中的 GamePanel 类表示游戏面板类,该类继承了JPanel面板类,同时实现了KeyListener 键盘事件监听接口。GamePanel类有很多成员属性,其中恐龙对象、背景图片对象、障碍集合和得分都是游戏界面中可以看到的元素。此外,还有很多后台使用的属性,如游戏结束标志、障碍计时器等。
        游戏采用双缓冲机制防止界面闪烁,image对象就是缓冲图片对象,也可以成为主图片对象,所有的游戏画面都绘制在image对象中,然后再将image对象绘制到游戏面板中。GamePanel类的定义如下:

public class GamePanel extends JPanel implements KeyListener {
    private BufferedImage image;// 主图片
    private BackgroundImage background;// 背景图片
    private Dinosaur golden;// 恐龙
    private Graphics2D g2;// 主图片绘图对象
    private int addObstacleTimer = 0;// 添加障碍计时器
    private boolean finish = false;// 游戏结束标志
    private List<Obstacle> list = new ArrayList<Obstacle>();// 障碍集合
    private final int FREASH = FreshThread.FREASH;// 刷新时间

    int score = 0;// 得分
    int scoreTimer = 0;// 分数计时器

    public GamePanel() {
        // 主图片采用宽800高300的彩色图片
        image = new BufferedImage(800, 300, BufferedImage.TYPE_INT_BGR);
        g2 = image.createGraphics();// 获取主图片绘图对象
        background = new BackgroundImage();// 初始化滚动背景
        golden = new Dinosaur();// 初始化小恐龙
        list.add(new Obstacle());// 添加第一个障碍
        FreshThread t = new FreshThread(this);// 刷新帧线程
        t.start();// 启动线程
    }
}

        在paintlmage)方法中会让每一个游戏元素都执行各自的运动,如背景图片的滚动、恐龙的移动和障碍的移动等。在绘制障碍之前,会先判断障碍集合中的障碍对象是否是有效的,如是无效障碍,则会删除。paintImage0方法的具体代码如下:

    /**
     * 绘制主图片
     */
    private void paintImage() {
        background.roll();// 背景图片开始滚动
        golden.move();// 恐龙开始移动
        g2.drawImage(background.image, 0, 0, this);// 绘制滚动背景
        if (addObstacleTimer == 1300) {// 每过1300毫秒
            if (Math.random() * 100 > 40) {// 60%概率出现障碍
                list.add(new Obstacle());
            }
            addObstacleTimer = 0;// 重新计时
        }

        for (int i = 0; i < list.size(); i++) {// 遍历障碍集合
            Obstacle o = list.get(i);// 获取障碍对象
            if (o.isLive()) {// 如果是有效障碍
                o.move();// 障碍移动
                g2.drawImage(o.image, o.x, o.y, this);// 绘制障碍
                // 如果恐龙头脚碰到障碍
                if (o.getBounds().intersects(golden.getFootBounds())
                        || o.getBounds().intersects(golden.getHeadBounds())) {
                    Sound.hit();// 播放撞击声音
                    gameOver();// 游戏结束
                }
            } else {// 如果不是有效障碍
                list.remove(i);// 删除此障碍
                i--;// 循环变量前移
            }
        }
        g2.drawImage(golden.image, golden.x, golden.y, this);// 绘制恐龙
        if (scoreTimer >= 500) {// 每过500毫秒
            score += 10;// 加十分
            scoreTimer = 0;// 重新计时
        }

        g2.setColor(Color.BLACK);// 使用黑色
        g2.setFont(new Font("黑体", Font.BOLD, 24));// 设置字体
        g2.drawString(String.format("%06d", score), 700, 30);// 绘制分数

        addObstacleTimer += FREASH;// 障碍计时器递增
        scoreTimer += FREASH;// 分数计时器递增
    }

        重绘组件的方法,以及判断游戏是否结束等方法都要实现,还有因为我们类实现的结构,就要实现具体的方法。

    /**
     * 重写绘制组件方法
     */
    public void paint(Graphics g) {
        paintImage();// 绘制主图片内容
        g.drawImage(image, 0, 0, this);
    }

    /**
     * 游戏是否结束
     * 
     * @return
     */
    public boolean isFinish() {
        return finish;
    }

    /**
     * 使游戏结束
     */
    public void gameOver() {
        ScoreRecorder.addNewScore(score);// 记录当前分数
        finish = true;
    }

    /**
     * 实现按下键盘按键方法
     */
    public void keyPressed(KeyEvent e) {
        int code = e.getKeyCode();// 获取按下的按键值
        if (code == KeyEvent.VK_SPACE) {// 如果是空格
            golden.jump();// 恐龙跳跃
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    @Override
    public void keyTyped(KeyEvent e) {

    }


三   成绩对话框

        成绩对话框会在游戏结束时弹出,对话框中会显示目前为止记录的前3名成绩,单击对话框底部的按钮会重新开始游戏。项目中的 view.ScoreDialog就是成绩对话框类,该类继承JDialog对话框类。
        ScoreDialog类中有一个构造方法,构造方法参数为对话框的父窗体。构造方法第一行调用了父类的构造方法,通过父类构造方法阻塞父窗体,这样可以保证弹出成绩对话框之后,主窗体内会停止全部功能且不可选中。这样可以保证玩家单击“重新开始”按钮后,主窗体才会执行restart()方法。

public class ScoreDialog extends JDialog {

    /**
     * 构造方法
     * 
     * @param frame
     *            父窗体
     */
    public ScoreDialog(JFrame frame) {
        super(frame, true);// 调用父类构造方法,阻塞父窗体
        int scores[] = ScoreRecorder.getScores();// 获取当前前三名成绩
        JPanel scoreP = new JPanel(new GridLayout(4, 1));// 成绩面板,4行1列
        scoreP.setBackground(Color.WHITE);// 白色背景
        JLabel title = new JLabel("得分排行榜", JLabel.CENTER);// 标题标签,居中
        title.setFont(new Font("黑体", Font.BOLD, 20));// 设置字体
        title.setForeground(Color.RED);// 红色体字
        JLabel first = new JLabel("第一名:" + scores[2], JLabel.CENTER);// 第一名标签
        JLabel second = new JLabel("第二名:" + scores[1], JLabel.CENTER);// 第二名标签
        JLabel third = new JLabel("第三名:" + scores[0], JLabel.CENTER);// 第三名标签
        JButton restart = new JButton("重新开始");// 重新开始按钮
        restart.addActionListener(new ActionListener() {// 按钮添加事件监听
            @Override
            public void actionPerformed(ActionEvent e) {// 当点击时
                dispose();// 销毁对话框
            }
        });

        scoreP.add(title);// 成绩面板添加标签
        scoreP.add(first);
        scoreP.add(second);
        scoreP.add(third);

        Container c = getContentPane();// 获取主容器
        c.setLayout(new BorderLayout());// 使用边界布局
        c.add(scoreP, BorderLayout.CENTER);// 成绩面板放中间
        c.add(restart, BorderLayout.SOUTH);// 按钮放底部

        setTitle("游戏结束");// 对话框标题

        int width, height;// 对话框宽高
        width = height = 200;// 对话框宽高均为200
        // 获得主窗体中居中位置的横坐标
        int x = frame.getX() + (frame.getWidth() - width) / 2;
        // 获得主窗体中居中位置的纵坐标
        int y = frame.getY() + (frame.getHeight() - height) / 2;
        setBounds(x, y, width, height);// 设置坐标和宽高
        setVisible(true);// 显示对话框
    }
}


五   游戏核心功能设计

 一  刷新帧

        帧是一个量词,一幅静态画面就是一帧。无数不同的静态画面交替放映,就形成了动画。帧的刷新频率决定着画面中的动作是否流畅,列如,电影在正常情况下是24帧,也就是影片一秒钟会闪过24幅静态画面。想让游戏中的物体运动起来,就需要让游戏画面不断地刷新,像播放电影一样,这就是刷新帧的概念。
        项目中的service.FreshThead类就是游戏中的刷新帧线程类,该类继承于Thread线程类,并在线程的主方法中无限地循环,每过20毫秒就执行游戏面板的repaint)方法,每次执行 repaint0方法前都会先执行用户输入的指令,这样每次绘制的画面就会都不一样,极短时间内切换画面就形成了动画效果。游戏面板的isFinish)方法返回 false,就代表游戏结束,当前线程才会停止。
        当刷新帧的业务停止后,程序会获取加载游戏面板的主窗体对象,然后弹出成绩对话框,最后让主窗体对象重新开始新游戏。FreshThead类的具体代码如下:

public class FreshThread extends Thread {
    public static final int FREASH = 20;// 刷新时间
    GamePanel p;// 游戏面板

    public FreshThread(GamePanel p) {
        this.p = p;
    }

    public void run() {
        while (!p.isFinish()) {// 如果游戏未结束
            p.repaint();// 重绘游戏面板
            try {
                Thread.sleep(FREASH);// 按照刷新时间休眠
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Container c = p.getParent();// 获取面板父容器
        while (!(c instanceof MainFrame)) {// 如果父容器不是主窗体类
            c = c.getParent();// 继续获取父容器的父容器
        }
        MainFrame frame = (MainFrame) c;// 将容器强制转换为主窗体类
        new ScoreDialog(frame);// 弹出得分记录对话框
        frame.restart();// 主窗体重载开始游戏
    }
}

二  滚动背景

        前面我们提到了小恐龙的实际运动是在原地踏步,要想实现移动效果实际上是背景图片在向后移动,我们设计的背景图片一共有两张,通过这两张的不断循环,无缝衔接来实现背景滚动的效果。

public class BackgroundImage {
    public BufferedImage image;// 背景图片
    private BufferedImage image1, image2;// 滚动的两个图片
    private Graphics2D g;// 背景图片的绘图对象
    public int x1, x2;// 两个滚动图片的坐标
    public static final int SPEED = 4;// 滚动速度
}

构造方法

    public BackgroundImage() {
        try {
            image1 = ImageIO.read(new File("image/背景.png"));
            image2 = ImageIO.read(new File("image/背景.png"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 主图片采用宽800高300的彩色图片
        image = new BufferedImage(800, 300, BufferedImage.TYPE_INT_RGB);
        g = image.createGraphics();// 获取主图片绘图对象
        x1 = 0;// 第一幅图片初始坐标为0
        x2 = 800;// 第二幅图片初始横坐标为800
        g.drawImage(image1, x1, 0, null);
    }

        roll方法让图片实现不断的滚动,当有任意一张图片移动出画面时,就立刻回到右侧是初始位置,准备下一轮的滚动。

    /**
     * 滚动
     */
    public void roll() {
        x1 -= SPEED;// 第一幅图片左移
        x2 -= SPEED;// 第二幅图片左移
        if (x1 <= -800) {// 如果第一幅图片移出屏幕
            x1 = 800;// 回到屏幕右侧
        }
        if (x2 <= -800) {// 如果第二幅图片移出屏幕
            x2 = 800;// 回到屏幕右侧
        }
        g.drawImage(image1, x1, 0, null); // 在主图片中绘制两幅图片
        g.drawImage(image2, x2, 0, null);
    }

 

三  碰撞检测

        java awt.Rectangle类提供了intersects(Rectangle r)方法来判断两个边界是否发生了交汇。当两个边界对象发生交汇时,intersects()方法的返回结果为true;当两个边界对象没有交汇时,intersects()方法的返回结果为false。

        因为我们前面为恐龙和石头以及仙人掌做了边界处理,因此我们可以用这个方法来检测是否发生碰撞。

        在GamePanel游戏面板类的paintImage()方法中,绘制完每一个障碍后,会判断刚刚绘制的障碍对象是否碰到了恐龙。利用上述的方法进行判断,只要存在true结果,就让游戏结束。

  if (o.getBounds().intersects(golden.getFootBounds())
       || o.getBounds().intersects(golden.getHeadBounds())) {
   Sound.hit();// 播放撞击声音
   gameOver();// 游戏结束
 }

四  键盘监听

        前面GamePanel类实现了KeyListener的接口,该接口实现了三种方法(键盘监听的方法):keyPressed,keyReleased,keyTyped.这里我们就用到了按下的监听事件实现的方法,如果点击空格键就让恐龙实现跳跃的方法。

    public void keyPressed(KeyEvent e) {
        int code = e.getKeyCode();// 获取按下的按键值
        if (code == KeyEvent.VK_SPACE) {// 如果是空格
            golden.jump();// 恐龙跳跃
        }
    }

【 游戏运行效果】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1449835.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

判断一个时间序列中的元素是否属于一个月的第一天或最后一天

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 判断一个时间序列中的元素 是否属于一个月的第一天或最后一天 Series.dt.is_month_start Series.dt.is_month_end [太阳]选择题 以下代码的输出结果中正确的是? import pandas as pd ts pd.S…

书生浦语大模型实战营-课程作业(3)

下载sentence_transformer的代码运行情况。sentence_transformer用于embedding&#xff08;转向量&#xff09; 本地构建持久化向量数据库。就是把txt和md文件抽取出纯文本&#xff0c;分割成定长&#xff08;500&#xff09;后转换成向量&#xff0c;保存到本地&#xff0c;称…

抽象的前端

问题背景&#xff1a;vue3&#xff0c;axios 直接导致问题&#xff1a;路由渲染失败 问题报错&#xff1a;Uncaught SyntaxError: The requested module /node_modules/.vite/deps/axios.js?v7bee3286 does not provide an export named post (at LoginIn.vue:16:9) 引入组…

Ps:创建联系表

Ps菜单&#xff1a;文件/自动/联系表 II Automate/Contact sheet II Photoshop 的“联系表 II” Contact Sheet II命令为快速生成图像集合的预览和打印目录提供了一种高效的方法。 此命令可以通过自动化过程读取指定的图像文件&#xff0c;然后根据用户定义的参数&#xff08;如…

【Cocos入门】物理系统(物理碰撞)

物理碰撞 物理引擎默认是关闭状态以节省资源开销。开启方法和之前的普通碰撞类似&#xff1a;cc.director.getPhysicsManager().enabled true但有一个区别&#xff0c;物理引擎的开启必须放在onLoad函数内运行&#xff0c;否则不生效。 物理碰撞组件也同样具有碰撞回调函数。…

第13章 网络 Page735~736 “I/O对象”的链式传递 计数器继承enable_shared_from_this<DownCounter>

使用enable_shared_from_this基类和该基类带来的shared_from_this()方法。DownCounter被加上基类enable_shared_from_this<T> 代码如下&#xff1a; 代码先通过shared_from_this()方法安全正确地复制智能指针counter&#xff0c;再通过lambda表达式以“捕获”的方式实现…

骑士遍历初级版

时间限制&#xff1a;1秒 内存限制&#xff1a;128M 题目描述 如图&#xff0c;从左下角A点出发&#xff0c;马只能向右走&#xff0c;根据马走日字的规则&#xff0c;究竟如何走才能到达右上角B点 输入描述 两个整数x、y&#xff0c;代表右上角B点的坐标&#xff0c…

具有集中目录服务器的 P2P 工作方式

P2P 工作方式概述 在 P2P 工作方式下&#xff0c;所有的音频/视频文件都是在普通的互联网用户之间传输。 具有集中目录服务器的 P2P 工作方式 Napster 最早使用 P2P 技术&#xff0c;提供免费下载 MP3 音乐。 Napster 将所有音乐文件的索引信息都集中存放在 Napster 目录服务…

单片机学习笔记---DS18B20温度读取

目录 OneWire.c 模拟初始化的时序 模拟发送一位的时序 模拟接收一位的时序 模拟发送一个字节的时序 模拟接收一个字节的时序 OneWire.h DS18B20.c DS18B20数据帧 模拟温度变换的数据帧 模拟温度读取的数据帧 DS18B20.h main.c 上一篇讲了DS18B20温度传感器的工作原…

[GYCTF2020]Blacklist

感觉是[强网杯 2019]随便注 的加强版&#xff0c;之前做的是最后可以通过prepare和execute实现对select的绕过&#xff0c;但是这题把这两个关键字也过滤了。 前面堆叠注入没啥问题&#xff0c;卡在了最后读取flag 查看其他师傅的wp&#xff0c;发现这个handler的可以当作丐版s…

CSGO搬砖项目怎么样?分享一下个人的看法!

对于steam搬砖平台&#xff0c;无人不知&#xff0c;无人不晓啊&#xff0c;全球最大的一个游戏平台&#xff0c;像我们知道的PUBG&#xff0c;CS:GO&#xff0c;都是里面的&#xff0c;比较火的一个平台。 对于想了解Steam搬砖的&#xff0c;今天分享一下个人的看法。 首先&a…

Linux操作系统基础(十四):集群服务器搭建

文章目录 集群服务器搭建 一、新增Linux服务器 1、克隆虚拟机 2、修改虚拟机的硬件配置 3、修改虚拟机的网络配置 二、关闭防火墙 1、关闭firewalld 2、关闭SElinux 三、修改主机名 四、修改hosts映射 五、SSH免密登录 六、时钟同步 七、远程文件拷贝 1、从本机拷…

[缓存] - 2.分布式缓存重磅中间件 Redis

1. 高性能 尽量使用短key 不要存过大的数据 避免使用keys *&#xff1a;使用SCAN,来代替 在存到Redis之前压缩数据 设置 key 有效期 选择回收策略(maxmemory-policy) 减少不必要的连接 限制redis的内存大小&#xff08;防止swap&#xff0c;OOM&#xff09; slowLog …

单片机学习笔记---LCD1602功能函数代码

目录 LCD1602.c 模拟写指令的时序 模拟写数据的时序 初始化 显示字符 显示字符串 显示数字 显示有符号的数字 显示16进制数字 显示二进制数 LCD1602.h main.c 上一篇讲了LCD1602的工作原理&#xff0c;这一节开始代码演示&#xff01; 新创建工程&#xff1a;LCD1…

HGAME 2024 WEEK2 WP

文章目录 WEBWhat the cow say?Select More Coursesmyflask CryptomidRSAmidRSA revengebackpackbackpack revengebabyRSA奇怪的图片plus MISC我要成为华容道高手ek1ng_want_girlfriendezWord龙之舞 回老家了&#xff0c;初七晚上才回去&#xff0c;估计week3前几天不怎么能做…

Android14之Android Rust模块编译语法(一百八十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

SPFA最短路复习

文章目录 从Bellman-Ford开始核心思想模拟算法执行过程时间复杂度模板 spfaspfa优化的思想模板 从Bellman-Ford开始 对于所有边权都大于等于0的图&#xff0c;任意两个顶点之间的最短路&#xff0c;显然不会经过重复的顶点或者边。也就是说任意一条最短路经过的定点数不会超过…

《汇编语言》- 读书笔记 - 第9章 - 转移指令的原理

《汇编语言》- 读书笔记 - 第9章 - 转移指令的原理 总结9.1 操作符 offset问题 9.1 9.2 jmp 指令9.3 依据位移进行转移的 jmp 指令jmp short 标号程序 9.1程序 9.2图 9.2 程序 9.2 的机器码 jmp near ptr 标号 9.4 转移的目的地址在指令中的 jmp 指令如何选择 jmp short、jmp n…

Github用人工智能(AI)帮你的代码修正安全漏洞

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

Java:继承——继承概念+父子类成员、构造访问顺序+super、this关键字(代码+画图超详解!)

一、什么是继承 1、继承的概念 举例理解&#xff1a; 根据打印机的原理&#xff0c;我们可以知道不管是彩色打印机还是黑白打印机&#xff0c;实现的都是一个功能&#xff1a;打印&#xff0c;这是二者的共性。彩色打印机和黑白打印机都继承了打印机的打印功能&#xff0c;且二…