写点什么

2 小时开发《点球射门游戏》,动画演示思路(下),代码已开源

作者:非喵鱼
  • 2022-12-08
    重庆
  • 本文字数:7645 字

    阅读完需:约 25 分钟

2 小时开发《点球射门游戏》,动画演示思路(下),代码已开源

前沿

首选感谢各位对我这边文章(2小时开发《点球射门游戏》,动画演示思路(上),代码已开源)的点赞、收藏与支持,今天在这里主要是接上一篇文章,讲一讲游戏界面中的一些动画与逻辑的实现,希望大家一如既往的点赞、收藏+关注,鼓励一下勇哥。对于游戏怎么怎么画,请看上那篇文章。



本篇内容有那些值得大家学习【重点】

  • 使用二次曲线实现球的瞄准轨迹线【见下】。这个在 QQ 桌球、王者荣耀、愤怒的小鸟等游戏中都用到了!

  • 使用多线程实现守门员移动、飞球、蓄力区、时间轴等动画。



有那些逻辑需要实现【必看】

如下图,整个游戏的实现逻辑,按照对象来分,则为以下:

球对象逻辑:

  • 拖动鼠标:调整球射出的轨迹逻辑,支持上下左右的拖动调整★★★

  • Ctrl+拖动鼠标:摆放球逻辑

  • 点击球:球按照轨迹飞出运动的逻辑★★★

星星对象逻辑:

  • 球在飞行轨迹与星星重合则消除星星的逻辑★★★

守门员对象逻辑:

  • 在球门区左右来回移动的逻辑

石头对象逻辑:

  • 禁止遮挡部分球门,球不能从此射进的逻辑

球门对象逻辑:

  • 进球逻辑★★★

积分区对象逻辑:

  • 记时逻辑

  • 进球积分逻辑


游戏逻辑实现思路 &代码

调整射门轨迹的逻辑实现

轨迹实现的思路看起来难,实际还是挺难的,思路如下:

  • 假设有两点,黄色点为足球的中心点,红色点是球门的中心点

  • 在黄点和红点之间就存在一条红色线段

  • 在红色线段上随机取 N 个点,用白色表示,这样就形成了一个直线的轨迹点

接着再说一下拖动鼠标,轨迹跟着鼠标移动的实现思路:

  • 鼠标向上拖动,黄点和红点同步向上平移,这样线段上的轨迹点也同步平移

  • 鼠标向下拖动,黄点和红点同步向下平移,这样线段上的轨迹点也同步平移

  • 鼠标向右拖动,黄点和红点同步向右平移,这样线段上的轨迹点也同步平移

  • 鼠标向左拖动,黄点和红点同步向左平移,这样线段上的轨迹点也同步平移

最后注意,黄点和红点之间如果是曲线,效果更贴近自然,所以最后还需要把黄点和红点之间使用二次曲线进行实现。



轨迹线,参考实现代码:


// 记录红黄点 public void reDraw(Ball ball,BackgroundPanel backgroundPanel,int stepX,int stepY,boolean isControlDown){     // 开始点——黄点     startY = ball.getY()+ball.getHeight()/2;     startX = ball.getX()+ball.getWidth()/2;     // 结束点——红点     endX = getWidth()/2+stepX/3;     endY = 0; }// 画出轨迹线public void paintComponent(Graphics g) {    Graphics2D g2d = (Graphics2D) g;    g2d.setColor(Color.RED);    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);    // 二次曲线    QuadCurve2D quadCurve2D = new QuadCurve2D.Double(startX,startY, startX+stepX,endY+stepY+50, endX, endY+stepY);    // 通过二次曲线,随机生成线上的几个点    PathIterator pi = quadCurve2D.getPathIterator(g2d.getTransform(),6);// 从二次曲线中取出特征点    points = new ArrayList<>(25);    while (!pi.isDone()) {        double[] coords = new double[6];        switch (pi.currentSegment(coords)) {            case PathIterator.SEG_MOVETO:            case PathIterator.SEG_LINETO:                points.add(new Point2D.Double(coords[0], coords[1]));                break;        }        pi.next();    }    // 在每个点 画上小白圆圈    g2d.setColor(Color.WHITE);    Point2D.Double point = null;    for (Point2D.Double temp : points) {        point = temp;        g2d.fillOval((int) point.x, (int) point.y,10,10);//在每个特征点上画一个小圆圈    }}
复制代码


鼠标拖动参考实现代码:


ball.addMouseMotionListener(new MouseAdapter() {    public void mouseDragged(MouseEvent e){        // 记录拖动最后的坐标点,用于记录拖动平移的差量        int stepX = e.getX();        int stepY = e.getY();        line.reDraw(ball,BackgroundPanel.this,stepX,stepY,e.isControlDown());        repaint();    }});
复制代码

拖动球,摆放球的逻辑实现

拖动球,可以摆放球的位置,相关实现思路是:

  • 拖动开始时(按下鼠标时)设置一个其实点,黄点

  • 拖动过程中(按下鼠标,并同时移动位置)换点跟随鼠标点

  • 拖动结束时(松开鼠标)球平移到最后的位置



参考实现代码:


 public void reDraw(Ball ball,BackgroundPanel backgroundPanel,int stepX,int stepY,boolean isControlDown){    // 移动最后的位置点    this.stepX = stepX;    this.stepY = stepY;    // 按下Ctrl键拖动才是,摆放    if(isControlDown) {        this.setVisible(false);        ball.setBounds(ball.getX() + stepX, ball.getY(), ball.getWidth(), ball.getHeight());    }}
复制代码

射门,球按照轨迹飞行实现

鼠标点击球,球就按照之前的轨迹点飞行,实现的思路如下:

  • 获取到从二次曲线上的随机轨迹点,如下图中的白色小圆

  • 点击球是,开启一个线程

  • 在线程中,把球平移到轨迹点的第一个点,并休眠 100 毫秒

  • 100 毫秒后,又把球平移到轨迹点的下一个点,再次休眠 100 毫秒

  • ....重复上一步动作,知道球平移到最后一个轨迹点



参考实现代码:


// 监听点击球ball.addMouseListener(new MouseAdapter() {    @Override    public void mouseClicked(MouseEvent e) {        // 用一个线程让球按照瞄准轨迹飞行        new Thread(new Runnable() {            @Override            public void run() {                int star =0;                // 获取到轨迹线上的轨迹点                for (Point2D.Double point : line.getPoints()) {                    // 把球移动到轨迹点上                    ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());                    try {                        // 休眠100毫秒                        Thread.sleep(100);                    } catch (InterruptedException ex) {                        ex.printStackTrace();                    }                }            }        }).start();    }});
复制代码

消除轨迹点重合的星星逻辑实现

消除星星,实际就是判断球的坐标点是否与星星重合,如果是则消除,具体实现思路如下:

  • 星星当作是一个正方形,有 4 个点

  • 球也当作是一个正方形,有 4 个点

  • 球在飞行过程中,每移动到一个轨迹点时,检查星星和球的四个点是否有重合的区域

  • 如果有重合的区域则,消除星星,从游戏界面中删除星星



参考实现代码:


ball.addMouseListener(new MouseAdapter() {    @Override    public void mouseClicked(MouseEvent e) {        new Thread(new Runnable() {            @Override            public void run() {                int star =0;                for (Point2D.Double point : line.getPoints()) {                    ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());                    try {                        // 球在飞行过程中,没移动一个点,需要检查一下是否有星星需要消除                        star += obstacleStart();                        Thread.sleep(100);                    } catch (InterruptedException ex) {                        ex.printStackTrace();                    }                }                check(star);            }        }).start();        CLICK_CLIP.play();    }});public int obstacleStart(){    Rectangle ballBounds = ball.getBounds();    int tempX[] = {ballBounds.x,ballBounds.x+ballBounds.width,ballBounds.x,ballBounds.x+ballBounds.width};    int tempY[] = {ballBounds.y,ballBounds.y,ballBounds.y+ballBounds.height,ballBounds.y+ballBounds.height};    int count = 0;    // 获取所有的星星,进行循环检查    for (Component component : this.getComponents()) {        if(component instanceof Star){            Star obstacle = (Star)component;            Rectangle goalkeeperBounds = obstacle.getBounds();            int minX = goalkeeperBounds.x;            int maxX = goalkeeperBounds.x+goalkeeperBounds.width;            int minY = goalkeeperBounds.y;            int maxY = goalkeeperBounds.y+goalkeeperBounds.height;            miukoo:for (int i = 0; i < tempY.length; i++) {                // 如何球的4个点,在星星的区域内,则命中                if(tempX[i]>minX&&tempX[i]<maxX&&tempY[i]>minY&&tempY[i]<maxY){                    System.out.println("================命中星星");                    count++;                    STAR_CLIP.play();                    this.remove(obstacle);// 消除星星,自己删除即可                    break miukoo;                }            }        }    }    return count;}
复制代码

守门员来回移动的逻辑实现

守门员在球门前,左右移动,干扰射球的飞行过程,实现思路:

  • 开启一个线程

  • 向右平移守门员位置+30 像素,并判断是否超出了最右边球门边缘,如果是则设置向左移动,然后休眠 100 毫秒

  • 向左平移守门员位置-30 像素,并判断是否超出了最左边球门边缘,如果是则设置向右移动,然后休眠 100 毫秒



public void move() {    try {        Rectangle bounds = this.getBounds();        double width = bounds.getX();        if (isAdd) {// 向右            width += 30;            if (width < backgroundPanel.getWidth() * 4 / 5) {                backgroundPanel.repaint();                this.setBounds((int) width, bounds.y, bounds.width, bounds.height);                Thread.sleep(100);            } else {                isAdd = false;            }        } else {// 向左            width -= 30;            if (width > backgroundPanel.getWidth() * 1 / 5) {                backgroundPanel.repaint();                this.setBounds((int) width, bounds.y, bounds.width, bounds.height);                Thread.sleep(100);            } else {                isAdd = true;            }        }    }catch (Exception e){        e.printStackTrace();    }}
@Overridepublic void run() { while (true&&!Thread.interrupted()){ move(); }}
复制代码

石头的逻辑实现

石头就是一张图片,把其摆放到对应位置即可。其遮挡射门的逻辑,主要在球门逻辑中去判断。

参考代码:


public class Shitou extends JLabel implements Obstacle {    BackgroundPanel backgroundPanel;    public Shitou(BackgroundPanel backgroundPanel){        this.backgroundPanel = backgroundPanel;        this.setBounds(backgroundPanel.getWidth()/2+50,100,316,100);//设置图片放置的位置        this.setPreferredSize(new Dimension(316,100));        this.setIcon(new ImageIcon(ResourcesUtil.getRootPath()+"\\ball\\st.png"));    }
@Override public String name() { return "石头"; }
@Override public JComponent getComponent() { return this; }
public void start(){ }
public void stop(){ }
}
复制代码

进球逻辑实现

进球逻辑看起难,实际还是一个对象边框重合检查的过程,其实现思路如下:

  • 球门、守门员、石头、球都有自己的边界,都是平行四边形

  • 当前射出的球移动到轨迹最后一个点时,开始判断以上元素的边界是否重合,依此来判断是否进球

  • 进球依据:球的四个点都在球门四个点内部

  • 守住依据:球与守门员、石头有任意一个点的重合即为守住

  • 出界依据:球任意一个点不在球门范围内,则为球出界



参考代码:


ball.addMouseListener(new MouseAdapter() {    @Override    public void mouseClicked(MouseEvent e) {        infoPanel.addCount();        new Thread(new Runnable() {            @Override            public void run() {                int star =0;                for (Point2D.Double point : line.getPoints()) {                    ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());                    try {                        star += obstacleStart();                        Thread.sleep(100);                    } catch (InterruptedException ex) {                        ex.printStackTrace();                    }                }                // 轨迹点移动完成后,开始检查球是否进球                check(star);            }        }).start();    }});
复制代码


public void check(int star){    //停止所有的线程,以便获取守门员最后的位置,与球判重    stop();    Rectangle ballBounds = ball.getBounds();    // 检查是否被石头还有守门员    boolean isLan = obstacle();    int x = ballBounds.x;    int y = ballBounds.y;    boolean isOut = false;    if(!isLan) {        // 判断是否出界        if (x < getWidth() / 5 || x > getWidth() * 4 / 5 || y < 85) {            isOut = true;            FAIL_CLIP.play();            repaint();            JOptionPane.showMessageDialog(null, "球出界了...", "Tipe", JOptionPane.ERROR_MESSAGE);        }    }    if(!isLan&&!isOut){        // 没有被守住,也没有出界,则进球啦~~~        if(result!=null){            this.remove(result);        }        infoPanel.addScore(star);        System.out.println("===========赢了,开始显示祝贺彩带和播放音乐");        result = new Result();        result.setPreferredSize(new Dimension(230,187));        result.setBounds((getWidth()-230)/2,(getHeight()-187)/2,230,187);        this.add(result);        this.repaint();        WIN_CLIP.play();        // 进球后,休眠5秒,然后自动复位球        try {            Thread.sleep(5000);        } catch (InterruptedException e) {            e.printStackTrace();        }        WIN_CLIP.stop();        this.remove(result);    }else{        FAIL_CLIP.stop();    }    line.setVisible(false);    ball.setBounds((getWidth()-64)/2,470,64,64);    // 重新开启线程,让守门员再次动起来    start();}
复制代码


public boolean obstacle(){    Rectangle ballBounds = ball.getBounds();    int tempX[] = {ballBounds.x,ballBounds.x+ballBounds.width,ballBounds.x,ballBounds.x+ballBounds.width};    int tempY[] = {ballBounds.y,ballBounds.y,ballBounds.y+ballBounds.height,ballBounds.y+ballBounds.height};    for (Component component : this.getComponents()) {        if(component instanceof Obstacle){// 守门员和石头都抽象成障碍物,判断球是否与障碍物重合            Obstacle obstacle = (Obstacle)component;            Rectangle goalkeeperBounds = obstacle.getComponent().getBounds();            int minX = goalkeeperBounds.x;            int maxX = goalkeeperBounds.x+goalkeeperBounds.width;            int minY = goalkeeperBounds.y;            int maxY = goalkeeperBounds.y+goalkeeperBounds.height;            boolean isLan = false;            for (int i = 0; i < tempY.length; i++) {                if(tempX[i]>minX&&tempX[i]<maxX&&tempY[i]>minY&&tempY[i]<maxY){                    isLan = true;                    FAIL_CLIP.play();                    repaint();                    JOptionPane.showMessageDialog(null,"球被"+obstacle.name()+"守住了...","Tipe",JOptionPane.ERROR_MESSAGE);                    return true;                }            }        }    }    return false;}
复制代码

记时逻辑实现

记时实现的思路如下:

  • 进入游戏时初始一个数字变量

  • 开启一个线程,把数字变量增加 1,然后休眠 1 秒

  • 循环增加 1、循环休眠 1 秒


参考代码:


long time = 0;
@Overridepublic void run() { while (true){ try { Thread.sleep(1000); time++; } catch (InterruptedException e) { e.printStackTrace(); } repaint();// 刷新线程时间 // 显示时间时把time转成对应格式,g2d.drawString(String.format("%02d:%02d",time/60,time%60),345,47); }}
复制代码

进球积分逻辑实现

在本游戏中积分的规则有以下两点:

  • 进球得 1 分

  • 进球的同时,消除一颗星星得 1 分

举个例子,如下图,射门进球同时消除了一颗星星,则得 2 分。

相关实现逻辑思路如下:

  • 在球飞行过程中进来消除星星的数量

  • 在进球时,把星星的数量当作分数累计



参考代码:


ball.addMouseListener(new MouseAdapter() {    @Override    public void mouseClicked(MouseEvent e) {        infoPanel.addCount();        new Thread(new Runnable() {            @Override            public void run() {                // 记录飞行过程中消除星星的数量                int star =0;                for (Point2D.Double point : line.getPoints()) {                    ball.setBounds((int) point.x, (int) point.y,ball.getWidth(),ball.getHeight());                    try {                        // 计算星星是否被消除,如果是则累计                        star += obstacleStart();                        Thread.sleep(100);                    } catch (InterruptedException ex) {                        ex.printStackTrace();                    }                }                // 把消除星星作为分数,传递给计分区                check(star);            }        }).start();        CLICK_CLIP.play();    }});
复制代码


// 检查是否进球public void check(int star){    ........    if(!isLan&&!isOut){// 进球了        if(result!=null){            this.remove(result);        }        // 增加计分区的数字,有多少星星则记多少        infoPanel.addScore(star);        ........}
复制代码


发布于: 刚刚阅读数: 4
用户头像

非喵鱼

关注

技术专业一点,才能多一点时间陪家人! 2018-11-28 加入

Java生态开发高效工具 Tinkle、Boom的作者,欢迎大家持续关注!

评论

发布
暂无评论
2 小时开发《点球射门游戏》,动画演示思路(下),代码已开源_Java_非喵鱼_InfoQ写作社区