HTML5 Canvas开发详解(6) -- 边界/碰撞检测

2022-04-07 16:00:37 浏览数 (1)

1. 边界检测

边界检测,指的是检测一个物体所处“运动环境的范围”,简单来说,就是给运动物体限定一个范围,从而实现某些动画效果。

在Canvas动画中,我们可以为物体设置一个运动范围,这个运动范围可以是整个画布,也可以是画布的一部分,大多数情况下,都会把物体运动范围设置为整个画布。

1.1 边界限制

边界限制,指的是通过边界检测的办法来限制物体的运动范围,使得其无法超出这个运动范围,而只限在范围里面运动。

语法:

代码语言:javascript复制
if(ball.x < ball.radius){
    //小球“碰到”左边界时
}else if(ball.x > cnv.width - ball.radius){
    //小球“碰到”右边界时
}
if(ball.y < ball.radius){
    //小球“碰到”上边界时
}else if(ball.y > cnv.height - ball.radius){
    //小球“碰到”下边界时
}

1.2 边界环绕

边界环绕,指的是当物体从一个边界消失后,它就会从对立的边界重新出现,从而形成一种环绕效果。

语法:

代码语言:javascript复制
if(ball.x < -ball.radius){
    //小球“完全超出”左边界时
}else if(ball.x > cnv.width   ball.radius){
    //小球“完全超出”右边界时
}
if(ball.y < -ball.radius){
    //小球“完全超出”上边界时
}else if(ball.y > cnv.height   ball.radius){
    //小球“完全超出”下边界时
}

1.3 边界生成

边界生成,指的是物体完全超出边界之后,会在最开始的位置重新生成。这种技巧可用于创建喷泉以及各种粒子效果。

边界生成可以源源不断地为Canvas提供运动物体,而不用担心Canvas上的物体过多以至于影响浏览器的性能速度,因为物体的数量是固定不变的。

语法:

代码语言:javascript复制
if(ball.x < -ball.radius ||
  ball.x > cnv.width   ball.radius ||
  ball.y < -ball.radius ||
  ball.y > cnv.height   ball.radius
){
    ...
}

示例:不断下落的小球

代码语言:javascript复制
//tools.js

//随机生成一个十六进制颜色值的字符串
tools.getRandomColor = function(){
    let color = '#';
    for(let i = 0; i < 6; i  ){
        let n = Math.floor(Math.random() * 16);
        color  = '0123456789abcdef'[n];
    }
    return color;
}
代码语言:javascript复制
//my-canvas.vue

//...

import tools from '@/api/tools'
import {Arrow, Ball} from '@/api/tools'

<script>
    //...
    
    export default {
        data(){
            return {
                //...
                optBtnData: [// 操作按钮数据
                    //...
                    {text: '边界生成', clickBtnFunc: () => {this.createManyBalls(this.cxtObj, this.cnvObj)}},
                ],
                //...
            }
        },
        methods: {
            //...
            
            //不断下落的小球
            createManyBalls(cxt, cnv){
                let balls = [];
                let n = 50;
                let gravity = 0.15;
                for(let i = 0; i < n; i  ){
                    let ball = new Ball(cnv.width / 2, 0, 5, tools.getRandomColor());
                    //ball.vx和ball.vy取值为:-3~3之间的任意数
                    ball.vx = (Math.random() * 2 -1) * 3;
                    ball.vy = (Math.random() * 2 -1) * 3;
                    balls.push(ball);
                }
                (function frame(){
                    requestAnimationFrame(frame);
                    cxt.clearRect(0, 0, cnv.width, cnv.height);
                    balls.forEach(ball => {
                        if(ball.x < -ball.radius ||
                          ball.x > cnv.width   ball.radius ||
                          ball.y < -ball.radius ||
                          ball.y > cnv.height   ball.radius){
                            ball.x = cnv.width / 2;
                            ball.y = 0;
                            ball.vx = (Math.random() * 2 -1) * 3;
                            ball.vy = (Math.random() * 2 -1) * 3;
                        }
                        ball.draw(cxt, 'fill');
                        ball.x  = ball.vx;
                        ball.y  = ball.vy;
                        ball.vy  = gravity;
                    })
                })();
            },
        }
    }
</script>

我们可以通过调整随机数的范围,控制物体的运动方向和范围,从而实现各种有趣效果。

1.4 边界反弹

边界反弹,指的是物体触碰到边界之后就会反弹回来,就像现实世界中小球碰到墙壁反弹一样。

在物体碰到边界后,我们需要做两件事,即保持它的位置不变和改变它的速度力量。也就是说,如果物体碰到左边界或右边界的时候,就对vx取反,而vy不变;如果物体碰到上边界或下边界的时候,就对vy取反,vx不变。

语法:

代码语言:javascript复制
//碰到左边界
if(ball.x < ball.radius){
    ball.x = ball.radius;
    vx = -vx;
//碰到右边界
}else if(ball.x > cnv.width - ball.radius){
    ball.x = cnv.width - ball.radius;
    vx = -vx;
}
//碰到上边界
if(ball.y < ball.radius){
    ball.y = ball.radius;
    vy = -vy;
//碰到下边界
}else if(ball.y > cnv.height- ball.radius){
    ball.y = cnv.height- ball.radius;
    vy = -vy;
}

示例:多球反弹

代码语言:javascript复制
//my-canvas.vue

//...

import tools from '@/api/tools'
import {Arrow, Ball} from '@/api/tools'

<script>
    //...
    
    export default {
        data(){
            return {
                //...
                optBtnData: [// 操作按钮数据
                    //...
                    {text: '多球反弹', clickBtnFunc: () => {this.ballsRebound(this.cxtObj, this.cnvObj)}},
                ],
                //...
            }
        },
        methods: {
            //...
            
            //多球反弹
            ballsRebound(cxt, cnv){
                let balls = [];
                let n = 10;
                for(let i = 0; i < n; i  ){
                    let ball = new Ball(cnv.width / 2, cnv.height / 2, 8, tools.getRandomColor());
                    //ball.vx和ball.vy取值为:-3~3之间的任意数
                    ball.vx = (Math.random() * 2 -1) * 3;
                    ball.vy = (Math.random() * 2 -1) * 3;
                    balls.push(ball);
                }
                (function frame(){
                    requestAnimationFrame(frame);
                    cxt.clearRect(0, 0, cnv.width, cnv.height);
                    balls.forEach(ball => {
                        ball.x  = ball.vx;
                        ball.y  = ball.vy;
                        if(ball.x < ball.radius){
                            ball.x = ball.radius;
                            ball.vx = -ball.vx;
                        }else if(ball.x > cnv.width - ball.radius){
                            ball.x = cnv.width - ball.radius;
                            ball.vx = -ball.vx;
                        }
                        if(ball.y < ball.radius){
                            ball.y = ball.radius;
                            ball.vy = -ball.vy;
                        }else if(ball.y > cnv.height - ball.radius){
                            ball.y = cnv.height - ball.radius;
                            ball.vy = -ball.vy;
                        }
                        ball.draw(cxt, 'fill');
                    })
                })();
            },
        }
    }
</script>

对于多物体运动,一般情况下采用以下三个步骤进行处理:

1)定义一个数组来存放多个物体;

2)使用for循环生成单个物体,然后添加到数组中;

3)在动画循环中,使用forEach()方法遍历数组,从而处理单个物体。

上面示例效果:

2. 碰撞检测

在边界检测中,我们检测的是“物体与边界”之间是否发生碰撞;而在碰撞检测中,检测的则是“物体与物体”之间是否发生碰撞。

碰撞检测常用的两种方法:外接矩形判定法和外接圆判定法。

2.1 外接矩形判定法

外接矩形判定法,指的是如果检测物体是一个矩形或者近似矩形,就可以把这个物体抽象成一个矩形,然后用判断两个矩形是否碰撞的方法进行检测。

对于外接矩形判定法,一般需要两个步骤,即找出物体的外接矩形然后对外接矩形进行碰撞检测。

判断两个矩形是否发生碰撞,只需要判断两个矩形左上角的坐标所处的范围,如果两个矩形左上角的坐标满足一定条件,则两个矩形就发生了碰撞。

语法:

代码语言:javascript复制
tools.checkRect = function(rectA, rectB){
    return !(rectA.x   rectA.width < rectB.x ||
        rectB.x   rectB.width < rectA.x ||
        rectA.y   rectA.height < rectB.y ||
        rectB.y   rectB.height < rectA.y
    )
}

如果上面四个条件都不满足的话,checkRect()方法返回的值是true,表示两个矩形已经发生了碰撞。

示例:简易俄罗斯方块

代码语言:javascript复制
//tools.js

//判断两个矩形是否发生碰撞
tools.checkRect = function(rectA, rectB){
    return !(rectA.x   rectA.width < rectB.x ||
            rectB.x   rectB.width < rectA.x ||
            rectA.y   rectA.height < rectB.y ||
            rectB.y   rectB.height < rectA.y)
}
代码语言:javascript复制
//my-canvas.vue

//...

methods: {
    //创建一个块
    createBox(cnv){
        let x = Math.random() * cnv.width;
        let y = 0;
        let width = Math.random() * 40   10;
        let height = Math.random() * 40   10;
        let color = tools.getRandomColor();
        let box = new Box(x, y, width, height, color);
        return box;
    }, 
    //俄罗斯方块
    drawTetris(cxt, cnv){
        let vy = 20;
        let self = this;
        let boxes = [];
        let activeBox = this.createBox(cnv);
        boxes.push(activeBox);
        (function frame(){
            requestAnimationFrame(frame);
            cxt.clearRect(0, 0, cnv.width, cnv.height);
            activeBox.y  = vy;
            if(activeBox.y > cnv.height - activeBox.height){
                activeBox.y = cnv.height - activeBox.height;
                activeBox = self.createBox(cnv);
                boxes.push(activeBox);
            }
            let isDraw = true;
            boxes.forEach(box => {
                if(box.y <= 0){
                    isDraw = false;
                }
                if(activeBox !== box && tools.checkRect(activeBox, box)){
                    activeBox.y = box.y - activeBox.height;
                    activeBox = self.createBox(cnv);
                    boxes.push(activeBox);
                 }
                 if(isDraw){
                     box.draw(cxt, 'fill');
                 }
            })
        })()
    }, 
}

2.2 外接圆判定法

外接圆判定法,指的是如果检测物体是一个圆或者近似圆,我们可以把这个物体抽象成一个圆,然后用判断两个圆是否碰撞的方法进行检测。

对于外接圆判定法,一般也需要两个步骤,即找出物体的外接圆然后对外接圆进行碰撞检测。

判断两个圆是否发生碰撞,只需要判断两个圆心之间的距离。如果两个圆心之间的距离大于或等于两个圆的半径之和,则两个圆没有发生碰撞;如果两个圆心之间的距离小于两个圆的半径之和,则两个圆发生了碰撞。

语法:

代码语言:javascript复制
tools.checkCircle = function(circleB, circleA){
    let dx = circleB.x - circleA.x;
    let dy = circleB.y - circleA.y;
    let distance = Math.sqrt(dx * dx   dy * dy);
    return distance < circleA.radius   circleB.radius ? true : false;
}

示例:两个球碰撞

代码语言:javascript复制
//tools.js

//判断两个圆形是否发生碰撞
tools.checkCircle = function(circleB, circleA){
    let dx = circleB.x - circleA.x;
    let dy = circleB.y - circleA.y;
    let distance = Math.sqrt(dx * dx   dy * dy);
    return distance < circleA.radius   circleB.radius ? true : false;
}
代码语言:javascript复制
//my-canvas.vue

//...

methods: {
    //两个小球碰撞检测
    twoBallsCrash(cxt, cnv){
        let ballA = new Ball(12, cnv.height / 2, 12, '#f69');
        let ballB = new Ball(cnv.width - 12, cnv.height / 2, 12, '#6cf');
        let vx = 6;
        (function frame(){
            requestAnimationFrame(frame);
            cxt.clearRect(0, 0, cnv.width, cnv.height);
            ballA.x  = vx;
            ballB.x  = -vx;
            if(tools.checkCircle(ballB, ballA) || ballA.x < ballA.radius || ballB.x > cnv.width - ballA.radius){
                vx = -vx;
            }
            ballA.draw(cxt, 'fill');
            ballB.draw(cxt, 'fill');
        })()
    },
}

外接矩形判定法和外接圆判定法都可能存在误差,但这两个方法可以减少大量的计算量,实现起来比较简单。对于两个物体的碰撞检测,哪种方式的误差小,就选哪个。

上面示例效果:

2.3 多物体碰撞

如果有n个物体,根据排列组合可以知道,此时共有n*(n-1)/2种碰撞情况。

语法:

代码语言:javascript复制
balls.forEach((ballA, i) => {
    for(let j = i   1; j < balls.length; j  ){
        let ballB = balls[j];
        if(tools.checkCircle(ballA, ballB)){
            //...
        }
    }
})

示例:多球碰撞

代码语言:javascript复制
//my-canvas.vue

//...

methods: {
    //多球碰撞
    ballsCrash(cxt, cnv){
        let self = this;
        let n = 10;
        let balls = [];
        for(let i = 0; i < n; i  ){
            let ball = new Ball();
            ball.x = Math.random() * cnv.width;
            ball.y = Math.random() * cnv.height;
            ball.radius = 10;
            ball.color = tools.getRandomColor();
            ball.vx = Math.random() * 6 - 3;
            ball.vy = Math.random() * 6 - 3;
            balls.push(ball);
        }
        (function frame(){
            requestAnimationFrame(frame);
            cxt.clearRect(0, 0, cnv.width, cnv.height);
             //碰撞检测
             balls.forEach((ballA, i) => {
                 for(let j = i   1; j < balls.length; j  ){
                     let ballB = balls[j];
                         if(tools.checkCircle(ballB, ballA)){
                             ballA.vx = -ballA.vx;
                             ballA.vy = -ballA.vy;
                             ballB.vx = -ballB.vx;
                             ballB.vy = -ballB.vy;
                         }
                  }
              });
              balls.forEach(ball => {
                  //边界检测
                  if(ball.x < ball.radius){
                      ball.x = ball.radius;
                      ball.vx = -ball.vx;
                  }else if(ball.x > cnv.width - ball.radius){
                      ball.x = cnv.width - ball.radius;
                      ball.vx = -ball.vx;
                  }
                  if(ball.y < ball.radius){
                      ball.y = ball.radius;
                      ball.vy = -ball.vy;
                  }else if(ball.y > cnv.height - ball.radius){
                      ball.y = cnv.height - ball.radius;
                      ball.vy = -ball.vy;
                  }
                  //绘制小球
                  ball.draw(cxt, 'fill');
                  ball.x  = ball.vx;
                  ball.y  = ball.vy;
            });
        })()
    },
}

示例效果:

http://mpvideo.qpic.cn/0bf2kaaaoaaacuaajbonbzpfaugda5iaabya.f10003.mp4?dis_k=e2238467b0a8d4882cdaf9fb2bee3864&dis_t=1649318399&vid=wxv_1357306148571185153&format_id=10003&support_redirect=0&mmversion=false

0 人点赞