当前位置:首页 » 《随便一记》 » 正文

奔跑吧小恐龙(Java)

18 人参与  2024年02月23日 14:41  分类 : 《随便一记》  评论

点击全文阅读


前言 

        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;//纵坐标默认起始值是120image1=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://m.zhangshiyu.com/post/69613.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1