Step by Step,用JAVA做一个FLAPPYBIRD游戏(一)

游戏整体框架

先来一个一个非常简洁,经典的游戏主循环:

while(true) {
    input();//接收输入
    logic();//修改数据
    draw();//更新画面
}

我们后面的游戏主循环就是基于这种结构。

一图胜千言。来张项目类结构截图(NetBeans IDE):
FlappyBird
Entity表示游戏中的物体(这命名。。貌似暴露了Web程序员的身份23333),看名字大概能知道他们是什么。例如Bird类就是我们最主要的主角小鸟了。
FlappyBird类是java的主类,里面包含一个Main方法。他要做的只是实例化我们的窗体(。-_-。)
MyGame视图,主要负责展示游戏的画面,其实这里偷了个懒,因为控制比较简单,View同时负责处理了鼠标按键消息。MyGame继承了JFrame。

我们的MyGame类初始化后会实例化一个线程Thread,Thread的任务就是不停的更新数据,然后刷新画面。

首先写一个MyGame类

public class MyGame extends MouseAdapter implements Runnable{

    //主线程和主线程标志
    private boolean flag;
    private Thread th;
    //游戏的3种状态常量
    private final int GAME_STATE_MENU = 0;
    private final int GAME_STATE_PLAYING = 1;
    private final int GAME_STATE_OVER = 2;
    //游戏状态
    private int gameState;
    //屏幕宽高
    public static final int gameW=390,gameH=476;
    //是否向上的状态,当小鸟正在向上时,鼠标点击暂时无效
    private boolean isUp;
    //游戏窗口和JPanel
    private JFrame gameFrame;
    private JPanel gamePanel;
    //将一帧的图片画到这里面,再在每一帧结束后把这幅图画到JPanel上,起到缓冲的作用,否则会明显闪屏
    private BufferedImage bfImg;
    //向上帧计数器
    private int upCount=0;
    //记录分数
    private int score;
   //即将出现的管道类型,保证上下两张类型的管道交替出现
    private boolean isUpPiepe = false;
    public MyGame() {
        this.doInit();
    }
    @Override
    public void run() {...}
    private void doLogic() {...}
    private void doDraw() {...}
    private void doInit() {
    ...
    this.th = new Thread(this);
        th.start();
    }
}

构造函数先执行doInit做一些窗体的初始化(设置大小,位置,窗口标题等操作)。这是Swing的内容,就不多说了,直接贴上代码:
这里面有个有意思的成员BufferedImage,这是为了防止图像闪烁,我们后面会详细讲解。

 private void doInit() {
        this.gameFrame = new JFrame("FlappyBird");
        this.gameFrame.setBounds(20, 40, gameW, gameH);
        this.gameFrame.setResizable(false);
        this.gameFrame.setVisible(true);
        this.gamePanel = new JPanel();
        this.gamePanel.addMouseListener(this);
        this.gamePanel.setFocusable(true);
        this.gameFrame.setContentPane(gamePanel);
        this.gameFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                flag = false;
                gameFrame.dispose();
                System.exit(0);
            }
            });
        this.score = 0;
        this.menu = new Menu();
        this.bird = new Bird();
        this.pipes = new Vector<>();
        this.bg = new Background();
        this.om = new overMenu();
        this.bfImg = new BufferedImage(gameW, gameH, TYPE_INT_RGB);
        this.g = bfImg.getGraphics();
        g.setFont(new Font("宋体",Font.BOLD,40));
        this.flag = true;
        this.isUp = false;
        this.gameState = GAME_STATE_MENU;
        this.th = new Thread(this);
        th.start();
    }

然后Thread调用Start方法后就会去执行run,flag是控制主线程什么时候结束的,这样我们可以方便的通过设置flag来使游戏开始或者结束。
run的代码类似下面:

    @Override
    public void run() {
        while(flag) {
        long start = System.currentTimeMillis();
        this.doDraw();
        this.doLogic();
        long end = System.currentTimeMillis();
        long useTime = end - start;
        //每一帧持续50ms
        if(useTime<50) {
            try {
                Thread.sleep(50-useTime);
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        } 
        }
    }

其中比较重要的是doLogic和doDraw,前者是处理数据的变化,后者负责根据更新后的数据,刷新画面。每次循环还加入了Thread.sleep,也就是控制每一次刷新至少要50ms。对游戏比较熟悉的朋友应该知道,一次刷新50ms,一秒钟就能刷新20次,既一秒20帧,也就是FPS 20的含义。为什么要这么限制呢。因为我们这个游戏做的事情比较简单,doLogic和doDraw很快就能完成,如果不限制最小延迟,那画面刷新速度会非常快。。。。(有点像某些游戏锁帧数上限)

这就是主线程的循环,接下来我们看看重要的doLogic和doDraw里面要做什么。

private void doLogic() {
    switch(gameState) {
            case GAME_STATE_MENU:
            ...
            break;
            case GAME_STATE_PLAYING:
            ...
            break;
            case GAME_STATE_OVER:
            ...
            break;
}

doDraw类似,记得我们上一篇的3个界面吗,对应的就是这里3种游戏状态,这样写我们就能通过改变gameState的值来使主循环执行不同游戏状态下的代码<( ̄ˇ ̄)/。

接下来,给MyGame类加上游戏中的物体也就是Entity的声明:

    //开始菜单和小鸟
    private Menu menu;
    private Bird bird;
    //管道的容器(水管有好多个,所以是Vector)
    private Vector<Pipe> pipes;
    //背景对象和结束菜单对象
    private Background bg;
    private overMenu om;
    private Graphics g;

Graphic g是给doDraw方法用于更新画面的。
前面说了每次循环调用doLogic和doDraw更新所有Entity的数据和图像,Entity有那么多,代码全写在doLogic和doDraw里面显然会很乱。
我们让每个Entity自己实现doLogic和doDraw,然后主循环的doLogic和doDraw只要调用每个Entity的doLogic和doDraw就好了。

这样我们的doLogic和doDraw代码变成了这样:

private void doLogic() {
    switch(gameState) {
            case GAME_STATE_MENU:
                menu.doLogic();
                break;
            case GAME_STATE_PLAYING:
                bg.logic();
                bird.logic();
                for(int i=0;i<pipes.size();i++) {
                    Pipe pipe = pipes.get(i);
                    pipe.doLogic();
                }
                break;
            case GAME_STATE_OVER:

                break;
}

    private void doDraw() {
         switch(gameState) {
            case GAME_STATE_MENU:
                menu.draw(g);
                break;
            case GAME_STATE_PLAYING:
                bg.draw(g);
                bird.draw(g);
                for(int i=0;i<pipes.size();i++) {
                    pipes.get(i).draw(g);
                }
                break;
            case GAME_STATE_OVER:
                om.draw(g,this.score);
                break;
        }
    }

值得一提的是,不是每个Entity在当前gameState下都要做事情的。。比如case GAME_STATE_OVER,当游戏状态时游戏结束时,只有overMenu既结束菜单这个Entity被调用draw和logic。

不知道大家注意到没有,我们的MyGame还是继承了MouseAdapter,为了响应鼠标点击事件,我们重写mousePressed方法,处理的办法类似doLogic和doDraw也是按不同的gameState不同处理方式。

@Override
    public void mousePressed(MouseEvent e) {
         switch(gameState) {
            case GAME_STATE_MENU:
                gameState = GAME_STATE_PLAYING;
                break;
            case GAME_STATE_PLAYING:
                break;
            case GAME_STATE_OVER:
                int x = e.getX();
                int y = e.getY();
                if(x>=om.btnX 
            && y>=om.btnY 
            && y<=om.btnY+om.btnH) {
                    th.stop();
                    gameFrame.dispose();
                    this.doInit();
                }
                break;
        }
        super.mousePressed(e); 
    }

代码可以看出当gameState是GAME_STATE_MENU也就是开始菜单时,我们按下鼠标,然后gameState会被赋值成GAME_STATE_PLAYING,这样当下一帧(也就是下一次主循环)就回走gameState等于GAME_STATE_PLAYING的代码了,这样都实现了游戏画面的转换。

这篇就先讲这么多,下一篇我们开始做开始菜单~~

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页